Please bookmark this page to avoid losing your image tool!

Image Perspective Distortion Filter

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
// Helper class: PerspectiveTransform
// Adapted from https://github.com/ivank-/PerspectiveTransform.js (Public Domain / Unlicense)
// ivank-'s version is a fork of https://github.com/Albergo/PerspectiveTransform.js (BSD-3-Clause like license)
// This class calculates coefficients for perspective transformation and its inverse.
class PerspectiveTransform {
    constructor(srcPts, dstPts) {
        // srcPts and dstPts are arrays of 8 values: [x0,y0, x1,y1, x2,y2, x3,y3]
        // Point order for this class: Top-Left, Top-Right, Bottom-Left, Bottom-Right
        // (x0,y0) = Top-Left
        // (x1,y1) = Top-Right
        // (x2,y2) = Bottom-Left
        // (x3,y3) = Bottom-Right
        
        this.coeffs = this._calculateCoefficients(srcPts, dstPts);
        this.coeffsInv = this._calculateCoefficients(dstPts, srcPts);
    }

    _gaussianElimination(matrix, N, rhs) {
        // Standard Gaussian elimination with partial pivoting to solve Ax = b
        // matrix is A (NxN), rhs is b (Nx1)
        
        // Forward elimination
        for (let i = 0; i < N; i++) {
            let maxRow = i;
            for (let j = i + 1; j < N; j++) {
                if (Math.abs(matrix[j][i]) > Math.abs(matrix[maxRow][i])) {
                    maxRow = j;
                }
            }

            // Swap rows in matrix and rhs vector
            [matrix[i], matrix[maxRow]] = [matrix[maxRow], matrix[i]];
            [rhs[i], rhs[maxRow]] = [rhs[maxRow], rhs[i]];

            // Check for singularity (pivot is too small)
            if (Math.abs(matrix[i][i]) <= 1e-10) { // Using 1e-10 as epsilon for zero
                return null; // Singular matrix, no unique solution
            }

            // Eliminate column i variable from subsequent rows
            for (let j = i + 1; j < N; j++) {
                const factor = matrix[j][i] / matrix[i][i];
                for (let k = i; k < N; k++) {
                    matrix[j][k] -= factor * matrix[i][k];
                }
                rhs[j] -= factor * rhs[i];
            }
        }

        // Back substitution to find solution x
        const solution = new Array(N);
        for (let i = N - 1; i >= 0; i--) {
            let sum = 0;
            for (let j = i + 1; j < N; j++) {
                sum += matrix[i][j] * solution[j];
            }
            solution[i] = (rhs[i] - sum) / matrix[i][i];
        }
        return solution; // Array of 8 coefficients: [a,b,c,d,e,f,g,h]
    }

    _calculateCoefficients(src, dst) {
        // Perspective transform equations:
        //   X = (ax + by + c) / (gx + hy + 1)
        //   Y = (dx + ey + f) / (gx + hy + 1)
        // (where src points are (x,y) and dst points are (X,Y))
        //
        // Rearranging them into a system of 8 linear DLT equations:
        //   X = ax + by + c - gXx - hXy
        //   Y = dx + ey + f - gYx - hYy
        // (This sets up an 8x8 matrix for the 8 unknown coefficients a,b,c,d,e,f,g,h)

        const r1 = [src[0], src[1], 1, 0, 0, 0, -dst[0] * src[0], -dst[0] * src[1]];
        const r2 = [0, 0, 0, src[0], src[1], 1, -dst[1] * src[0], -dst[1] * src[1]];
        const r3 = [src[2], src[3], 1, 0, 0, 0, -dst[2] * src[2], -dst[2] * src[3]];
        const r4 = [0, 0, 0, src[2], src[3], 1, -dst[3] * src[2], -dst[3] * src[3]];
        const r5 = [src[4], src[5], 1, 0, 0, 0, -dst[4] * src[4], -dst[4] * src[5]];
        const r6 = [0, 0, 0, src[4], src[5], 1, -dst[5] * src[4], -dst[5] * src[5]];
        const r7 = [src[6], src[7], 1, 0, 0, 0, -dst[6] * src[6], -dst[6] * src[7]];
        const r8 = [0, 0, 0, src[6], src[7], 1, -dst[7] * src[6], -dst[7] * src[7]];

        const matrix = [r1, r2, r3, r4, r5, r6, r7, r8];
        const rhs = [dst[0], dst[1], dst[2], dst[3], dst[4], dst[5], dst[6], dst[7]];
        const N = 8; // Number of equations/unknowns

        return this._gaussianElimination(matrix, N, rhs);
    }

    transform(srcX, srcY) {
        if (!this.coeffs) return null; // Singular matrix was detected during calculation
        const [a, b, c, d, e, f, g, h] = this.coeffs;

        const denominator = g * srcX + h * srcY + 1;
        if (Math.abs(denominator) < 1e-10) return null; // Avoid division by zero (point on vanishing line)

        const dstX = (a * srcX + b * srcY + c) / denominator;
        const dstY = (d * srcX + e * srcY + f) / denominator;
        return [dstX, dstY];
    }

    transformInverse(dstX, dstY) {
        if (!this.coeffsInv) return null; // Singular matrix was detected
        const [a, b, c, d, e, f, g, h] = this.coeffsInv;

        const denominator = g * dstX + h * dstY + 1;
        if (Math.abs(denominator) < 1e-10) return null; // Avoid division by zero

        const srcX = (a * dstX + b * dstY + c) / denominator;
        const srcY = (d * dstX + e * dstY + f) / denominator;
        return [srcX, srcY];
    }
}

function processImage(originalImg, dst_x0, dst_y0, dst_x1, dst_y1, dst_x2, dst_y2, dst_x3, dst_y3) {
    const W = originalImg.width;
    const H = originalImg.height;

    // Default destination points (no distortion). Parameters define the destination quadrilateral
    // Parameter order: Top-Left, Top-Right, Bottom-Right, Bottom-Left.
    dst_x0 = (dst_x0 === undefined) ? 0 : dst_x0;
    dst_y0 = (dst_y0 === undefined) ? 0 : dst_y0;
    dst_x1 = (dst_x1 === undefined) ? W : dst_x1;
    dst_y1 = (dst_y1 === undefined) ? 0 : dst_y1;
    dst_x2 = (dst_x2 === undefined) ? W : dst_x2;
    dst_y2 = (dst_y2 === undefined) ? H : dst_y2;
    dst_x3 = (dst_x3 === undefined) ? 0 : dst_x3;
    dst_y3 = (dst_y3 === undefined) ? H : dst_y3;
    
    // Source quadrilateral (entire image). Order for PerspectiveTransform class: TL, TR, BL, BR.
    const libSrcCorners = [
        0, 0, // Top-Left
        W, 0, // Top-Right
        0, H, // Bottom-Left
        W, H  // Bottom-Right
    ];

    // Destination quadrilateral (from function parameters). Must be mapped to PerspectiveTransform's expected order: TL, TR, BL, BR.
    // Input params are: (dst_x0,y0 TL), (dst_x1,y1 TR), (dst_x2,y2 BR), (dst_x3,y3 BL)
    const libDstCorners = [
        dst_x0, dst_y0, // Top-Left (from param dst_x0, dst_y0)
        dst_x1, dst_y1, // Top-Right (from param dst_x1, dst_y1)
        dst_x3, dst_y3, // Bottom-Left (from param dst_x3, dst_y3)
        dst_x2, dst_y2  // Bottom-Right (from param dst_x2, dst_y2)
    ];

    const transform = new PerspectiveTransform(libSrcCorners, libDstCorners);

    const minX = Math.min(dst_x0, dst_x1, dst_x2, dst_x3);
    const maxX = Math.max(dst_x0, dst_x1, dst_x2, dst_x3);
    const minY = Math.min(dst_y0, dst_y1, dst_y2, dst_y3);
    const maxY = Math.max(dst_y0, dst_y1, dst_y2, dst_y3);

    const canvasWidth = Math.round(maxX - minX);
    const canvasHeight = Math.round(maxY - minY);

    const canvas = document.createElement('canvas');
    
    if (canvasWidth <= 0 || canvasHeight <= 0) {
        canvas.width = Math.max(1, canvasWidth); // Ensure positive dimensions
        canvas.height = Math.max(1, canvasHeight);
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = 'rgba(255,0,0,0.1)';
        ctx.fillRect(0,0,canvas.width,canvas.height);
        ctx.font = "10px Arial"; ctx.fillStyle = "red"; ctx.textAlign = "center";
        ctx.fillText("Error: Output has no area.", canvas.width/2, canvas.height/2, Math.max(0, canvas.width-4));
        return canvas;
    }
    
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;
    const ctx = canvas.getContext('2d');

    if (!transform.coeffsInv) { // Matrix calculation failed (e.g. singular)
        console.error("Perspective transformation failed: singular matrix. Likely due to degenerate destination quadrilateral.");
        ctx.fillStyle = 'rgba(255,0,0,0.1)';
        ctx.fillRect(0,0, canvas.width, canvas.height);
        ctx.font = "12px Arial"; ctx.fillStyle = "red"; ctx.textAlign = "center";
        ctx.fillText("Error: Could not apply transform.", canvas.width/2, canvas.height/2, Math.max(0, canvas.width-4));
        return canvas;
    }
    
    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = W; tempCanvas.height = H;
    const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
    tempCtx.drawImage(originalImg, 0, 0, W, H);
    const srcImageData = tempCtx.getImageData(0, 0, W, H);
    const srcPixels = srcImageData.data;

    const destImageData = ctx.createImageData(canvasWidth, canvasHeight);
    const destPixels = destImageData.data;

    for (let y = 0; y < canvasHeight; y++) {
        for (let x = 0; x < canvasWidth; x++) {
            const currentDstX = x + minX; // Coordinate in the space of libDstCorners
            const currentDstY = y + minY;

            const srcCoords = transform.transformInverse(currentDstX, currentDstY);
            let r = 0, g = 0, b = 0, a = 0;

            if (srcCoords) { // If transformInverse succeeded (non-null)
                const srcX = srcCoords[0];
                const srcY = srcCoords[1];

                // Check if the source point is within the original image bounds
                if (srcX >= 0 && srcX < W && srcY >= 0 && srcY < H) {
                    // Bilinear interpolation
                    const x_floor = Math.floor(srcX);
                    const y_floor = Math.floor(srcY);
                    // Clamp ceiling coordinates to be within image bounds (W-1, H-1)
                    const x_ceil = Math.min(W - 1, x_floor + 1);
                    const y_ceil = Math.min(H - 1, y_floor + 1);

                    const tx = srcX - x_floor; // Fractional part of x
                    const ty = srcY - y_floor; // Fractional part of y

                    // Indices for the four surrounding pixels in the source image data array
                    const p00_idx = (y_floor * W + x_floor) * 4;
                    const p10_idx = (y_floor * W + x_ceil) * 4;  // Pixel to the right
                    const p01_idx = (y_ceil * W + x_floor) * 4;  // Pixel below
                    const p11_idx = (y_ceil * W + x_ceil) * 4;   // Pixel diagonally down-right
                    
                    // Interpolate R, G, B, A channels
                    for (let i = 0; i < 4; i++) {
                        const c00 = srcPixels[p00_idx + i]; // Top-left
                        const c10 = srcPixels[p10_idx + i]; // Top-right
                        const c01 = srcPixels[p01_idx + i]; // Bottom-left
                        const c11 = srcPixels[p11_idx + i]; // Bottom-right

                        let interp_val = 
                            c00 * (1 - tx) * (1 - ty) +  // Weight for c00
                            c10 * tx * (1 - ty) +        // Weight for c10
                            c01 * (1 - tx) * ty +        // Weight for c01
                            c11 * tx * ty;               // Weight for c11
                        
                        if (i === 0) r = interp_val;
                        else if (i === 1) g = interp_val;
                        else if (i === 2) b = interp_val;
                        else if (i === 3) a = interp_val;
                    }
                }
            } // else: srcCoords is null or out of bounds, pixel remains transparent black (r,g,b,a = 0)
            
            const destIdx = (y * canvasWidth + x) * 4;
            destPixels[destIdx]     = r;
            destPixels[destIdx + 1] = g;
            destPixels[destIdx + 2] = b;
            destPixels[destIdx + 3] = a;
        }
    }

    ctx.putImageData(destImageData, 0, 0);
    return canvas;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Perspective Distortion Filter tool allows users to apply perspective transformations to images by defining a quadrilateral shape. This tool is particularly useful for correcting distortion in images where the perspective appears skewed, such as photos taken at an angle or when capturing rectangular objects at a non-frontal angle. Users can specify the locations of the corners of the desired output shape, enabling them to create effects such as simulating 3D depth or correcting distortions in architectural photography. Additionally, it can be used in graphic design, digital art, or any scenario where perspective manipulation is required.

Leave a Reply

Your email address will not be published. Required fields are marked *