feat: Implement OCR debug panel visibility sync and enhance OCR processing with improved canvas handling

This commit is contained in:
Ross
2026-05-11 12:42:00 +01:00
parent 35887fbcaf
commit 6b9c7be633
+101 -13
View File
@@ -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();