pcb-retrace/docs/cv-core.js

180 lines
5 KiB
JavaScript

/*
* Copyright (c) 2025-2026 Taras Greben
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
* See LICENSE file for details.
*/
/* cv-core.js - Computer Vision Logic for PCB ReTrace Suite */
class CVManager {
constructor() {
this.ready = false;
this.initAttempts = 0;
this.detector = null;
this.matcher = null;
}
async init() {
if (this.ready) return;
if (typeof cv !== 'undefined' && cv.Mat) {
try {
// Tuned Settings: ORB 5000, Hamming
this.detector = new cv.ORB(5000);
this.matcher = new cv.BFMatcher(cv.NORM_HAMMING, false);
this.ready = true;
console.log("CV Core Initialized");
} catch (e) {
console.error("CV Init Error:", e);
}
} else {
this.initAttempts++;
if (this.initAttempts < 50) setTimeout(() => this.init(), 500);
else console.error("CV Failed to load OpenCV.js");
}
}
// Helper: Project (x,y) using Homography Matrix
projectPoint(x, y, h) {
if (!h || h.length < 9) return null;
const Z = h[6] * x + h[7] * y + h[8];
if (Math.abs(Z) < 0.0001) return null;
return {
x: (h[0] * x + h[1] * y + h[2]) / Z,
y: (h[3] * x + h[4] * y + h[5]) / Z
};
}
// Helper: Resize for processing (Speed optimization)
async createSmallMat(blob) {
const maxWidth = 2000; // Tuned for optimal accuracy/speed
const bmp = await createImageBitmap(blob);
const canvas = document.createElement('canvas');
const scale = Math.min(1, maxWidth / bmp.width);
canvas.width = bmp.width * scale;
canvas.height = bmp.height * scale;
const ctx = canvas.getContext('2d');
ctx.drawImage(bmp, 0, 0, canvas.width, canvas.height);
const mat = cv.imread(canvas);
const gray = new cv.Mat();
cv.cvtColor(mat, gray, cv.COLOR_RGBA2GRAY);
mat.delete();
// Cleanup canvas to free memory
canvas.width = 0; canvas.height = 0;
return { mat: gray, scale: scale, width: bmp.width, height: bmp.height };
}
// Solve Manual Stitch (Least Squares)
solveManual(pairs) {
if (pairs.length < 4) return null;
const srcMat = new cv.Mat(pairs.length, 1, cv.CV_32FC2);
const dstMat = new cv.Mat(pairs.length, 1, cv.CV_32FC2);
for (let i = 0; i < pairs.length; i++) {
srcMat.data32F[i * 2] = pairs[i].s.x;
srcMat.data32F[i * 2 + 1] = pairs[i].s.y;
dstMat.data32F[i * 2] = pairs[i].d.x;
dstMat.data32F[i * 2 + 1] = pairs[i].d.y;
}
// Method 0 = Least Squares (Best for manual precise points)
const H = cv.findHomography(srcMat, dstMat, 0);
if (H.empty()) {
srcMat.delete(); dstMat.delete(); H.delete();
return null;
}
const hd = [];
const Hi = H.inv(cv.DECOMP_LU);
const ihd = [];
for (let i = 0; i < 9; i++) hd.push(H.data64F[i]);
for (let i = 0; i < 9; i++) ihd.push(Hi.data64F[i]);
srcMat.delete(); dstMat.delete(); H.delete(); Hi.delete();
return { hData: hd, invHData: ihd };
}
// Extract Features
async feats(blob) {
if (!this.ready) return null;
const { mat, scale, width, height } = await this.createSmallMat(blob);
const kp = new cv.KeyPointVector();
const des = new cv.Mat();
const mask = new cv.Mat();
this.detector.detectAndCompute(mat, mask, kp, des);
mask.delete();
mat.delete();
return { kp, des, scale, w: width, h: height };
}
// Chain two homographies: H_total = H2 * H1
multiplyH(H2, H1) {
// H1 and H2 are flat arrays [0..8] representing 3x3 matrices
// Standard Matrix Multiplication
const res = new Array(9);
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
let sum = 0;
for (let k = 0; k < 3; k++) {
sum += H2[r*3 + k] * H1[k*3 + c];
}
res[r*3 + c] = sum;
}
}
return res;
}
// Find Homography between two feature sets
findH(s, d) {
if (s.des.rows === 0 || d.des.rows === 0) return null;
const mv = new cv.DMatchVectorVector();
this.matcher.knnMatch(s.des, d.des, mv, 2);
const good = [];
for (let i = 0; i < mv.size(); i++) {
const m = mv.get(i).get(0), n = mv.get(i).get(1);
if (m.distance < 0.70 * n.distance) good.push(m); // Ratio Test 0.70
}
mv.delete();
if (good.length > 8) {
const sp = new cv.Mat(good.length, 1, cv.CV_32FC2);
const dp = new cv.Mat(good.length, 1, cv.CV_32FC2);
for (let i = 0; i < good.length; i++) {
const k1 = s.kp.get(good[i].queryIdx);
const k2 = d.kp.get(good[i].trainIdx);
sp.data32F[i * 2] = k1.pt.x / s.scale;
sp.data32F[i * 2 + 1] = k1.pt.y / s.scale;
dp.data32F[i * 2] = k2.pt.x / d.scale;
dp.data32F[i * 2 + 1] = k2.pt.y / d.scale;
}
const mask = new cv.Mat();
// RANSAC 8.0 (Tuned for 2000px resolution)
const H = cv.findHomography(sp, dp, cv.RANSAC, 8.0, mask);
if (H.empty()) {
sp.delete(); dp.delete(); mask.delete(); H.delete();
return null;
}
const hd = [];
const Hi = H.inv(cv.DECOMP_LU);
const ihd = [];
for (let i = 0; i < 9; i++) hd.push(H.data64F[i]);
for (let i = 0; i < 9; i++) ihd.push(Hi.data64F[i]);
sp.delete(); dp.delete(); mask.delete(); H.delete(); Hi.delete();
return { hData: hd, invHData: ihd, matches: good.length };
}
return null;
}
}