multimodalart's picture
Update index.html
62b2e90 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ChatGPT yellow tint corrector</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
padding: 20px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
flex: 1;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
color: #333;
}
.upload-area {
border: 2px dashed #ccc;
border-radius: 4px;
padding: 40px;
text-align: center;
cursor: pointer;
background: #fafafa;
margin-bottom: 20px;
}
.upload-area:hover {
border-color: #999;
background: #f0f0f0;
}
.upload-area.dragover {
border-color: #4CAF50;
background: #f0f8f0;
}
input[type="file"] {
display: none;
}
.processing {
display: none;
text-align: center;
padding: 20px;
color: #666;
}
.progress-bar {
width: 100%;
height: 20px;
background: #f0f0f0;
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
}
.progress-fill {
height: 100%;
background: #4CAF50;
transition: width 0.3s ease;
}
.results {
display: none;
}
.single-result {
display: none;
}
.bulk-result {
display: none;
}
.image-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
margin-bottom: 20px;
}
.gallery-item {
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
cursor: pointer;
position: relative;
aspect-ratio: 1;
}
.gallery-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.gallery-item canvas {
width: 100%;
height: 100%;
object-fit: cover;
}
.gallery-more {
display: flex;
align-items: center;
justify-content: center;
background: #f0f0f0;
color: #666;
font-size: 24px;
font-weight: bold;
}
.image-container {
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
}
.image-container h3 {
font-size: 14px;
margin-bottom: 10px;
color: #666;
}
canvas {
display: block;
width: 100%;
height: auto;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #45a049;
}
button.secondary {
background: #757575;
}
button.secondary:hover {
background: #616161;
}
.info {
margin-top: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 4px;
font-size: 13px;
color: #666;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
z-index: 1000;
padding: 20px;
}
.modal-content {
max-width: 90%;
max-height: 90%;
margin: auto;
position: relative;
top: 50%;
transform: translateY(-50%);
background: white;
border-radius: 8px;
padding: 20px;
}
.modal-close {
position: absolute;
top: 10px;
right: 10px;
font-size: 24px;
cursor: pointer;
background: none;
border: none;
color: #666;
}
.modal-image-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.modal-image {
text-align: center;
}
.modal-image h3 {
margin-bottom: 10px;
color: #666;
}
.modal-image canvas {
max-width: 100%;
height: auto;
}
footer {
text-align: center;
padding: 20px;
color: #666;
font-size: 12px;
}
footer a {
color: #4CAF50;
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.image-grid {
grid-template-columns: 1fr;
}
.gallery-grid {
grid-template-columns: repeat(2, 1fr);
}
.modal-image-container {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>ChatGPT yellow tint corrector</h1>
<div class="upload-area" id="uploadArea">
<p>Drop image(s) here or click to upload</p>
<p style="font-size: 12px; color: #999; margin-top: 10px;">Supports JPG, PNG, WebP • Multiple files supported</p>
<input type="file" id="fileInput" accept="image/*" multiple>
</div>
<div class="processing" id="processing">
<p>Processing <span id="currentFile">0</span> of <span id="totalFiles">0</span> images...</p>
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
</div>
<div class="results" id="results">
<div class="single-result" id="singleResult">
<div class="image-grid">
<div class="image-container">
<h3>Original</h3>
<canvas id="originalCanvas"></canvas>
</div>
<div class="image-container">
<h3>Corrected</h3>
<canvas id="correctedCanvas"></canvas>
</div>
</div>
<div class="controls">
<button onclick="downloadImage()">Download Corrected</button>
<button class="secondary" onclick="resetApp()">Process More Images</button>
</div>
</div>
<div class="bulk-result" id="bulkResult">
<h3 style="margin-bottom: 15px; color: #666;">Corrected Images</h3>
<div class="gallery-grid" id="galleryGrid"></div>
<div class="controls">
<button onclick="downloadAll()">Download All</button>
<button class="secondary" onclick="resetApp()">Process More Images</button>
</div>
</div>
<div class="info" id="info"></div>
</div>
</div>
<footer>
Created by <a href="https://x.com/multimodalart" target="_blank">multimodalart</a><br>
The images are processed on your browser and are never sent to a server
</footer>
<div class="modal" id="imageModal">
<div class="modal-content">
<button class="modal-close" onclick="closeModal()">×</button>
<div class="modal-image-container">
<div class="modal-image">
<h3>Original</h3>
<canvas id="modalOriginal"></canvas>
</div>
<div class="modal-image">
<h3>Corrected</h3>
<canvas id="modalCorrected"></canvas>
</div>
</div>
<div style="text-align: center; margin-top: 20px;">
<button onclick="downloadModalImage()">Download Corrected</button>
</div>
</div>
</div>
<script>
// Exact port of Python auto_white_balance_final() function
class ImageProcessor {
constructor() {
this.processedImages = [];
this.currentModalIndex = -1;
this.setupEventListeners();
}
setupEventListeners() {
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
uploadArea.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => this.handleFiles(e.target.files));
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
this.handleFiles(e.dataTransfer.files);
}
});
}
handleFiles(files) {
const imageFiles = Array.from(files).filter(file => file.type.startsWith('image/'));
if (imageFiles.length === 0) {
alert('Please select image files');
return;
}
this.processedImages = [];
this.processMultipleImages(imageFiles);
}
async processMultipleImages(files) {
document.getElementById('uploadArea').style.display = 'none';
document.getElementById('processing').style.display = 'block';
document.getElementById('totalFiles').textContent = files.length;
for (let i = 0; i < files.length; i++) {
document.getElementById('currentFile').textContent = i + 1;
document.getElementById('progressFill').style.width = `${((i + 1) / files.length) * 100}%`;
await this.processFile(files[i]);
}
document.getElementById('processing').style.display = 'none';
this.displayResults();
}
processFile(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const result = this.processImage(img);
this.processedImages.push({
name: file.name,
original: result.original,
corrected: result.corrected,
width: img.width,
height: img.height
});
resolve();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
}
processImage(img) {
// Create canvases for processing
const originalCanvas = document.createElement('canvas');
const correctedCanvas = document.createElement('canvas');
originalCanvas.width = img.width;
originalCanvas.height = img.height;
correctedCanvas.width = img.width;
correctedCanvas.height = img.height;
const originalCtx = originalCanvas.getContext('2d');
const correctedCtx = correctedCanvas.getContext('2d');
// Draw original
originalCtx.drawImage(img, 0, 0);
// Get image data
const imageData = originalCtx.getImageData(0, 0, img.width, img.height);
// Apply exact algorithm from Python
const correctedData = this.autoWhiteBalanceFinal(imageData);
// Draw corrected
correctedCtx.putImageData(correctedData, 0, 0);
return {
original: originalCanvas,
corrected: correctedCanvas
};
}
displayResults() {
document.getElementById('results').style.display = 'block';
if (this.processedImages.length === 1) {
// Single image display
document.getElementById('singleResult').style.display = 'block';
document.getElementById('bulkResult').style.display = 'none';
const original = document.getElementById('originalCanvas');
const corrected = document.getElementById('correctedCanvas');
original.width = this.processedImages[0].width;
original.height = this.processedImages[0].height;
corrected.width = this.processedImages[0].width;
corrected.height = this.processedImages[0].height;
original.getContext('2d').drawImage(this.processedImages[0].original, 0, 0);
corrected.getContext('2d').drawImage(this.processedImages[0].corrected, 0, 0);
} else {
// Bulk display
document.getElementById('singleResult').style.display = 'none';
document.getElementById('bulkResult').style.display = 'block';
const galleryGrid = document.getElementById('galleryGrid');
galleryGrid.innerHTML = '';
const maxDisplay = 16;
const displayCount = Math.min(this.processedImages.length, maxDisplay);
for (let i = 0; i < displayCount; i++) {
if (i === 15 && this.processedImages.length > maxDisplay) {
// Show "more" indicator
const moreDiv = document.createElement('div');
moreDiv.className = 'gallery-item gallery-more';
moreDiv.textContent = `+${this.processedImages.length - 15}`;
moreDiv.onclick = () => this.showAllImages();
galleryGrid.appendChild(moreDiv);
} else {
const item = document.createElement('div');
item.className = 'gallery-item';
item.onclick = () => this.showModal(i);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 200;
canvas.height = 200;
// Draw centered crop
const img = this.processedImages[i].corrected;
const scale = Math.max(200 / img.width, 200 / img.height);
const w = img.width * scale;
const h = img.height * scale;
ctx.drawImage(img, (200 - w) / 2, (200 - h) / 2, w, h);
item.appendChild(canvas);
galleryGrid.appendChild(item);
}
}
}
document.getElementById('info').innerHTML = `
Processed ${this.processedImages.length} image${this.processedImages.length > 1 ? 's' : ''}
`;
}
showModal(index) {
this.currentModalIndex = index;
const modal = document.getElementById('imageModal');
const modalOriginal = document.getElementById('modalOriginal');
const modalCorrected = document.getElementById('modalCorrected');
const img = this.processedImages[index];
modalOriginal.width = img.width;
modalOriginal.height = img.height;
modalCorrected.width = img.width;
modalCorrected.height = img.height;
modalOriginal.getContext('2d').drawImage(img.original, 0, 0);
modalCorrected.getContext('2d').drawImage(img.corrected, 0, 0);
modal.style.display = 'block';
}
showAllImages() {
// In a real implementation, this could show a paginated view
alert(`Showing all ${this.processedImages.length} images would be implemented here`);
}
autoWhiteBalanceFinal(imageData) {
const data = new Float32Array(imageData.data);
const width = imageData.width;
const height = imageData.height;
// Step 1: Smart white balance with feedback
this.smartWhiteBalance(data);
// Step 2: Adaptive exposure compensation
const meanBrightness = this.calculateMeanBrightness(data);
const targetBrightness = 140;
let exposureCompensation = targetBrightness / (meanBrightness + 1);
exposureCompensation = Math.min(Math.max(exposureCompensation, 0.9), 1.3);
for (let i = 0; i < data.length; i += 4) {
data[i] *= exposureCompensation;
data[i + 1] *= exposureCompensation;
data[i + 2] *= exposureCompensation;
}
// Clip again
for (let i = 0; i < data.length; i += 4) {
data[i] = Math.min(255, Math.max(0, data[i]));
data[i + 1] = Math.min(255, Math.max(0, data[i + 1]));
data[i + 2] = Math.min(255, Math.max(0, data[i + 2]));
}
// Step 3: S-curve for professional contrast (strength=0.25)
this.applySCurve(data, 0.25);
// Step 4: Local contrast (clarity) - radius=15, amount=0.25
const localContrastData = this.enhanceLocalContrast(data, width, height, 15, 0.25);
// Step 5: Balanced vibrance (vibrance=0.30)
this.enhanceColorVibrance(localContrastData, 0.30);
// Step 6: Micro-contrast for crispness
const finalData = this.applyMicroContrast(localContrastData, width, height);
// Step 7: Guarantee pure whites
this.ensureWhites(finalData);
// Convert back to Uint8ClampedArray
const result = new Uint8ClampedArray(finalData.length);
for (let i = 0; i < finalData.length; i++) {
result[i] = Math.min(255, Math.max(0, Math.round(finalData[i])));
}
return new ImageData(result, width, height);
}
smartWhiteBalance(data) {
// Find what should be white in the image
const whitePoint = this.findWhitePoint(data);
if (!whitePoint) {
// No clear white point, use robust mean method with full correction
this.fallbackWhiteBalance(data);
return;
}
// Calculate correction needed to make the white point actually white
const targetWhite = 255; // Pure white target
// Calculate multipliers
let scaleR = targetWhite / whitePoint.r;
let scaleG = targetWhite / whitePoint.g;
let scaleB = targetWhite / whitePoint.b;
// Normalize scales relative to green (most accurate channel)
const baseScale = scaleG;
scaleR = scaleR / baseScale;
scaleB = scaleB / baseScale;
scaleG = scaleG / baseScale;
// Apply overall brightness adjustment to reach target
const brightnessFactor = baseScale;
scaleR *= brightnessFactor;
scaleG *= brightnessFactor;
scaleB *= brightnessFactor;
// Apply safety limits but allow more aggressive correction
scaleR = Math.min(Math.max(scaleR, 0.5), 3.0);
scaleG = Math.min(Math.max(scaleG, 0.5), 3.0);
scaleB = Math.min(Math.max(scaleB, 0.5), 3.5); // Allow more blue boost
// Apply correction
for (let i = 0; i < data.length; i += 4) {
data[i] *= scaleR;
data[i + 1] *= scaleG;
data[i + 2] *= scaleB;
data[i] = Math.min(255, Math.max(0, data[i]));
data[i + 1] = Math.min(255, Math.max(0, data[i + 1]));
data[i + 2] = Math.min(255, Math.max(0, data[i + 2]));
}
}
findWhitePoint(data) {
// Find pixels that should be white
// These are bright pixels with low color variation
const candidates = [];
for (let i = 0; i < data.length; i += 40) { // Sample every 10th pixel for speed
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const brightness = 0.299 * r + 0.587 * g + 0.114 * b;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const saturation = max > 0 ? (max - min) / max : 0;
// Look for bright, desaturated pixels
if (brightness > 190 && saturation < 0.2) { // Slightly lower threshold to catch more whites
candidates.push({ r, g, b, brightness });
}
}
if (candidates.length === 0) {
return null;
}
// Sort by brightness and take top 2% (more selective)
candidates.sort((a, b) => b.brightness - a.brightness);
const topCount = Math.max(1, Math.floor(candidates.length * 0.02));
// Average the top candidates
let sumR = 0, sumG = 0, sumB = 0;
for (let i = 0; i < topCount; i++) {
sumR += candidates[i].r;
sumG += candidates[i].g;
sumB += candidates[i].b;
}
return {
r: sumR / topCount,
g: sumG / topCount,
b: sumB / topCount
};
}
fallbackWhiteBalance(data) {
// Full correction when no clear white point (not conservative)
const { avgR, avgG, avgB } = this.robustMean(data);
// Detect yellow tint severity
const yellowFactor = ((avgR + avgG) / 2) / (avgB + 1);
const yellowSeverity = Math.min(Math.max((yellowFactor - 1.0) / 0.5, 0), 1);
// Full correction strength (same as original)
const targetGray = 165 + yellowSeverity * 20;
const blueBoost = 1.08 + yellowSeverity * 0.12;
const redReduction = 0.96 - yellowSeverity * 0.04;
let scaleB = (targetGray * blueBoost) / avgB;
let scaleG = targetGray / avgG;
let scaleR = (targetGray * redReduction) / avgR;
// Safety limits
scaleB = Math.min(Math.max(scaleB, 0.7), 3.0);
scaleG = Math.min(Math.max(scaleG, 0.7), 2.5);
scaleR = Math.min(Math.max(scaleR, 0.7), 2.5);
// Apply correction
for (let i = 0; i < data.length; i += 4) {
data[i] *= scaleR;
data[i + 1] *= scaleG;
data[i + 2] *= scaleB;
data[i] = Math.min(255, Math.max(0, data[i]));
data[i + 1] = Math.min(255, Math.max(0, data[i + 1]));
data[i + 2] = Math.min(255, Math.max(0, data[i + 2]));
}
}
robustMean(data) {
const rValues = [];
const gValues = [];
const bValues = [];
// Collect non-zero values
for (let i = 0; i < data.length; i += 4) {
if (data[i] > 10) rValues.push(data[i]);
if (data[i + 1] > 10) gValues.push(data[i + 1]);
if (data[i + 2] > 10) bValues.push(data[i + 2]);
}
// Sort arrays
rValues.sort((a, b) => a - b);
gValues.sort((a, b) => a - b);
bValues.sort((a, b) => a - b);
// Get mean of percentile 20 to 80
const getPercentileMean = (arr) => {
if (arr.length === 0) return 128;
const start = Math.floor(arr.length * 0.2);
const end = Math.floor(arr.length * 0.8);
let sum = 0;
for (let i = start; i < end; i++) {
sum += arr[i];
}
return sum / (end - start);
};
return {
avgR: getPercentileMean(rValues),
avgG: getPercentileMean(gValues),
avgB: getPercentileMean(bValues)
};
}
calculateMeanBrightness(data) {
let sum = 0;
let count = 0;
for (let i = 0; i < data.length; i += 4) {
// RGB to grayscale
const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
sum += gray;
count++;
}
return sum / count;
}
applySCurve(data, strength) {
// Create S-curve lookup table
const k = strength * 10;
const midpoint = 0.5;
const curve = new Float32Array(256);
for (let i = 0; i < 256; i++) {
const x = i / 255;
const y = 1 / (1 + Math.exp(-k * (x - midpoint)));
curve[i] = y;
}
// Normalize curve
const minCurve = Math.min(...curve);
const maxCurve = Math.max(...curve);
for (let i = 0; i < 256; i++) {
curve[i] = (curve[i] - minCurve) / (maxCurve - minCurve) * 255;
}
// Apply curve to each channel
for (let i = 0; i < data.length; i += 4) {
data[i] = curve[Math.round(data[i])];
data[i + 1] = curve[Math.round(data[i + 1])];
data[i + 2] = curve[Math.round(data[i + 2])];
}
}
enhanceLocalContrast(data, width, height, radius, amount) {
// Create Gaussian kernel
const kernel = this.createGaussianKernel(radius);
// Apply Gaussian blur to get low-frequency component
const blurred = this.applyGaussianBlur(data, width, height, kernel);
// High pass = original - blurred, then add back with amount
const result = new Float32Array(data.length);
for (let i = 0; i < data.length; i += 4) {
result[i] = data[i] + (data[i] - blurred[i]) * amount;
result[i + 1] = data[i + 1] + (data[i + 1] - blurred[i + 1]) * amount;
result[i + 2] = data[i + 2] + (data[i + 2] - blurred[i + 2]) * amount;
result[i + 3] = data[i + 3];
}
return result;
}
createGaussianKernel(radius) {
const size = radius * 2 + 1;
const kernel = new Float32Array(size * size);
const sigma = radius / 3;
const sigma2 = sigma * sigma;
let sum = 0;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const dx = x - radius;
const dy = y - radius;
const value = Math.exp(-(dx * dx + dy * dy) / (2 * sigma2));
kernel[y * size + x] = value;
sum += value;
}
}
// Normalize
for (let i = 0; i < kernel.length; i++) {
kernel[i] /= sum;
}
return { data: kernel, size: size, radius: radius };
}
applyGaussianBlur(data, width, height, kernel) {
const result = new Float32Array(data.length);
const { data: kernelData, size, radius } = kernel;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let r = 0, g = 0, b = 0;
for (let ky = 0; ky < size; ky++) {
for (let kx = 0; kx < size; kx++) {
const px = Math.min(width - 1, Math.max(0, x + kx - radius));
const py = Math.min(height - 1, Math.max(0, y + ky - radius));
const idx = (py * width + px) * 4;
const weight = kernelData[ky * size + kx];
r += data[idx] * weight;
g += data[idx + 1] * weight;
b += data[idx + 2] * weight;
}
}
const idx = (y * width + x) * 4;
result[idx] = r;
result[idx + 1] = g;
result[idx + 2] = b;
result[idx + 3] = data[idx + 3];
}
}
return result;
}
enhanceColorVibrance(data, vibrance) {
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Convert to HSV-like calculations
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const saturation = max > 0 ? (max - min) / max : 0;
// Less saturated colors get more boost
const saturationBoost = 1.0 + vibrance * (1.0 - saturation);
// Check if it's a skin tone (protect from over-saturation)
const hue = this.calculateHue(r, g, b);
const isSkintone = (hue < 25 || hue > 330) && saturation > 0.1;
const boost = isSkintone ? 1.0 + vibrance * 0.3 : saturationBoost;
// Apply vibrance
const avg = (r + g + b) / 3;
data[i] = avg + (r - avg) * boost;
data[i + 1] = avg + (g - avg) * boost;
data[i + 2] = avg + (b - avg) * boost;
}
}
calculateHue(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const delta = max - min;
if (delta === 0) return 0;
let hue;
if (max === r) {
hue = ((g - b) / delta) % 6;
} else if (max === g) {
hue = (b - r) / delta + 2;
} else {
hue = (r - g) / delta + 4;
}
hue = Math.round(hue * 60);
if (hue < 0) hue += 360;
return hue;
}
applyMicroContrast(data, width, height) {
// Small radius Gaussian blur
const kernel = this.createGaussianKernel(1);
const blurred = this.applyGaussianBlur(data, width, height, kernel);
// Unsharp mask: original * 1.3 - blurred * 0.3
const result = new Float32Array(data.length);
for (let i = 0; i < data.length; i += 4) {
result[i] = data[i] * 1.3 - blurred[i] * 0.3;
result[i + 1] = data[i + 1] * 1.3 - blurred[i + 1] * 0.3;
result[i + 2] = data[i + 2] * 1.3 - blurred[i + 2] * 0.3;
result[i + 3] = data[i + 3];
}
return result;
}
ensureWhites(data) {
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Calculate brightness and saturation
const brightness = 0.299 * r + 0.587 * g + 0.114 * b;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const saturation = max > 0 ? (max - min) / max : 0;
// If very bright and low saturation, make it pure white
if (brightness > 240 && saturation < 0.06) {
data[i] = 255;
data[i + 1] = 255;
data[i + 2] = 255;
}
}
}
}
// Initialize
const processor = new ImageProcessor();
function downloadImage() {
const canvas = document.getElementById('correctedCanvas');
const link = document.createElement('a');
link.download = 'corrected_image.png';
link.href = canvas.toDataURL('image/png');
link.click();
}
function downloadModalImage() {
const canvas = document.getElementById('modalCorrected');
const link = document.createElement('a');
link.download = `corrected_${processor.currentModalIndex + 1}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
}
async function downloadAll() {
for (let i = 0; i < processor.processedImages.length; i++) {
const link = document.createElement('a');
link.download = `corrected_${processor.processedImages[i].name}`;
link.href = processor.processedImages[i].corrected.toDataURL('image/png');
link.click();
await new Promise(resolve => setTimeout(resolve, 100));
}
}
function resetApp() {
document.getElementById('results').style.display = 'none';
document.getElementById('uploadArea').style.display = 'block';
document.getElementById('fileInput').value = '';
processor.processedImages = [];
}
function closeModal() {
document.getElementById('imageModal').style.display = 'none';
}
// Close modal on background click
document.getElementById('imageModal').addEventListener('click', (e) => {
if (e.target.id === 'imageModal') {
closeModal();
}
});
</script>
</body>
</html>