Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

230 lines
9.8 KiB
JavaScript

// verify_all.js - verify all images and show distRatio/areaRatio details
const { createCanvas, loadImage } = require('canvas');
const { Img, PST_NAMES } = require('./paper-recognizer.js');
const fs = require('fs');
const path = require('path');
const GP = {
MIN_CONTOUR_AREA: 100,
MIN_RECTANGULARITY: 0.5,
BIG_LOCATOR_RATIO_SHORT: 2.0,
BIG_LOCATOR_RATIO_LONG: 3.0,
SMALL_LOCATOR_RATIO: 1.667,
MIN_BIG_LOCATOR_AREA: 200,
MAX_BIG_LOCATOR_AREA: 8000,
MIN_SMALL_LOCATOR_AREA: 50,
MAX_SMALL_LOCATOR_AREA: 2000,
AREA_RATIO_SHORT: 3.33,
AREA_RATIO_LONG: 5.0,
};
function matchConfig(distRatio, areaRatio, bigType) {
const Min_AR_S = 2.6, Max_AR_S = 4.0, Min_AR_L = 4.0, Max_AR_L = 6.0;
if (areaRatio < Min_AR_S || areaRatio > Max_AR_L) return -1;
const isSmall = areaRatio <= Max_AR_S;
if (isSmall) {
if (distRatio >= 18.5 && distRatio <= 26.5) return 3; // 14_S
if (distRatio >= 14.1 && distRatio < 18.5) return 5; // 11_S
if (distRatio >= 11.6 && distRatio < 14.1) return 7; // 8_S
} else {
if (distRatio >= 18.5 && distRatio <= 26.98) return 4; // 14_L
if (distRatio >= 14.5 && distRatio < 18.5) return 6; // 11_L
if (distRatio >= 12.0 && distRatio < 14.5) return 8; // 8_L
}
return -1;
}
// Relaxed area limits for high-res images
function getAreaLimits(w, h) {
const scale = Math.min(w, h) / 100; // scale relative to typical strip width ~100px
return {
minBig: GP.MIN_BIG_LOCATOR_AREA,
maxBig: Math.max(GP.MAX_BIG_LOCATOR_AREA, 200 * scale * scale),
minSmall: GP.MIN_SMALL_LOCATOR_AREA,
maxSmall: Math.max(GP.MAX_SMALL_LOCATOR_AREA, 50 * scale * scale)
};
}
// Wider ratio tolerances for narrow strips
function getRatioTols(w, h) {
const minDim = Math.min(w, h);
if (minDim < 80) return { tolBig: 0.50, tolSmall: 0.70 };
return { tolBig: 0.35, tolSmall: 0.50 };
}
function trace(fpath, fname, useRelaxed = false) {
return loadImage(fpath).then(img => {
const c = createCanvas(img.width, img.height);
const ctx = c.getContext('2d');
ctx.drawImage(img, 0, 0);
const id = ctx.getImageData(0, 0, img.width, img.height);
const w = img.width, h = img.height;
const raw = new Uint8Array(id.data.buffer);
const bgr = new Uint8Array(w * h * 3);
for (let i = 0; i < w * h; i++) {
bgr[i * 3] = raw[i * 4 + 2];
bgr[i * 3 + 1] = raw[i * 4 + 1];
bgr[i * 3 + 2] = raw[i * 4];
}
const { tolBig, tolSmall } = getRatioTols(w, h);
const limits = getAreaLimits(w, h);
// HSV masks
const hsv = Img.bgrImageToHsv(bgr, w, h);
// Tighter blue thresholds to reduce false positives
const sMinB = useRelaxed ? 40 : 30, vMinB = useRelaxed ? 40 : 20;
const blueMask = Img.hsvInRange(hsv, w, h, 180, 260, sMinB, 255, vMinB, 220);
const greenMask = Img.hsvInRange(hsv, w, h, 80, 170, 30, 255, 30, 220);
const redMask1 = Img.hsvInRange(hsv, w, h, 0, 15, 30, 255, 30, 220);
const redMask2 = Img.hsvInRange(hsv, w, h, 340, 360, 30, 255, 30, 220);
const redMask = new Uint8Array(w * h);
for (let i = 0; i < w * h; i++) redMask[i] = Math.max(redMask1[i], redMask2[i]);
const colorMask = new Uint8Array(w * h);
for (let i = 0; i < w * h; i++) {
colorMask[i] = Math.max(blueMask[i], greenMask[i], redMask[i]);
}
// Smaller morph close for narrow strips
const morphKernel = Math.min(w, h) < 80 ? 3 : 5;
const closed = Img.morphClose(colorMask, w, h, morphKernel);
const contours = Img.findContours(closed, w, h);
const candidates = [];
for (const ct of contours) {
const area = Img.contourArea(ct);
if (area < GP.MIN_CONTOUR_AREA) continue;
const brect = Img.boundingRect(ct);
const bbArea = brect.w * brect.h;
if (area / bbArea < GP.MIN_RECTANGULARITY) continue;
const rotatedRect = Img.minAreaRect(ct);
const rectArea = rotatedRect.size.w * rotatedRect.size.h;
if (area / rectArea < GP.MIN_RECTANGULARITY) continue;
const aspectRatio = Math.max(rotatedRect.size.w, rotatedRect.size.h) /
Math.max(Math.min(rotatedRect.size.w, rotatedRect.size.h), 1);
if (aspectRatio >= 0.9 && aspectRatio <= 1.1) continue;
if (aspectRatio > 4.0) continue;
const distToBigShort = Math.abs(aspectRatio - GP.BIG_LOCATOR_RATIO_SHORT);
const distToBigLong = Math.abs(aspectRatio - GP.BIG_LOCATOR_RATIO_LONG);
const distToSmall = Math.abs(aspectRatio - GP.SMALL_LOCATOR_RATIO);
const nearestBigDist = Math.min(distToBigShort, distToBigLong);
const isBig = nearestBigDist < tolBig && nearestBigDist < distToSmall;
const isSmall = distToSmall < tolSmall && distToSmall <= nearestBigDist;
if (!isBig && !isSmall) continue;
const center = rotatedRect.center;
const cx = Math.round(center.x), cy = Math.round(center.y);
const [b_, g_, r_] = Img.getBgr(bgr, w, h, cx, cy);
const [hh, ss, vv] = Img.bgrToHsv(b_, g_, r_);
let locColor = 0;
if (hh >= 180 && hh <= 260) locColor = 3;
else if (hh >= 80 && hh <= 170) locColor = 2;
else if (hh <= 15 || hh >= 340) locColor = 1;
candidates.push({
center: {x: center.x, y: center.y}, area,
width: rotatedRect.size.w, height: rotatedRect.size.h,
color: locColor, isBig, isSmall, aspectRatio,
contour: ct
});
}
const bigCands = candidates.filter(c => c.isBig && c.area >= limits.minBig && c.area <= limits.maxBig);
const smallCands = candidates.filter(c => c.isSmall && c.area >= limits.minSmall && c.area <= limits.maxSmall);
// Fallback: use largest-small as big or smallest-big as small
if (bigCands.length === 0 || smallCands.length === 0) {
// Strategy 1: two bigs → smallest as small
if (bigCands.length >= 2 && smallCands.length === 0) {
const sortedBigs = [...bigCands].sort((a, b) => a.area - b.area);
for (const big of sortedBigs) {
for (const small of sortedBigs) {
if (big === small) continue;
if (big.area <= small.area) continue;
const ar = big.area / Math.max(small.area, 1);
if (ar < 1.5 || ar > 8.0) continue;
smallCands.push(small);
}
}
}
// Strategy 2: two smalls → largest as big
if (bigCands.length === 0 && smallCands.length >= 2) {
const sortedSmalls = [...smallCands].sort((a, b) => b.area - a.area);
// largest small becomes a pseudo-big
if (sortedSmalls[0].area > sortedSmalls[1].area * 1.5) {
bigCands.push(sortedSmalls[0]);
}
}
}
if (bigCands.length === 0 || smallCands.length === 0) {
console.log(fname.padEnd(20) + ' | -3 NO LOC (b=' + bigCands.length + ' s=' + smallCands.length + ' cand=' + candidates.length + ')');
return;
}
// Pairing
let bestPair = null, bestScore = Infinity;
for (const big of bigCands) {
for (const small of smallCands) {
if (big === small) continue;
const areaRatio = big.area / Math.max(small.area, 1);
// Allow inverted area ratio (big could be smaller if locators are reversed)
const invAreaRatio = small.area / Math.max(big.area, 1);
const yDiff = Math.abs(big.center.y - small.center.y);
const colorMatch = big.color === small.color && big.color !== 0;
// Score: prefer color match, prefer areaRatio ~4.0 OR invAreaRatio ~4.0
const arScore = Math.min(Math.abs(areaRatio - 4.0), Math.abs(invAreaRatio - 4.0));
const score = arScore + yDiff * 0.1 + (colorMatch ? 0 : 5);
// Cross-color pairs need to be far apart and on same y
if (!colorMatch)
console.log(' X-COLOR: b=' + ['?','R','G','B'][big.color] + ' s=' + ['?','R','G','B'][small.color] + ' ar=' + areaRatio.toFixed(2) + ' dist=' + Math.abs(big.center.x - small.center.x).toFixed(0));
if (score < bestScore) {
bestScore = score;
bestPair = { big, small, areaRatio, invAreaRatio, colorMatch };
}
}
}
if (!bestPair) {
console.log(fname.padEnd(20) + ' | -4 NO PAIR');
return;
}
const { big: bigLoc_, small: smallLoc_, areaRatio: areaRatio2, invAreaRatio, colorMatch } = bestPair;
// Check if area ratio is inverted - use whichever gives better areaRatio
let useAreaRatio = areaRatio2;
if (invAreaRatio > areaRatio2 && invAreaRatio >= 2.6 && invAreaRatio <= 6.0) {
useAreaRatio = invAreaRatio;
}
const smallOnLeft = smallLoc_.center.x < bigLoc_.center.x;
const loc1 = smallOnLeft ? smallLoc_ : bigLoc_;
const loc2 = smallOnLeft ? bigLoc_ : smallLoc_;
const dx = loc1.center.x - loc2.center.x;
const dy = loc1.center.y - loc2.center.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const loc1Half = Math.max(loc1.width, loc1.height) / 2;
const loc2Half = Math.max(loc2.width, loc2.height) / 2;
const edgeDist = dist - loc1Half - loc2Half;
const refH = Math.max(loc1.height, loc2.height);
const distRatio = edgeDist / Math.max(refH, 1);
const bigType = useAreaRatio < (GP.AREA_RATIO_SHORT + GP.AREA_RATIO_LONG) / 2 ? 'SHORT' : 'LONG';
const paperType = matchConfig(distRatio, useAreaRatio, bigType);
const pName = paperType > 0 ? PST_NAMES[paperType] : 'UNK';
console.log(fname.padEnd(20) + ' | ' + pName.padEnd(6) + ' | dr=' + distRatio.toFixed(2) + ' ar=' + useAreaRatio.toFixed(2) + ' ' + bigType + ' | c=' + ['?','R','G','B'][bigLoc_.color] + (colorMatch?'':'✗') + ' | inv=' + (areaRatio2 < 1 ? 'YES' : 'no'));
});
}
// Test with relaxed parameters
(async () => {
const dir = '/home/ubuntu/nanocode/web-opencv/cpp/images/';
const files = fs.readdirSync(dir).filter(f => /\.(jpg|jpeg|png|JPG|PNG)$/i.test(f)).sort();
console.log('=== With RELAXED parameters ===');
for (const f of files) {
try { await trace(path.join(dir, f), f, true); } catch (e) { console.log(f + ' ERROR: ' + e.message); }
}
})();