Spaces:
Running
Running
<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> |