feat: Implement OCR debug panel visibility sync and enhance OCR processing with improved canvas handling
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user