Please bookmark this page to avoid losing your image tool!

Photo To Realistic CCTV Style Image Converter

(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.
/**
 * Processes an input image to make it look like it was taken by a realistic CCTV camera.
 * 
 * @param {HTMLImageElement} originalImg - The original source image to process.
 * @param {number|string} resolutionWidth - The width to scale down to (simulates low-res sensor).
 * @param {number|string} resolutionHeight - The height to scale down to (simulates low-res sensor).
 * @param {number|string} grayscaleAmount - 0.0 to 1.0 (typically CCTV is heavily desaturated or B&W).
 * @param {number|string} contrastAmount - 0.0 to 2.0 (CCTV usually has blown out or harsh contrast).
 * @param {number|string} brightnessAmount - 0.0 to 2.0 (Simulates exposure).
 * @param {number|string} blurAmount - Blur radius in pixels (low-quality lens focus).
 * @param {number|string} noiseAmount - Intensity of random sensor noise (0-255).
 * @param {number|string} jpegQuality - 0.0 to 1.0 (forces heavy compression artifacts, e.g. 0.15).
 * @param {number|string} motionBlurLength - Number of pixels to smear for simulated motion blur.
 * @param {number|string} motionBlurAngle - Angle in degrees for the motion blur direction.
 * @param {number|string} addOverlay - 1 to add fake camera date/time text, 0 to disable.
 * @returns {HTMLCanvasElement} A canvas element containing the processed image.
 */
async function processImage(
    originalImg,
    resolutionWidth = 640,
    resolutionHeight = 480,
    grayscaleAmount = 0.85,
    contrastAmount = 1.3,
    brightnessAmount = 0.9,
    blurAmount = 1.2,
    noiseAmount = 25,
    jpegQuality = 0.15,
    motionBlurLength = 0,
    motionBlurAngle = 0,
    addOverlay = 1
) {
    // Parse all parameters from strings to numbers to ensure math works correctly
    const width = Number(resolutionWidth);
    const height = Number(resolutionHeight);
    const grayscale = Number(grayscaleAmount);
    const contrast = Number(contrastAmount);
    const brightness = Number(brightnessAmount);
    const blur = Number(blurAmount);
    const noise = Number(noiseAmount);
    const jQuality = Number(jpegQuality);
    const mBlurLen = Number(motionBlurLength);
    const mBlurAng = Number(motionBlurAngle);
    const overlay = Number(addOverlay);

    // Create a new canvas element that will hold the processing steps
    const canvas = document.createElement('canvas');
    // Set the canvas width to the target low resolution
    canvas.width = width;
    // Set the canvas height to the target low resolution
    canvas.height = height;
    // Get the 2D drawing context from the canvas
    const ctx = canvas.getContext('2d', { willReadFrequently: true });

    // Apply multiple CSS-like filters: Grayscale, Contrast, Brightness, and Gaussian Blur
    ctx.filter = `grayscale(${grayscale * 100}%) contrast(${contrast * 100}%) brightness(${brightness * 100}%) blur(${blur}px)`;

    // Check if motion blur is requested (length > 0)
    if (mBlurLen > 0) {
        // Calculate transparency so overlapping frames add up to full opacity
        ctx.globalAlpha = 1.0 / (mBlurLen + 1);
        // Convert the blur angle from degrees to radians
        const angleRad = mBlurAng * Math.PI / 180;
        // Loop 'mBlurLen' times to draw shifted frames
        for (let i = 0; i <= mBlurLen; i++) {
            // Calculate X offset using cosine
            const offsetX = Math.cos(angleRad) * i;
            // Calculate Y offset using sine
            const offsetY = Math.sin(angleRad) * i;
            // Draw the image at the shifted position to construct motion blur
            ctx.drawImage(originalImg, offsetX, offsetY, width, height);
        }
        // Reset the global alpha back to 1.0 (fully opaque)
        ctx.globalAlpha = 1.0;
    } else {
        // If no motion blur, simply draw the image stretched to the low-res canvas
        ctx.drawImage(originalImg, 0, 0, width, height);
    }

    // Reset the context filter back to none so future items (noise, text) aren't blurred
    ctx.filter = 'none';

    // Check if sensor noise should be added
    if (noise > 0) {
        // Retrieve the pixel data of the entire canvas
        const imgData = ctx.getImageData(0, 0, width, height);
        // Reference the underlying 1D array of RGBA color values
        const data = imgData.data;
        // Loop through each pixel (4 values per pixel: R, G, B, A)
        for (let i = 0; i < data.length; i += 4) {
            // Generate a random noise value between -noise and +noise
            const n = (Math.random() - 0.5) * 2 * noise;
            // Apply noise to Red channel and clamp between 0-255
            data[i] = Math.max(0, Math.min(255, data[i] + n));
            // Apply noise to Green channel and clamp between 0-255
            data[i+1] = Math.max(0, Math.min(255, data[i+1] + n));
            // Apply noise to Blue channel and clamp between 0-255
            data[i+2] = Math.max(0, Math.min(255, data[i+2] + n));
            // Alpha channel (data[i+3]) is left unchanged
        }
        // Put the modified noisy pixel data back onto the canvas
        ctx.putImageData(imgData, 0, 0);
    }

    // Apply JPEG compression artifacts if quality is less than 1.0
    if (jQuality < 1.0) {
        // Convert the current canvas state into a low-quality JPEG Data URL string
        const jpegDataUrl = canvas.toDataURL('image/jpeg', jQuality);
        // Create a temporary Image object to load the compressed version
        const tempImg = new Image();
        // Set the source to the compressed JPEG string
        tempImg.src = jpegDataUrl;
        // Create a Promise to pause execution until the image finishes loading
        await new Promise(resolve => {
            // Resolve promise when load is successful
            tempImg.onload = resolve;
            // Resolve promise even if it errors to avoid freezing
            tempImg.onerror = resolve; 
        });
        // Clear the canvas to prep for drawing the artifact-heavy image
        ctx.clearRect(0, 0, width, height);
        // Draw the highly compressed JPEG image back over the canvas
        ctx.drawImage(tempImg, 0, 0, width, height);
    }

    // Add faint scanlines giving it an old interlaced monitor feel
    ctx.fillStyle = 'rgba(0, 0, 0, 0.15)'; // Semi-transparent black
    // Loop through the canvas vertically, skipping 3 pixels at a time
    for (let y = 0; y < height; y += 3) {
        // Draw a 1-pixel high horizontal line across the canvas width
        ctx.fillRect(0, y, width, 1);
    }

    // Check if the timestamp and camera ID overlay is requested
    if (overlay === 1) {
        // Calculate a responsive font size based on the canvas height (min 12px)
        const fontSize = Math.max(12, Math.floor(height * 0.04));
        // Set the font style to monospace to look like technical output
        ctx.font = `bold ${fontSize}px "Courier New", Courier, monospace`;
        // Align text to start drawing from the given X coordinate going right
        ctx.textAlign = 'left';
        // Align text baseline to top so it anchors safely at the top edge
        ctx.textBaseline = 'top';

        // Generate the current date and time
        const now = new Date();
        // Format the date/time string strictly (YYYY-MM-DD HH:MM:SS)
        const dateString = now.toISOString().replace('T', ' ').substring(0, 19);
        // Create a fake camera name like CAM-04 with the timestamp
        const camText = `CAM-${Math.floor(Math.random() * 9 + 1).toString().padStart(2, '0')} ${dateString}`;

        // Set the outline thickness for the text so it can be seen uniformly
        ctx.lineWidth = 3;
        // Set stroke color to black
        ctx.strokeStyle = '#000000';
        // Set fill color to white
        ctx.fillStyle = '#FFFFFF';

        // Draw the black outline of the top-left camera text
        ctx.strokeText(camText, 10, 10);
        // Draw the white inside of the top-left camera text
        ctx.fillText(camText, 10, 10);

        // Switch text alignment to right so the right-side text anchor works
        ctx.textAlign = 'right';
        // Define standard recording indicator text
        const recText = 'REC ';
        // Draw the black outline of 'REC' in the top-right corner
        ctx.strokeText(recText, width - 25, 10);
        // Draw the white inside of 'REC' in the top-right corner
        ctx.fillText(recText, width - 25, 10);

        // Change fill color to red to draw the REC circle
        ctx.fillStyle = '#FF0000';
        // Start a new vector path for the recording dot
        ctx.beginPath();
        // Define the X position for the center of the recording dot
        const dotX = width - 15;
        // Define the Y position for the center of the recording dot
        const dotY = 10 + (fontSize / 2);
        // Map out a full circle for the dot
        ctx.arc(dotX, dotY, fontSize / 3, 0, Math.PI * 2);
        // Fill the circle with the red color
        ctx.fill();
        // Stroke the black outline around the circle for extra contrast
        ctx.stroke();
    }

    // Return the final processed Canvas element containing the realistic CCTV image
    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

This tool transforms your standard photos into realistic CCTV-style security footage. It simulates the visual characteristics of surveillance cameras by adjusting resolution, applying grayscale filters, and manipulating contrast and brightness. Additionally, it can add technical effects such as sensor noise, motion blur, JPEG compression artifacts, and scanlines. You can also include a digital overlay featuring a timestamp and a recording indicator to complete the look. This tool is ideal for creative projects, film production, or adding a suspenseful atmosphere to digital content.

Leave a Reply

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