import { useEffect, useMemo, useState } from "react"; const MAX_SIZE: number = 500; function getBoundedDimensions(width: number, height: number): [number, number] { if (width <= MAX_SIZE && height <= MAX_SIZE) { return [width, height]; } const scale = Math.min(MAX_SIZE / width, MAX_SIZE / height); const boundedWidth = Math.max(1, Math.round(width * scale)); const boundedHeight = Math.max(1, Math.round(height * scale)); return [boundedWidth, boundedHeight]; } async function getImageData(imageUrl: string): Promise { const image = await new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => resolve(img); img.onerror = reject; img.src = imageUrl; }) const sourceWidth = image.naturalWidth || image.width; const sourceHeight = image.naturalHeight || image.height; const [targetWidth, targetHeight] = getBoundedDimensions(sourceWidth, sourceHeight); const canvas = document.createElement("canvas") canvas.width = targetWidth; canvas.height = targetHeight; const ctx = canvas.getContext("2d"); if (!ctx) { throw new Error("Failed to get canvas context"); } ctx.drawImage(image, 0, 0, targetWidth, targetHeight); return ctx.getImageData(0, 0, canvas.width, canvas.height); } function getImageUrl(imageData: ImageData): string { const canvas = document.createElement("canvas"); canvas.width = imageData.width; canvas.height = imageData.height; const ctx = canvas.getContext("2d") if (!ctx) { throw new Error("Failed to get canvas context"); } ctx.putImageData(imageData, 0, 0); return canvas.toDataURL("image/png"); } function convertToGrayscale(imageData: ImageData): ImageData { const output = new ImageData( new Uint8ClampedArray(imageData.data), imageData.width, imageData.height, ); const data = output.data; for (let i = 0; i < data.length; i += 4) { const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; const gray = 0.299 * r + 0.587 * g + 0.114 * b; data[i] = data[i + 1] = data[i + 2] = gray; } return output; } function convolve(imageData: ImageData, kernel: number[][] | number[][][]): ImageData { if (Array.isArray(kernel[0][0])) { // 3D kernel (color) return convolveColor(imageData, kernel as number[][][]); } else { // 2D kernel (grayscale) return convolveGray(imageData, kernel as number[][]); } } function convolveGray(image: ImageData, kernel: number[][]): ImageData { const kernelWidth = kernel[0].length; const kernelHeight = kernel.length; const width = image.width; const height = image.height; const inputData = image.data; const outputWidth = width - kernelWidth + 1; const outputHeight = height - kernelHeight + 1; const outputData = new Uint8ClampedArray(outputWidth * outputHeight * 4); for (let y = 0; y < outputHeight; ++y) { for (let x = 0; x < outputWidth; ++x) { // dot product let sum = 0; for (let ky = 0; ky < kernelHeight; ++ky) { for (let kx = 0; kx < kernelWidth; ++kx) { const pixelIndex = ((y + ky) * width + (x + kx)) * 4; const pixelValue = inputData[pixelIndex]; const kernelValue = kernel[ky][kx]; sum += pixelValue * kernelValue; } } const outputIndex = (y * outputWidth + x) * 4; const clampedValue = Math.min(Math.max(sum, 0), 255); outputData[outputIndex] = clampedValue; // R outputData[outputIndex + 1] = clampedValue; // G outputData[outputIndex + 2] = clampedValue; // B outputData[outputIndex + 3] = 255; // A } } return new ImageData(outputData, outputWidth, outputHeight); } function convolveColor(image: ImageData, kernel: number[][][]): ImageData { const kernelWidth = kernel[0][0].length; const kernelHeight = kernel[0].length; const width = image.width; const height = image.height; const inputData = image.data; const outputWidth = width - kernelWidth + 1; const outputHeight = height - kernelHeight + 1; const outputData = new Uint8ClampedArray(outputWidth * outputHeight * 4); for (let y = 0; y < outputHeight; ++y) { for (let x = 0; x < outputWidth; ++x) { // dot product over 3 channels let sum = 0; for (let ky = 0; ky < kernelHeight; ++ky) { for (let kx = 0; kx < kernelWidth; ++kx) { const pixelIndex = ((y + ky) * width + (x + kx)) * 4; const r = inputData[pixelIndex]; const g = inputData[pixelIndex + 1]; const b = inputData[pixelIndex + 2]; const kernelR = kernel[0][ky][kx]; const kernelG = kernel[1][ky][kx]; const kernelB = kernel[2][ky][kx]; sum += r * kernelR + g * kernelG + b * kernelB; } } const outputIndex = (y * outputWidth + x) * 4; const clampedValue = Math.min(Math.max(sum, 0), 255); outputData[outputIndex] = clampedValue; // R outputData[outputIndex + 1] = clampedValue; // G outputData[outputIndex + 2] = clampedValue; // B outputData[outputIndex + 3] = 255; // A } } return new ImageData(outputData, outputWidth, outputHeight); } export default function useConvolutionProcessing( rawInputImage: string, kernel: number[][][] | number[][], ): [string | null, string | null] { const useColor = Array.isArray(kernel[0][0]); // true if 3D kernel, false if 2D kernel const [rawImageData, setRawImageData] = useState(null); // extract input image data (array) useEffect(() => { let cancelled = false; async function processImage() { const imageData = await getImageData(rawInputImage); if (!cancelled) { setRawImageData(imageData); } } processImage(); return () => { cancelled = true; } }, [rawInputImage]); const processedImageData = useMemo(() => { if (!rawImageData) return null; return useColor ? rawImageData : convertToGrayscale(rawImageData); }, [rawImageData, useColor]); const outputImageData = useMemo(() => { if (!processedImageData) return null; return convolve(processedImageData, kernel); }, [processedImageData, kernel]); const inputImage = useMemo(() => { if (!processedImageData) return null; return getImageUrl(processedImageData); }, [processedImageData]); const outputImage = useMemo(() => { if (!outputImageData) return null; return getImageUrl(outputImageData); }, [outputImageData]); return [inputImage, outputImage]; }