// diagnose_fails.js — 诊断失败的试纸图片 const { createCanvas, loadImage } = require('canvas'); const { PaperRecognizer, PST, PST_NAMES, Img } = require('./paper-recognizer.js'); const fs = require('fs'); const path = require('path'); // Mirror the GP constants from paper-recognizer.js const GP = { DEFAULT_SAMPLE_RADIUS: 5, MIN_CONTOUR_AREA: 100, MAX_AREA_RATIO: 0.3, 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, COLOR_DISTANCE_THRESHOLD: 60, AREA_RATIO_SHORT: 3.33, AREA_RATIO_LONG: 5.0, AREA_ERROR_RATIO: 0.4 }; const LOCATOR_COLOR = { UNKNOWN:0, RED:1, GREEN:2, BLUE:3 }; const COLOR_NAMES = {1:'RED', 2:'GREEN', 3:'BLUE'}; function diagnose(fpath, fname) { 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); // Convert RGBA to BGR const bgr = new Uint8Array(w * h * 3); for (let i = 0; i < w * h; i++) { bgr[i * 3] = raw[i * 4 + 2]; // B bgr[i * 3 + 1] = raw[i * 4 + 1]; // G bgr[i * 3 + 2] = raw[i * 4]; // R } console.log('\n=== ' + fname + ' (' + w + 'x' + h + ') ==='); const RED_H = [0, 15], RED_H2 = [340, 360]; const GREEN_H = [80, 170]; const BLUE_H = [180, 260]; const MIN_S = 30, MIN_V = 20; const allCandidates = []; for (const color of [1, 2, 3]) { let hRange1, hRange2; if (color === 1) { hRange1 = RED_H; hRange2 = RED_H2; } else if (color === 2) { hRange1 = GREEN_H; hRange2 = null; } else { hRange1 = BLUE_H; hRange2 = null; } // HSV mask const mask = new Uint8Array(w * h); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const i = (y * w + x) * 3; const bb = bgr[i], gg = bgr[i + 1], rr = bgr[i + 2]; const r = rr / 255, g = gg / 255, b = bb / 255; const mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = mx - mn; let H = 0; if (d !== 0) { if (mx === r) H = 60 * (((g - b) / d) % 6); else if (mx === g) H = 60 * (((b - r) / d) + 2); else H = 60 * (((r - g) / d) + 4); } if (H < 0) H += 360; const S = mx === 0 ? 0 : (d / mx) * 100; const V = mx * 100; let ok = false; if (H >= hRange1[0] && H <= hRange1[1]) ok = S >= MIN_S && V >= MIN_V; if (!ok && hRange2 && H >= hRange2[0] && H <= hRange2[1]) ok = S >= MIN_S && V >= MIN_V; if (ok) mask[y * w + x] = 255; } } const closed = Img.morphClose(mask, w, h, 5); const contours = Img.findContours(closed, w, h); let colorContours = 0; 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); // square filter 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 tolBig = 0.35, tolSmall = 0.50; 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 [bb_c, gg_c, rr_c] = Img.getBgr(bgr, w, h, cx, cy); const [hh, ss, vv] = Img.bgrToHsv(bb_c, gg_c, rr_c); 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; allCandidates.push({ color, locColor, isBig, isSmall, area, x: cx, y: cy, ratio: aspectRatio, w: rotatedRect.size.w, h: rotatedRect.size.h, hsv: [hh.toFixed(1), ss.toFixed(1), vv.toFixed(1)] }); colorContours++; } } // Show all candidates console.log(' Candidates found: ' + allCandidates.length); for (const ct of allCandidates) { const cat = ct.isBig ? 'BIG' : 'SMALL'; const hsv = ct.hsv.join(','); console.log(' ' + COLOR_NAMES[ct.color] + ' [' + ct.x + ',' + ct.y + ' ' + ct.w.toFixed(0) + 'x' + ct.h.toFixed(0) + '] ratio=' + ct.ratio.toFixed(2) + ' area=' + ct.area + ' ' + cat + ' HSV(' + hsv + ')'); } // Group by color for (const color of [1, 2, 3]) { const cands = allCandidates.filter(c => c.color === color); const bigCands = cands.filter(c => c.isBig && c.area >= GP.MIN_BIG_LOCATOR_AREA && c.area <= GP.MAX_BIG_LOCATOR_AREA); const smallCands = cands.filter(c => c.isSmall && c.area >= GP.MIN_SMALL_LOCATOR_AREA && c.area <= GP.MAX_SMALL_LOCATOR_AREA); console.log(' ' + COLOR_NAMES[color] + ': bigs=' + bigCands.length + ' smalls=' + smallCands.length); if (bigCands.length === 0 && smallCands.length === 0 && cands.length > 0) { const filteredBig = cands.filter(c => c.isBig); const filteredSmall = cands.filter(c => c.isSmall); for (const b of filteredBig) console.log(' BIG filtered: area=' + b.area + ' x=' + b.x + ',y=' + b.y + ' (area range: ' + GP.MIN_BIG_LOCATOR_AREA + '-' + GP.MAX_BIG_LOCATOR_AREA + ')'); for (const s of filteredSmall) console.log(' SMALL filtered: area=' + s.area + ' x=' + s.x + ',y=' + s.y + ' (area range: ' + GP.MIN_SMALL_LOCATOR_AREA + '-' + GP.MAX_SMALL_LOCATOR_AREA + ')'); } // Pairing if (bigCands.length >= 1 && smallCands.length >= 1) { for (const big of bigCands) { for (const small of smallCands) { if (big === small) continue; const areaRatio = big.area / Math.max(small.area, 1); const dx = big.x - small.x, dy = big.y - small.y; const dist = Math.sqrt(dx * dx + dy * dy); const distRatio = dist / Math.max(Math.max(big.w, big.h), Math.max(small.w, small.h)); const bigType = areaRatio < (GP.AREA_RATIO_SHORT + GP.AREA_RATIO_LONG) / 2 ? 'SHORT' : 'LONG'; console.log(' PAIR: dist=' + dist.toFixed(0) + ' distRatio=' + distRatio.toFixed(2) + ' areaRatio=' + areaRatio.toFixed(2) + ' bigType=' + bigType); } } } } }); } (async () => { const dir = '/home/ubuntu/nanocode/web-opencv/cpp/images/'; const fails = [ '4b85f78723d00b4fb4ef06cc4ca6f1e8.jpg', '4fa2f133456bb7620482a400f9d06a65.jpg', '4fbbe9223faf5a2e1f3e6c85f6464af2.jpg', 'a8ec33379c878fc8350224bcfeaa7dcb.jpg', 'da753742af5c2b51a1fcb00437fda470.jpg', 'aec56e059994ac921d3bda5338f3a5d5.jpg', 'IMG_0541.JPG', 'IMG_0547.JPG', 'IMG_20190927_154351.png' ]; for (const f of fails) { try { const fpath = path.join(dir, f); await diagnose(fpath, f); } catch (e) { console.log('Error: ' + f + ': ' + e.message); } } })();