diff --git a/atlas/templates/atlas/new_uploads.html b/atlas/templates/atlas/new_uploads.html
index 7f168fc5..ff32d7d6 100644
--- a/atlas/templates/atlas/new_uploads.html
+++ b/atlas/templates/atlas/new_uploads.html
@@ -74,6 +74,11 @@
+
+ OCR Debug Panel
+ OCR has not run yet.
+
+
Potential anonymisation issues were detected. Flagged series are highlighted below.
@@ -181,6 +186,7 @@
duplicateSeriesUrls: new Set(),
forceAnonymisationOverride: false,
ocrWorker: null,
+ ocrDebug: null,
currentTagsRecord: null,
duplicateCheckInProgress: false,
duplicateCheckRunId: 0,
@@ -223,6 +229,67 @@
return div.innerHTML;
}
+ function resetOcrDebug(status = "idle", message = "OCR has not run yet.") {
+ window.uploadPreview.ocrDebug = {
+ status,
+ message,
+ totalSamples: 0,
+ processedSamples: 0,
+ flaggedSeriesCount: 0,
+ entries: [],
+ updatedAt: new Date().toISOString(),
+ };
+ renderOcrDebugPanel();
+ }
+
+ function renderOcrDebugPanel() {
+ const root = document.getElementById("ocr-debug-content");
+ if (!root) return;
+
+ const debug = window.uploadPreview.ocrDebug;
+ if (!debug) {
+ root.innerHTML = "OCR has not run yet.";
+ return;
+ }
+
+ const sampleRows = debug.entries.slice(-30).map((entry) => {
+ const reasons = entry.reasons?.length ? escapeHtml(entry.reasons.join(", ")) : "No PHI pattern matched";
+ return `
+
+ | ${escapeHtml(entry.seriesUid)} |
+ ${escapeHtml(entry.path)} |
+ ${entry.flagged ? 'Flagged' : 'Clear'} |
+ ${reasons} |
+ ${escapeHtml(entry.preview)} |
+
+ `;
+ }).join("");
+
+ root.innerHTML = `
+
+ Status: ${escapeHtml(debug.status)}
+ Message: ${escapeHtml(debug.message)}
+ Samples: ${debug.processedSamples}/${debug.totalSamples} ยท Flagged series: ${debug.flaggedSeriesCount}
+
+ ${sampleRows ? `
+
+
+
+
+ | Series UID |
+ File |
+ Result |
+ Match reasons |
+ OCR preview |
+
+
+ ${sampleRows}
+
+
+ ` : ""}
+ `;
+ }
+
async function parseDicomMetadata(file) {
try {
const buffer = await file.arrayBuffer();
@@ -664,19 +731,34 @@
return window.uploadPreview.ocrWorker;
}
- function detectOcrPhi(text) {
+ function getOcrPhiReasons(text) {
const value = (text || "").toLowerCase();
+ const reasons = [];
// Catch common clinical identifier markers and identifier-like tokens.
- if (value.includes("accession") || value.includes("patient") || value.includes("nhs") || value.includes("hospital")) {
- return true;
- }
+ if (value.includes("accession")) reasons.push("contains 'accession'");
+ if (value.includes("patient")) reasons.push("contains 'patient'");
+ if (value.includes("nhs")) reasons.push("contains 'nhs'");
+ if (value.includes("hospital")) reasons.push("contains 'hospital'");
+
+ if (/(ref|rk9|rh8|ra9|rbz|rba)\d+/i.test(value)) reasons.push("site prefix + number pattern");
+ if (/\b\d{3}[\s-]?\d{3}[\s-]?\d{4}\b/.test(value)) reasons.push("10-digit grouped number pattern");
+ if (/\b\d{10}\b/.test(value)) reasons.push("10-digit number pattern");
+ if (/\b\d{6,8}\b/.test(value)) reasons.push("6-8 digit number pattern");
+ if (/\b(?:dob|d\.o\.b|birth|born)\b/.test(value)) reasons.push("date-of-birth keyword");
+
+ return reasons;
+ }
+
+ function detectOcrPhi(text) {
+ return getOcrPhiReasons(text).length > 0;
+ }
return /(ref|rk9|rh8|ra9|rbz|rba)\d+/i.test(value)
- || /\b\d{3}[\s-]?\d{3}[\s-]?\d{4}\b/.test(value)
- || /\b\d{10}\b/.test(value)
- || /\b\d{6,8}\b/.test(value)
- || /\b(?:dob|d\.o\.b|birth|born)\b/.test(value);
+ || /\b\d{3}[\s-]?\d{3}[\s-]?\d{4}\b/.test(value)
+ || /\b\d{10}\b/.test(value)
+ || /\b\d{6,8}\b/.test(value)
+ || /\b(?:dob|d\.o\.b|birth|born)\b/.test(value);
}
function buildOcrSamples(records) {
@@ -755,17 +837,33 @@
async function runOcrChecks() {
if (!document.getElementById("check-anonymisation-ocr").checked) {
window.uploadPreview.records.forEach(r => { r.ocrWarning = false; });
+ resetOcrDebug("disabled", "OCR check is disabled in Upload Settings.");
return;
}
if (!window.Tesseract || typeof window.Tesseract.createWorker !== "function") {
window.uploadPreview.records.forEach(r => { r.ocrWarning = false; });
+ resetOcrDebug("error", "OCR engine is unavailable in this browser session.");
toastr.error("OCR engine is unavailable in this browser session.");
return;
}
const samples = buildOcrSamples(window.uploadPreview.records);
- if (!samples.length) return;
+ if (!samples.length) {
+ resetOcrDebug("idle", "No included files available for OCR sampling.");
+ return;
+ }
+
+ window.uploadPreview.ocrDebug = {
+ status: "running",
+ message: "OCR checks in progress.",
+ totalSamples: samples.length,
+ processedSamples: 0,
+ flaggedSeriesCount: 0,
+ entries: [],
+ updatedAt: new Date().toISOString(),
+ };
+ renderOcrDebugPanel();
const worker = await ensureOcrWorker();
let index = 0;
@@ -778,17 +876,46 @@
const blob = await getDownscaledOcrBlob(record.file);
const result = await worker.recognize(blob);
const text = result?.data?.text || "";
- if (detectOcrPhi(text)) {
+ const reasons = getOcrPhiReasons(text);
+ const flagged = reasons.length > 0;
+ if (flagged) {
flaggedSeriesUids.add(record.seriesUid);
}
+
+ const preview = text.replace(/\s+/g, " ").trim().slice(0, 180);
+ window.uploadPreview.ocrDebug.entries.push({
+ seriesUid: record.seriesUid,
+ path: record.path,
+ flagged,
+ reasons,
+ preview,
+ });
} catch (err) {
// Ignore per-image OCR errors and continue with other samples.
+ window.uploadPreview.ocrDebug.entries.push({
+ seriesUid: record.seriesUid,
+ path: record.path,
+ flagged: false,
+ reasons: ["OCR error"],
+ preview: String(err?.message || "OCR failed"),
+ });
+ } finally {
+ window.uploadPreview.ocrDebug.processedSamples = index;
+ window.uploadPreview.ocrDebug.flaggedSeriesCount = flaggedSeriesUids.size;
+ window.uploadPreview.ocrDebug.updatedAt = new Date().toISOString();
+ renderOcrDebugPanel();
}
}
window.uploadPreview.records.forEach((record) => {
record.ocrWarning = record.include && flaggedSeriesUids.has(record.seriesUid);
});
+
+ window.uploadPreview.ocrDebug.status = "complete";
+ window.uploadPreview.ocrDebug.message = `OCR complete. ${flaggedSeriesUids.size} series flagged from ${samples.length} sampled image(s).`;
+ window.uploadPreview.ocrDebug.flaggedSeriesCount = flaggedSeriesUids.size;
+ window.uploadPreview.ocrDebug.updatedAt = new Date().toISOString();
+ renderOcrDebugPanel();
}
async function calculateBlake3Hash(file) {
@@ -985,6 +1112,7 @@
window.uploadPreview.forceAnonymisationOverride = false;
resetUploadResults();
+ resetOcrDebug();
window.uploadPreview.records.forEach((r) => {
if (r.objectUrl) URL.revokeObjectURL(r.objectUrl);
@@ -1296,6 +1424,8 @@
return;
}
+ resetOcrDebug();
+
document.addEventListener("click", async function (e) {
const includeSeries = e.target.closest(".include-series");
if (includeSeries) {
@@ -1494,5 +1624,22 @@
.table-warning th {
color: #ffc107;
}
+
+ #ocr-debug-panel {
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 8px;
+ padding: 8px 10px;
+ background: rgba(255, 255, 255, 0.02);
+ }
+
+ .ocr-debug-table-wrap {
+ max-height: 320px;
+ }
+
+ .ocr-debug-table code {
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-size: 0.75rem;
+ }
{% endblock %}