diff --git a/atlas/templates/atlas/new_uploads.html b/atlas/templates/atlas/new_uploads.html index ae056c4d..9586535d 100644 --- a/atlas/templates/atlas/new_uploads.html +++ b/atlas/templates/atlas/new_uploads.html @@ -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.