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;
|
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.") {
|
function resetOcrDebug(status = "idle", message = "OCR has not run yet.") {
|
||||||
window.uploadPreview.ocrDebug = {
|
window.uploadPreview.ocrDebug = {
|
||||||
status,
|
status,
|
||||||
@@ -728,6 +742,12 @@
|
|||||||
async function ensureOcrWorker() {
|
async function ensureOcrWorker() {
|
||||||
if (window.uploadPreview.ocrWorker) return window.uploadPreview.ocrWorker;
|
if (window.uploadPreview.ocrWorker) return window.uploadPreview.ocrWorker;
|
||||||
window.uploadPreview.ocrWorker = await Tesseract.createWorker("eng");
|
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;
|
return window.uploadPreview.ocrWorker;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -786,7 +806,58 @@
|
|||||||
return samples.slice(0, OCR_MAX_SAMPLES);
|
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");
|
const element = document.createElement("div");
|
||||||
element.style.width = "512px";
|
element.style.width = "512px";
|
||||||
element.style.height = "512px";
|
element.style.height = "512px";
|
||||||
@@ -813,12 +884,8 @@
|
|||||||
const ctx = outCanvas.getContext("2d", { alpha: false });
|
const ctx = outCanvas.getContext("2d", { alpha: false });
|
||||||
ctx.drawImage(sourceCanvas, 0, 0, outCanvas.width, outCanvas.height);
|
ctx.drawImage(sourceCanvas, 0, 0, outCanvas.width, outCanvas.height);
|
||||||
|
|
||||||
return await new Promise((resolve, reject) => {
|
const enhancedCanvas = buildEnhancedOcrCanvas(outCanvas);
|
||||||
outCanvas.toBlob((blob) => {
|
return { outCanvas, enhancedCanvas };
|
||||||
if (!blob) reject(new Error("Failed to build OCR blob"));
|
|
||||||
else resolve(blob);
|
|
||||||
}, "image/jpeg", 0.75);
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
try { cornerstone.disable(element); } catch (err) {}
|
try { cornerstone.disable(element); } catch (err) {}
|
||||||
URL.revokeObjectURL(objectUrl);
|
URL.revokeObjectURL(objectUrl);
|
||||||
@@ -828,6 +895,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runOcrChecks() {
|
async function runOcrChecks() {
|
||||||
|
syncOcrDebugPanelVisibility();
|
||||||
if (!document.getElementById("check-anonymisation-ocr").checked) {
|
if (!document.getElementById("check-anonymisation-ocr").checked) {
|
||||||
window.uploadPreview.records.forEach(r => { r.ocrWarning = false; });
|
window.uploadPreview.records.forEach(r => { r.ocrWarning = false; });
|
||||||
resetOcrDebug("disabled", "OCR check is disabled in Upload Settings.");
|
resetOcrDebug("disabled", "OCR check is disabled in Upload Settings.");
|
||||||
@@ -866,22 +934,39 @@
|
|||||||
index += 1;
|
index += 1;
|
||||||
setProgress(`OCR ${index} of ${samples.length}: ${record.path}`);
|
setProgress(`OCR ${index} of ${samples.length}: ${record.path}`);
|
||||||
try {
|
try {
|
||||||
const blob = await getDownscaledOcrBlob(record.file);
|
const { outCanvas, enhancedCanvas } = await getDownscaledOcrCanvases(record.file);
|
||||||
const result = await worker.recognize(blob);
|
const baseBlob = await canvasToBlob(outCanvas);
|
||||||
const text = result?.data?.text || "";
|
const baseResult = await worker.recognize(baseBlob);
|
||||||
const reasons = getOcrPhiReasons(text);
|
|
||||||
|
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;
|
const flagged = reasons.length > 0;
|
||||||
if (flagged) {
|
if (flagged) {
|
||||||
flaggedSeriesUids.add(record.seriesUid);
|
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({
|
window.uploadPreview.ocrDebug.entries.push({
|
||||||
seriesUid: record.seriesUid,
|
seriesUid: record.seriesUid,
|
||||||
path: record.path,
|
path: record.path,
|
||||||
flagged,
|
flagged,
|
||||||
reasons,
|
reasons,
|
||||||
preview,
|
preview: `${preview} (conf ${Math.round(bestConfidence)}%)`,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Ignore per-image OCR errors and continue with other samples.
|
// Ignore per-image OCR errors and continue with other samples.
|
||||||
@@ -1106,6 +1191,7 @@
|
|||||||
window.uploadPreview.forceAnonymisationOverride = false;
|
window.uploadPreview.forceAnonymisationOverride = false;
|
||||||
resetUploadResults();
|
resetUploadResults();
|
||||||
resetOcrDebug();
|
resetOcrDebug();
|
||||||
|
syncOcrDebugPanelVisibility();
|
||||||
|
|
||||||
window.uploadPreview.records.forEach((r) => {
|
window.uploadPreview.records.forEach((r) => {
|
||||||
if (r.objectUrl) URL.revokeObjectURL(r.objectUrl);
|
if (r.objectUrl) URL.revokeObjectURL(r.objectUrl);
|
||||||
@@ -1418,6 +1504,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
resetOcrDebug();
|
resetOcrDebug();
|
||||||
|
syncOcrDebugPanelVisibility();
|
||||||
|
|
||||||
document.addEventListener("click", async function (e) {
|
document.addEventListener("click", async function (e) {
|
||||||
const includeSeries = e.target.closest(".include-series");
|
const includeSeries = e.target.closest(".include-series");
|
||||||
@@ -1458,6 +1545,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
bindIfExists("check-anonymisation-ocr", "change", async () => {
|
bindIfExists("check-anonymisation-ocr", "change", async () => {
|
||||||
|
syncOcrDebugPanelVisibility();
|
||||||
showLoading(true, "Updating OCR checks...");
|
showLoading(true, "Updating OCR checks...");
|
||||||
await runOcrChecks();
|
await runOcrChecks();
|
||||||
renderGroupedPreview();
|
renderGroupedPreview();
|
||||||
|
|||||||
Reference in New Issue
Block a user