feat: Enhance OCR processing with increased sample counts and improved candidate selection logic
This commit is contained in:
@@ -194,8 +194,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SITE_PREFIXES = ["ref", "rk9", "ra9", "rh8", "rbz", "rba"];
|
const SITE_PREFIXES = ["ref", "rk9", "ra9", "rh8", "rbz", "rba"];
|
||||||
const OCR_SERIES_SAMPLE_COUNT = 3;
|
const OCR_SERIES_SAMPLE_COUNT = 6;
|
||||||
const OCR_MAX_SAMPLES = 120;
|
const OCR_MAX_SAMPLES = 180;
|
||||||
const OCR_CANVAS_MAX = 768;
|
const OCR_CANVAS_MAX = 768;
|
||||||
const DUPLICATE_BATCH_SIZE = 500;
|
const DUPLICATE_BATCH_SIZE = 500;
|
||||||
|
|
||||||
@@ -751,6 +751,11 @@
|
|||||||
return window.uploadPreview.ocrWorker;
|
return window.uploadPreview.ocrWorker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForPaint() {
|
||||||
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||||
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||||
|
}
|
||||||
|
|
||||||
function getOcrPhiReasons(text) {
|
function getOcrPhiReasons(text) {
|
||||||
const value = (text || "").toLowerCase();
|
const value = (text || "").toLowerCase();
|
||||||
const reasons = [];
|
const reasons = [];
|
||||||
@@ -787,19 +792,15 @@
|
|||||||
const samples = [];
|
const samples = [];
|
||||||
for (const seriesRecords of Object.values(seriesMap)) {
|
for (const seriesRecords of Object.values(seriesMap)) {
|
||||||
if (!seriesRecords.length) continue;
|
if (!seriesRecords.length) continue;
|
||||||
|
const sampleCount = Math.min(OCR_SERIES_SAMPLE_COUNT, seriesRecords.length);
|
||||||
const candidateIndexes = [
|
|
||||||
0,
|
|
||||||
Math.floor((seriesRecords.length - 1) / 2),
|
|
||||||
seriesRecords.length - 1,
|
|
||||||
];
|
|
||||||
|
|
||||||
const used = new Set();
|
const used = new Set();
|
||||||
for (const index of candidateIndexes) {
|
for (let i = 0; i < sampleCount; i++) {
|
||||||
|
const index = sampleCount === 1
|
||||||
|
? 0
|
||||||
|
: Math.floor((i * (seriesRecords.length - 1)) / (sampleCount - 1));
|
||||||
if (used.has(index)) continue;
|
if (used.has(index)) continue;
|
||||||
used.add(index);
|
used.add(index);
|
||||||
samples.push(seriesRecords[index]);
|
samples.push(seriesRecords[index]);
|
||||||
if (used.size >= OCR_SERIES_SAMPLE_COUNT) break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,6 +874,7 @@
|
|||||||
cornerstone.enable(element);
|
cornerstone.enable(element);
|
||||||
const image = await cornerstone.loadAndCacheImage(imageId);
|
const image = await cornerstone.loadAndCacheImage(imageId);
|
||||||
cornerstone.displayImage(element, image);
|
cornerstone.displayImage(element, image);
|
||||||
|
await waitForPaint();
|
||||||
|
|
||||||
const sourceCanvas = element.querySelector("canvas");
|
const sourceCanvas = element.querySelector("canvas");
|
||||||
if (!sourceCanvas) throw new Error("No canvas");
|
if (!sourceCanvas) throw new Error("No canvas");
|
||||||
@@ -894,6 +896,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildOcrCandidateCanvases(baseCanvas, enhancedCanvas) {
|
||||||
|
const candidates = [
|
||||||
|
{ name: "full", canvas: baseCanvas },
|
||||||
|
{ name: "full-enhanced", canvas: enhancedCanvas },
|
||||||
|
];
|
||||||
|
|
||||||
|
const makeCrop = (src, sx, sy, sw, sh) => {
|
||||||
|
const c = document.createElement("canvas");
|
||||||
|
c.width = Math.max(1, sw);
|
||||||
|
c.height = Math.max(1, sh);
|
||||||
|
const ctx = c.getContext("2d", { alpha: false });
|
||||||
|
ctx.drawImage(src, sx, sy, sw, sh, 0, 0, c.width, c.height);
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addRegions = (src, suffix) => {
|
||||||
|
const w = src.width;
|
||||||
|
const h = src.height;
|
||||||
|
const headerH = Math.max(24, Math.floor(h * 0.23));
|
||||||
|
const footerH = Math.max(24, Math.floor(h * 0.18));
|
||||||
|
const cornerW = Math.max(32, Math.floor(w * 0.45));
|
||||||
|
|
||||||
|
candidates.push({ name: `top-left${suffix}`, canvas: makeCrop(src, 0, 0, cornerW, headerH) });
|
||||||
|
candidates.push({ name: `top-right${suffix}`, canvas: makeCrop(src, Math.max(0, w - cornerW), 0, cornerW, headerH) });
|
||||||
|
candidates.push({ name: `bottom-left${suffix}`, canvas: makeCrop(src, 0, Math.max(0, h - footerH), cornerW, footerH) });
|
||||||
|
candidates.push({ name: `bottom-right${suffix}`, canvas: makeCrop(src, Math.max(0, w - cornerW), Math.max(0, h - footerH), cornerW, footerH) });
|
||||||
|
candidates.push({ name: `header-strip${suffix}`, canvas: makeCrop(src, 0, 0, w, headerH) });
|
||||||
|
candidates.push({ name: `footer-strip${suffix}`, canvas: makeCrop(src, 0, Math.max(0, h - footerH), w, footerH) });
|
||||||
|
};
|
||||||
|
|
||||||
|
addRegions(baseCanvas, "");
|
||||||
|
addRegions(enhancedCanvas, "-enhanced");
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
async function runOcrChecks() {
|
async function runOcrChecks() {
|
||||||
syncOcrDebugPanelVisibility();
|
syncOcrDebugPanelVisibility();
|
||||||
if (!document.getElementById("check-anonymisation-ocr").checked) {
|
if (!document.getElementById("check-anonymisation-ocr").checked) {
|
||||||
@@ -935,26 +972,37 @@
|
|||||||
setProgress(`OCR ${index} of ${samples.length}: ${record.path}`);
|
setProgress(`OCR ${index} of ${samples.length}: ${record.path}`);
|
||||||
try {
|
try {
|
||||||
const { outCanvas, enhancedCanvas } = await getDownscaledOcrCanvases(record.file);
|
const { outCanvas, enhancedCanvas } = await getDownscaledOcrCanvases(record.file);
|
||||||
const baseBlob = await canvasToBlob(outCanvas);
|
const ocrCandidates = buildOcrCandidateCanvases(outCanvas, enhancedCanvas);
|
||||||
const baseResult = await worker.recognize(baseBlob);
|
|
||||||
|
|
||||||
const baseText = normaliseOcrText(baseResult?.data?.text || "");
|
let bestText = "";
|
||||||
let bestText = baseText;
|
let bestConfidence = 0;
|
||||||
let bestConfidence = Number(baseResult?.data?.confidence || 0);
|
let bestCandidateName = "none";
|
||||||
|
let bestReasons = [];
|
||||||
|
|
||||||
// Fallback pass with high-contrast image when OCR output is weak/blank.
|
for (const candidate of ocrCandidates) {
|
||||||
if (bestText.length < 6) {
|
const blob = await canvasToBlob(candidate.canvas);
|
||||||
const enhancedBlob = await canvasToBlob(enhancedCanvas);
|
const result = await worker.recognize(blob);
|
||||||
const enhancedResult = await worker.recognize(enhancedBlob);
|
const text = normaliseOcrText(result?.data?.text || "");
|
||||||
const enhancedText = normaliseOcrText(enhancedResult?.data?.text || "");
|
const confidence = Number(result?.data?.confidence || 0);
|
||||||
const enhancedConfidence = Number(enhancedResult?.data?.confidence || 0);
|
const reasons = getOcrPhiReasons(text);
|
||||||
if (enhancedText.length > bestText.length || enhancedConfidence > bestConfidence) {
|
|
||||||
bestText = enhancedText;
|
const isBetter = reasons.length > bestReasons.length
|
||||||
bestConfidence = enhancedConfidence;
|
|| (reasons.length === bestReasons.length && text.length > bestText.length)
|
||||||
|
|| (reasons.length === bestReasons.length && text.length === bestText.length && confidence > bestConfidence);
|
||||||
|
|
||||||
|
if (isBetter) {
|
||||||
|
bestText = text;
|
||||||
|
bestConfidence = confidence;
|
||||||
|
bestCandidateName = candidate.name;
|
||||||
|
bestReasons = reasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reasons.length > 0 && confidence >= 40 && text.length >= 6) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reasons = getOcrPhiReasons(bestText);
|
const reasons = bestReasons;
|
||||||
const flagged = reasons.length > 0;
|
const flagged = reasons.length > 0;
|
||||||
if (flagged) {
|
if (flagged) {
|
||||||
flaggedSeriesUids.add(record.seriesUid);
|
flaggedSeriesUids.add(record.seriesUid);
|
||||||
@@ -966,7 +1014,7 @@
|
|||||||
path: record.path,
|
path: record.path,
|
||||||
flagged,
|
flagged,
|
||||||
reasons,
|
reasons,
|
||||||
preview: `${preview} (conf ${Math.round(bestConfidence)}%)`,
|
preview: `${preview} (region ${bestCandidateName}, 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user