using System.Text.RegularExpressions; using Microsoft.Extensions.Options; using Tesseract; public class TesseractOcrService : IOcrService { private static readonly (Regex Pattern, string FieldName)[] ClinicalPatterns = [ (new Regex(@"(?:Patient\s+)?Name[:\s]+(.+)", RegexOptions.IgnoreCase | RegexOptions.Multiline), "patient.fullName"), (new Regex(@"DOB[:\s]+(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})", RegexOptions.IgnoreCase), "patient.dateOfBirth"), (new Regex(@"(?:Date of Birth|Birth Date)[:\s]+(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})", RegexOptions.IgnoreCase), "patient.dateOfBirth"), (new Regex(@"(?:Sex|Gender)[:\s]+(\S+)", RegexOptions.IgnoreCase), "patient.sex"), (new Regex(@"(?:Admission Date|Admitted)[:\s]+(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})", RegexOptions.IgnoreCase), "encounter.admissionDate"), (new Regex(@"(?:Department|Ward|Unit)[:\s]+(.+)", RegexOptions.IgnoreCase | RegexOptions.Multiline), "encounter.department"), (new Regex(@"(?:Room(?:/Bed)?|Bed)[:\s]+(.+)", RegexOptions.IgnoreCase | RegexOptions.Multiline), "encounter.roomBed"), (new Regex(@"HR[:\s]+(\d+(?:\.\d+)?)", RegexOptions.IgnoreCase), "observation.HEART_RATE.value"), (new Regex(@"(?:Heart Rate|Pulse)[:\s]+(\d+(?:\.\d+)?)", RegexOptions.IgnoreCase), "observation.HEART_RATE.value"), (new Regex(@"(?:Temp|Temperature)[:\s]+(\d+(?:\.\d+)?)", RegexOptions.IgnoreCase), "observation.TEMP_C.value"), (new Regex(@"(?:BP Sys|Systolic)[:\s]+(\d+(?:\.\d+)?)", RegexOptions.IgnoreCase), "observation.BP_SYSTOLIC.value"), (new Regex(@"(?:BP Dia|Diastolic)[:\s]+(\d+(?:\.\d+)?)", RegexOptions.IgnoreCase), "observation.BP_DIASTOLIC.value"), (new Regex(@"(?:RR|Resp(?:iratory)?\s*Rate)[:\s]+(\d+(?:\.\d+)?)", RegexOptions.IgnoreCase), "observation.RESP_RATE.value"), (new Regex(@"(?:SpO2|O2 Sat)[:\s]+(\d+(?:\.\d+)?)", RegexOptions.IgnoreCase), "observation.SPO2.value"), ]; private readonly TesseractOcrOptions _options; private readonly ImagePreprocessor _preprocessor; private readonly ILogger _logger; public TesseractOcrService( IOptions options, ImagePreprocessor preprocessor, ILogger logger) { _options = options.Value.Tesseract; _preprocessor = preprocessor; _logger = logger; } public async Task ExtractAsync( Stream documentStream, string contentType) { var sw = System.Diagnostics.Stopwatch.StartNew(); using var preprocessed = _preprocessor.Preprocess(documentStream, contentType); using var memStream = new MemoryStream(); await preprocessed.CopyToAsync(memStream); var imageBytes = memStream.ToArray(); using var engine = new TesseractEngine( _options.DataPath, _options.Language, EngineMode.Default); using var pix = Pix.LoadFromMemory(imageBytes); using var page = engine.Process(pix); var rawText = page.GetText(); var meanConfidence = page.GetMeanConfidence(); var fields = ParseClinicalText(rawText, meanConfidence); sw.Stop(); return new OcrExtractionResult(fields, rawText, (int)sw.ElapsedMilliseconds); } private static List ParseClinicalText(string text, float meanConfidence) { var fields = new List(); if (string.IsNullOrWhiteSpace(text)) return fields; var baseConfidence = Math.Clamp(meanConfidence / 100f, 0.0, 1.0); var matchedFields = new HashSet(StringComparer.Ordinal); foreach (var (pattern, fieldName) in ClinicalPatterns) { if (matchedFields.Contains(fieldName)) continue; var match = pattern.Match(text); if (!match.Success) continue; var value = match.Groups[1].Value.Trim(); if (value.Length == 0) continue; fields.Add(new OcrExtractedField(fieldName, value, baseConfidence)); matchedFields.Add(fieldName); } return fields; } }