You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg) {
// Initialize a static-like variable on the function object itself for the script loading promise.
// This helps manage script loading status across multiple calls to processImage.
if (typeof processImage.dspJsLoadingPromise === 'undefined') {
processImage.dspJsLoadingPromise = null;
}
// --- Helper: Dynamically load dsp.js library ---
async function ensureDspJsLoadedInternal() {
const dspJsUrl = 'https://cdnjs.cloudflare.com/ajax/libs/dsp.js/1.0.1/dsp.min.js';
// If dsp.js is already loaded and good, resolve immediately
if (typeof dsp !== 'undefined' && typeof dsp.FFT !== 'undefined') {
return Promise.resolve();
}
// If a loading promise already exists, return it
if (processImage.dspJsLoadingPromise) {
return processImage.dspJsLoadingPromise;
}
// Create a new promise for loading the script
processImage.dspJsLoadingPromise = new Promise((resolve, reject) => {
const existingScript = document.querySelector(`script[src="${dspJsUrl}"]`);
if (existingScript) {
// If script tag exists, it might be loading or loaded. Poll for dsp.FFT.
let attempts = 0;
const interval = setInterval(() => {
attempts++;
if (typeof dsp !== 'undefined' && typeof dsp.FFT !== 'undefined') {
clearInterval(interval);
resolve();
} else if (attempts > 60) { // Max 6 seconds wait (60 * 100ms)
clearInterval(interval);
// Clean up promise to allow retry if script failed to make dsp.FFT available
processImage.dspJsLoadingPromise = null;
// Optionally remove script if it's problematic:
// if (document.head.contains(existingScript)) document.head.removeChild(existingScript);
reject(new Error(`dsp.js script was present but dsp.FFT never became available.`));
}
}, 100);
return;
}
// If no script tag exists, create and append it
const script = document.createElement('script');
script.src = dspJsUrl;
script.onload = () => {
if (typeof dsp !== 'undefined' && typeof dsp.FFT !== 'undefined') {
resolve();
} else {
processImage.dspJsLoadingPromise = null; // Allow retry
if (document.head.contains(script)) document.head.removeChild(script);
reject(new Error(`dsp.js loaded but dsp.FFT is not defined.`));
}
};
script.onerror = (event) => {
processImage.dspJsLoadingPromise = null; // Allow retry
if (document.head.contains(script)) document.head.removeChild(script);
// event is an ErrorEvent, event.message might not be available
// For script load errors, target.src gives URL, event.type gives 'error'
reject(new Error(`Failed to load dsp.js from ${dspJsUrl}. Event type: ${event.type}`));
};
document.head.appendChild(script);
});
return processImage.dspJsLoadingPromise;
}
// --- Helper: Calculate the next power of 2 ---
function nextPowerOf2(n) {
if (n === 0) return 1;
n--;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
n++;
return n;
}
// --- Error Canvas Utility ---
function createErrorCanvas(message, width, height) {
const errCanvas = document.createElement('canvas');
errCanvas.width = width || 200;
errCanvas.height = height || 100;
const ctx = errCanvas.getContext('2d');
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, errCanvas.width, errCanvas.height);
ctx.fillStyle = 'red';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Simple text wrapping
const words = message.split(' ');
let line = '';
let y = errCanvas.height / 2 - ( (message.length > 30 ? 1 : 0) * 10); // Adjust y for multi-line
for(let i = 0; i < words.length; i++) {
const testLine = line + words[i] + ' ';
if (ctx.measureText(testLine).width > errCanvas.width - 20 && i > 0) {
ctx.fillText(line, errCanvas.width / 2, y);
line = words[i] + ' ';
y += 20; // Line height
} else {
line = testLine;
}
}
ctx.fillText(line, errCanvas.width / 2, y);
return errCanvas;
}
// --- Main Image Processing Logic ---
try {
await ensureDspJsLoadedInternal();
// Verify dsp object after loading
if (typeof dsp === 'undefined' || typeof dsp.FFT === 'undefined') {
return createErrorCanvas('DSP library loaded but FFT is not available.', originalImg.width, originalImg.height);
}
} catch (error) {
console.error("Error loading DSP library:", error);
return createErrorCanvas(`Error loading DSP library: ${error.message}`, originalImg.width, originalImg.height);
}
if (!originalImg || originalImg.width === 0 || originalImg.height === 0) {
console.error("Invalid input image provided.");
return createErrorCanvas("Invalid input image.", 200,100);
}
const H = originalImg.height;
const W = originalImg.width;
// Use dimensions that are powers of 2 for FFT efficiency
const fftHeight = nextPowerOf2(H);
const fftWidth = nextPowerOf2(W);
// 1. Create a canvas to get image data and convert to grayscale
const inputCanvas = document.createElement('canvas');
inputCanvas.width = fftWidth;
inputCanvas.height = fftHeight;
const inputCtx = inputCanvas.getContext('2d');
// Draw image and pad with black if fft dimensions are larger
inputCtx.fillStyle = 'black';
inputCtx.fillRect(0, 0, fftWidth, fftHeight);
inputCtx.drawImage(originalImg, 0, 0, W, H);
const imageData = inputCtx.getImageData(0, 0, fftWidth, fftHeight);
const pixelData = imageData.data;
// Grayscale data array [fftHeight][fftWidth]
const grayData = Array(fftHeight).fill(null).map(() => Array(fftWidth).fill(0));
for (let r = 0; r < fftHeight; r++) {
for (let c = 0; c < fftWidth; c++) {
const i = (r * fftWidth + c) * 4;
// Standard luminance calculation
grayData[r][c] = 0.299 * pixelData[i] + 0.587 * pixelData[i+1] + 0.114 * pixelData[i+2];
}
}
// 2. Perform 2D FFT
// `tempComplexData` stores {real, imag} after row FFTs
const tempComplexData = Array(fftHeight).fill(null).map(() => Array(fftWidth).fill(null));
// a. Row-wise FFTs
const rowFFT = new dsp.FFT(fftWidth, 44100); // Sample rate doesn't matter for images
for (let r = 0; r < fftHeight; r++) {
const rowSignal = grayData[r];
const spectrum = rowFFT.forward(rowSignal); // Returns [re, im, re, im, ...]
for (let c = 0; c < fftWidth; c++) {
tempComplexData[r][c] = { real: spectrum[c*2], imag: spectrum[c*2+1] };
}
}
// `ftComplex` stores final {real, imag} after column FFTs
const ftComplex = Array(fftHeight).fill(null).map(() => Array(fftWidth).fill(null));
// b. Column-wise FFTs
const colFFT = new dsp.FFT(fftHeight, 44100);
for (let c = 0; c < fftWidth; c++) {
const colSignalReal = new Float32Array(fftHeight);
const colSignalImag = new Float32Array(fftHeight);
for (let r = 0; r < fftHeight; r++) {
colSignalReal[r] = tempComplexData[r][c].real;
colSignalImag[r] = tempComplexData[r][c].imag;
}
const spectrumReal = colFFT.forward(colSignalReal); // FFT of real parts
const spectrumImag = colFFT.forward(colSignalImag); // FFT of imaginary parts
for (let r = 0; r < fftHeight; r++) {
// F(u,v) = FFT_col( FFT_row(f(x,y)) )
// FFT_col( R_rc + i*I_rc ) = FFT_col(R_rc) + i*FFT_col(I_rc)
// Let FFT_col(R_rc) = Ar + i*Br for current (r,c)
// Let FFT_col(I_rc) = Cr + i*Dr for current (r,c)
// Result is (Ar + i*Br) + i*(Cr + i*Dr) = (Ar - Dr) + i*(Br + Cr)
const Ar = spectrumReal[r*2];
const Br = spectrumReal[r*2+1];
const Cr = spectrumImag[r*2];
const Dr = spectrumImag[r*2+1];
ftComplex[r][c] = { real: Ar - Dr, imag: Br + Cr };
}
}
// 3. FFT Shift (center the DC component)
const shiftedFtComplex = Array(fftHeight).fill(null).map(() => Array(fftWidth).fill(null));
const H_half = Math.floor(fftHeight / 2);
const W_half = Math.floor(fftWidth / 2);
for (let r = 0; r < fftHeight; r++) {
for (let c = 0; c < fftWidth; c++) {
const shifted_r = (r + H_half) % fftHeight;
const shifted_c = (c + W_half) % fftWidth;
shiftedFtComplex[shifted_r][shifted_c] = ftComplex[r][c];
}
}
// 4. Calculate Magnitude Spectrum and apply Logarithmic Scale
const logMagnitude = Array(fftHeight).fill(null).map(() => Array(fftWidth).fill(0));
let minLogMag = Infinity;
let maxLogMag = -Infinity;
for (let r = 0; r < fftHeight; r++) {
for (let c = 0; c < fftWidth; c++) {
const val = shiftedFtComplex[r][c];
const mag = Math.sqrt(val.real * val.real + val.imag * val.imag);
const logMagVal = Math.log(1 + mag); // log(1+M) to handle M=0 and compress dynamic range
logMagnitude[r][c] = logMagVal;
if (logMagVal < minLogMag) minLogMag = logMagVal;
if (logMagVal > maxLogMag) maxLogMag = logMagVal;
}
}
// 5. Normalize to 0-255 for display
const outputCanvas = document.createElement('canvas');
outputCanvas.width = fftWidth;
outputCanvas.height = fftHeight;
const outputCtx = outputCanvas.getContext('2d');
const outputImageData = outputCtx.createImageData(fftWidth, fftHeight);
const outputPixelData = outputImageData.data;
const range = maxLogMag - minLogMag;
const scale = (range === 0) ? 0 : 255 / range;
for (let r = 0; r < fftHeight; r++) {
for (let c = 0; c < fftWidth; c++) {
const normVal = Math.round((logMagnitude[r][c] - minLogMag) * scale);
const i = (r * fftWidth + c) * 4;
outputPixelData[i] = normVal; // R
outputPixelData[i+1] = normVal; // G
outputPixelData[i+2] = normVal; // B
outputPixelData[i+3] = 255; // A
}
}
outputCtx.putImageData(outputImageData, 0, 0);
return outputCanvas;
}
Apply Changes