/** * diagnose.js — 诊断工具:显示中间处理步骤,帮助调参 * 用法: node diagnose.js */ 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 '); console.log('示例: node diagnose.js /home/ubuntu/nanocode/web-opencv/cpp/images/14red_1.png'); process.exit(1); } await diagnose(imagePath); } main();