Please bookmark this page to avoid losing your image tool!

Image Low Poly Filter Application

(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.
async function processImage(originalImg, numPoints = 1500, pointSelectionMode = "edge_priority", edgeThreshold = 50, blurRadius = 1.0) {

    // Helper function for Sobel edge detection
    function _getEdgeMagnitudeMap(imageData, W, H, threshold) {
        const grayData = new Uint8ClampedArray(W * H);
        const magnitudeData = new Float32Array(W * H); // Store float magnitudes
        let maxMagnitude = 0;

        // Convert to grayscale (luminosity method)
        for (let i = 0; i < imageData.data.length; i += 4) {
            const r = imageData.data[i];
            const g = imageData.data[i + 1];
            const b = imageData.data[i + 2];
            grayData[i / 4] = 0.299 * r + 0.587 * g + 0.114 * b;
        }

        // Sobel kernels
        const Gx = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
        const Gy = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];

        for (let y = 0; y < H; y++) {
            for (let x = 0; x < W; x++) {
                let sumX = 0;
                let sumY = 0;
                
                for (let i = -1; i <= 1; i++) {
                    for (let j = -1; j <= 1; j++) {
                        // Clamp coordinates to handle image borders
                        const currentY = Math.max(0, Math.min(H - 1, y + i));
                        const currentX = Math.max(0, Math.min(W - 1, x + j));
                        const pixelVal = grayData[currentY * W + currentX];
                        sumX += pixelVal * Gx[i + 1][j + 1];
                        sumY += pixelVal * Gy[i + 1][j + 1];
                    }
                }
                
                const magnitude = Math.sqrt(sumX * sumX + sumY * sumY);
                magnitudeData[y * W + x] = magnitude;
                if (magnitude > maxMagnitude) {
                    maxMagnitude = magnitude;
                }
            }
        }
        
        const edgeMap = new Uint8ClampedArray(W * H); // 0 (not edge) or 255 (edge)
        if (maxMagnitude > 0) { // Avoid division by zero for blank images
            for (let i = 0; i < magnitudeData.length; i++) {
                const normalizedMagnitude = (magnitudeData[i] / maxMagnitude) * 255;
                if (normalizedMagnitude > threshold) {
                    edgeMap[i] = 255;
                } else {
                    edgeMap[i] = 0;
                }
            }
        }
        return { edgeMap, magnitudeData, maxMagnitude }; // Return all parts, edgeMap is main output
    }

    // Parameter validation and type coercion
    numPoints = Math.max(5, Number(numPoints)); // Need at least a few points for triangulation
    blurRadius = Math.max(0, Number(blurRadius));
    edgeThreshold = Math.max(0, Math.min(255, Number(edgeThreshold)));
    if (!["random", "grid", "edge_priority"].includes(String(pointSelectionMode))) {
        pointSelectionMode = "edge_priority"; // Default if invalid mode provided
    }
    
    const width = originalImg.naturalWidth || originalImg.width;
    const height = originalImg.naturalHeight || originalImg.height;

    if (width === 0 || height === 0) {
        console.error("Image has zero width or height. Ensure it's loaded and valid.");
        const emptyCanvas = document.createElement('canvas');
        emptyCanvas.width = 1; 
        emptyCanvas.height = 1; // Return a 1x1 empty canvas for error cases
        return emptyCanvas;
    }

    // 0. Dynamically load d3-delaunay library if not already available
    if (typeof d3 === 'undefined' || typeof d3.Delaunay === 'undefined') {
        try {
            await new Promise((resolve, reject) => {
                const script = document.createElement('script');
                script.src = 'https://cdn.jsdelivr.net/npm/d3-delaunay@6'; // UMD bundle
                script.async = true;
                script.onload = resolve;
                script.onerror = () => reject(new Error("Failed to load d3-delaunay library from CDN."));
                document.head.appendChild(script);
            });
        } catch (error) {
            console.error(error.message);
            throw error; // Re-throw if library loading is critical
        }

        // Check again after script loading attempt
        if (typeof d3 === 'undefined' || typeof d3.Delaunay === 'undefined') {
            const err = new Error("d3.Delaunay is not available even after attempting to load from CDN.");
            console.error(err.message);
            throw err;
        }
    }

    // 1. Prepare canvases: one for original image data, one for processing (blur + edge detection)
    const inputCanvas = document.createElement('canvas');
    inputCanvas.width = width;
    inputCanvas.height = height;
    const inputCtx = inputCanvas.getContext('2d');
    inputCtx.drawImage(originalImg, 0, 0, width, height);
    const originalImageData = inputCtx.getImageData(0, 0, width, height); // For final color sampling

    const processingCanvas = document.createElement('canvas');
    processingCanvas.width = width;
    processingCanvas.height = height;
    const processingCtx = processingCanvas.getContext('2d');

    if (blurRadius > 0) {
        processingCtx.filter = `blur(${blurRadius}px)`;
    }
    processingCtx.drawImage(originalImg, 0, 0, width, height);
    processingCtx.filter = 'none'; // Reset filter
    const blurredImageData = processingCtx.getImageData(0, 0, width, height); // For edge detection


    // 2. Point Selection Logic
    const points = [];
    // Add mandatory corner points
    points.push([0, 0], [width - 1, 0], [0, height - 1], [width - 1, height - 1]);
    
    // Add some boundary points for well-defined edges
    // Estimate: approx sqrt(numPoints)*0.125 points per side. e.g. for 1500 pts, ~5 points/side
    const boundaryPointsPerSide = Math.max(0, Math.floor(Math.sqrt(numPoints) * 0.125)); 
    if (boundaryPointsPerSide > 0) {
        for (let i = 1; i <= boundaryPointsPerSide; i++) {
            const W_step = width / (boundaryPointsPerSide + 1);
            const H_step = height / (boundaryPointsPerSide + 1);
            points.push([i * W_step, 0]); // Top edge
            points.push([i * W_step, height - 1]); // Bottom edge
            points.push([0, i * H_step]); // Left edge
            points.push([width - 1, i * H_step]); // Right edge
        }
    }
    
    // Simple deduplication of corner/boundary points (in case of overlaps from calculations)
    const uniquePointsMap = new Map();
    const tempUniquePoints = [];
    for(const p of points) {
        const key = `${Math.round(p[0])},${Math.round(p[1])}`; // Round to avoid float precision key issues
        if(!uniquePointsMap.has(key)) {
            uniquePointsMap.set(key, true);
            tempUniquePoints.push(p);
        }
    }
    points.length = 0; 
    points.push(...tempUniquePoints);


    const pointsToGenerate = Math.max(0, numPoints - points.length);

    if (pointsToGenerate > 0) {
        if (pointSelectionMode === "random") {
            for (let i = 0; i < pointsToGenerate; i++) {
                points.push([Math.random() * width, Math.random() * height]);
            }
        } else if (pointSelectionMode === "grid") {
            const aspectRatio = width / height;
            // Calculate M (columns) and N (rows) for a grid that roughly matches numPoints
            const M_cols = Math.ceil(Math.sqrt(pointsToGenerate * aspectRatio));
            const N_rows = Math.ceil(pointsToGenerate / M_cols);
            
            if (M_cols > 0 && N_rows > 0) {
                const xStep = width / M_cols;
                const yStep = height / N_rows;
                for (let i = 0; i < N_rows; i++) {
                    for (let j = 0; j < M_cols; j++) {
                        if (points.length < numPoints) {
                            points.push([
                                (j + 0.5) * xStep + (Math.random() - 0.5) * xStep * 0.3, // Jitter: 15% of ste
                                (i + 0.5) * yStep + (Math.random() - 0.5) * yStep * 0.3
                            ]);
                        } else break;
                    }
                    if (points.length >= numPoints) break;
                }
            }
            // Fill any remaining points randomly if grid didn't reach numPoints target
            while(points.length < numPoints) {
                 points.push([Math.random() * width, Math.random() * height]);
            }
        } else { // "edge_priority" (default mode)
            const { edgeMap } = _getEdgeMagnitudeMap(blurredImageData, width, height, edgeThreshold);
            const edgePixelCoords = [];
            for (let y = 0; y < height; y++) {
                for (let x = 0; x < width; x++) {
                    if (edgeMap[y * width + x] === 255) { // If it's an edge pixel
                        edgePixelCoords.push([x, y]);
                    }
                }
            }

            // Aim for ~70% of remaining points from edges, rest random
            const numEdgePointsToAttempt = Math.floor(pointsToGenerate * 0.7);
            let actualEdgePointsAdded = 0;
            if (edgePixelCoords.length > 0) {
                for (let i = 0; i < numEdgePointsToAttempt; i++) {
                    if (points.length >= numPoints) break;
                    const randIdx = Math.floor(Math.random() * edgePixelCoords.length);
                    points.push(edgePixelCoords[randIdx]); 
                    actualEdgePointsAdded++;
                }
            }
            
            const numRandomPoints = pointsToGenerate - actualEdgePointsAdded;
            for (let i = 0; i < numRandomPoints; i++) {
                if (points.length >= numPoints) break;
                points.push([Math.random() * width, Math.random() * height]);
            }
        }
    }
    
    // Final pass: ensure all points are finite numbers and within image boundaries.
    const finalPoints = [];
    for(let i = 0; i < points.length; i++) {
        let x = points[i][0];
        let y = points[i][1];
        // Ensure x and y are valid numbers before clamping
        if (Number.isFinite(x) && Number.isFinite(y)) {
             finalPoints.push([
                Math.max(0, Math.min(width - 1, x)),
                Math.max(0, Math.min(height - 1, y))
            ]);
        }
    }
    // Delaunay triangulation requires at least 3 non-collinear points.
    if (finalPoints.length < 3) {
        console.warn("Not enough valid points for triangulation. Adding fallback random points.");
        while(finalPoints.length < 3 && finalPoints.length < numPoints) { // ensure not adding too many
            finalPoints.push([Math.random() * (width-1), Math.random() * (height-1)]);
        }
         // One last check to make sure we always have at least 3 for d3.Delaunay
        while(finalPoints.length < 3){
            finalPoints.push([Math.random() * (width-1), Math.random() * (height-1)]);
        }
    }


    // 3. Perform Delaunay Triangulation
    const delaunay = d3.Delaunay.from(finalPoints);

    // 4. Create output canvas and draw the low-poly triangles
    const outputCanvas = document.createElement('canvas');
    outputCanvas.width = width;
    outputCanvas.height = height;
    const outCtx = outputCanvas.getContext('2d');
    outCtx.imageSmoothingEnabled = false; // Often preferred for crisp triangles
    outCtx.clearRect(0, 0, width, height);

    for (let i = 0; i < delaunay.triangles.length; i += 3) {
        const p1_idx = delaunay.triangles[i];
        const p2_idx = delaunay.triangles[i+1];
        const p3_idx = delaunay.triangles[i+2];

        const t_points = [
            finalPoints[p1_idx],
            finalPoints[p2_idx],
            finalPoints[p3_idx]
        ];
        
        // Calculate centroid of the triangle to sample color
        const cx = (t_points[0][0] + t_points[1][0] + t_points[2][0]) / 3;
        const cy = (t_points[0][1] + t_points[1][1] + t_points[2][1]) / 3;

        // Clamp coordinates and get integer pixel index for color sampling
        const x_sample = Math.floor(Math.max(0, Math.min(width - 1, cx)));
        const y_sample = Math.floor(Math.max(0, Math.min(height - 1, cy)));
        
        const originalPixelIndex = (y_sample * width + x_sample) * 4;
        const r = originalImageData.data[originalPixelIndex];
        const g = originalImageData.data[originalPixelIndex + 1];
        const b = originalImageData.data[originalPixelIndex + 2];
        // const a = originalImageData.data[originalPixelIndex + 3]; // Alpha, if needed

        outCtx.fillStyle = `rgb(${r},${g},${b})`;
        
        outCtx.beginPath();
        outCtx.moveTo(t_points[0][0], t_points[0][1]);
        outCtx.lineTo(t_points[1][0], t_points[1][1]);
        outCtx.lineTo(t_points[2][0], t_points[2][1]);
        outCtx.closePath();
        outCtx.fill();
        
        // Optional: Add stroke to triangles (can be a parameter)
        // outCtx.strokeStyle = "rgba(0,0,0,0.05)"; 
        // outCtx.lineWidth = 0.5;
        // outCtx.stroke();
    }

    return outputCanvas;
}

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 Low Poly Filter Application enables users to transform images into a low poly art style. By adjusting parameters such as the number of points, selection modes, and edge detection thresholds, users can create unique visual effects that emphasize angular shapes and color gradients. This tool is perfect for artists, designers, and anyone looking to create stylized graphics, illustrations, or designs for digital artwork, game assets, and social media content.

Leave a Reply

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