<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: 'Segoe UI', Arial, sans-serif; background: #1a1a1a; color: #eaeaea; margin: 0; padding: 0; }
header { background: #23272f; padding: 2rem 1rem 1rem 1rem; text-align: center; }
h1 { margin: 0; font-size: 2.5rem; color: #7fd8be; }
main { max-width: 900px; margin: 2rem auto; background: #23272f; border-radius: 12px; box-shadow: 0 0 24px #0006; padding: 2rem; }
section { margin-bottom: 2.5rem; }
label { display: block; margin-bottom: 0.7rem; font-weight: bold; }
input[type="file"] { margin-bottom: 1.2rem; }
.canvas-wrapper { display: flex; gap: 2rem; flex-wrap: wrap; justify-content: center; }
.canvas-group { text-align: center; }
canvas { background: #111; border-radius: 8px; margin-bottom: 0.5rem; }
.controls { margin: 1.5rem 0; display: flex; flex-wrap: wrap; gap: 1.5rem; justify-content: center; }
.controls label { font-weight: normal; }
.btn { background: #7fd8be; color: #23272f; border: none; border-radius: 6px; padding: 0.7rem 1.5rem; font-size: 1.1rem; font-weight: bold; cursor: pointer; transition: background 0.2s; }
.btn:hover { background: #5fb89e; }
.error { color: #f66; font-weight: bold; }
@media (max-width: 700px) {
.canvas-wrapper { flex-direction: column; align-items: center; }
}
</style>
</head>
<body>
<header>
<h1>Frequency Domain Image Manipulation</h1>
<p>
Upload your image and apply a <b>band-stop filter</b> in the frequency domain, optimized to confuse AI models while maintaining easy human interpretation.<br>
The default settings are tuned for a strong anti-matching effect, removing a mid-frequency band that disrupts AI feature extraction but preserves overall visual clarity for people.
</p>
</header>
<main>
<section>
<label for="imageUpload">Select Image (JPG/PNG, max 8MP):</label>
<input type="file" id="imageUpload" accept="image/png,image/jpeg" />
<span class="error" id="errorMsg"></span>
</section>
<section class="controls" id="controlsSection" style="display:none;">
<div>
<label for="filterType">Filter Type:</label>
<select id="filterType">
<option value="none">None</option>
<option value="lowpass">Low Pass</option>
<option value="highpass">High Pass</option>
<option value="bandstop">Band Stop</option>
<option value="notch">Notch</option>
<option value="custom">Custom Mask</option>
</select>
</div>
<div id="paramGroup">
<label for="radius">Radius / Width:</label>
<input type="range" id="radius" min="5" max="200" value="60" />
<span id="radiusValue">60</span>
</div>
<div id="bandGroup" style="display:none;">
<label for="bandWidth">Band Width:</label>
<input type="range" id="bandWidth" min="5" max="200" value="60" />
<span id="bandWidthValue">60</span>
</div>
<button class="btn" id="applyBtn">Apply Filter</button>
<button class="btn" id="downloadBtn">Download Result</button>
</section>
<section class="canvas-wrapper">
<div class="canvas-group">
<div>Original Image</div>
<canvas id="originalCanvas" width="320" height="320"></canvas>
</div>
<div class="canvas-group">
<div>Frequency Spectrum (Log Magnitude)</div>
<canvas id="spectrumCanvas" width="320" height="320"></canvas>
</div>
<div class="canvas-group">
<div>Modified Image</div>
<canvas id="resultCanvas" width="320" height="320"></canvas>
</div>
</section>
</main>
<script>
function isPowerOf2(x) { return (x & (x - 1)) === 0; }
function nextPowerOf2(x) { return 2 ** Math.ceil(Math.log2(x)); }
function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
class Complex {
constructor(re, im) { this.re = re; this.im = im; }
add(other) { return new Complex(this.re + other.re, this.im + other.im); }
sub(other) { return new Complex(this.re - other.re, this.im - other.im); }
mul(other) {
return new Complex(
this.re * other.re - this.im * other.im,
this.re * other.im + this.im * other.re
);
}
scale(s) { return new Complex(this.re * s, this.im * s); }
conj() { return new Complex(this.re, -this.im); }
abs() { return Math.hypot(this.re, this.im); }
}
function fft1d(arr, inverse = false) {
const n = arr.length;
if (!isPowerOf2(n)) throw new Error("Array length must be power of 2");
const pi = Math.PI;
for (let i = 0, j = 0; i < n; i++) {
if (i < j) [arr[i], arr[j]] = [arr[j], arr[i]];
let m = n >> 1;
while (m >= 1 && j >= m) { j -= m; m >>= 1; }
j += m;
}
for (let len = 2; len <= n; len <<= 1) {
const ang = 2 * pi / len * (inverse ? 1 : -1);
const wlen = new Complex(Math.cos(ang), Math.sin(ang));
for (let i = 0; i < n; i += len) {
let w = new Complex(1, 0);
for (let j = 0; j < len / 2; j++) {
const u = arr[i + j], v = arr[i + j + len / 2].mul(w);
arr[i + j] = u.add(v);
arr[i + j + len / 2] = u.sub(v);
w = w.mul(wlen);
}
}
}
if (inverse) for (let i = 0; i < n; i++) arr[i] = arr[i].scale(1 / n);
}
function fft2d(mat, inverse = false) {
const n = mat.length, m = mat[0].length;
for (let i = 0; i < n; i++) fft1d(mat[i], inverse);
for (let j = 0; j < m; j++) {
const col = Array(n);
for (let i = 0; i < n; i++) col[i] = mat[i][j];
fft1d(col, inverse);
for (let i = 0; i < n; i++) mat[i][j] = col[i];
}
}
function fftshift(mat) {
const n = mat.length, m = mat[0].length;
const out = Array.from({length: n}, () => Array(m));
for (let i = 0; i < n; i++)
for (let j = 0; j < m; j++)
out[i][j] = mat[(i + n/2) % n][(j + m/2) % m];
return out;
}
function ifftshift(mat) {
const n = mat.length, m = mat[0].length;
const out = Array.from({length: n}, () => Array(m));
for (let i = 0; i < n; i++)
for (let j = 0; j < m; j++)
out[i][j] = mat[(i + Math.floor(n/2)) % n][(j + Math.floor(m/2)) % m];
return out;
}
function drawSpectrum(ctx, freqMat) {
const n = freqMat.length, m = freqMat[0].length;
let maxMag = 0, minMag = 1e10;
const mag = Array.from({length: n}, () => Array(m));
for (let i = 0; i < n; i++)
for (let j = 0; j < m; j++) {
const v = freqMat[i][j].abs();
mag[i][j] = v;
if (v > maxMag) maxMag = v;
if (v < minMag) minMag = v;
}
for (let i = 0; i < n; i++)
for (let j = 0; j < m; j++)
mag[i][j] = Math.log(1 + mag[i][j]) / Math.log(1 + maxMag);
const imgData = ctx.createImageData(m, n);
for (let i = 0; i < n; i++)
for (let j = 0; j < m; j++) {
const v = Math.round(mag[i][j] * 255);
const idx = (i * m + j) * 4;
imgData.data[idx] = imgData.data[idx+1] = imgData.data[idx+2] = v;
imgData.data[idx+3] = 255;
}
ctx.putImageData(imgData, 0, 0);
}
function genMask(type, n, m, radius, bandWidth, notch) {
const mask = Array.from({length: n}, () => Array(m).fill(1));
const cx = n/2, cy = m/2;
for (let i = 0; i < n; i++) for (let j = 0; j < m; j++) {
const d = Math.hypot(i-cx, j-cy);
switch(type) {
case 'lowpass':
mask[i][j] = d <= radius ? 1 : 0;
break;
case 'highpass':
mask[i][j] = d >= radius ? 1 : 0;
break;
case 'bandstop':
mask[i][j] = (d < radius || d > radius+bandWidth) ? 1 : 0;
break;
case 'notch':
const dx = Math.abs(i-cx), dy = Math.abs(j-cy);
if ((Math.abs(dx-radius)<bandWidth && Math.abs(dy)<bandWidth) ||
(Math.abs(dy-radius)<bandWidth && Math.abs(dx)<bandWidth))
mask[i][j] = 0;
break;
case 'custom':
const decay = 10;
if (d < radius-decay) mask[i][j] = 1;
else if (d > radius+decay) mask[i][j] = 0;
else mask[i][j] = 0.5 + 0.5 * Math.cos(Math.PI * (d-radius)/decay);
break;
}
}
return mask;
}
function applyMask(freqMat, mask) {
const n = freqMat.length, m = freqMat[0].length;
const out = Array.from({length: n}, () => Array(m));
for (let i = 0; i < n; i++)
for (let j = 0; j < m; j++)
out[i][j] = freqMat[i][j].scale(mask[i][j]);
return out;
}
function imageToGrayMatrix(imgData, n, m) {
const mat = Array.from({length: n}, () => Array(m));
for (let i = 0; i < n; i++)
for (let j = 0; j < m; j++) {
const idx = (i * m + j) * 4;
const r = imgData.data[idx], g = imgData.data[idx+1], b = imgData.data[idx+2];
mat[i][j] = 0.299*r + 0.587*g + 0.114*b;
}
return mat;
}
function grayMatrixToImage(mat, ctx) {
const n = mat.length, m = mat[0].length;
const imgData = ctx.createImageData(m, n);
let min = 1e10, max = -1e10;
for (let i = 0; i < n; i++) for (let j = 0; j < m; j++) {
if (mat[i][j] < min) min = mat[i][j];
if (mat[i][j] > max) max = mat[i][j];
}
for (let i = 0; i < n; i++) for (let j = 0; j < m; j++) {
const v = clamp(Math.round(255*(mat[i][j]-min)/(max-min)), 0, 255);
const idx = (i * m + j) * 4;
imgData.data[idx] = imgData.data[idx+1] = imgData.data[idx+2] = v;
imgData.data[idx+3] = 255;
}
ctx.putImageData(imgData, 0, 0);
}
const originalCanvas = document.getElementById('originalCanvas');
const spectrumCanvas = document.getElementById('spectrumCanvas');
const resultCanvas = document.getElementById('resultCanvas');
const errorMsg = document.getElementById('errorMsg');
const controlsSection = document.getElementById('controlsSection');
const filterType = document.getElementById('filterType');
const radiusSlider = document.getElementById('radius');
const radiusValue = document.getElementById('radiusValue');
const bandGroup = document.getElementById('bandGroup');
const bandWidthSlider = document.getElementById('bandWidth');
const bandWidthValue = document.getElementById('bandWidthValue');
const applyBtn = document.getElementById('applyBtn');
const downloadBtn = document.getElementById('downloadBtn');
let origImage = null, grayMat = null, freqMat = null, freqMatShifted = null;
let n = 0, m = 0;
window.addEventListener('DOMContentLoaded', function() {
filterType.value = 'bandstop';
bandGroup.style.display = '';
radiusSlider.value = 60; radiusValue.textContent = 60;
bandWidthSlider.value = 60; bandWidthValue.textContent = 60;
});
filterType.addEventListener('change', function() {
if (filterType.value === 'bandstop' || filterType.value === 'notch')
bandGroup.style.display = '';
else
bandGroup.style.display = 'none';
});
radiusSlider.addEventListener('input', () => radiusValue.textContent = radiusSlider.value);
bandWidthSlider.addEventListener('input', () => bandWidthValue.textContent = bandWidthSlider.value);
document.getElementById('imageUpload').addEventListener('change', function(e) {
errorMsg.textContent = '';
const file = e.target.files[0];
if (!file) return;
if (!file.type.match(/image.*/)) {
errorMsg.textContent = 'Unsupported file type.';
return;
}
const img = new window.Image();
const reader = new FileReader();
reader.onload = function(ev) {
img.onload = function() {
let width = img.width, height = img.height;
const maxDim = 512;
if (width > maxDim || height > maxDim) {
if (width > height) {
height = Math.round(height * maxDim / width);
width = maxDim;
} else {
width = Math.round(width * maxDim / height);
height = maxDim;
}
}
n = nextPowerOf2(height);
m = nextPowerOf2(width);
originalCanvas.width = m;
originalCanvas.height = n;
spectrumCanvas.width = m;
spectrumCanvas.height = n;
resultCanvas.width = m;
resultCanvas.height = n;
const ctx = originalCanvas.getContext('2d');
ctx.clearRect(0,0,m,n);
ctx.drawImage(img, 0, 0, m, n);
origImage = ctx.getImageData(0, 0, m, n);
grayMat = imageToGrayMatrix(origImage, n, m);
freqMat = Array.from({length: n}, (_,i) =>
Array.from({length: m}, (_,j) =>
new Complex(grayMat[i][j], 0)
)
);
try {
fft2d(freqMat, false);
} catch (err) {
errorMsg.textContent = 'FFT error: ' + err.message;
controlsSection.style.display = 'none';
return;
}
freqMatShifted = fftshift(freqMat);
drawSpectrum(spectrumCanvas.getContext('2d'), freqMatShifted);
controlsSection.style.display = '';
autoApplyDefaultFilter();
};
img.onerror = function() {
errorMsg.textContent = 'Image load error.';
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
});
function autoApplyDefaultFilter() {
// Use band-stop with radius and width tuned for mid-frequencies
let mask = genMask(
'bandstop',
n, m,
parseInt(radiusSlider.value),
parseInt(bandWidthSlider.value),
null
);
let freqMasked = applyMask(freqMatShifted, mask);
freqMasked = ifftshift(freqMasked);
try {
fft2d(freqMasked, true);
} catch (err) {
errorMsg.textContent = 'Inverse FFT error: ' + err.message;
return;
}
const resultMat = Array.from({length: n}, () => Array(m));
for (let i = 0; i < n; i++)
for (let j = 0; j < m; j++)
resultMat[i][j] = freqMasked[i][j].re;
grayMatrixToImage(resultMat, resultCanvas.getContext('2d'));
drawSpectrum(spectrumCanvas.getContext('2d'), freqMatShifted);
}
applyBtn.addEventListener('click', function() {
if (!freqMat || !grayMat) return;
try {
let mask = genMask(
filterType.value,
n, m,
parseInt(radiusSlider.value),
parseInt(bandWidthSlider.value),
null
);
let freqMasked = applyMask(freqMatShifted, mask);
freqMasked = ifftshift(freqMasked);
try {
fft2d(freqMasked, true);
} catch (err) {
errorMsg.textContent = 'Inverse FFT error: ' + err.message;
return;
}
const resultMat = Array.from({length: n}, () => Array(m));
for (let i = 0; i < n; i++)
for (let j = 0; j < m; j++)
resultMat[i][j] = freqMasked[i][j].re;
grayMatrixToImage(resultMat, resultCanvas.getContext('2d'));
drawSpectrum(spectrumCanvas.getContext('2d'), freqMatShifted);
} catch (err) {
errorMsg.textContent = 'Processing error: ' + err.message;
}
});
downloadBtn.addEventListener('click', function() {
const link = document.createElement('a');
link.download = 'frequency_modified.png';
link.href = resultCanvas.toDataURL('image/png');
link.click();
});
window.addEventListener('error', function(e) {
errorMsg.textContent = 'Unexpected error: ' + e.message;
return false;
});
</script>
</body>
</html>
/* Add your styles here */
// Add your code here