| 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-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 : |
import Jimp from 'jimp';
type RectifyOpts = {
enable?: boolean;
maxRotateDeg?: number; // harte Kappung, z.B. 15°
maxShearDeg?: number; // harte Kappung, z.B. 8°
sampleStep?: number; // Abtastraster für Gradienten
magThresh?: number; // Magnituden-Schwelle für Kanten
};
/**
* Haupt-Funktion: leichte Projektivkorrektur (Rotation + X-Shear).
* - Robust bei leichter Schiefe / geringer Perspektive
* - Kein vollständiger Homography-Warp (bewusst "lightweight")
*/
export async function maybeRectifyPerspective(img: Jimp, opts: RectifyOpts = {}): Promise<Jimp> {
const {
enable = true,
maxRotateDeg = 15,
maxShearDeg = 8,
sampleStep = 3,
magThresh = 18,
} = opts;
if (!enable) return img;
// Für Winkel-Schätzung in kleiner Auflösung arbeiten (Performance)
const thumb = img.clone();
const maxSide = 900;
const scale = Math.min(1, maxSide / Math.max(img.bitmap.width, img.bitmap.height));
if (scale < 1) thumb.resize(Math.round(img.bitmap.width * scale), Math.round(img.bitmap.height * scale));
// 1) Dominante Kantenwinkel schätzen (Grad in [-90, 90))
const { peaks } = estimateEdgeAngles(thumb, { sampleStep, magThresh });
if (peaks.length === 0) return img;
// 2) Rotationswinkel so wählen, dass stärkste Spitze nahe 0° oder 90° liegt (kleinster Absolutwert)
const best = bestRotationFromPeaks(peaks);
let rotateDeg = clamp(best, -maxRotateDeg, maxRotateDeg);
// winzige Winkel ignorieren
if (Math.abs(rotateDeg) < 0.25) rotateDeg = 0;
let rotated = img;
if (rotateDeg !== 0) {
// Jimp.rotate(rot, false) -> bilinear; keep alpha
rotated = img.clone().rotate(rotateDeg, false);
}
// 3) X-Shear schätzen: nach der Rotation sollten "vertikale" Kanten nahe 0° liegen.
// Übrig bleibt bei leichter Perspektive oft ein kleiner Restwinkel -> als Shear kompensieren.
const { peaks: peaks2 } = estimateEdgeAngles(rotated.clone(), { sampleStep, magThresh });
const nearZero = pickPeakNearZero(peaks2);
if (nearZero == null) return rotated;
let shearDeg = clamp(nearZero, -maxShearDeg, maxShearDeg);
if (Math.abs(shearDeg) < 0.2) return rotated;
// Shear-Faktor k = tan(shearDeg)
const k = Math.tan((shearDeg * Math.PI) / 180);
const sheared = await shearX(rotated, k);
return sheared;
}
/** Shear in X-Richtung: x' = x + k*(y - h/2). Bildgröße bleibt erhalten, Ränder werden transparent gefüllt. */
async function shearX(src: Jimp, k: number): Promise<Jimp> {
const { width: w, height: h } = src.bitmap;
const dst = new Jimp(w, h, 0x00000000); // transparent
const yCenter = (h - 1) / 2;
src.scan(0, 0, w, h, function (x, y) {
// Quell->Ziel: einfacher Forward-Mapping
const shift = k * (y - yCenter);
const xd = Math.round(x + shift);
if (xd >= 0 && xd < w) {
const rgba = src.getPixelColor(x, y);
dst.setPixelColor(rgba, xd, y);
}
});
// Optional: kleine Lücken durch horizontale Blur/AA schlauchen (sehr mild)
const aa = dst.clone();
// 1px blur reicht i.d.R. um "Zahnkanten" zu glätten
aa.blur(1);
// Alpha-preserving blend: nur fehlende Pixel sanft ergänzen
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const c = dst.getPixelColor(x, y);
const a = (c >>> 24) & 0xff;
if (a < 8) {
dst.setPixelColor(aa.getPixelColor(x, y), x, y);
}
}
}
return dst;
}
function clamp(v: number, lo: number, hi: number) {
return Math.max(lo, Math.min(hi, v));
}
/** Gradienten-orientierte Winkel-Histogramm-Analyse (leichtgewichtige Sobel-Approx). */
function estimateEdgeAngles(img: Jimp, opts: { sampleStep: number; magThresh: number }) {
const { width: w, height: h } = img.bitmap;
const step = Math.max(2, opts.sampleStep | 0);
const histBins = 180; // [-90,90)
const hist = new Float64Array(histBins);
// Greyscale Luma per Pixel
const L = (x: number, y: number) => {
const c = img.getPixelColor(x, y);
const r = (c >> 16) & 0xff;
const g = (c >> 8) & 0xff;
const b = c & 0xff;
// Rec.601 luma
return (299 * r + 587 * g + 114 * b) / 1000;
};
for (let y = 1; y < h - 1; y += step) {
for (let x = 1; x < w - 1; x += step) {
const gx = L(x + 1, y) - L(x - 1, y); // dI/dx
const gy = L(x, y + 1) - L(x, y - 1); // dI/dy
const mag = Math.hypot(gx, gy);
if (mag < opts.magThresh) continue;
// Orientierung des Gradienten (−pi..pi), auf (−90..90] mappen
let ang = (Math.atan2(gy, gx) * 180) / Math.PI; // Grad
while (ang <= -90) ang += 180;
while (ang > 90) ang -= 180;
// Bin befüllen (Gewichtung mit Magnitude)
const bin = Math.max(0, Math.min(histBins - 1, Math.round(((ang + 90) / 180) * (histBins - 1))));
hist[bin] += mag;
}
}
// Peaks finden (Top-2, mit Mindestabstand)
const peaks: number[] = [];
for (let i = 1; i < histBins - 1; i++) {
const v = hist[i];
if (v > hist[i - 1] && v > hist[i + 1]) peaks.push(i);
}
peaks.sort((a, b) => hist[b] - hist[a]); // nach Stärke
const toAngle = (bin: number) => bin * (180 / (histBins - 1)) - 90;
const selected: number[] = [];
for (const p of peaks) {
const a = toAngle(p);
if (selected.every((s) => Math.abs(deltaAngle(a, s)) >= 25)) {
selected.push(a);
if (selected.length === 2) break;
}
}
return { peaks: selected, hist };
}
// kleinste Differenz zweier Winkel in Grad (periodisch 180°)
function deltaAngle(a: number, b: number) {
let d = a - b;
while (d <= -90) d += 180;
while (d > 90) d -= 180;
return d;
}
/** Wähle eine kleine Rotation, die stärksten Peak auf 0° oder 90° bringt. */
function bestRotationFromPeaks(peaks: number[]): number {
// Kandidaten: auf 0°, 90° oder -90° ausrichten -> kleinstmögliche Drehung
let best = 0;
let bestAbs = Infinity;
for (const a of peaks) {
const cand = [a, a - 90, a + 90]; // wieviel Grad bis zum Ziel 0°
for (const c of cand) {
const rot = -c;
const ar = Math.abs(rot);
if (ar < bestAbs) {
bestAbs = ar;
best = rot;
}
}
}
return best;
}
/** Wähle den Peak, der nach Rotation am nächsten bei 0° liegt (Restschiefe). */
function pickPeakNearZero(peaks: number[]): number | null {
if (peaks.length === 0) return null;
let best = peaks[0];
for (const p of peaks) if (Math.abs(p) < Math.abs(best)) best = p;
return best;
}