You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, omega = 0.95, t0 = 0.1, patchSize = 15, airlightPercentile = 0.001, transmissionBlurRadius = 0) {
// Ensure numeric parameters are valid
omega = Number(omega);
t0 = Number(t0);
patchSize = Math.max(1, Math.floor(Number(patchSize)));
airlightPercentile = Number(airlightPercentile);
transmissionBlurRadius = Math.max(0, Math.floor(Number(transmissionBlurRadius)));
const canvas = document.createElement('canvas');
// Use { willReadFrequently: true } for potential performance optimization hint
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = originalImg.width;
canvas.height = originalImg.height;
if (canvas.width === 0 || canvas.height === 0) {
return canvas; // Return empty canvas for zero-dimension image
}
ctx.drawImage(originalImg, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const { data, width, height } = imageData;
const numPixels = width * height;
// Helper: Min filter for a 2D array (single channel map)
// filterRadius: kernel extends 'filterRadius' pixels from center (kernel size (2*R+1)x(2*R+1))
function minFilter(inputMap, w, h, filterRadius) {
const outputMap = new Float32Array(w * h);
if (filterRadius <= 0) { // Kernel 1x1, no actual filtering
for(let i=0; i < inputMap.length; ++i) outputMap[i] = inputMap[i];
return outputMap;
}
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let minVal = Infinity;
for (let j = -filterRadius; j <= filterRadius; j++) {
for (let i = -filterRadius; i <= filterRadius; i++) {
const currentY = Math.min(h - 1, Math.max(0, y + j));
const currentX = Math.min(w - 1, Math.max(0, x + i));
minVal = Math.min(minVal, inputMap[currentY * w + currentX]);
}
}
outputMap[y * w + x] = minVal;
}
}
return outputMap;
}
// Helper: Box blur for a 2D array
// blurRadius: same meaning as filterRadius for minFilter.
function boxBlur(inputMap, w, h, blurRadius) {
if (blurRadius <= 0) { // No blurring
const outputMap = new Float32Array(w * h);
for(let i=0; i<inputMap.length; ++i) outputMap[i] = inputMap[i];
return outputMap;
}
const outputMap = new Float32Array(w * h);
const kernelPixelCount = (2 * blurRadius + 1) * (2 * blurRadius + 1);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let sum = 0;
for (let j = -blurRadius; j <= blurRadius; j++) {
for (let i = -blurRadius; i <= blurRadius; i++) {
const currentY = Math.min(h - 1, Math.max(0, y + j));
const currentX = Math.min(w - 1, Math.max(0, x + i));
sum += inputMap[currentY * w + currentX];
}
}
outputMap[y * w + x] = sum / kernelPixelCount;
}
}
return outputMap;
}
// 1. Dark Channel Calculation (for airlight estimation)
const darkChannel = new Float32Array(numPixels);
for (let i = 0; i < numPixels; i++) {
darkChannel[i] = Math.min(data[i * 4], data[i * 4 + 1], data[i * 4 + 2]);
}
// 2. Airlight Estimation (A = Ar, Ag, Ab)
const numAirlightPixelsToConsider = Math.max(1, Math.floor(numPixels * airlightPercentile));
const sortedDarkChannelPixels = Array.from(darkChannel)
.map((val, originalIndex) => ({ val, originalIndex }))
.sort((a, b) => b.val - a.val); // Sort descending by dark channel value
let Ar_sum = 0, Ag_sum = 0, Ab_sum = 0;
let numEffectiveAirlightPixels = 0;
for (let i = 0; i < numAirlightPixelsToConsider; i++) {
if (i >= sortedDarkChannelPixels.length) break;
const pixelOriginalIndex = sortedDarkChannelPixels[i].originalIndex;
Ar_sum += data[pixelOriginalIndex * 4];
Ag_sum += data[pixelOriginalIndex * 4 + 1];
Ab_sum += data[pixelOriginalIndex * 4 + 2];
numEffectiveAirlightPixels++;
}
let Ar, Ag, Ab;
if (numEffectiveAirlightPixels > 0) {
Ar = Ar_sum / numEffectiveAirlightPixels;
Ag = Ag_sum / numEffectiveAirlightPixels;
Ab = Ab_sum / numEffectiveAirlightPixels;
} else {
// Fallback if no pixels considered (e.g. tiny image, extreme percentile)
Ar = 220; Ag = 220; Ab = 220; // Default to a bright grey
}
// Ensure airlight components are not too small to avoid division issues.
Ar = Math.max(1.0, Ar);
Ag = Math.max(1.0, Ag);
Ab = Math.max(1.0, Ab);
// 3. Transmission Map Estimation (t(x))
const minNormalizedChannels = new Float32Array(numPixels);
for (let i = 0; i < numPixels; i++) {
minNormalizedChannels[i] = Math.min(
data[i * 4] / Ar,
data[i * 4 + 1] / Ag,
data[i * 4 + 2] / Ab
);
}
const cfgMinFilterRadius = Math.floor(patchSize / 2);
const darkChannelOfNormalizedPatched = minFilter(minNormalizedChannels, width, height, cfgMinFilterRadius);
const transmission = new Float32Array(numPixels);
for (let i = 0; i < numPixels; i++) {
let term = darkChannelOfNormalizedPatched[i];
// Clamp term: min_patch(min_c(Ic/Ac)) is an estimate for (1 - true_transmission), so should be [0,1]
term = Math.max(0.0, Math.min(term, 1.0));
transmission[i] = 1.0 - omega * term;
// Clamp transmission itself to [0,1]
transmission[i] = Math.max(0.0, Math.min(transmission[i], 1.0));
}
// 4. Transmission Map Refinement
let refinedTransmission = transmission;
if (transmissionBlurRadius > 0) {
refinedTransmission = boxBlur(transmission, width, height, transmissionBlurRadius);
}
// 5. Recover Scene Radiance (J)
const outputImageData = ctx.createImageData(width, height);
for (let i = 0; i < numPixels; i++) {
const t_val = Math.max(refinedTransmission[i], t0);
outputImageData.data[i * 4] = Math.max(0, Math.min(255, (data[i * 4] - Ar) / t_val + Ar));
outputImageData.data[i * 4 + 1] = Math.max(0, Math.min(255, (data[i * 4 + 1] - Ag) / t_val + Ag));
outputImageData.data[i * 4 + 2] = Math.max(0, Math.min(255, (data[i * 4 + 2] - Ab) / t_val + Ab));
outputImageData.data[i * 4 + 3] = data[i * 4 + 3]; // Preserve alpha
}
ctx.putImageData(outputImageData, 0, 0);
return canvas;
}
Apply Changes