| 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/app/api/analyze/ |
Upload File : |
/* eslint-disable @typescript-eslint/no-unused-vars */
import { NextRequest, NextResponse } from 'next/server';
import sharp from 'sharp';
import Jimp from 'jimp';
import { maybeRectifyPerspective } from './rectify';
import { detectGridWithOcclusionCached as detectGridWithOcclusion } from './robust';
export const runtime = 'nodejs';
function toBuffer(ab: ArrayBuffer): Buffer {
return Buffer.from(new Uint8Array(ab));
}
/** einfache Gleitmittelung */
function smooth1D(arr: Float32Array, win = 5): Float32Array {
const out = new Float32Array(arr.length);
const half = Math.max(1, Math.floor(win / 2));
for (let i = 0; i < arr.length; i++) {
let s = 0, c = 0;
for (let k = -half; k <= half; k++) {
const j = i + k;
if (j >= 0 && j < arr.length) { s += arr[j]; c++; }
}
out[i] = c ? s / c : arr[i];
}
return out;
}
function autoPeriod(sig: Float32Array, minP: number, maxP: number): number | null {
const n = sig.length;
let mean = 0;
for (let i = 0; i < n; i++) mean += sig[i];
mean /= n;
const x = new Float32Array(n);
for (let i = 0; i < n; i++) x[i] = sig[i] - mean;
let bestLag = -1;
let bestVal = -Infinity;
for (let lag = minP; lag <= Math.min(maxP, n - 2); lag++) {
let s = 0;
for (let i = 0; i + lag < n; i++) s += x[i] * x[i + lag];
if (s > bestVal) { bestVal = s; bestLag = lag; }
}
return bestLag > 0 ? bestLag : null;
}
function bestPhase(sig: Float32Array, T: number): number {
let best = 0;
let bestVal = -Infinity;
for (let phase = 0; phase < T; phase++) {
let s = 0;
for (let i = phase; i < sig.length; i += T) s += sig[i];
if (s > bestVal) { bestVal = s; best = phase; }
}
return best;
}
/** Overlay mit sharp/RAW-Buffer (kein Jimp mehr hier) */
async function buildOverlaySharp(
w: number,
h: number,
vlines: number[],
hlines: number[],
mask: Uint8Array | null
): Promise<string> {
const px = Buffer.allocUnsafe(w * h * 4);
// transparent füllen
for (let i = 0; i < px.length; i += 4) {
px[i + 0] = 0; // R
px[i + 1] = 0; // G
px[i + 2] = 0; // B
px[i + 3] = 0; // A
}
const put = (x: number, y: number, r: number, g: number, b: number, a: number) => {
if (x < 0 || y < 0 || x >= w || y >= h) return;
const p = (y * w + x) * 4;
px[p + 0] = r; px[p + 1] = g; px[p + 2] = b; px[p + 3] = a;
};
// Maske (magenta, halbtransparent)
if (mask) {
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
if (mask[y * w + x] > 0) {
put(x, y, 255, 0, 255, 80);
}
}
}
}
// vertikale Linien (rot, voll)
for (const x of vlines) {
for (let y = 0; y < h; y++) put(Math.round(x), y, 255, 0, 0, 255);
}
// horizontale Linien (grün, voll)
for (const y of hlines) {
for (let x = 0; x < w; x++) put(x, Math.round(y), 0, 255, 0, 255);
}
const png = await sharp(px, { raw: { width: w, height: h, channels: 4 } }).png().toBuffer();
return `data:image/png;base64,${png.toString('base64')}`;
}
async function decodeWithSharpToGray(buf: Buffer, maxWidth = 1000) {
const meta = await sharp(buf, { failOnError: false }).metadata();
const srcW = meta.width ?? 0;
const srcH = meta.height ?? 0;
if (!(Number.isFinite(srcW) && Number.isFinite(srcH) && srcW > 0 && srcH > 0)) {
throw new Error('unsupported_or_invalid_image');
}
const scale = Math.min(1, maxWidth / srcW);
const W = Math.max(1, Math.round(srcW * scale));
const H = Math.max(1, Math.round(srcH * scale));
// Graustufen als RAW (1 Kanal bevorzugt)
const { data, info } = await sharp(buf, { failOnError: false })
.resize({ width: W, height: H, fit: 'inside' })
.greyscale()
.raw()
.toBuffer({ resolveWithObject: true });
let gray: Uint8Array;
if (info.channels === 1) {
gray = new Uint8Array(data);
} else {
const u8 = new Uint8Array(data);
gray = new Uint8Array(info.width * info.height);
// nur ersten Kanal verwenden (keine Mittelung über Alpha)
for (let i = 0, p = 0; i < gray.length; i++, p += info.channels) {
gray[i] = u8[p] ?? 0;
}
}
return { gray, W: info.width, H: info.height, srcW, srcH, scale };
}
async function decodeMaskToMono(mbuf: Buffer, W: number, H: number): Promise<Uint8Array | null> {
try {
const { data, info } = await sharp(mbuf, { failOnError: false })
.resize({ width: W, height: H, fit: 'fill' })
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
const u8 = new Uint8Array(data);
const mono = new Uint8Array(W * H);
for (let i = 0, p = 0; i < mono.length; i++, p += info.channels) {
const r = u8[p] ?? 0, g = u8[p + 1] ?? 0, b = u8[p + 2] ?? 0, a = u8[p + 3] ?? 255;
mono[i] = (a > 10 && (r + g + b) > 0) ? 1 : 0;
}
return mono;
} catch {
return null;
}
}
export async function POST(req: NextRequest) {
const url = new URL(req.url);
const rectifyParam = url.searchParams.get("rectify");
const rectify = rectifyParam !== "0"; // default: an
try {
const url = new URL(req.url);
const wantDebug = url.searchParams.get('debug') != null;
const form = await req.formData();
const file = form.get('file') as unknown as File | null;
const maskFile = (form.get('mask') as unknown as File | null) || null;
if (!file) return NextResponse.json({ ok: false, error: 'No file' }, { status: 422 });
const buf = toBuffer(await file.arrayBuffer());
if (!buf || buf.length < 16) return NextResponse.json({ ok: false, error: 'Empty or too small image' }, { status: 422 });
// Primär sharp
let grayU8: Uint8Array, W: number, H: number, srcW: number, srcH: number, scale: number;
try {
const d = await decodeWithSharpToGray(buf, 1000);
grayU8 = d.gray; W = d.W; H = d.H; srcW = d.srcW; srcH = d.srcH; scale = d.scale;
} catch (e: any) {
// Fallback Jimp
try {
const j = await Jimp.read(buf);
srcW = j.getWidth(); srcH = j.getHeight();
if (!(Number.isFinite(srcW) && Number.isFinite(srcH) && srcW > 0 && srcH > 0)) {
return NextResponse.json({ ok: false, error: 'Decode error: image width/height invalid (w and h must be numbers).' }, { status: 422 });
}
const maxW = 1000;
const s = Math.min(1, maxW / srcW);
W = Math.max(1, Math.round(srcW * s));
H = Math.max(1, Math.round(srcH * s));
scale = s;
const work = j.clone().resize(W, H, Jimp.RESIZE_BILINEAR).greyscale();
const u8 = new Uint8Array(W * H);
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const { r } = Jimp.intToRGBA(work.getPixelColor(x, y));
u8[y * W + x] = r;
}
}
grayU8 = u8;
} catch {
return NextResponse.json({ ok: false, error: 'Unsupported image format or decode failure' }, { status: 415 });
}
}
// Maske optional
let maskArr: Uint8Array | null = null;
if (maskFile) {
const mbuf = toBuffer(await maskFile.arrayBuffer());
if (mbuf && mbuf.length > 0) maskArr = await decodeMaskToMono(mbuf, W, H);
}
// Sobel
const gx = new Float32Array(W * H);
const gy = new Float32Array(W * H);
const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
for (let y = 1; y < H - 1; y++) {
for (let x = 1; x < W - 1; x++) {
let sxv = 0, syv = 0, k = 0;
for (let j = -1; j <= 1; j++) {
for (let i = -1; i <= 1; i++) {
const v = grayU8[(y + j) * W + (x + i)];
sxv += v * sobelX[k];
syv += v * sobelY[k];
k++;
}
}
const idx = y * W + x;
if (maskArr && maskArr[idx] === 1) { gx[idx] = 0; gy[idx] = 0; }
else { gx[idx] = Math.abs(sxv); gy[idx] = Math.abs(syv); }
}
}
// Projektionen
const projX = new Float32Array(W);
const projY = new Float32Array(H);
for (let x = 0; x < W; x++) { let s = 0; for (let y = 0; y < H; y++) s += gx[y * W + x]; projX[x] = s; }
for (let y = 0; y < H; y++) { let s = 0; for (let x = 0; x < W; x++) s += gy[y * W + x]; projY[y] = s; }
const spx = smooth1D(projX, 7);
const spy = smooth1D(projY, 7);
const minPeriod = Math.max(8, Math.floor(Math.min(W, H) * 0.02));
const Tx = autoPeriod(spx, minPeriod, Math.max(minPeriod + 1, Math.floor(W / 3)));
const Ty = autoPeriod(spy, minPeriod, Math.max(minPeriod + 1, Math.floor(H / 3)));
if (!Tx || !Ty) {
return NextResponse.json({ ok: false, error: 'Could not detect periodic grid (Tx/Ty null).' }, { status: 422 });
}
const phaseX = bestPhase(spx, Tx);
const phaseY = bestPhase(spy, Ty);
const vlinesWork: number[] = [];
for (let x = phaseX; x < W; x += Tx) vlinesWork.push(Math.round(x));
const hlinesWork: number[] = [];
for (let y = phaseY; y < H; y += Ty) hlinesWork.push(Math.round(y));
const groutWork = Math.round(Math.max(1, Math.min(Tx, Ty) * 0.06));
const invScale = 1 / scale;
const vlines = vlinesWork.map((x) => Math.max(1, Math.round(x * invScale)));
const hlines = hlinesWork.map((y) => Math.max(1, Math.round(y * invScale)));
const tile_w_px = Math.round(Tx * invScale);
const tile_h_px = Math.round(Ty * invScale);
const grout_px = groutWork * invScale;
let debug_overlay: string | null = null;
if (wantDebug) debug_overlay = await buildOverlaySharp(W, H, vlinesWork, hlinesWork, maskArr);
return NextResponse.json({
ok: true,
image: { width: srcW, height: srcH },
tile_w_px,
tile_h_px,
grout_px,
tile_w_mm: null,
tile_h_mm: null,
grout_mm: null,
grid: { vlines, hlines },
source: 'cv@sharp+projection+autocorr',
debug_overlay,
name: (file as any)?.name,
type: (file as any)?.type,
});
} catch (e: any) {
const msg = e?.message || String(e);
if (process.env.DEBUG_ANALYZE === '1') {
console.error('API /api/analyze fatal:', msg);
}
const code = /unsupported|decode|invalid image size|power of two/i.test(msg) ? 422 : (/unsupported|decode/i.test(msg) ? 415 : 500);
return NextResponse.json({ ok: false, error: msg }, { status: code });
}
}