223 lines
8.4 KiB
JavaScript
223 lines
8.4 KiB
JavaScript
/**
|
||
* diagnose.js — 诊断工具:显示中间处理步骤,帮助调参
|
||
* 用法: node diagnose.js <image_path>
|
||
*/
|
||
|
||
const { createCanvas, loadImage } = require('canvas');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const { Img, PaperRecognizer, PaperSpatialType, LocatorColor, PARAMS } = require('./paper-recognizer.js');
|
||
|
||
const OUTPUT_DIR = '/home/ubuntu/nanocode/web-opencv/miniappB/samples/';
|
||
|
||
function saveDebugImage(label, data, w, h, isGray = false) {
|
||
const canvas = createCanvas(w, h);
|
||
const ctx = canvas.getContext('2d');
|
||
const imgData = ctx.createImageData(w, h);
|
||
for (let i = 0; i < w * h; i++) {
|
||
const val = data[i];
|
||
imgData.data[i * 4] = isGray ? val : val;
|
||
imgData.data[i * 4 + 1] = isGray ? val : val;
|
||
imgData.data[i * 4 + 2] = isGray ? val : val;
|
||
imgData.data[i * 4 + 3] = 255;
|
||
}
|
||
ctx.putImageData(imgData, 0, 0);
|
||
const outPath = path.join(OUTPUT_DIR, label + '.png');
|
||
fs.writeFileSync(outPath, canvas.toBuffer('image/png'));
|
||
console.log(` [debug] ${label} → ${outPath}`);
|
||
}
|
||
|
||
async function diagnose(imagePath) {
|
||
const fname = path.basename(imagePath);
|
||
console.log(`诊断: ${fname}`);
|
||
|
||
const img = await loadImage(imagePath);
|
||
const w = img.width, h = img.height;
|
||
console.log(`尺寸: ${w}×${h}`);
|
||
|
||
const canvas = createCanvas(w, h);
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.drawImage(img, 0, 0);
|
||
const imageData = ctx.getImageData(0, 0, w, h);
|
||
const rgba = new Uint8Array(imageData.data.buffer);
|
||
|
||
// ---- 预处理 ----
|
||
const bgr = new Float64Array(w * h * 3);
|
||
const hsvArr = new Float64Array(w * h * 3);
|
||
for (let y = 0; y < h; y++) {
|
||
for (let x = 0; x < w; x++) {
|
||
const i4 = (y * w + x) * 4;
|
||
const i3 = (y * w + x) * 3;
|
||
const r = rgba[i4], g = rgba[i4 + 1], b = rgba[i4 + 2];
|
||
bgr[i3] = b; bgr[i3 + 1] = g; bgr[i3 + 2] = r;
|
||
const [hh, ss, vv] = Img.rgbToHsv(r, g, b);
|
||
hsvArr[i3] = hh; hsvArr[i3 + 1] = ss; hsvArr[i3 + 2] = vv;
|
||
}
|
||
}
|
||
|
||
// ---- HSV 统计 ----
|
||
// Sample center of image to understand typical HSV values
|
||
const cx = Math.floor(w / 2), cy = Math.floor(h / 2);
|
||
const sampleSize = 50;
|
||
let hSum = 0, sSum = 0, vSum = 0, count = 0;
|
||
console.log(`\n 中心区域(${sampleSize}×${sampleSize}) HSV 采样:`);
|
||
const colorBins = { red: 0, green: 0, blue: 0, other: 0 };
|
||
for (let dy = -sampleSize / 2; dy < sampleSize / 2; dy += 2) {
|
||
for (let dx = -sampleSize / 2; dx < sampleSize / 2; dx += 2) {
|
||
const sx = cx + dx, sy = cy + dy;
|
||
if (sx < 0 || sx >= w || sy < 0 || sy >= h) continue;
|
||
const i3 = (sy * w + sx) * 3;
|
||
hSum += hsvArr[i3]; sSum += hsvArr[i3 + 1]; vSum += hsvArr[i3 + 2];
|
||
count++;
|
||
// Classify
|
||
const hue = hsvArr[i3];
|
||
if ((hue >= 0 && hue <= 10) || hue >= 160) colorBins.red++;
|
||
else if (hue >= 35 && hue <= 85) colorBins.green++;
|
||
else if (hue >= 90 && hue <= 150) colorBins.blue++;
|
||
else colorBins.other++;
|
||
}
|
||
}
|
||
console.log(` 平均 HSV: H=${(hSum/count).toFixed(1)} S=${(sSum/count).toFixed(1)} V=${(vSum/count).toFixed(1)}`);
|
||
const total = colorBins.red + colorBins.green + colorBins.blue + colorBins.other;
|
||
console.log(` 色相分布: 红=${(colorBins.red/total*100).toFixed(1)}% 绿=${(colorBins.green/total*100).toFixed(1)}% 蓝=${(colorBins.blue/total*100).toFixed(1)}% 其他=${(colorBins.other/total*100).toFixed(1)}%`);
|
||
|
||
// ---- 颜色分割 ----
|
||
const redMask1 = Img.hsvInRange(hsvArr, w, h, 0, 10, 70, 255, 50, 255);
|
||
const redMask2 = Img.hsvInRange(hsvArr, w, h, 160, 180, 70, 255, 50, 255);
|
||
const redMask = new Uint8Array(w * h);
|
||
const greenMask = Img.hsvInRange(hsvArr, w, h, 35, 85, 70, 255, 50, 255);
|
||
const blueMask = Img.hsvInRange(hsvArr, w, h, 90, 150, 50, 255, 10, 120);
|
||
|
||
let redCount = 0, greenCount = 0, blueCount = 0;
|
||
const colorMask = new Uint8Array(w * h);
|
||
for (let i = 0; i < w * h; i++) {
|
||
const rv = Math.max(redMask1[i], redMask2[i]);
|
||
redMask[i] = rv;
|
||
colorMask[i] = Math.max(rv, greenMask[i], blueMask[i]);
|
||
if (rv > 0) redCount++;
|
||
if (greenMask[i] > 0) greenCount++;
|
||
if (blueMask[i] > 0) blueCount++;
|
||
}
|
||
|
||
const totalPixels = w * h;
|
||
console.log(`\n 颜色分割 (像素占比):`);
|
||
console.log(` 红色像素: ${redCount} (${(redCount/totalPixels*100).toFixed(2)}%)`);
|
||
console.log(` 绿色像素: ${greenCount} (${(greenCount/totalPixels*100).toFixed(2)}%)`);
|
||
console.log(` 蓝色像素: ${blueCount} (${(blueCount/totalPixels*100).toFixed(2)}%)`);
|
||
|
||
saveDebugImage('01_colorMask', colorMask, w, h, true);
|
||
|
||
// ---- 形态学 ----
|
||
let mask = Img.morphClose(colorMask, w, h, 5);
|
||
saveDebugImage('02_morph', mask, w, h, true);
|
||
|
||
// ---- 轮廓提取 ----
|
||
const contours = Img.findContours(mask, w, h);
|
||
console.log(`\n 检测到 ${contours.length} 个轮廓`);
|
||
|
||
const imageArea = w * h;
|
||
let passed = 0;
|
||
const candidates = [];
|
||
for (const contour of contours) {
|
||
const area = Img.contourArea(contour);
|
||
if (area < PARAMS.MIN_CONTOUR_AREA) continue;
|
||
if (area > imageArea * PARAMS.MAX_AREA_RATIO) continue;
|
||
|
||
const rect = Img.minAreaRect(contour);
|
||
const ww = Math.max(rect.size.width, rect.size.height);
|
||
const hh = Math.min(rect.size.width, rect.size.height);
|
||
if (hh < 1) continue;
|
||
const aspectRatio = ww / hh;
|
||
|
||
// Square filter
|
||
if (aspectRatio >= 0.9 && aspectRatio <= 1.1) continue;
|
||
if (aspectRatio > 4.0) continue;
|
||
|
||
const tolerance = 0.35;
|
||
const isBigRatio = Math.abs(aspectRatio - PARAMS.BIG_LOCATOR_RATIO_SHORT) < tolerance ||
|
||
Math.abs(aspectRatio - PARAMS.BIG_LOCATOR_RATIO_LONG) < tolerance;
|
||
const isSmallRatio = Math.abs(aspectRatio - PARAMS.SMALL_LOCATOR_RATIO) < tolerance;
|
||
|
||
const rectArea = rect.size.width * rect.size.height;
|
||
const rectangularity = area / rectArea;
|
||
|
||
console.log(` #${passed} area=${area.toFixed(0)} ratio=${aspectRatio.toFixed(2)} rectangularity=${rectangularity.toFixed(2)} ` +
|
||
`center=(${rect.center.x.toFixed(0)},${rect.center.y.toFixed(0)}) ` +
|
||
`isBig=${isBigRatio} isSmall=${isSmallRatio} size=(${ww.toFixed(0)}×${hh.toFixed(0)})`);
|
||
|
||
if (!isBigRatio && !isSmallRatio) {
|
||
console.log(` → 跳过: 宽高比不匹配`);
|
||
continue;
|
||
}
|
||
if (rectangularity < PARAMS.MIN_RECTANGULARITY) {
|
||
console.log(` → 跳过: 矩形度不足`);
|
||
continue;
|
||
}
|
||
|
||
const isBig = isBigRatio;
|
||
if (isBig) {
|
||
if (area < PARAMS.MIN_BIG_LOCATOR_AREA) {
|
||
console.log(` → 跳过: 大定位块面积太小`);
|
||
continue;
|
||
}
|
||
if (area > PARAMS.MAX_BIG_LOCATOR_AREA) {
|
||
console.log(` → 跳过: 大定位块面积太大`);
|
||
continue;
|
||
}
|
||
} else {
|
||
if (area < PARAMS.MIN_SMALL_LOCATOR_AREA) {
|
||
console.log(` → 跳过: 小定位块面积太小`);
|
||
continue;
|
||
}
|
||
if (area > PARAMS.MAX_SMALL_LOCATOR_AREA) {
|
||
console.log(` → 跳过: 小定位块面积太大`);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
console.log(` ✓ 通过筛选`);
|
||
passed++;
|
||
candidates.push({ contour, area, rect, isBig });
|
||
}
|
||
|
||
console.log(`\n ${passed} 个通过筛选的候选者`);
|
||
|
||
// Also try edge detection fallback
|
||
if (contours.length < 2) {
|
||
console.log(`\n --- 备用: Canny边缘检测 ---`);
|
||
const gray = new Float64Array(w * h);
|
||
for (let i = 0; i < w * h; i++) {
|
||
const i4 = i * 4;
|
||
gray[i] = 0.299 * rgba[i4] + 0.587 * rgba[i4 + 1] + 0.114 * rgba[i4 + 2];
|
||
}
|
||
const blur = Img.gaussianBlur5(gray, w, h);
|
||
const edges = Img.canny(blur, w, h, 50, 150);
|
||
saveDebugImage('03_edges', edges, w, h, true);
|
||
|
||
const closed = Img.close(edges, w, h, 9);
|
||
saveDebugImage('04_edges_closed', closed, w, h, true);
|
||
|
||
const econtours = Img.findContours(closed, w, h);
|
||
console.log(` 边缘检测到 ${econtours.length} 个轮廓`);
|
||
|
||
for (let i = 0; i < Math.min(econtours.length, 10); i++) {
|
||
const contour = econtours[i];
|
||
const area = Img.contourArea(contour);
|
||
const rect = Img.minAreaRect(contour);
|
||
console.log(` #${i} area=${area.toFixed(0)} center=(${rect.center.x.toFixed(0)},${rect.center.y.toFixed(0)}) size=(${rect.size.width.toFixed(0)}×${rect.size.height.toFixed(0)})`);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function main() {
|
||
const imagePath = process.argv[2];
|
||
if (!imagePath) {
|
||
console.log('用法: node diagnose.js <image_path>');
|
||
console.log('示例: node diagnose.js /home/ubuntu/nanocode/web-opencv/cpp/images/14red_1.png');
|
||
process.exit(1);
|
||
}
|
||
await diagnose(imagePath);
|
||
}
|
||
|
||
main();
|