From 6b9c7be633fe47b655551309f6b6d945fc43ca5e Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 11 May 2026 12:42:00 +0100 Subject: [PATCH] feat: Implement OCR debug panel visibility sync and enhance OCR processing with improved canvas handling --- atlas/templates/atlas/new_uploads.html | 114 ++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 13 deletions(-) diff --git a/atlas/templates/atlas/new_uploads.html b/atlas/templates/atlas/new_uploads.html index c058e911..ae056c4d 100644 --- a/atlas/templates/atlas/new_uploads.html +++ b/atlas/templates/atlas/new_uploads.html @@ -229,6 +229,20 @@ return div.innerHTML; } + function isOcrEnabled() { + return !!document.getElementById("check-anonymisation-ocr")?.checked; + } + + function syncOcrDebugPanelVisibility() { + const panel = document.getElementById("ocr-debug-panel"); + if (!panel) return; + const show = isOcrEnabled(); + panel.classList.toggle("d-none", !show); + if (!show) { + panel.removeAttribute("open"); + } + } + function resetOcrDebug(status = "idle", message = "OCR has not run yet.") { window.uploadPreview.ocrDebug = { status, @@ -728,6 +742,12 @@ async function ensureOcrWorker() { if (window.uploadPreview.ocrWorker) return window.uploadPreview.ocrWorker; window.uploadPreview.ocrWorker = await Tesseract.createWorker("eng"); + try { + await window.uploadPreview.ocrWorker.setParameters({ + tessedit_pageseg_mode: "6", + preserve_interword_spaces: "1", + }); + } catch (err) {} return window.uploadPreview.ocrWorker; } @@ -786,7 +806,58 @@ return samples.slice(0, OCR_MAX_SAMPLES); } - async function getDownscaledOcrBlob(file) { + function normaliseOcrText(text) { + return String(text || "").replace(/\s+/g, " ").trim(); + } + + function buildEnhancedOcrCanvas(sourceCanvas) { + const enhanced = document.createElement("canvas"); + enhanced.width = sourceCanvas.width; + enhanced.height = sourceCanvas.height; + const ectx = enhanced.getContext("2d", { alpha: false }); + ectx.drawImage(sourceCanvas, 0, 0); + + const imageData = ectx.getImageData(0, 0, enhanced.width, enhanced.height); + const data = imageData.data; + + let minLum = 255; + let maxLum = 0; + let totalLum = 0; + for (let i = 0; i < data.length; i += 4) { + const lum = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]; + if (lum < minLum) minLum = lum; + if (lum > maxLum) maxLum = lum; + totalLum += lum; + } + + const meanLum = totalLum / (data.length / 4 || 1); + const invert = meanLum < 100; + const range = Math.max(1, maxLum - minLum); + + for (let i = 0; i < data.length; i += 4) { + let lum = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]; + lum = ((lum - minLum) * 255) / range; + if (invert) lum = 255 - lum; + const bw = lum > 150 ? 255 : 0; + data[i] = bw; + data[i + 1] = bw; + data[i + 2] = bw; + } + + ectx.putImageData(imageData, 0, 0); + return enhanced; + } + + async function canvasToBlob(canvas, mimeType = "image/png") { + return await new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (!blob) reject(new Error("Failed to create OCR blob")); + else resolve(blob); + }, mimeType); + }); + } + + async function getDownscaledOcrCanvases(file) { const element = document.createElement("div"); element.style.width = "512px"; element.style.height = "512px"; @@ -813,12 +884,8 @@ const ctx = outCanvas.getContext("2d", { alpha: false }); ctx.drawImage(sourceCanvas, 0, 0, outCanvas.width, outCanvas.height); - return await new Promise((resolve, reject) => { - outCanvas.toBlob((blob) => { - if (!blob) reject(new Error("Failed to build OCR blob")); - else resolve(blob); - }, "image/jpeg", 0.75); - }); + const enhancedCanvas = buildEnhancedOcrCanvas(outCanvas); + return { outCanvas, enhancedCanvas }; } finally { try { cornerstone.disable(element); } catch (err) {} URL.revokeObjectURL(objectUrl); @@ -828,6 +895,7 @@ } async function runOcrChecks() { + syncOcrDebugPanelVisibility(); if (!document.getElementById("check-anonymisation-ocr").checked) { window.uploadPreview.records.forEach(r => { r.ocrWarning = false; }); resetOcrDebug("disabled", "OCR check is disabled in Upload Settings."); @@ -866,22 +934,39 @@ index += 1; setProgress(`OCR ${index} of ${samples.length}: ${record.path}`); try { - const blob = await getDownscaledOcrBlob(record.file); - const result = await worker.recognize(blob); - const text = result?.data?.text || ""; - const reasons = getOcrPhiReasons(text); + const { outCanvas, enhancedCanvas } = await getDownscaledOcrCanvases(record.file); + const baseBlob = await canvasToBlob(outCanvas); + const baseResult = await worker.recognize(baseBlob); + + const baseText = normaliseOcrText(baseResult?.data?.text || ""); + let bestText = baseText; + let bestConfidence = Number(baseResult?.data?.confidence || 0); + + // 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; + } + } + + const reasons = getOcrPhiReasons(bestText); const flagged = reasons.length > 0; if (flagged) { flaggedSeriesUids.add(record.seriesUid); } - const preview = text.replace(/\s+/g, " ").trim().slice(0, 180); + const preview = bestText ? bestText.slice(0, 180) : "[no text extracted]"; window.uploadPreview.ocrDebug.entries.push({ seriesUid: record.seriesUid, path: record.path, flagged, reasons, - preview, + preview: `${preview} (conf ${Math.round(bestConfidence)}%)`, }); } catch (err) { // Ignore per-image OCR errors and continue with other samples. @@ -1106,6 +1191,7 @@ window.uploadPreview.forceAnonymisationOverride = false; resetUploadResults(); resetOcrDebug(); + syncOcrDebugPanelVisibility(); window.uploadPreview.records.forEach((r) => { if (r.objectUrl) URL.revokeObjectURL(r.objectUrl); @@ -1418,6 +1504,7 @@ } resetOcrDebug(); + syncOcrDebugPanelVisibility(); document.addEventListener("click", async function (e) { const includeSeries = e.target.closest(".include-series"); @@ -1458,6 +1545,7 @@ }); bindIfExists("check-anonymisation-ocr", "change", async () => { + syncOcrDebugPanelVisibility(); showLoading(true, "Updating OCR checks..."); await runOcrChecks(); renderGroupedPreview();