Please bookmark this page to avoid losing your image tool!

Image Grillz And Smile Overlay Tool

(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, smileImgSrc = 'data:image/png;base64,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') {

    const FACE_API_URL = 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js';
    const MODEL_URL = 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/weights';

    /**
     * Dynamically loads the face-api.js library and its models if not already loaded.
     */
    const loadFaceApi = async () => {
        if (!window.faceapi) {
            await new Promise((resolve, reject) => {
                const script = document.createElement('script');
                script.src = FACE_API_URL;
                script.onload = resolve;
                script.onerror = reject;
                document.head.appendChild(script);
            });
        }
        
        if (!window.faceapi.nets.ssdMobilenetv1.isLoaded) {
             await Promise.all([
                faceapi.nets.ssdMobilenetv1.loadFromUri(MODEL_URL),
                faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL)
            ]);
        }
    };

    /**
     * Loads an image from a given source URL or data URI.
     * @param {string} src The source of the image.
     * @returns {Promise<Image>} A promise that resolves with the loaded Image object.
     */
    const loadImage = (src) => {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.crossOrigin = "Anonymous"; // Handle potential CORS issues with image sources
            img.onload = () => resolve(img);
            img.onerror = (err) => reject(new Error(`Failed to load image from src: ${src.substring(0, 100)}...`));
            img.src = src;
        });
    };

    /**
     * Returns a canvas with the original image Drawn on it.
     * Used as a fallback for errors or when no faces are detected.
     */
    const getOriginalAsCanvas = () => {
        const canvas = document.createElement('canvas');
        canvas.width = originalImg.width;
        canvas.height = originalImg.height;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(originalImg, 0, 0);
        return canvas;
    };


    try {
        await loadFaceApi();
        const smileImg = await loadImage(smileImgSrc);

        // Detect faces and landmarks in both images
        const detectionOptions = new faceapi.SsdMobilenetv1Options();
        const [originalDetections, smileDetections] = await Promise.all([
            faceapi.detectAllFaces(originalImg, detectionOptions).withFaceLandmarks(),
            faceapi.detectAllFaces(smileImg, detectionOptions).withFaceLandmarks()
        ]);

        if (originalDetections.length === 0 || smileDetections.length === 0) {
            console.warn("Could not detect a face in one or both images. Returning original image.");
            return getOriginalAsCanvas();
        }

        // Use the first detected face in each image
        const originalLandmarks = originalDetections[0].landmarks;
        const smileLandmarks = smileDetections[0].landmarks;

        // --- 1. Extract the smile from the source image ---
        const sourceMouth = smileLandmarks.getMouth(); // Landmarks 48-67
        
        // Find bounding box for the mouth to create a tight crop
        const bounds = faceapi.getMediaDimensions(smileImg);
        const MouthBoundingBox = new faceapi.Rect(
          Math.min(...sourceMouth.map(p => p.x)),
          Math.min(...sourceMouth.map(p => p.y)),
          Math.max(...sourceMouth.map(p => p.x)) - Math.min(...sourceMouth.map(p => p.x)),
          Math.max(...sourceMouth.map(p => p.y)) - Math.min(...sourceMouth.map(p => p.y))
        );

        const smileCropCanvas = document.createElement('canvas');
        smileCropCanvas.width = MouthBoundingBox.width;
        smileCropCanvas.height = MouthBoundingBox.height;
        const cropCtx = smileCropCanvas.getContext('2d');

        // Create a clipping mask from the mouth landmarks
        cropCtx.translate(-MouthBoundingBox.x, -MouthBoundingBox.y);
        cropCtx.beginPath();
        cropCtx.moveTo(sourceMouth[0].x, sourceMouth[0].y);
        for(let i = 1; i < sourceMouth.length; i++){
            // use outer contour for a better clip shape
            cropCtx.lineTo(sourceMouth[i].x, sourceMouth[i].y);
        }
        cropCtx.closePath();
        cropCtx.clip();
        cropCtx.drawImage(smileImg, 0, 0, bounds.width, bounds.height);


        // --- 2. Calculate transformation parameters ---
        const sourceLeftCorner = smileLandmarks.getMouth()[0]; // Landmark 48
        const sourceRightCorner = smileLandmarks.getMouth()[6]; // Landmark 54
        const sourceWidth = sourceRightCorner.x - sourceLeftCorner.x;
        const sourceCenter = {
             x: (sourceLeftCorner.x + sourceRightCorner.x) / 2,
             y: (sourceLeftCorner.y + sourceRightCorner.y) / 2
        };


        const targetMouth = originalLandmarks.getMouth();
        const targetLeftCorner = targetMouth[0];
        const targetRightCorner = targetMouth[6];
        const targetCenter = {
            x: (targetLeftCorner.x + targetRightCorner.x) / 2,
            y: (targetLeftCorner.y + targetRightCorner.y) / 2
        };

        const targetWidth = Math.sqrt(Math.pow(targetRightCorner.x - targetLeftCorner.x, 2) + Math.pow(targetRightCorner.y - targetLeftCorner.y, 2));
        const targetAngle = Math.atan2(targetRightCorner.y - targetLeftCorner.y, targetRightCorner.x - targetLeftCorner.x);
        const scale = targetWidth / sourceWidth;
        
        // --- 3. Composite the images ---
        const resultCanvas = document.createElement('canvas');
        resultCanvas.width = originalImg.width;
        resultCanvas.height = originalImg.height;
        const resultCtx = resultCanvas.getContext('2d');

        // Draw the original image first
        resultCtx.drawImage(originalImg, 0, 0);

        // Transform and draw the smile overlay
        resultCtx.save();
        resultCtx.translate(targetCenter.x, targetCenter.y);
        resultCtx.rotate(targetAngle);
        
        // Center the cropped smile image based on the mouth's center within the crop
        const sourceCenterInCrop = {
          x: sourceCenter.x - MouthBoundingBox.x,
          y: sourceCenter.y - MouthBoundingBox.y
        }

        const scaledCropWidth = smileCropCanvas.width * scale;
        const scaledCropHeight = smileCropCanvas.height * scale;

        resultCtx.drawImage(
            smileCropCanvas,
            -sourceCenterInCrop.x * scale,
            -sourceCenterInCrop.y * scale,
            scaledCropWidth,
            scaledCropHeight
        );

        resultCtx.restore();

        return resultCanvas;

    } catch (error) {
        console.error("An error occurred during image processing:", error);
        // Fallback to returning the original image
        return getOriginalAsCanvas();
    }
}

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 Grillz and Smile Overlay Tool allows users to overlay a smiling mouth image onto a photo of a face. This tool is particularly useful for creating humorous images, memes, or digital art where a cheerful expression is added to a neutral face. By detecting facial landmarks, the tool precisely aligns the smile to fit seamlessly onto the original image, providing an easy and entertaining way to enhance personal photos or create fun graphics.

Leave a Reply

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