// 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); } } })();