230 lines
9.8 KiB
JavaScript
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); }
|
|
}
|
|
})();
|