193 lines
7.8 KiB
JavaScript
193 lines
7.8 KiB
JavaScript
// 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);
|
|
}
|
|
}
|
|
})();
|