| Server IP : 217.154.3.148 / Your IP : 216.73.216.90 Web Server : Apache System : Linux blissful-wright.217-154-3-148.plesk.page 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:53:05 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/app/api/analyze/ |
Upload File : |
import Jimp from 'jimp';
type Grid = { vlines: number[]; hlines: number[] };
export type RobustResult = {
tile_w_px: number;
tile_h_px: number;
grout_px: number;
grid: Grid;
source: string;
};
type Opts = {
maxWorkSide?: number;
minPeriodPx?: number;
maxPeriodPx?: number;
smoothWin?: number;
tolFrac?: number;
minLines?: number;
};
const DEFAULTS: Required<Opts> = {
maxWorkSide: 1200,
minPeriodPx: 12,
maxPeriodPx: 400,
smoothWin: 7,
tolFrac: 0.18,
minLines: 3,
};
/* ---------------- LRU Cache (phash-basiert) ---------------- */
class LRU<V> {
private map = new Map<string, V>();
constructor(private max = 50) {}
get(k: string): V | undefined {
const v = this.map.get(k);
if (v !== undefined) { this.map.delete(k); this.map.set(k, v); }
return v;
}
set(k: string, v: V) {
if (this.map.has(k)) this.map.delete(k);
this.map.set(k, v);
if (this.map.size > this.max) {
const first = this.map.keys().next().value;
if (first !== undefined) {
this.map.delete(first);
}
}
}
}
const lru = new LRU<RobustResult>(60);
/** Öffentliche API: gecachte robuste Erkennung */
export async function detectGridWithOcclusionCached(img: Jimp, opts: Opts = {}): Promise<RobustResult> {
const key = "robust:" + safePhash(img) + ":" + JSON.stringify({ v:1, o: shapeOpts(opts) });
const hit = lru.get(key);
if (hit) return hit;
const res = await detectGridWithOcclusion(img, opts);
lru.set(key, res);
return res;
}
/** Öffentliche API: robuste Erkennung ohne Cache */
export async function detectGridWithOcclusion(img: Jimp, opts: Opts = {}): Promise<RobustResult> {
const o = { ...DEFAULTS, ...opts };
// Arbeitskopie
const w0 = img.bitmap.width, h0 = img.bitmap.height;
const scale = Math.min(1, o.maxWorkSide / Math.max(w0, h0));
const work = img.clone();
if (scale < 1) work.resize(Math.round(w0 * scale), Math.round(h0 * scale), Jimp.RESIZE_BILINEAR);
const w = work.bitmap.width, h = work.bitmap.height;
const sx = w0 / w, sy = h0 / h;
// Einmal Graustufen buffer bauen (uint8)
const gray = toGrayU8(work);
// Projektionen aus Raw-Buffer (ohne getPixelColor)
const projX = projectionAbsGxFast(gray, w, h);
const projY = projectionAbsGyFast(gray, w, h);
// Glätten
const fx = smooth1D(projX, o.smoothWin);
const fy = smooth1D(projY, o.smoothWin);
// Periode via Autokorrelation
const periodX = estimatePeriodAC(fx, o.minPeriodPx, Math.min(o.maxPeriodPx, Math.floor(fx.length/2)));
const periodY = estimatePeriodAC(fy, o.minPeriodPx, Math.min(o.maxPeriodPx, Math.floor(fy.length/2)));
if (!periodX || !periodY) throw new Error('period not found');
const tolX = Math.max(1, Math.round(periodX * o.tolFrac));
const tolY = Math.max(1, Math.round(periodY * o.tolFrac));
const gx = fitGrid1D(fx, periodX, tolX);
const gy = fitGrid1D(fy, periodY, tolY);
if (gx.lines.length < o.minLines || gy.lines.length < o.minLines) {
throw new Error('too few lines');
}
// Fugenbreite per FWHM
const widthsX = gx.lines.map(x => peakWidthAt(fx, x, gx.period));
const widthsY = gy.lines.map(y => peakWidthAt(fy, y, gy.period));
const groutPxWork = median(widthsX.concat(widthsY).filter(n => isFinite(n) && n>0)) || 2;
// auf Originalmaß zurück
const vlines = gx.lines.map(x => Math.round(x * sx)).filter(x => x>0 && x<w0);
const hlines = gy.lines.map(y => Math.round(y * sy)).filter(y => y>0 && y<h0);
const tile_w_px = Math.max(1, Math.round(gx.period * sx));
const tile_h_px = Math.max(1, Math.round(gy.period * sy));
const grout_px = Math.max(1, Math.round(groutPxWork * ((sx+sy)/2)));
return {
tile_w_px,
tile_h_px,
grout_px,
grid: { vlines, hlines },
source: 'cv@robust1d+ac+tolerant-peaks+rawbuf',
};
}
/* ---------------- Hilfen: Raw-Buffer/Projektionen ---------------- */
function toGrayU8(im: Jimp): Uint8Array {
const { data } = im.bitmap as unknown as { data: Buffer };
const w = im.bitmap.width, h = im.bitmap.height;
const out = new Uint8Array(w*h);
let j=0;
for (let i=0; i<data.length; i+=4, j++) {
const r = data[i], g = data[i+1], b = data[i+2];
out[j] = ( (299*r + 587*g + 114*b) / 1000 )|0;
}
return out;
}
function projectionAbsGxFast(gray: Uint8Array, w: number, h: number): number[] {
const col = new Float64Array(w);
for (let y=0; y<h; y++) {
const row = y*w;
for (let x=1; x<w-1; x++) {
const gx = gray[row + x + 1] - gray[row + x - 1];
col[x] += Math.abs(gx);
}
// Rand glätten minimal
col[0] += col[1];
col[w-1] += col[w-2];
}
col[0] = col[1]; col[w-1] = col[w-2];
return Array.from(col);
}
function projectionAbsGyFast(gray: Uint8Array, w: number, h: number): number[] {
const row = new Float64Array(h);
for (let y=1; y<h-1; y++) {
const ym = (y-1)*w;
const yp = (y+1)*w;
for (let x=0; x<w; x++) {
const gy = gray[yp + x] - gray[ym + x];
row[y] += Math.abs(gy);
}
}
row[0] = row[1]; row[h-1] = row[h-2];
return Array.from(row);
}
/* ---------------- Hilfen: 1D Signalverarbeitung ---------------- */
function smooth1D(a: number[], win: number): number[] {
const n = a.length, k = Math.max(1, win|0);
const out = new Array<number>(n).fill(0);
let s=0;
for (let i=0;i<n;i++) {
s += a[i];
if (i>=k) s -= a[i-k];
const len = Math.min(i+1, k);
out[i] = s / len;
}
// rückwärts
s = 0;
const out2 = new Array<number>(n).fill(0);
for (let i=n-1;i>=0;i--) {
s += out[i];
const idx = (n-1-i);
if (idx>=k) s -= out[i+k] || 0;
const len = Math.min(idx+1, k);
out2[i] = s / len;
}
return out2;
}
function estimatePeriodAC(sig: number[], minLag: number, maxLag: number): number | null {
const n = sig.length;
if (maxLag <= minLag+2) return null;
const mean = sig.reduce((p,v)=>p+v,0)/n;
const x = sig.map(v=>v-mean);
let bestLag=0, best=-Infinity;
const maxL = Math.min(maxLag, Math.floor(n/2));
for (let lag=minLag; lag<=maxL; lag++) {
let num=0, den0=0, den1=0;
for (let i=0;i<n-lag;i++) {
const a=x[i], b=x[i+lag];
num += a*b; den0 += a*a; den1 += b*b;
}
const corr = num / Math.sqrt((den0||1)*(den1||1));
if (corr>best) { best=corr; bestLag=lag; }
}
return bestLag||null;
}
function fitGrid1D(sig: number[], period: number, tol: number) {
const n = sig.length, p = Math.max(1, Math.round(period));
let bestOff=0, bestScore=-Infinity;
const window = Math.max(1, tol|0);
for (let off=0; off<p; off++) {
let score=0;
for (let k=0;;k++) {
const pos = off + k*p; if (pos>=n) break;
const a = Math.max(0, pos-window), b = Math.min(n-1, pos+window);
let localMax=0; for (let i=a;i<=b;i++) if (sig[i]>localMax) localMax=sig[i];
score += localMax;
}
if (score>bestScore) { bestScore=score; bestOff=off; }
}
const lines:number[]=[];
for (let k=0;;k++) {
const pos = bestOff + k*p; if (pos>=n) break;
const a = Math.max(0, pos-window), b = Math.min(n-1, pos+window);
let bestI=pos, bestV=-Infinity;
for (let i=a;i<=b;i++) if (sig[i]>bestV) { bestV=sig[i]; bestI=i; }
lines.push(bestI);
}
return { offset: bestOff, period: p, lines };
}
function peakWidthAt(sig: number[], center: number, period: number): number {
const n = sig.length, c = Math.max(0, Math.min(n-1, Math.round(center)));
const peak = sig[c]; if (!isFinite(peak) || peak<=0) return 1;
const thr = peak * 0.5;
const maxSpan = Math.max(2, Math.round(period*0.6));
let L=c, R=c;
for (let i=c;i>=Math.max(0, c-maxSpan);i--) { L=i; if (sig[i]<thr) break; }
for (let i=c;i<=Math.min(n-1, c+maxSpan);i++) { R=i; if (sig[i]<thr) break; }
return Math.max(1, R-L+1);
}
function median(a:number[]):number{
if (a.length===0) return NaN;
const s=a.slice().sort((x,y)=>x-y); const m=Math.floor(s.length/2);
return s.length%2 ? s[m] : (s[m-1]+s[m])/2;
}
function safePhash(img: Jimp): string {
try { return img.hash?.(16) ?? `${img.bitmap.width}x${img.bitmap.height}:${img.bitmap.data.length}`; }
catch { return `${img.bitmap.width}x${img.bitmap.height}:${img.bitmap.data.length}`; }
}
function shapeOpts(o: Opts){ return {
m:o.maxWorkSide, a:o.minPeriodPx, b:o.maxPeriodPx, s:o.smoothWin, t:o.tolFrac, l:o.minLines
}; }