feat: Enhance OCR processing with increased sample counts and improved candidate selection logic

This commit is contained in:
Ross
2026-05-11 12:49:22 +01:00
parent 6b9c7be633
commit 0bda5e2c41
+75 -27
View File
@@ -194,8 +194,8 @@
};
const SITE_PREFIXES = ["ref", "rk9", "ra9", "rh8", "rbz", "rba"];
const OCR_SERIES_SAMPLE_COUNT = 3;
const OCR_MAX_SAMPLES = 120;
const OCR_SERIES_SAMPLE_COUNT = 6;
const OCR_MAX_SAMPLES = 180;
const OCR_CANVAS_MAX = 768;
const DUPLICATE_BATCH_SIZE = 500;
@@ -751,6 +751,11 @@
return window.uploadPreview.ocrWorker;
}
async function waitForPaint() {
await new Promise((resolve) => requestAnimationFrame(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
}
function getOcrPhiReasons(text) {
const value = (text || "").toLowerCase();
const reasons = [];
@@ -787,19 +792,15 @@
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 sampleCount = Math.min(OCR_SERIES_SAMPLE_COUNT, seriesRecords.length);
const used = new Set();
for (const index of candidateIndexes) {
for (let i = 0; i < sampleCount; i++) {
const index = sampleCount === 1
? 0
: Math.floor((i * (seriesRecords.length - 1)) / (sampleCount - 1));
if (used.has(index)) continue;
used.add(index);
samples.push(seriesRecords[index]);
if (used.size >= OCR_SERIES_SAMPLE_COUNT) break;
}
}
@@ -873,6 +874,7 @@
cornerstone.enable(element);
const image = await cornerstone.loadAndCacheImage(imageId);
cornerstone.displayImage(element, image);
await waitForPaint();
const sourceCanvas = element.querySelector("canvas");
if (!sourceCanvas) throw new Error("No canvas");
@@ -894,6 +896,41 @@
}
}
function buildOcrCandidateCanvases(baseCanvas, enhancedCanvas) {
const candidates = [
{ name: "full", canvas: baseCanvas },
{ name: "full-enhanced", canvas: enhancedCanvas },
];
const makeCrop = (src, sx, sy, sw, sh) => {
const c = document.createElement("canvas");
c.width = Math.max(1, sw);
c.height = Math.max(1, sh);
const ctx = c.getContext("2d", { alpha: false });
ctx.drawImage(src, sx, sy, sw, sh, 0, 0, c.width, c.height);
return c;
};
const addRegions = (src, suffix) => {
const w = src.width;
const h = src.height;
const headerH = Math.max(24, Math.floor(h * 0.23));
const footerH = Math.max(24, Math.floor(h * 0.18));
const cornerW = Math.max(32, Math.floor(w * 0.45));
candidates.push({ name: `top-left${suffix}`, canvas: makeCrop(src, 0, 0, cornerW, headerH) });
candidates.push({ name: `top-right${suffix}`, canvas: makeCrop(src, Math.max(0, w - cornerW), 0, cornerW, headerH) });
candidates.push({ name: `bottom-left${suffix}`, canvas: makeCrop(src, 0, Math.max(0, h - footerH), cornerW, footerH) });
candidates.push({ name: `bottom-right${suffix}`, canvas: makeCrop(src, Math.max(0, w - cornerW), Math.max(0, h - footerH), cornerW, footerH) });
candidates.push({ name: `header-strip${suffix}`, canvas: makeCrop(src, 0, 0, w, headerH) });
candidates.push({ name: `footer-strip${suffix}`, canvas: makeCrop(src, 0, Math.max(0, h - footerH), w, footerH) });
};
addRegions(baseCanvas, "");
addRegions(enhancedCanvas, "-enhanced");
return candidates;
}
async function runOcrChecks() {
syncOcrDebugPanelVisibility();
if (!document.getElementById("check-anonymisation-ocr").checked) {
@@ -935,26 +972,37 @@
setProgress(`OCR ${index} of ${samples.length}: ${record.path}`);
try {
const { outCanvas, enhancedCanvas } = await getDownscaledOcrCanvases(record.file);
const baseBlob = await canvasToBlob(outCanvas);
const baseResult = await worker.recognize(baseBlob);
const ocrCandidates = buildOcrCandidateCanvases(outCanvas, enhancedCanvas);
const baseText = normaliseOcrText(baseResult?.data?.text || "");
let bestText = baseText;
let bestConfidence = Number(baseResult?.data?.confidence || 0);
let bestText = "";
let bestConfidence = 0;
let bestCandidateName = "none";
let bestReasons = [];
// Fallback pass with high-contrast image when OCR output is weak/blank.
if (bestText.length < 6) {
const enhancedBlob = await canvasToBlob(enhancedCanvas);
const enhancedResult = await worker.recognize(enhancedBlob);
const enhancedText = normaliseOcrText(enhancedResult?.data?.text || "");
const enhancedConfidence = Number(enhancedResult?.data?.confidence || 0);
if (enhancedText.length > bestText.length || enhancedConfidence > bestConfidence) {
bestText = enhancedText;
bestConfidence = enhancedConfidence;
for (const candidate of ocrCandidates) {
const blob = await canvasToBlob(candidate.canvas);
const result = await worker.recognize(blob);
const text = normaliseOcrText(result?.data?.text || "");
const confidence = Number(result?.data?.confidence || 0);
const reasons = getOcrPhiReasons(text);
const isBetter = reasons.length > bestReasons.length
|| (reasons.length === bestReasons.length && text.length > bestText.length)
|| (reasons.length === bestReasons.length && text.length === bestText.length && confidence > bestConfidence);
if (isBetter) {
bestText = text;
bestConfidence = confidence;
bestCandidateName = candidate.name;
bestReasons = reasons;
}
if (reasons.length > 0 && confidence >= 40 && text.length >= 6) {
break;
}
}
const reasons = getOcrPhiReasons(bestText);
const reasons = bestReasons;
const flagged = reasons.length > 0;
if (flagged) {
flaggedSeriesUids.add(record.seriesUid);
@@ -966,7 +1014,7 @@
path: record.path,
flagged,
reasons,
preview: `${preview} (conf ${Math.round(bestConfidence)}%)`,
preview: `${preview} (region ${bestCandidateName}, conf ${Math.round(bestConfidence)}%)`,
});
} catch (err) {
// Ignore per-image OCR errors and continue with other samples.