feat: Improve OCR sample selection and detection of clinical identifiers
This commit is contained in:
@@ -60,7 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-text mt-2">
|
<div class="form-text mt-2">
|
||||||
OCR now runs sequentially on one sample image per series (capped) to reduce browser memory use.
|
OCR runs sequentially on multiple sample images per series (capped) to improve burned-in text detection.
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,7 +188,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SITE_PREFIXES = ["ref", "rk9", "ra9", "rh8", "rbz", "rba"];
|
const SITE_PREFIXES = ["ref", "rk9", "ra9", "rh8", "rbz", "rba"];
|
||||||
const OCR_MAX_SERIES_SAMPLES = 40;
|
const OCR_SERIES_SAMPLE_COUNT = 3;
|
||||||
|
const OCR_MAX_SAMPLES = 120;
|
||||||
const OCR_CANVAS_MAX = 768;
|
const OCR_CANVAS_MAX = 768;
|
||||||
const DUPLICATE_BATCH_SIZE = 500;
|
const DUPLICATE_BATCH_SIZE = 500;
|
||||||
|
|
||||||
@@ -665,7 +666,49 @@
|
|||||||
|
|
||||||
function detectOcrPhi(text) {
|
function detectOcrPhi(text) {
|
||||||
const value = (text || "").toLowerCase();
|
const value = (text || "").toLowerCase();
|
||||||
return value.includes("accession") || value.includes("patient") || /(ref|rk9|rh8|ra9|rbz|rba)\d+/i.test(value);
|
|
||||||
|
// Catch common clinical identifier markers and identifier-like tokens.
|
||||||
|
if (value.includes("accession") || value.includes("patient") || value.includes("nhs") || value.includes("hospital")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return /(ref|rk9|rh8|ra9|rbz|rba)\d+/i.test(value)
|
||||||
|
|| /\b\d{3}[\s-]?\d{3}[\s-]?\d{4}\b/.test(value)
|
||||||
|
|| /\b\d{10}\b/.test(value)
|
||||||
|
|| /\b\d{6,8}\b/.test(value)
|
||||||
|
|| /\b(?:dob|d\.o\.b|birth|born)\b/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOcrSamples(records) {
|
||||||
|
const includeRecords = records.filter((record) => record.include);
|
||||||
|
const seriesMap = {};
|
||||||
|
for (const record of includeRecords) {
|
||||||
|
if (!seriesMap[record.seriesUid]) {
|
||||||
|
seriesMap[record.seriesUid] = [];
|
||||||
|
}
|
||||||
|
seriesMap[record.seriesUid].push(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
const samples = [];
|
||||||
|
for (const seriesRecords of Object.values(seriesMap)) {
|
||||||
|
if (!seriesRecords.length) continue;
|
||||||
|
|
||||||
|
const candidateIndexes = [
|
||||||
|
0,
|
||||||
|
Math.floor((seriesRecords.length - 1) / 2),
|
||||||
|
seriesRecords.length - 1,
|
||||||
|
];
|
||||||
|
|
||||||
|
const used = new Set();
|
||||||
|
for (const index of candidateIndexes) {
|
||||||
|
if (used.has(index)) continue;
|
||||||
|
used.add(index);
|
||||||
|
samples.push(seriesRecords[index]);
|
||||||
|
if (used.size >= OCR_SERIES_SAMPLE_COUNT) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return samples.slice(0, OCR_MAX_SAMPLES);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDownscaledOcrBlob(file) {
|
async function getDownscaledOcrBlob(file) {
|
||||||
@@ -715,16 +758,18 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sampleBySeries = {};
|
if (!window.Tesseract || typeof window.Tesseract.createWorker !== "function") {
|
||||||
for (const record of window.uploadPreview.records) {
|
window.uploadPreview.records.forEach(r => { r.ocrWarning = false; });
|
||||||
if (!sampleBySeries[record.seriesUid]) sampleBySeries[record.seriesUid] = record;
|
toastr.error("OCR engine is unavailable in this browser session.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const samples = Object.values(sampleBySeries).slice(0, OCR_MAX_SERIES_SAMPLES);
|
const samples = buildOcrSamples(window.uploadPreview.records);
|
||||||
if (!samples.length) return;
|
if (!samples.length) return;
|
||||||
|
|
||||||
const worker = await ensureOcrWorker();
|
const worker = await ensureOcrWorker();
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
const flaggedSeriesUids = new Set();
|
||||||
|
|
||||||
for (const record of samples) {
|
for (const record of samples) {
|
||||||
index += 1;
|
index += 1;
|
||||||
@@ -733,11 +778,17 @@
|
|||||||
const blob = await getDownscaledOcrBlob(record.file);
|
const blob = await getDownscaledOcrBlob(record.file);
|
||||||
const result = await worker.recognize(blob);
|
const result = await worker.recognize(blob);
|
||||||
const text = result?.data?.text || "";
|
const text = result?.data?.text || "";
|
||||||
record.ocrWarning = detectOcrPhi(text);
|
if (detectOcrPhi(text)) {
|
||||||
|
flaggedSeriesUids.add(record.seriesUid);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
record.ocrWarning = false;
|
// Ignore per-image OCR errors and continue with other samples.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.uploadPreview.records.forEach((record) => {
|
||||||
|
record.ocrWarning = record.include && flaggedSeriesUids.has(record.seriesUid);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function calculateBlake3Hash(file) {
|
async function calculateBlake3Hash(file) {
|
||||||
|
|||||||
Reference in New Issue
Block a user