| Server IP : 217.154.3.148 / Your IP : 216.73.216.90 Web Server : nginx/1.30.3 System : Linux blissful-wright.217-154-3-148.plesk.page 6.8.0-124-generic #124-Ubuntu SMP PREEMPT_DYNAMIC Tue May 26 13:00:45 UTC 2026 x86_64 User : gracious-boyd_hfhh1h8vo9n ( 10000) PHP Version : 8.3.31 Disable Function : opcache_get_status MySQL : OFF | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /var/www/vhosts/gracious-boyd.217-154-3-148.plesk.page/nextjs/lib/ |
Upload File : |
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
export type Homography = number[][]; // 3x3 matrix
export type VP = { x: number; y: number };
let cvPromise: Promise<any> | null = null;
async function loadCvModule() {
const currentDir = path.dirname(fileURLToPath(import.meta.url));
const wasmTarget = path.join(currentDir, 'opencv.wasm');
const wasmSource = path.join(process.cwd(), 'node_modules', 'opencv-wasm', 'opencv.wasm');
try {
if (!fs.existsSync(wasmTarget) && fs.existsSync(wasmSource)) {
fs.copyFileSync(wasmSource, wasmTarget);
}
} catch (error) {
console.warn('opencv-wasm: unable to copy wasm binary', error);
}
const opencvModule = await import('opencv-wasm');
const cvExport = (opencvModule as any).cv ?? opencvModule;
if (cvExport && typeof cvExport.then === 'function') {
return cvExport;
}
return cvExport;
}
async function getCv() {
if (!cvPromise) {
cvPromise = loadCvModule();
}
return cvPromise;
}
function median(values: number[]): number {
if (!values.length) return 0;
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
}
function toGray(cv: any, src: any) {
const dst = new cv.Mat();
cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY);
return dst;
}
function canny(cv: any, srcGray: any, lo = 50, hi = 150) {
const edges = new cv.Mat();
cv.Canny(srcGray, edges, lo, hi);
return edges;
}
function houghP(cv: any, edge: any, rho = 1, theta = Math.PI / 180, thresh = 80, minLineLen = 40, maxGap = 8) {
const lines = new cv.Mat();
cv.HoughLinesP(edge, lines, rho, theta, thresh, minLineLen, maxGap);
const out: { x1: number; y1: number; x2: number; y2: number }[] = [];
const data = lines.data32S;
for (let i = 0; i < data.length; i += 4) {
out.push({
x1: data[i],
y1: data[i + 1],
x2: data[i + 2],
y2: data[i + 3],
});
}
lines.delete();
return out;
}
function angleOf(l: { x1: number; y1: number; x2: number; y2: number }) {
const dx = l.x2 - l.x1;
const dy = l.y2 - l.y1;
return Math.atan2(dy, dx);
}
function kmeansAngles(angles: number[]) {
if (angles.length < 2) {
return { a1: 0, a2: Math.PI / 2, g1: angles.map((_, idx) => idx), g2: [] as number[] };
}
const normAngles = angles.map((v) => ((v % Math.PI) + Math.PI) % Math.PI);
let c1 = normAngles[0];
let c2 = normAngles[1] ?? (normAngles[0] + Math.PI / 2);
for (let it = 0; it < 12; it += 1) {
const g1: number[] = [];
const g2: number[] = [];
normAngles.forEach((v, idx) => {
const d1 = Math.min(Math.abs(v - c1), Math.PI - Math.abs(v - c1));
const d2 = Math.min(Math.abs(v - c2), Math.PI - Math.abs(v - c2));
if (d1 <= d2) g1.push(idx);
else g2.push(idx);
});
const med = (idxs: number[]) => median(idxs.map((i) => normAngles[i]));
const nc1 = g1.length ? med(g1) : c1;
const nc2 = g2.length ? med(g2) : c2;
if (Math.abs(nc1 - c1) + Math.abs(nc2 - c2) < 1e-3) break;
c1 = nc1;
c2 = nc2;
}
const g1Idx: number[] = [];
const g2Idx: number[] = [];
normAngles.forEach((v, idx) => {
const d1 = Math.min(Math.abs(v - c1), Math.PI - Math.abs(v - c1));
const d2 = Math.min(Math.abs(v - c2), Math.PI - Math.abs(v - c2));
if (d1 <= d2) g1Idx.push(idx);
else g2Idx.push(idx);
});
return { a1: c1, a2: c2, g1: g1Idx, g2: g2Idx };
}
function lineFromSeg(l: { x1: number; y1: number; x2: number; y2: number }) {
const { x1, y1, x2, y2 } = l;
const a = y1 - y2;
const b = x2 - x1;
const c = x1 * y2 - x2 * y1;
return { a, b, c };
}
function intersect(l1: { a: number; b: number; c: number }, l2: { a: number; b: number; c: number }): VP | null {
const d = l1.a * l2.b - l2.a * l1.b;
if (Math.abs(d) < 1e-9) return null;
const x = (l2.c * l1.b - l1.c * l2.b) / d;
const y = (l1.c * l2.a - l2.c * l1.a) / d;
return { x, y };
}
function medianPt(pts: VP[]): VP {
if (!pts.length) return { x: 0, y: 0 };
const xs = pts.map((p) => p.x).sort((a, b) => a - b);
const ys = pts.map((p) => p.y).sort((a, b) => a - b);
const mx = xs.length % 2 ? xs[(xs.length - 1) / 2] : (xs[xs.length / 2 - 1] + xs[xs.length / 2]) / 2;
const my = ys.length % 2 ? ys[(ys.length - 1) / 2] : (ys[ys.length / 2 - 1] + ys[ys.length / 2]) / 2;
return { x: mx, y: my };
}
function homographyAffineRectify(linf: [number, number, number]): Homography {
const [l1, l2, l3] = linf;
const denom = l3 || 1e-9;
return [
[1, 0, 0],
[0, 1, 0],
[l1 / denom, l2 / denom, 1],
];
}
function invert33(H: Homography): Homography {
const a = H[0][0];
const b = H[0][1];
const c = H[0][2];
const d = H[1][0];
const e = H[1][1];
const f = H[1][2];
const g = H[2][0];
const h = H[2][1];
const i = H[2][2];
const A = e * i - f * h;
const B = -(d * i - f * g);
const C = d * h - e * g;
const D = -(b * i - c * h);
const E = a * i - c * g;
const F = -(a * h - b * g);
const G = b * f - c * e;
const Hh = -(a * f - b * d);
const I = a * e - b * d;
const det = a * A + b * B + c * C || 1e-9;
return [
[A / det, D / det, G / det],
[B / det, E / det, Hh / det],
[C / det, F / det, I / det],
];
}
function applyHToPoint(H: Homography, x: number, y: number) {
const X = H[0][0] * x + H[0][1] * y + H[0][2];
const Y = H[1][0] * x + H[1][1] * y + H[1][2];
const W = H[2][0] * x + H[2][1] * y + H[2][2];
const denom = W || 1e-9;
return { x: X / denom, y: Y / denom };
}
function warpPerspectiveRGBA(cv: any, src: any, H: Homography, width: number, height: number) {
const dst = new cv.Mat();
const matH = cv.matFromArray(3, 3, cv.CV_64F, H.flat());
const size = new cv.Size(width, height);
cv.warpPerspective(src, dst, matH, size, cv.INTER_LINEAR, cv.BORDER_REPLICATE, new cv.Scalar());
matH.delete();
return dst;
}
export async function rectifyImageWasm(
rgba: Uint8Array,
width: number,
height: number,
): Promise<{
rectRGBA: Uint8ClampedArray;
rectW: number;
rectH: number;
H: Homography;
Hinv: Homography;
v1: VP;
v2: VP;
}> {
const cv = await getCv();
const { ImageData } = await import('canvas');
const sourceImage = new ImageData(new Uint8ClampedArray(rgba), width, height);
const src = cv.matFromImageData(sourceImage);
const gray = toGray(cv, src);
const edges = canny(cv, gray);
const segs = houghP(cv, edges);
if (segs.length < 2) {
src.delete();
gray.delete();
edges.delete();
throw new Error('insufficient line segments for perspective rectification');
}
const angles = segs.map(angleOf);
const { g1, g2 } = kmeansAngles(angles);
const lines1 = g1.map((idx) => lineFromSeg(segs[idx]));
const lines2 = g2.map((idx) => lineFromSeg(segs[idx]));
const intersections = (lines: { a: number; b: number; c: number }[]) => {
const pts: VP[] = [];
for (let i = 0; i < lines.length; i += 1) {
for (let j = i + 1; j < lines.length; j += 1) {
const p = intersect(lines[i], lines[j]);
if (p) pts.push(p);
}
}
return pts;
};
const v1 = medianPt(intersections(lines1));
const v2 = medianPt(intersections(lines2));
const l1: [number, number, number] = [v1.x, v1.y, 1];
const l2: [number, number, number] = [v2.x, v2.y, 1];
const linf: [number, number, number] = [
l1[1] * l2[2] - l1[2] * l2[1],
l1[2] * l2[0] - l1[0] * l2[2],
l1[0] * l2[1] - l1[1] * l2[0],
];
const H = homographyAffineRectify(linf);
const Hinv = invert33(H);
const rectW = width;
const rectH = height;
const rectMat = warpPerspectiveRGBA(cv, src, H, rectW, rectH);
const rectData = new Uint8ClampedArray(rectMat.data);
src.delete();
gray.delete();
edges.delete();
rectMat.delete();
return {
rectRGBA: rectData,
rectW,
rectH,
H,
Hinv,
v1,
v2,
};
}
export function backProjectVerticals(xList: number[], Hinv: Homography, rectH: number) {
return xList.map((x) => {
const p1 = applyHToPoint(Hinv, x, 0);
const p2 = applyHToPoint(Hinv, x, rectH);
return { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y };
});
}
export function backProjectHorizontals(yList: number[], Hinv: Homography, rectW: number) {
return yList.map((y) => {
const p1 = applyHToPoint(Hinv, 0, y);
const p2 = applyHToPoint(Hinv, rectW, y);
return { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y };
});
}