<!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