feature: Optional OCR-Assisted Draft Pre-Fill
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
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<TesseractOcrService> _logger;
|
||||
|
||||
public TesseractOcrService(
|
||||
IOptions<OcrOptions> options,
|
||||
ImagePreprocessor preprocessor,
|
||||
ILogger<TesseractOcrService> logger)
|
||||
{
|
||||
_options = options.Value.Tesseract;
|
||||
_preprocessor = preprocessor;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<OcrExtractionResult> 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<OcrExtractedField> ParseClinicalText(string text, float meanConfidence)
|
||||
{
|
||||
var fields = new List<OcrExtractedField>();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return fields;
|
||||
|
||||
var baseConfidence = Math.Clamp(meanConfidence / 100f, 0.0, 1.0);
|
||||
var matchedFields = new HashSet<string>(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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user