You can edit the below JavaScript code to customize the image tool.
Apply Changes
// Helper to manage FFT library loading
// Prefixing with _processImage_ to avoid global scope conflicts, ensuring it's unique.
let _processImage_fftJsPromise = null;
function _processImage_loadFftJs() {
if (!_processImage_fftJsPromise) {
_processImage_fftJsPromise = new Promise((resolve, reject) => {
if (typeof FFT !== 'undefined') { // Check if FFT class from fft.js is already global
resolve(FFT); // FFT is the constructor from fft.js
return;
}
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/fft.js/4.0.4/fft.js';
script.onload = () => {
if (typeof FFT !== 'undefined') {
resolve(FFT);
} else {
reject(new Error('FFT library loaded but FFT class not found.'));
}
};
script.onerror = (err) => {
console.error("Error loading FFT.js for Image Sound Spectrogram Filter:", err);
reject(new Error('Failed to load FFT.js library from CDN. Please check network connection or CDN status.'));
};
document.head.appendChild(script);
});
}
return _processImage_fftJsPromise;
}
// Color mapping data and functions, prefixed for uniqueness.
const _processImage_viridisPoints = [
{ val: 0.0, rgb: [0.267004, 0.004874, 0.329415] }, // #440154
{ val: 0.25, rgb: [0.229739, 0.322361, 0.548813] }, // #39568C
{ val: 0.5, rgb: [0.127568, 0.566949, 0.550556] }, // #22908C
{ val: 0.75, rgb: [0.369214, 0.788888, 0.382930] }, // #5FC962
{ val: 1.0, rgb: [0.993248, 0.906157, 0.143936] } // #FDE725
];
function _processImage_viridisColorMap(t) {
t = Math.max(0, Math.min(1, t));
let p0 = _processImage_viridisPoints[0], p1 = _processImage_viridisPoints[0];
if (t <= _processImage_viridisPoints[0].val) {
// Handled by initial p0, p1
} else if (t >= _processImage_viridisPoints[_processImage_viridisPoints.length - 1].val) {
p0 = _processImage_viridisPoints[_processImage_viridisPoints.length - 1];
p1 = _processImage_viridisPoints[_processImage_viridisPoints.length - 1];
} else {
for (let i = 0; i < _processImage_viridisPoints.length - 1; i++) {
if (t >= _processImage_viridisPoints[i].val && t < _processImage_viridisPoints[i+1].val) {
p0 = _processImage_viridisPoints[i];
p1 = _processImage_viridisPoints[i+1];
break;
}
}
}
const T_val = (p1.val - p0.val === 0) ? 0 : (t - p0.val) / (p1.val - p0.val);
const r = Math.round((p0.rgb[0] * (1 - T_val) + p1.rgb[0] * T_val) * 255);
const g = Math.round((p0.rgb[1] * (1 - T_val) + p1.rgb[1] * T_val) * 255);
const b = Math.round((p0.rgb[2] * (1 - T_val) + p1.rgb[2] * T_val) * 255);
return [r,g,b];
}
function _processImage_jetColorMap(v) {
v = Math.max(0, Math.min(1, v));
// Standard Jet colormap: Dark Blue -> Blue -> Cyan -> Yellow -> Red -> Dark Red
const points = [
{ val: 0.0, rgb: [0, 0, 0.5] }, // Dark Blue
{ val: 0.125, rgb: [0, 0, 1] }, // Blue
{ val: 0.375, rgb: [0, 1, 1] }, // Cyan
{ val: 0.625, rgb: [1, 1, 0] }, // Yellow
{ val: 0.875, rgb: [1, 0, 0] }, // Red
{ val: 1.0, rgb: [0.5, 0, 0] } // Dark Red
];
let p0 = points[0], p1 = points[0];
if (v <= points[0].val) {
// Handled by initial p0,p1
} else if (v >= points[points.length - 1].val) {
p0 = points[points.length - 1];
p1 = points[points.length - 1];
} else {
for (let i = 0; i < points.length - 1; i++) {
if (v >= points[i].val && v < points[i+1].val) {
p0 = points[i];
p1 = points[i+1];
break;
}
}
}
const T_val = (p1.val - p0.val === 0) ? 0 : (v - p0.val) / (p1.val - p0.val);
const r = Math.round((p0.rgb[0] * (1 - T_val) + p1.rgb[0] * T_val) * 255);
const g = Math.round((p0.rgb[1] * (1 - T_val) + p1.rgb[1] * T_val) * 255);
const b = Math.round((p0.rgb[2] * (1 - T_val) + p1.rgb[2] * T_val) * 255);
return [r, g, b];
}
function _processImage_grayscaleColorMap(v) {
v = Math.max(0, Math.min(1, v));
const intensity = Math.round(v * 255);
return [intensity, intensity, intensity];
}
function _processImage_mapToColor(value, mapName) {
switch (mapName.toLowerCase()) {
case "jet":
return _processImage_jetColorMap(value);
case "viridis":
return _processImage_viridisColorMap(value);
case "grayscale":
default:
return _processImage_grayscaleColorMap(value);
}
}
async function processImage(originalImg, colorMapName = "viridis", logScaleMagnitude = 1, intensityFactor = 1.0, fftWindowFunction = "hann") {
const FFTConstructor = await _processImage_loadFftJs();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Ensure originalImg is loaded, though Image object implies it is.
// If originalImg.width/height are 0, it might not be loaded.
if (!originalImg.width || !originalImg.height) {
console.error("Original image not loaded or has zero dimensions.");
// Return an empty canvas or throw error
canvas.width = 1; canvas.height = 1;
return canvas;
}
canvas.width = originalImg.width;
canvas.height = originalImg.height;
const tempCanvas = document.createElement('canvas');
tempCanvas.width = originalImg.width;
tempCanvas.height = originalImg.height;
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
tempCtx.drawImage(originalImg, 0, 0);
const imageData = tempCtx.getImageData(0, 0, originalImg.width, originalImg.height);
const pixels = imageData.data;
const width = originalImg.width;
const height = originalImg.height;
const outputImageData = ctx.createImageData(width, height);
const outputPixels = outputImageData.data;
const N_fft = 1 << Math.ceil(Math.log2(height)); // Smallest power of 2 >= height
const fft = new FFTConstructor(N_fft);
const complexCoeffs = fft.createComplexArray();
const signal = new Float64Array(N_fft);
const windowCoeffs = new Float64Array(height);
if (height > 0) { // Avoid NaN for height=0, though unlikely
if (fftWindowFunction === "hann" && height > 1) {
for (let i = 0; i < height; i++) windowCoeffs[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / (height - 1)));
} else if (fftWindowFunction === "hamming" && height > 1) {
for (let i = 0; i < height; i++) windowCoeffs[i] = 0.54 - 0.46 * Math.cos(2 * Math.PI * i / (height - 1));
} else {
for (let i = 0; i < height; i++) windowCoeffs[i] = 1.0;
}
}
const spectrumMagnitudes = new Float64Array(N_fft / 2 + 1);
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const r_idx = (y * width + x) * 4;
const r = pixels[r_idx];
const g = pixels[r_idx + 1];
const b = pixels[r_idx + 2];
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255.0; // Normalize 0-1
signal[y] = luminance * windowCoeffs[y];
}
for (let y = height; y < N_fft; y++) {
signal[y] = 0; // Zero-pad
}
fft.realTransform(complexCoeffs, signal);
spectrumMagnitudes[0] = Math.abs(complexCoeffs[0]); // DC
if (N_fft > 0) { // Check N_fft to avoid issues if height was 0 -> N_fft=0
for (let k = 1; k < N_fft / 2; k++) {
spectrumMagnitudes[k] = Math.sqrt(complexCoeffs[2 * k] * complexCoeffs[2 * k] + complexCoeffs[2 * k + 1] * complexCoeffs[2 * k + 1]);
}
if (N_fft / 2 > 0) { // Nyquist only if N_fft >= 2
spectrumMagnitudes[N_fft / 2] = Math.abs(complexCoeffs[1]); // Nyquist
}
}
let minMag = Infinity, maxMag = -Infinity;
for (let k = 0; k <= N_fft / 2; k++) {
let mag = spectrumMagnitudes[k] * Math.max(0, intensityFactor); // Ensure positive factor
if (logScaleMagnitude === 1) {
mag = Math.log1p(mag); // log(1+mag)
}
spectrumMagnitudes[k] = mag;
if (mag < minMag) minMag = mag;
if (mag > maxMag) maxMag = mag;
}
const range = (maxMag > minMag) ? (maxMag - minMag) : 1;
const num_bins = N_fft / 2 + 1;
for (let y_out = 0; y_out < height; y_out++) {
const freq_pos_norm = (height === 1) ? 0 : (height - 1 - y_out) / (height - 1);
const bin_idx_float = freq_pos_norm * (num_bins - 1);
const bin_idx0 = Math.floor(bin_idx_float);
const bin_idx1 = Math.min(Math.ceil(bin_idx_float), num_bins - 1); // Clamp idx1
const frac = bin_idx_float - bin_idx0;
let mag_interpolated;
if (num_bins === 1) { // Only DC component if N_fft is small (e.g. 0 or 1)
mag_interpolated = spectrumMagnitudes[0];
} else if (bin_idx0 >= num_bins -1) { // Should effectively be caught by min with num_bins-1 for idx1
mag_interpolated = spectrumMagnitudes[num_bins-1];
} else if (bin_idx0 < 0) { // Should not happen
mag_interpolated = spectrumMagnitudes[0];
} else {
mag_interpolated = spectrumMagnitudes[bin_idx0] * (1 - frac) + spectrumMagnitudes[bin_idx1] * frac;
}
const norm_mag = (mag_interpolated - minMag) / range;
const color = _processImage_mapToColor(norm_mag, colorMapName);
const out_idx = (y_out * width + x) * 4;
outputPixels[out_idx] = color[0];
outputPixels[out_idx + 1] = color[1];
outputPixels[out_idx + 2] = color[2];
outputPixels[out_idx + 3] = 255;
}
}
ctx.putImageData(outputImageData, 0, 0);
return canvas;
}
Apply Changes