<html><head><base href="https://www.example.com/advanced-image-preprocessing-ascii-converter/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advanced Image Preprocessing and ASCII Art Converter</title>
<style>
  :root {
    --fluid-5-20: clamp(0.3125rem, 0.2731rem + 1.2605vw, 1.25rem);
    --fluid-8-20: clamp(0.5rem, 0.4685rem + 1.0084vw, 1.25rem);
    --fluid-5-9: clamp(0.3125rem, 0.302rem + 0.3361vw, 0.5625rem);
    --fluid-2-5: clamp(0.125rem, 0.1168rem + 0.2609vw, 0.3125rem);
  }

  @font-face {
    font-family: 'jgs9';
    src: url('https://websim.ai/fonts/jgs9.woff2') format('woff2');
    font-weight: normal;
    font-style: normal;
  }

  body {
    font-family: 'Roboto', Arial, sans-serif;
    margin: 0;
    padding: 20px;
    background-color: #040404;
    color: #fdfdfd;
  }
  .container {
    max-width: 1200px;
    margin: 0 auto;
    background-color: #111;
    padding: 30px;
    border-radius: 8px;
    box-shadow: 0 4px 6px rgba(255,255,255,0.1);
  }
  h1, h2 {
    text-align: center;
    color: #fdfdfd;
    font-family: "jgs9", sans-serif;
  }
  .upload-section {
    text-align: center;
    margin-bottom: 30px;
  }
  #imageUpload {
    display: none;
  }
  .upload-btn {
    background-color: #3498db;
    color: white;
    padding: 12px 24px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
    transition: background-color 0.3s ease;
  }
  .upload-btn:hover {
    background-color: #2980b9;
  }
  .image-container {
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
    margin-bottom: 30px;
  }
  .image-preview {
    flex-basis: calc(33% - 20px);
    margin-bottom: 20px;
    background-color: #222;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 2px 4px rgba(255,255,255,0.1);
  }
  .image-preview h3 {
    background-color: #34495e;
    color: #fff;
    margin: 0;
    padding: 10px;
    font-size: 18px;
  }
  canvas {
    max-width: 100%;
    height: auto;
    display: block;
  }
  .controls {
    background-color: #222;
    padding: 20px;
    border-radius: 8px;
    margin-bottom: 20px;
  }
  .slider-container {
    margin-bottom: 15px;
  }
  .slider-container label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
    color: #fdfdfd;
  }
  .slider {
    -webkit-appearance: none;
    width: 100%;
    height: 10px;
    border-radius: 5px;
    background: #555;
    outline: none;
    opacity: 0.7;
    transition: opacity .2s;
  }
  .slider:hover {
    opacity: 1;
  }
  .slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: #3498db;
    cursor: pointer;
  }
  .slider::-moz-range-thumb {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: #3498db;
    cursor: pointer;
  }
  .number-input {
    width: 50px;
    margin-left: 10px;
    background-color: #333;
    color: #fdfdfd;
    border: 1px solid #555;
    border-radius: 3px;
    padding: 3px;
  }
  #processBtn {
    display: block;
    width: 100%;
    padding: 12px;
    background-color: #2ecc71;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 16px;
    cursor: pointer;
    transition: background-color 0.3s ease;
  }
  #processBtn:hover {
    background-color: #27ae60;
  }
  .ascii-art {
    font-family: "jgs9", monospace;
    white-space: pre;
    background-color: #222;
    padding: 20px;
    border-radius: 5px;
    overflow: auto;
    max-height: 600px;
    color: #fdfdfd;
    font-size: 2px;
    line-height: 1;
    text-align: center;
    box-shadow: 0 0 10px rgba(0,0,0,0.5);
    margin-top: 20px;
  }
  .download-buttons {
    display: flex;
    justify-content: center;
    gap: 10px;
    margin-top: 20px;
  }
  .download-btn {
    background-color: #4CAF50;
    border: none;
    color: white;
    padding: 10px 20px;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    font-size: 16px;
    margin: 4px 2px;
    cursor: pointer;
    border-radius: 5px;
  }
</style>
</head>
<body>
  <div id="loadingMessage" style="text-align: center; padding: 20px;">
    Loading OpenCV.js, please wait...
  </div>
  <div class="container">
    <h1>Advanced Image Preprocessing and ASCII Art Converter</h1>
    <div class="upload-section">
      <input type="file" id="imageUpload" accept="image/*">
      <label for="imageUpload" class="upload-btn">Upload Image</label>
    </div>
    <div class="image-container">
      <div class="image-preview">
        <h3>Original Image</h3>
        <canvas id="originalCanvas"></canvas>
      </div>
      <div class="image-preview">
        <h3>Preprocessed Image</h3>
        <canvas id="preprocessedCanvas"></canvas>
      </div>
      <div class="image-preview">
        <h3>Structure Map</h3>
        <canvas id="structureMapCanvas"></canvas>
      </div>
    </div>
    <div class="controls">
      <div class="slider-container">
        <label for="edgeThresholdSlider">Edge Detection Threshold:</label>
        <input type="range" id="edgeThresholdSlider" class="slider" min="0" max="255" value="100">
        <span id="edgeThresholdValue">100</span>
      </div>
      <div class="slider-container">
        <label for="gaussianBlurSlider">Gaussian Blur:</label>
        <input type="range" id="gaussianBlurSlider" class="slider" min="0" max="10" value="1" step="0.1">
        <span id="gaussianBlurValue">1</span>
      </div>
      <div class="slider-container">
        <label for="thinningIterationsSlider">Thinning Iterations:</label>
        <input type="range" id="thinningIterationsSlider" class="slider" min="1" max="10" value="3">
        <span id="thinningIterationsValue">3</span>
      </div>
      <div class="slider-container">
        <label for="scaleSlider">Scale (%):</label>
        <input type="range" id="scaleSlider" class="slider" min="10" max="200" value="100">
        <span id="scaleValue">100</span>
      </div>
      <div class="slider-container">
        <label for="dogThresholdSlider">DoG Threshold:</label>
        <input type="range" id="dogThresholdSlider" class="slider" min="0" max="255" value="100">
        <span id="dogThresholdValue">100</span>
      </div>
      <div class="slider-container">
        <label for="outputHeightSlider">Output Height (rows):</label>
        <input type="range" id="outputHeightSlider" class="slider" min="10" max="200" value="50">
        <input type="number" id="outputHeightValue" class="number-input" min="10" max="200" value="50">
      </div>
      <div class="slider-container">
        <label for="scaleCountSlider">Number of Scales:</label>
        <input type="range" id="scaleCountSlider" class="slider" min="1" max="8" value="4">
        <input type="number" id="scaleCountValue" class="number-input" min="1" max="8" value="4">
      </div>
      <div class="slider-container">
        <label for="ncrfRadiusSlider">Non-CRF Radius:</label>
        <input type="range" id="ncrfRadiusSlider" class="slider" min="1" max="20" value="5">
        <input type="number" id="ncrfRadiusValue" class="number-input" min="1" max="20" value="5">
      </div>
      <div class="slider-container">
        <label for="modulationStrengthSlider">Modulation Strength:</label>
        <input type="range" id="modulationStrengthSlider" class="slider" min="0" max="1" step="0.1" value="0.5">
        <input type="number" id="modulationStrengthValue" class="number-input" min="0" max="1" step="0.1" value="0.5">
      </div>
      <div class="slider-container">
        <label for="detailThresholdSlider">Detail Threshold:</label>
        <input type="range" id="detailThresholdSlider" class="slider" min="0" max="1" step="0.05" value="0.1">
        <input type="number" id="detailThresholdValue" class="number-input" min="0" max="1" step="0.05" value="0.1">
      </div>
      <div class="slider-container">
        <label for="charSetSelect">Character Set:</label>
        <select id="charSetSelect">
          <option value="standard">Standard</option>
          <option value="structure">Structure</option>
          <option value="outline">Outline</option>
          <option value="blocks">Blocks</option>
          <option value="geometric">Geometric</option>
          <option value="dots">Dots</option>
        </select>
      </div>
      <div class="slider-container">
        <label for="invertColorsCheckbox">
          <input type="checkbox" id="invertColorsCheckbox"> Invert Colors
        </label>
      </div>
      <button id="processBtn">Process Image</button>
    </div>
    <div class="ascii-output">
      <h2>ASCII Output</h2>
      <div id="asciiOutput" class="ascii-art"></div>
    </div>
    <div class="download-buttons">
      <button id="downloadPreprocessed" class="download-btn">Download Preprocessed Image</button>
      <button id="downloadStructureMap" class="download-btn">Download Structure Map</button>
      <button id="downloadAscii" class="download-btn">Download ASCII Art</button>
    </div>
  </div>

  <script>
    let originalMat, preprocessedMat, structureMap;
    let originalCanvas, preprocessedCanvas, structureMapCanvas;

    function handleImageUpload(event) {
      const file = event.target.files[0];
      const reader = new FileReader();
      reader.onload = function(e) {
        const img = new Image();
        img.onload = function() {
          try {
            originalMat = cv.imread(img);
            cv.imshow('originalCanvas', originalMat);
            processImage();
          } catch (err) {
            console.error('Error processing image:', err);
            alert('Error processing image. Please try again.');
          }
        }
        img.src = e.target.result;
      }
      reader.readAsDataURL(file);
    }

    function openCvReady() {
      document.getElementById('loadingMessage').style.display = 'none';
      document.getElementById('imageUpload').disabled = false;
      document.getElementById('processBtn').disabled = false;

      originalCanvas = document.getElementById('originalCanvas');
      preprocessedCanvas = document.getElementById('preprocessedCanvas');
      structureMapCanvas = document.getElementById('structureMapCanvas');

      document.getElementById('imageUpload').addEventListener('change', handleImageUpload);
      document.getElementById('processBtn').addEventListener('click', processImage);
      document.getElementById('downloadPreprocessed').addEventListener('click', downloadPreprocessedImage);
      document.getElementById('downloadStructureMap').addEventListener('click', downloadStructureMap);
      document.getElementById('downloadAscii').addEventListener('click', downloadAsciiArt);
    }
  </script>
  <script async src="https://docs.opencv.org/4.5.1/opencv.js" onload="openCvReady();" type="text/javascript"></script>
  <script>
    function structureLineExtraction(src) {
      let dst = new cv.Mat();
      cv.Canny(src, dst, 50, 150, 3, false);
      return dst;
    }

    function edgeDetection(src) {
      let dst = new cv.Mat();
      let ksize = new cv.Size(3, 3);
      let sobel_x = new cv.Mat();
      let sobel_y = new cv.Mat();
      cv.Sobel(src, sobel_x, cv.CV_64F, 1, 0, ksize);
      cv.Sobel(src, sobel_y, cv.CV_64F, 0, 1, ksize);
      cv.magnitude(sobel_x, sobel_y, dst);
      cv.normalize(dst, dst, 0, 255, cv.NORM_MINMAX, cv.CV_8U);
      return dst;
    }

    function preThinning(src) {
      let dst = src.clone();
      let rows = src.rows;
      let cols = src.cols;
      
      for (let i = 1; i < rows - 1; i++) {
        for (let j = 1; j < cols - 1; j++) {
          let p = src.ucharPtr(i, j)[0];
          if (p === 0) continue;
          
          let p1 = src.ucharPtr(i-1, j)[0];
          let p3 = src.ucharPtr(i, j+1)[0];
          let p5 = src.ucharPtr(i+1, j)[0];
          let p7 = src.ucharPtr(i, j-1)[0];
          
          let B_odd = p1 + p3 + p5 + p7;
          
          if (B_odd < 2) {
            dst.ucharPtr(i, j)[0] = 0;
          } else if (B_odd > 2) {
            dst.ucharPtr(i, j)[0] = 255;
          }
        }
      }
      return dst;
    }

    function improvedThinning(src, iterations) {
      let dst = src.clone();
      let changed;
      
      for (let i = 0; i < iterations; i++) {
        changed = false;
        dst = preThinning(dst);
        changed |= zhangSuenSubIteration(dst, 0);
        changed |= zhangSuenSubIteration(dst, 1);
        if (!changed) break;
      }
      
      return dst;
    }

    function processImage() {
      if (!originalMat) {
        console.error('No image loaded');
        return;
      }

      try {
        const edgeThreshold = parseInt(document.getElementById('edgeThresholdSlider').value);
        const blurSize = parseFloat(document.getElementById('gaussianBlurSlider').value);
        const iterations = parseInt(document.getElementById('thinningIterationsSlider').value);
        const outputHeight = parseInt(document.getElementById('outputHeightSlider').value);
        const scaleCount = parseInt(document.getElementById('scaleCountSlider').value);
        const ncrfRadius = parseInt(document.getElementById('ncrfRadiusSlider').value);
        const modulationStrength = parseFloat(document.getElementById('modulationStrengthSlider').value);
        const detailThreshold = parseFloat(document.getElementById('detailThresholdSlider').value);

        let blurred = new cv.Mat();
        let gray = new cv.Mat();
        let edges = new cv.Mat();

        cv.GaussianBlur(originalMat, blurred, new cv.Size(0, 0), blurSize, blurSize, cv.BORDER_DEFAULT);
        cv.cvtColor(blurred, gray, cv.COLOR_RGBA2GRAY);
        cv.Canny(gray, edges, edgeThreshold, edgeThreshold * 2, 3, false);

        preprocessedMat = improvedThinning(edges, iterations);
        cv.imshow('preprocessedCanvas', preprocessedMat);

        structureMap = extractStructure(gray, scaleCount, ncrfRadius, modulationStrength);
        displayStructureMap(structureMap);

        convertToASCII(structureMap, detailThreshold);

        blurred.delete();
        gray.delete();
        edges.delete();
      } catch (err) {
        console.error('Error processing image:', err);
        alert('Error processing image. Please try again.');
      }
    }

    function zhangSuenThinning(src, iterations) {
      let dst = src.clone();
      let changed;

      for (let i = 0; i < iterations; i++) {
        changed = false;
        changed |= zhangSuenSubIteration(dst, 0);
        changed |= zhangSuenSubIteration(dst, 1);
        if (!changed) break;
      }

      return dst;
    }

    function zhangSuenSubIteration(img, step) {
      let changed = false;
      let rows = img.rows;
      let cols = img.cols;
      let markers = new cv.Mat(rows, cols, cv.CV_8U, new cv.Scalar(0));

      for (let i = 1; i < rows - 1; i++) {
        for (let j = 1; j < cols - 1; j++) {
          if (img.ucharPtr(i, j)[0] === 0) continue;

          let p2 = img.ucharPtr(i-1, j)[0];
          let p3 = img.ucharPtr(i-1, j+1)[0];
          let p4 = img.ucharPtr(i, j+1)[0];
          let p5 = img.ucharPtr(i+1, j+1)[0];
          let p6 = img.ucharPtr(i+1, j)[0];
          let p7 = img.ucharPtr(i+1, j-1)[0];
          let p8 = img.ucharPtr(i, j-1)[0];
          let p9 = img.ucharPtr(i-1, j-1)[0];

          let A = ((p2 === 0 && p3 === 255) + (p3 === 0 && p4 === 255) +
                   (p4 === 0 && p5 === 255) + (p5 === 0 && p6 === 255) +
                   (p6 === 0 && p7 === 255) + (p7 === 0 && p8 === 255) +
                   (p8 === 0 && p9 === 255) + (p9 === 0 && p2 === 255));
          let B = p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9;
          
          let m1 = step === 0 ? (p2 * p4 * p6) : (p2 * p4 * p8);
          let m2 = step === 0 ? (p4 * p6 * p8) : (p2 * p6 * p8);

          if (A === 1 && (B >= 2 * 255 && B <= 6 * 255) && m1 === 0 && m2 === 0) {
            markers.ucharPtr(i, j)[0] = 255;
            changed = true;
          }
        }
      }

      for (let i = 0; i < rows; i++) {
        for (let j = 0; j < cols; j++) {
          if (markers.ucharPtr(i, j)[0] === 255) {
            img.ucharPtr(i, j)[0] = 0;
          }
        }
      }

      markers.delete();
      return changed;
    }

    function extractStructure(grayMat, scaleCount, ncrfRadius, modulationStrength) {
      const width = grayMat.cols;
      const height = grayMat.rows;
      const data = grayMat.data;
      
      const scales = [];
      for (let i = 0; i < scaleCount; i++) {
        scales.push(Math.pow(2, i));
      }
      let structureMap = new Array(width * height).fill(0);

      for (let scale of scales) {
        const response = simulateCRFResponse(data, width, height, scale);
        for (let i = 0; i < structureMap.length; i++) {
          structureMap[i] = Math.max(structureMap[i], response[i]);
        }
      }

      structureMap = applyNonCRFModulation(structureMap, width, height, ncrfRadius, modulationStrength);
      return structureMap;
    }

    function simulateCRFResponse(data, width, height, scale) {
      const response = new Array(width * height).fill(0);
      
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          const i = y * width + x;
          const grayValue = data[i];
          
          if (x > scale && x < width - scale && y > scale && y < height - scale) {
            const diff = Math.abs(grayValue - data[y * width + x - scale]) +
                         Math.abs(grayValue - data[y * width + x + scale]) +
                         Math.abs(grayValue - data[(y - scale) * width + x]) +
                         Math.abs(grayValue - data[(y + scale) * width + x]);
            response[i] = diff / (4 * 255);
          }
        }
      }
      
      return response;
    }

    function applyNonCRFModulation(structureMap, width, height, radius, strength) {
      const modulated = new Array(width * height).fill(0);
      
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          const i = y * width + x;
          let sum = 0;
          let count = 0;
          
          for (let dy = -radius; dy <= radius; dy++) {
            for (let dx = -radius; dx <= radius; dx++) {
              const nx = x + dx;
              const ny = y + dy;
              if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
                sum += structureMap[ny * width + nx];
                count++;
              }
            }
          }
          
          const avgResponse = sum / count;
          modulated[i] = structureMap[i] * (1 - strength * avgResponse);
        }
      }
      
      return modulated;
    }

    function displayStructureMap(structureMap) {
      const width = preprocessedCanvas.width;
      const height = preprocessedCanvas.height;
      const ctx = structureMapCanvas.getContext('2d');
      const imageData = ctx.createImageData(width, height);

      for (let i = 0; i < structureMap.length; i++) {
        const value = Math.floor(structureMap[i] * 255);
        imageData.data[i * 4] = value;
        imageData.data[i * 4 + 1] = value;
        imageData.data[i * 4 + 2] = value;
        imageData.data[i * 4 + 3] = 255;
      }

      structureMapCanvas.width = width;
      structureMapCanvas.height = height;
      ctx.putImageData(imageData, 0, 0);
    }

    function convertToASCII(structureMap, detailThreshold) {
      const width = preprocessedCanvas.width;
      const height = preprocessedCanvas.height;
      const outputHeight = parseInt(document.getElementById('outputHeightSlider').value);
      const outputWidth = Math.floor(outputHeight * width / height);
      const charSetType = document.getElementById('charSetSelect').value;
      const invert = document.getElementById('invertColorsCheckbox').checked;

      const charSets = {
        standard: '@%#*+=-:. ',
        structure: '.,/\\_|~\'*+^][}{ ',
        outline: '▓▒░ ',
        blocks: '█▓▒░ ',
        geometric: '■□▢▣▤▥▦▧▨▩ ',
        dots: '•·○●◌◍◎◉ '
      };
      const characters = charSets[charSetType];

      let asciiArt = '';
      for (let y = 0; y < outputHeight; y++) {
        for (let x = 0; x < outputWidth; x++) {
          const srcX = Math.floor(x * width / outputWidth);
          const srcY = Math.floor(y * height / outputHeight);
          const value = structureMap[srcY * width + srcX];
          
          let charIndex = Math.floor(value * (characters.length - 1));
          if (invert) {
            charIndex = characters.length - 1 - charIndex;
          }
          asciiArt += value > detailThreshold ? characters[charIndex] : ' ';
        }
        asciiArt += '\n';
      }

      document.getElementById('asciiOutput').textContent = asciiArt;
    }

    function downloadPreprocessedImage() {
      if (!preprocessedCanvas.toDataURL) {
        alert('No preprocessed image available.');
        return;
      }

      const link = document.createElement('a');
      link.download = 'preprocessed_image.png';
      link.href = preprocessedCanvas.toDataURL();
      link.click();
    }

    function downloadStructureMap() {
      if (!structureMapCanvas.toDataURL) {
        alert('No structure map available.');
        return;
      }

      const link = document.createElement('a');
      link.download = 'structure_map.png';
      link.href = structureMapCanvas.toDataURL();
      link.click();
    }

    function downloadAsciiArt() {
      const asciiArt = document.getElementById('asciiOutput').textContent;
      if (!asciiArt.trim()) {
        alert('No ASCII art available.');
        return;
      }

      const blob = new Blob([asciiArt], {type: 'text/plain'});
      const link = document.createElement('a');
      link.download = 'ascii_art.txt';
      link.href = URL.createObjectURL(blob);
      link.click();
    }

    document.getElementById('edgeThresholdSlider').addEventListener('input', function(e) {
      document.getElementById('edgeThresholdValue').textContent = e.target.value;
    });
    document.getElementById('gaussianBlurSlider').addEventListener('input', function(e) {
      document.getElementById('gaussianBlurValue').textContent = e.target.value;
    });
    document.getElementById('thinningIterationsSlider').addEventListener('input', function(e) {
      document.getElementById('thinningIterationsValue').textContent = e.target.value;
    });
    document.getElementById('scaleSlider').addEventListener('input', function(e) {
      document.getElementById('scaleValue').textContent = e.target.value;
    });
    document.getElementById('dogThresholdSlider').addEventListener('input', function(e) {
      document.getElementById('dogThresholdValue').textContent = e.target.value;
    });

    function syncInputs(slider, value) {
      slider.addEventListener('input', () => value.value = slider.value);
      value.addEventListener('input', () => slider.value = value.value);
    }

    syncInputs(document.getElementById('outputHeightSlider'), document.getElementById('outputHeightValue'));
    syncInputs(document.getElementById('scaleCountSlider'), document.getElementById('scaleCountValue'));
    syncInputs(document.getElementById('ncrfRadiusSlider'), document.getElementById('ncrfRadiusValue'));
    syncInputs(document.getElementById('modulationStrengthSlider'), document.getElementById('modulationStrengthValue'));
    syncInputs(document.getElementById('detailThresholdSlider'), document.getElementById('detailThresholdValue'));

    const sliders = document.querySelectorAll('.slider');
    sliders.forEach(slider => {
      slider.addEventListener('input', processImage);
    });

    document.getElementById('imageUpload').disabled = true;
    document.getElementById('processBtn').disabled = true;
  </script>
</body>
</html>