multimodalart HF Staff commited on
Commit
45dc5f3
·
verified ·
1 Parent(s): 538ad3b

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +935 -19
index.html CHANGED
@@ -1,19 +1,935 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>GPT-4o / GPT-Image-1 Generator yellow tint corrector</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16
+ background: #f5f5f5;
17
+ padding: 20px;
18
+ min-height: 100vh;
19
+ display: flex;
20
+ flex-direction: column;
21
+ }
22
+
23
+ .container {
24
+ max-width: 1400px;
25
+ margin: 0 auto;
26
+ background: white;
27
+ padding: 30px;
28
+ border-radius: 8px;
29
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
30
+ flex: 1;
31
+ }
32
+
33
+ h1 {
34
+ font-size: 24px;
35
+ margin-bottom: 20px;
36
+ color: #333;
37
+ }
38
+
39
+ .upload-area {
40
+ border: 2px dashed #ccc;
41
+ border-radius: 4px;
42
+ padding: 40px;
43
+ text-align: center;
44
+ cursor: pointer;
45
+ background: #fafafa;
46
+ margin-bottom: 20px;
47
+ }
48
+
49
+ .upload-area:hover {
50
+ border-color: #999;
51
+ background: #f0f0f0;
52
+ }
53
+
54
+ .upload-area.dragover {
55
+ border-color: #4CAF50;
56
+ background: #f0f8f0;
57
+ }
58
+
59
+ input[type="file"] {
60
+ display: none;
61
+ }
62
+
63
+ .processing {
64
+ display: none;
65
+ text-align: center;
66
+ padding: 20px;
67
+ color: #666;
68
+ }
69
+
70
+ .progress-bar {
71
+ width: 100%;
72
+ height: 20px;
73
+ background: #f0f0f0;
74
+ border-radius: 10px;
75
+ overflow: hidden;
76
+ margin: 10px 0;
77
+ }
78
+
79
+ .progress-fill {
80
+ height: 100%;
81
+ background: #4CAF50;
82
+ transition: width 0.3s ease;
83
+ }
84
+
85
+ .results {
86
+ display: none;
87
+ }
88
+
89
+ .single-result {
90
+ display: none;
91
+ }
92
+
93
+ .bulk-result {
94
+ display: none;
95
+ }
96
+
97
+ .image-grid {
98
+ display: grid;
99
+ grid-template-columns: 1fr 1fr;
100
+ gap: 20px;
101
+ margin-bottom: 20px;
102
+ }
103
+
104
+ .gallery-grid {
105
+ display: grid;
106
+ grid-template-columns: repeat(4, 1fr);
107
+ gap: 15px;
108
+ margin-bottom: 20px;
109
+ }
110
+
111
+ .gallery-item {
112
+ border: 1px solid #ddd;
113
+ border-radius: 4px;
114
+ overflow: hidden;
115
+ cursor: pointer;
116
+ position: relative;
117
+ aspect-ratio: 1;
118
+ }
119
+
120
+ .gallery-item:hover {
121
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
122
+ }
123
+
124
+ .gallery-item canvas {
125
+ width: 100%;
126
+ height: 100%;
127
+ object-fit: cover;
128
+ }
129
+
130
+ .gallery-more {
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: center;
134
+ background: #f0f0f0;
135
+ color: #666;
136
+ font-size: 24px;
137
+ font-weight: bold;
138
+ }
139
+
140
+ .image-container {
141
+ border: 1px solid #ddd;
142
+ border-radius: 4px;
143
+ padding: 10px;
144
+ }
145
+
146
+ .image-container h3 {
147
+ font-size: 14px;
148
+ margin-bottom: 10px;
149
+ color: #666;
150
+ }
151
+
152
+ canvas {
153
+ display: block;
154
+ width: 100%;
155
+ height: auto;
156
+ }
157
+
158
+ .controls {
159
+ display: flex;
160
+ gap: 10px;
161
+ flex-wrap: wrap;
162
+ }
163
+
164
+ button {
165
+ padding: 10px 20px;
166
+ background: #4CAF50;
167
+ color: white;
168
+ border: none;
169
+ border-radius: 4px;
170
+ cursor: pointer;
171
+ font-size: 14px;
172
+ }
173
+
174
+ button:hover {
175
+ background: #45a049;
176
+ }
177
+
178
+ button.secondary {
179
+ background: #757575;
180
+ }
181
+
182
+ button.secondary:hover {
183
+ background: #616161;
184
+ }
185
+
186
+ .info {
187
+ margin-top: 20px;
188
+ padding: 15px;
189
+ background: #f9f9f9;
190
+ border-radius: 4px;
191
+ font-size: 13px;
192
+ color: #666;
193
+ }
194
+
195
+ .modal {
196
+ display: none;
197
+ position: fixed;
198
+ top: 0;
199
+ left: 0;
200
+ right: 0;
201
+ bottom: 0;
202
+ background: rgba(0,0,0,0.8);
203
+ z-index: 1000;
204
+ padding: 20px;
205
+ }
206
+
207
+ .modal-content {
208
+ max-width: 90%;
209
+ max-height: 90%;
210
+ margin: auto;
211
+ position: relative;
212
+ top: 50%;
213
+ transform: translateY(-50%);
214
+ background: white;
215
+ border-radius: 8px;
216
+ padding: 20px;
217
+ }
218
+
219
+ .modal-close {
220
+ position: absolute;
221
+ top: 10px;
222
+ right: 10px;
223
+ font-size: 24px;
224
+ cursor: pointer;
225
+ background: none;
226
+ border: none;
227
+ color: #666;
228
+ }
229
+
230
+ .modal-image-container {
231
+ display: grid;
232
+ grid-template-columns: 1fr 1fr;
233
+ gap: 20px;
234
+ }
235
+
236
+ .modal-image {
237
+ text-align: center;
238
+ }
239
+
240
+ .modal-image h3 {
241
+ margin-bottom: 10px;
242
+ color: #666;
243
+ }
244
+
245
+ .modal-image canvas {
246
+ max-width: 100%;
247
+ height: auto;
248
+ }
249
+
250
+ footer {
251
+ text-align: center;
252
+ padding: 20px;
253
+ color: #666;
254
+ font-size: 12px;
255
+ }
256
+
257
+ footer a {
258
+ color: #4CAF50;
259
+ text-decoration: none;
260
+ }
261
+
262
+ footer a:hover {
263
+ text-decoration: underline;
264
+ }
265
+
266
+ @media (max-width: 768px) {
267
+ .image-grid {
268
+ grid-template-columns: 1fr;
269
+ }
270
+
271
+ .gallery-grid {
272
+ grid-template-columns: repeat(2, 1fr);
273
+ }
274
+
275
+ .modal-image-container {
276
+ grid-template-columns: 1fr;
277
+ }
278
+ }
279
+ </style>
280
+ </head>
281
+ <body>
282
+ <div class="container">
283
+ <h1>GPT-4o / GPT-Image-1 Generator yellow tint corrector</h1>
284
+
285
+ <div class="upload-area" id="uploadArea">
286
+ <p>Drop image(s) here or click to upload</p>
287
+ <p style="font-size: 12px; color: #999; margin-top: 10px;">Supports JPG, PNG, WebP • Multiple files supported</p>
288
+ <input type="file" id="fileInput" accept="image/*" multiple>
289
+ </div>
290
+
291
+ <div class="processing" id="processing">
292
+ <p>Processing <span id="currentFile">0</span> of <span id="totalFiles">0</span> images...</p>
293
+ <div class="progress-bar">
294
+ <div class="progress-fill" id="progressFill"></div>
295
+ </div>
296
+ </div>
297
+
298
+ <div class="results" id="results">
299
+ <div class="single-result" id="singleResult">
300
+ <div class="image-grid">
301
+ <div class="image-container">
302
+ <h3>Original</h3>
303
+ <canvas id="originalCanvas"></canvas>
304
+ </div>
305
+ <div class="image-container">
306
+ <h3>Corrected</h3>
307
+ <canvas id="correctedCanvas"></canvas>
308
+ </div>
309
+ </div>
310
+
311
+ <div class="controls">
312
+ <button onclick="downloadImage()">Download Corrected</button>
313
+ <button class="secondary" onclick="resetApp()">Process More Images</button>
314
+ </div>
315
+ </div>
316
+
317
+ <div class="bulk-result" id="bulkResult">
318
+ <h3 style="margin-bottom: 15px; color: #666;">Corrected Images</h3>
319
+ <div class="gallery-grid" id="galleryGrid"></div>
320
+
321
+ <div class="controls">
322
+ <button onclick="downloadAll()">Download All</button>
323
+ <button class="secondary" onclick="resetApp()">Process More Images</button>
324
+ </div>
325
+ </div>
326
+
327
+ <div class="info" id="info"></div>
328
+ </div>
329
+ </div>
330
+
331
+ <footer>
332
+ Created by <a href="https://x.com/multimodalart" target="_blank">multimodalart</a><br>
333
+ The images are processed on your browser and are never sent to a server
334
+ </footer>
335
+
336
+ <div class="modal" id="imageModal">
337
+ <div class="modal-content">
338
+ <button class="modal-close" onclick="closeModal()">×</button>
339
+ <div class="modal-image-container">
340
+ <div class="modal-image">
341
+ <h3>Original</h3>
342
+ <canvas id="modalOriginal"></canvas>
343
+ </div>
344
+ <div class="modal-image">
345
+ <h3>Corrected</h3>
346
+ <canvas id="modalCorrected"></canvas>
347
+ </div>
348
+ </div>
349
+ <div style="text-align: center; margin-top: 20px;">
350
+ <button onclick="downloadModalImage()">Download Corrected</button>
351
+ </div>
352
+ </div>
353
+ </div>
354
+
355
+ <script>
356
+ // Exact port of Python auto_white_balance_final() function
357
+
358
+ class ImageProcessor {
359
+ constructor() {
360
+ this.processedImages = [];
361
+ this.currentModalIndex = -1;
362
+ this.setupEventListeners();
363
+ }
364
+
365
+ setupEventListeners() {
366
+ const uploadArea = document.getElementById('uploadArea');
367
+ const fileInput = document.getElementById('fileInput');
368
+
369
+ uploadArea.addEventListener('click', () => fileInput.click());
370
+ fileInput.addEventListener('change', (e) => this.handleFiles(e.target.files));
371
+
372
+ uploadArea.addEventListener('dragover', (e) => {
373
+ e.preventDefault();
374
+ uploadArea.classList.add('dragover');
375
+ });
376
+
377
+ uploadArea.addEventListener('dragleave', () => {
378
+ uploadArea.classList.remove('dragover');
379
+ });
380
+
381
+ uploadArea.addEventListener('drop', (e) => {
382
+ e.preventDefault();
383
+ uploadArea.classList.remove('dragover');
384
+ if (e.dataTransfer.files.length > 0) {
385
+ this.handleFiles(e.dataTransfer.files);
386
+ }
387
+ });
388
+ }
389
+
390
+ handleFiles(files) {
391
+ const imageFiles = Array.from(files).filter(file => file.type.startsWith('image/'));
392
+ if (imageFiles.length === 0) {
393
+ alert('Please select image files');
394
+ return;
395
+ }
396
+
397
+ this.processedImages = [];
398
+ this.processMultipleImages(imageFiles);
399
+ }
400
+
401
+ async processMultipleImages(files) {
402
+ document.getElementById('uploadArea').style.display = 'none';
403
+ document.getElementById('processing').style.display = 'block';
404
+ document.getElementById('totalFiles').textContent = files.length;
405
+
406
+ for (let i = 0; i < files.length; i++) {
407
+ document.getElementById('currentFile').textContent = i + 1;
408
+ document.getElementById('progressFill').style.width = `${((i + 1) / files.length) * 100}%`;
409
+
410
+ await this.processFile(files[i]);
411
+ }
412
+
413
+ document.getElementById('processing').style.display = 'none';
414
+ this.displayResults();
415
+ }
416
+
417
+ processFile(file) {
418
+ return new Promise((resolve) => {
419
+ const reader = new FileReader();
420
+ reader.onload = (e) => {
421
+ const img = new Image();
422
+ img.onload = () => {
423
+ const result = this.processImage(img);
424
+ this.processedImages.push({
425
+ name: file.name,
426
+ original: result.original,
427
+ corrected: result.corrected,
428
+ width: img.width,
429
+ height: img.height
430
+ });
431
+ resolve();
432
+ };
433
+ img.src = e.target.result;
434
+ };
435
+ reader.readAsDataURL(file);
436
+ });
437
+ }
438
+
439
+ processImage(img) {
440
+ // Create canvases for processing
441
+ const originalCanvas = document.createElement('canvas');
442
+ const correctedCanvas = document.createElement('canvas');
443
+
444
+ originalCanvas.width = img.width;
445
+ originalCanvas.height = img.height;
446
+ correctedCanvas.width = img.width;
447
+ correctedCanvas.height = img.height;
448
+
449
+ const originalCtx = originalCanvas.getContext('2d');
450
+ const correctedCtx = correctedCanvas.getContext('2d');
451
+
452
+ // Draw original
453
+ originalCtx.drawImage(img, 0, 0);
454
+
455
+ // Get image data
456
+ const imageData = originalCtx.getImageData(0, 0, img.width, img.height);
457
+
458
+ // Apply exact algorithm from Python
459
+ const correctedData = this.autoWhiteBalanceFinal(imageData);
460
+
461
+ // Draw corrected
462
+ correctedCtx.putImageData(correctedData, 0, 0);
463
+
464
+ return {
465
+ original: originalCanvas,
466
+ corrected: correctedCanvas
467
+ };
468
+ }
469
+
470
+ displayResults() {
471
+ document.getElementById('results').style.display = 'block';
472
+
473
+ if (this.processedImages.length === 1) {
474
+ // Single image display
475
+ document.getElementById('singleResult').style.display = 'block';
476
+ document.getElementById('bulkResult').style.display = 'none';
477
+
478
+ const original = document.getElementById('originalCanvas');
479
+ const corrected = document.getElementById('correctedCanvas');
480
+
481
+ original.width = this.processedImages[0].width;
482
+ original.height = this.processedImages[0].height;
483
+ corrected.width = this.processedImages[0].width;
484
+ corrected.height = this.processedImages[0].height;
485
+
486
+ original.getContext('2d').drawImage(this.processedImages[0].original, 0, 0);
487
+ corrected.getContext('2d').drawImage(this.processedImages[0].corrected, 0, 0);
488
+
489
+ } else {
490
+ // Bulk display
491
+ document.getElementById('singleResult').style.display = 'none';
492
+ document.getElementById('bulkResult').style.display = 'block';
493
+
494
+ const galleryGrid = document.getElementById('galleryGrid');
495
+ galleryGrid.innerHTML = '';
496
+
497
+ const maxDisplay = 16;
498
+ const displayCount = Math.min(this.processedImages.length, maxDisplay);
499
+
500
+ for (let i = 0; i < displayCount; i++) {
501
+ if (i === 15 && this.processedImages.length > maxDisplay) {
502
+ // Show "more" indicator
503
+ const moreDiv = document.createElement('div');
504
+ moreDiv.className = 'gallery-item gallery-more';
505
+ moreDiv.textContent = `+${this.processedImages.length - 15}`;
506
+ moreDiv.onclick = () => this.showAllImages();
507
+ galleryGrid.appendChild(moreDiv);
508
+ } else {
509
+ const item = document.createElement('div');
510
+ item.className = 'gallery-item';
511
+ item.onclick = () => this.showModal(i);
512
+
513
+ const canvas = document.createElement('canvas');
514
+ const ctx = canvas.getContext('2d');
515
+ canvas.width = 200;
516
+ canvas.height = 200;
517
+
518
+ // Draw centered crop
519
+ const img = this.processedImages[i].corrected;
520
+ const scale = Math.max(200 / img.width, 200 / img.height);
521
+ const w = img.width * scale;
522
+ const h = img.height * scale;
523
+ ctx.drawImage(img, (200 - w) / 2, (200 - h) / 2, w, h);
524
+
525
+ item.appendChild(canvas);
526
+ galleryGrid.appendChild(item);
527
+ }
528
+ }
529
+ }
530
+
531
+ document.getElementById('info').innerHTML = `
532
+ Processed ${this.processedImages.length} image${this.processedImages.length > 1 ? 's' : ''}
533
+ `;
534
+ }
535
+
536
+ showModal(index) {
537
+ this.currentModalIndex = index;
538
+ const modal = document.getElementById('imageModal');
539
+ const modalOriginal = document.getElementById('modalOriginal');
540
+ const modalCorrected = document.getElementById('modalCorrected');
541
+
542
+ const img = this.processedImages[index];
543
+
544
+ modalOriginal.width = img.width;
545
+ modalOriginal.height = img.height;
546
+ modalCorrected.width = img.width;
547
+ modalCorrected.height = img.height;
548
+
549
+ modalOriginal.getContext('2d').drawImage(img.original, 0, 0);
550
+ modalCorrected.getContext('2d').drawImage(img.corrected, 0, 0);
551
+
552
+ modal.style.display = 'block';
553
+ }
554
+
555
+ showAllImages() {
556
+ // In a real implementation, this could show a paginated view
557
+ alert(`Showing all ${this.processedImages.length} images would be implemented here`);
558
+ }
559
+
560
+ autoWhiteBalanceFinal(imageData) {
561
+ const data = new Float32Array(imageData.data);
562
+ const width = imageData.width;
563
+ const height = imageData.height;
564
+
565
+ // Step 1: Robust color correction
566
+ const { avgR, avgG, avgB } = this.robustMean(data);
567
+
568
+ // Detect yellow tint severity
569
+ const yellowFactor = ((avgR + avgG) / 2) / (avgB + 1);
570
+ const yellowSeverity = Math.min(Math.max((yellowFactor - 1.0) / 0.5, 0), 1);
571
+
572
+ // Adaptive correction based on tint level
573
+ const targetGray = 165 + yellowSeverity * 20;
574
+ const blueBoost = 1.08 + yellowSeverity * 0.12;
575
+ const redReduction = 0.96 - yellowSeverity * 0.04;
576
+
577
+ let scaleB = (targetGray * blueBoost) / avgB;
578
+ let scaleG = targetGray / avgG;
579
+ let scaleR = (targetGray * redReduction) / avgR;
580
+
581
+ // Apply safety limits
582
+ scaleB = Math.min(Math.max(scaleB, 0.7), 3.0);
583
+ scaleG = Math.min(Math.max(scaleG, 0.7), 2.5);
584
+ scaleR = Math.min(Math.max(scaleR, 0.7), 2.5);
585
+
586
+ // Apply channel scaling
587
+ for (let i = 0; i < data.length; i += 4) {
588
+ data[i] *= scaleR;
589
+ data[i + 1] *= scaleG;
590
+ data[i + 2] *= scaleB;
591
+ }
592
+
593
+ // Clip
594
+ for (let i = 0; i < data.length; i += 4) {
595
+ data[i] = Math.min(255, Math.max(0, data[i]));
596
+ data[i + 1] = Math.min(255, Math.max(0, data[i + 1]));
597
+ data[i + 2] = Math.min(255, Math.max(0, data[i + 2]));
598
+ }
599
+
600
+ // Step 2: Adaptive exposure compensation
601
+ const meanBrightness = this.calculateMeanBrightness(data);
602
+ const targetBrightness = 140;
603
+ let exposureCompensation = targetBrightness / (meanBrightness + 1);
604
+ exposureCompensation = Math.min(Math.max(exposureCompensation, 0.9), 1.3);
605
+
606
+ for (let i = 0; i < data.length; i += 4) {
607
+ data[i] *= exposureCompensation;
608
+ data[i + 1] *= exposureCompensation;
609
+ data[i + 2] *= exposureCompensation;
610
+ }
611
+
612
+ // Clip again
613
+ for (let i = 0; i < data.length; i += 4) {
614
+ data[i] = Math.min(255, Math.max(0, data[i]));
615
+ data[i + 1] = Math.min(255, Math.max(0, data[i + 1]));
616
+ data[i + 2] = Math.min(255, Math.max(0, data[i + 2]));
617
+ }
618
+
619
+ // Step 3: S-curve for professional contrast (strength=0.25)
620
+ this.applySCurve(data, 0.25);
621
+
622
+ // Step 4: Local contrast (clarity) - radius=15, amount=0.25
623
+ const localContrastData = this.enhanceLocalContrast(data, width, height, 15, 0.25);
624
+
625
+ // Step 5: Balanced vibrance (vibrance=0.30)
626
+ this.enhanceColorVibrance(localContrastData, 0.30);
627
+
628
+ // Step 6: Micro-contrast for crispness
629
+ const finalData = this.applyMicroContrast(localContrastData, width, height);
630
+
631
+ // Step 7: Guarantee pure whites
632
+ this.ensureWhites(finalData);
633
+
634
+ // Convert back to Uint8ClampedArray
635
+ const result = new Uint8ClampedArray(finalData.length);
636
+ for (let i = 0; i < finalData.length; i++) {
637
+ result[i] = Math.min(255, Math.max(0, Math.round(finalData[i])));
638
+ }
639
+
640
+ return new ImageData(result, width, height);
641
+ }
642
+
643
+ robustMean(data) {
644
+ const rValues = [];
645
+ const gValues = [];
646
+ const bValues = [];
647
+
648
+ // Collect non-zero values
649
+ for (let i = 0; i < data.length; i += 4) {
650
+ if (data[i] > 10) rValues.push(data[i]);
651
+ if (data[i + 1] > 10) gValues.push(data[i + 1]);
652
+ if (data[i + 2] > 10) bValues.push(data[i + 2]);
653
+ }
654
+
655
+ // Sort arrays
656
+ rValues.sort((a, b) => a - b);
657
+ gValues.sort((a, b) => a - b);
658
+ bValues.sort((a, b) => a - b);
659
+
660
+ // Get mean of percentile 20 to 80
661
+ const getPercentileMean = (arr) => {
662
+ if (arr.length === 0) return 128;
663
+ const start = Math.floor(arr.length * 0.2);
664
+ const end = Math.floor(arr.length * 0.8);
665
+ let sum = 0;
666
+ for (let i = start; i < end; i++) {
667
+ sum += arr[i];
668
+ }
669
+ return sum / (end - start);
670
+ };
671
+
672
+ return {
673
+ avgR: getPercentileMean(rValues),
674
+ avgG: getPercentileMean(gValues),
675
+ avgB: getPercentileMean(bValues)
676
+ };
677
+ }
678
+
679
+ calculateMeanBrightness(data) {
680
+ let sum = 0;
681
+ let count = 0;
682
+ for (let i = 0; i < data.length; i += 4) {
683
+ // RGB to grayscale
684
+ const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
685
+ sum += gray;
686
+ count++;
687
+ }
688
+ return sum / count;
689
+ }
690
+
691
+ applySCurve(data, strength) {
692
+ // Create S-curve lookup table
693
+ const k = strength * 10;
694
+ const midpoint = 0.5;
695
+ const curve = new Float32Array(256);
696
+
697
+ for (let i = 0; i < 256; i++) {
698
+ const x = i / 255;
699
+ const y = 1 / (1 + Math.exp(-k * (x - midpoint)));
700
+ curve[i] = y;
701
+ }
702
+
703
+ // Normalize curve
704
+ const minCurve = Math.min(...curve);
705
+ const maxCurve = Math.max(...curve);
706
+ for (let i = 0; i < 256; i++) {
707
+ curve[i] = (curve[i] - minCurve) / (maxCurve - minCurve) * 255;
708
+ }
709
+
710
+ // Apply curve to each channel
711
+ for (let i = 0; i < data.length; i += 4) {
712
+ data[i] = curve[Math.round(data[i])];
713
+ data[i + 1] = curve[Math.round(data[i + 1])];
714
+ data[i + 2] = curve[Math.round(data[i + 2])];
715
+ }
716
+ }
717
+
718
+ enhanceLocalContrast(data, width, height, radius, amount) {
719
+ // Create Gaussian kernel
720
+ const kernel = this.createGaussianKernel(radius);
721
+
722
+ // Apply Gaussian blur to get low-frequency component
723
+ const blurred = this.applyGaussianBlur(data, width, height, kernel);
724
+
725
+ // High pass = original - blurred, then add back with amount
726
+ const result = new Float32Array(data.length);
727
+ for (let i = 0; i < data.length; i += 4) {
728
+ result[i] = data[i] + (data[i] - blurred[i]) * amount;
729
+ result[i + 1] = data[i + 1] + (data[i + 1] - blurred[i + 1]) * amount;
730
+ result[i + 2] = data[i + 2] + (data[i + 2] - blurred[i + 2]) * amount;
731
+ result[i + 3] = data[i + 3];
732
+ }
733
+
734
+ return result;
735
+ }
736
+
737
+ createGaussianKernel(radius) {
738
+ const size = radius * 2 + 1;
739
+ const kernel = new Float32Array(size * size);
740
+ const sigma = radius / 3;
741
+ const sigma2 = sigma * sigma;
742
+ let sum = 0;
743
+
744
+ for (let y = 0; y < size; y++) {
745
+ for (let x = 0; x < size; x++) {
746
+ const dx = x - radius;
747
+ const dy = y - radius;
748
+ const value = Math.exp(-(dx * dx + dy * dy) / (2 * sigma2));
749
+ kernel[y * size + x] = value;
750
+ sum += value;
751
+ }
752
+ }
753
+
754
+ // Normalize
755
+ for (let i = 0; i < kernel.length; i++) {
756
+ kernel[i] /= sum;
757
+ }
758
+
759
+ return { data: kernel, size: size, radius: radius };
760
+ }
761
+
762
+ applyGaussianBlur(data, width, height, kernel) {
763
+ const result = new Float32Array(data.length);
764
+ const { data: kernelData, size, radius } = kernel;
765
+
766
+ for (let y = 0; y < height; y++) {
767
+ for (let x = 0; x < width; x++) {
768
+ let r = 0, g = 0, b = 0;
769
+
770
+ for (let ky = 0; ky < size; ky++) {
771
+ for (let kx = 0; kx < size; kx++) {
772
+ const px = Math.min(width - 1, Math.max(0, x + kx - radius));
773
+ const py = Math.min(height - 1, Math.max(0, y + ky - radius));
774
+ const idx = (py * width + px) * 4;
775
+ const weight = kernelData[ky * size + kx];
776
+
777
+ r += data[idx] * weight;
778
+ g += data[idx + 1] * weight;
779
+ b += data[idx + 2] * weight;
780
+ }
781
+ }
782
+
783
+ const idx = (y * width + x) * 4;
784
+ result[idx] = r;
785
+ result[idx + 1] = g;
786
+ result[idx + 2] = b;
787
+ result[idx + 3] = data[idx + 3];
788
+ }
789
+ }
790
+
791
+ return result;
792
+ }
793
+
794
+ enhanceColorVibrance(data, vibrance) {
795
+ for (let i = 0; i < data.length; i += 4) {
796
+ const r = data[i];
797
+ const g = data[i + 1];
798
+ const b = data[i + 2];
799
+
800
+ // Convert to HSV-like calculations
801
+ const max = Math.max(r, g, b);
802
+ const min = Math.min(r, g, b);
803
+ const saturation = max > 0 ? (max - min) / max : 0;
804
+
805
+ // Less saturated colors get more boost
806
+ const saturationBoost = 1.0 + vibrance * (1.0 - saturation);
807
+
808
+ // Check if it's a skin tone (protect from over-saturation)
809
+ const hue = this.calculateHue(r, g, b);
810
+ const isSkintone = (hue < 25 || hue > 330) && saturation > 0.1;
811
+
812
+ const boost = isSkintone ? 1.0 + vibrance * 0.3 : saturationBoost;
813
+
814
+ // Apply vibrance
815
+ const avg = (r + g + b) / 3;
816
+ data[i] = avg + (r - avg) * boost;
817
+ data[i + 1] = avg + (g - avg) * boost;
818
+ data[i + 2] = avg + (b - avg) * boost;
819
+ }
820
+ }
821
+
822
+ calculateHue(r, g, b) {
823
+ r /= 255;
824
+ g /= 255;
825
+ b /= 255;
826
+
827
+ const max = Math.max(r, g, b);
828
+ const min = Math.min(r, g, b);
829
+ const delta = max - min;
830
+
831
+ if (delta === 0) return 0;
832
+
833
+ let hue;
834
+ if (max === r) {
835
+ hue = ((g - b) / delta) % 6;
836
+ } else if (max === g) {
837
+ hue = (b - r) / delta + 2;
838
+ } else {
839
+ hue = (r - g) / delta + 4;
840
+ }
841
+
842
+ hue = Math.round(hue * 60);
843
+ if (hue < 0) hue += 360;
844
+
845
+ return hue;
846
+ }
847
+
848
+ applyMicroContrast(data, width, height) {
849
+ // Small radius Gaussian blur
850
+ const kernel = this.createGaussianKernel(1);
851
+ const blurred = this.applyGaussianBlur(data, width, height, kernel);
852
+
853
+ // Unsharp mask: original * 1.3 - blurred * 0.3
854
+ const result = new Float32Array(data.length);
855
+ for (let i = 0; i < data.length; i += 4) {
856
+ result[i] = data[i] * 1.3 - blurred[i] * 0.3;
857
+ result[i + 1] = data[i + 1] * 1.3 - blurred[i + 1] * 0.3;
858
+ result[i + 2] = data[i + 2] * 1.3 - blurred[i + 2] * 0.3;
859
+ result[i + 3] = data[i + 3];
860
+ }
861
+
862
+ return result;
863
+ }
864
+
865
+ ensureWhites(data) {
866
+ for (let i = 0; i < data.length; i += 4) {
867
+ const r = data[i];
868
+ const g = data[i + 1];
869
+ const b = data[i + 2];
870
+
871
+ // Calculate brightness and saturation
872
+ const brightness = 0.299 * r + 0.587 * g + 0.114 * b;
873
+ const max = Math.max(r, g, b);
874
+ const min = Math.min(r, g, b);
875
+ const saturation = max > 0 ? (max - min) / max : 0;
876
+
877
+ // If very bright and low saturation, make it pure white
878
+ if (brightness > 240 && saturation < 0.06) {
879
+ data[i] = 255;
880
+ data[i + 1] = 255;
881
+ data[i + 2] = 255;
882
+ }
883
+ }
884
+ }
885
+ }
886
+
887
+ // Initialize
888
+ const processor = new ImageProcessor();
889
+
890
+ function downloadImage() {
891
+ const canvas = document.getElementById('correctedCanvas');
892
+ const link = document.createElement('a');
893
+ link.download = 'corrected_image.png';
894
+ link.href = canvas.toDataURL('image/png');
895
+ link.click();
896
+ }
897
+
898
+ function downloadModalImage() {
899
+ const canvas = document.getElementById('modalCorrected');
900
+ const link = document.createElement('a');
901
+ link.download = `corrected_${processor.currentModalIndex + 1}.png`;
902
+ link.href = canvas.toDataURL('image/png');
903
+ link.click();
904
+ }
905
+
906
+ async function downloadAll() {
907
+ for (let i = 0; i < processor.processedImages.length; i++) {
908
+ const link = document.createElement('a');
909
+ link.download = `corrected_${processor.processedImages[i].name}`;
910
+ link.href = processor.processedImages[i].corrected.toDataURL('image/png');
911
+ link.click();
912
+ await new Promise(resolve => setTimeout(resolve, 100));
913
+ }
914
+ }
915
+
916
+ function resetApp() {
917
+ document.getElementById('results').style.display = 'none';
918
+ document.getElementById('uploadArea').style.display = 'block';
919
+ document.getElementById('fileInput').value = '';
920
+ processor.processedImages = [];
921
+ }
922
+
923
+ function closeModal() {
924
+ document.getElementById('imageModal').style.display = 'none';
925
+ }
926
+
927
+ // Close modal on background click
928
+ document.getElementById('imageModal').addEventListener('click', (e) => {
929
+ if (e.target.id === 'imageModal') {
930
+ closeModal();
931
+ }
932
+ });
933
+ </script>
934
+ </body>
935
+ </html>