91 lines
4.1 KiB
C#
91 lines
4.1 KiB
C#
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;
|
|
}
|
|
}
|