Akash8150 commited on
Commit
1010bf9
Β·
1 Parent(s): b17d851

Add sample images with one-click denoising UI

Browse files
Dockerfile CHANGED
@@ -29,6 +29,7 @@ COPY best_autoencoder_model.h5 .
29
  COPY src/ ./src/
30
  COPY static/ ./static/
31
  COPY templates/ ./templates/
 
32
 
33
  # Hugging Face Spaces runs on port 7860
34
  EXPOSE 7860
 
29
  COPY src/ ./src/
30
  COPY static/ ./static/
31
  COPY templates/ ./templates/
32
+ COPY test_images/ ./test_images/
33
 
34
  # Hugging Face Spaces runs on port 7860
35
  EXPOSE 7860
app.py CHANGED
@@ -10,6 +10,7 @@ app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024
10
 
11
  MODEL_PATH = "best_autoencoder_model.h5"
12
  MODEL_INFO_PATH = "model_info.json"
 
13
  model = None
14
  model_info = None
15
 
@@ -23,8 +24,6 @@ def load_trained_model():
23
  from tensorflow.keras.models import Model
24
  from tensorflow.keras.layers import Input, Conv2D, MaxPooling2D, UpSampling2D
25
  try:
26
- # Rebuild the exact same architecture, then load weights only.
27
- # This bypasses Keras version deserialization issues with InputLayer config.
28
  inp = Input(shape=(28, 28, 1))
29
  x = Conv2D(32, (3, 3), activation="relu", padding="same")(inp)
30
  x = MaxPooling2D((2, 2), padding="same")(x)
@@ -89,6 +88,48 @@ def get_model_info():
89
  return jsonify({"error": "not available"}), 404
90
 
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  @app.route("/denoise", methods=["POST"])
93
  def denoise():
94
  if model is None:
 
10
 
11
  MODEL_PATH = "best_autoencoder_model.h5"
12
  MODEL_INFO_PATH = "model_info.json"
13
+ SAMPLES_DIR = "test_images"
14
  model = None
15
  model_info = None
16
 
 
24
  from tensorflow.keras.models import Model
25
  from tensorflow.keras.layers import Input, Conv2D, MaxPooling2D, UpSampling2D
26
  try:
 
 
27
  inp = Input(shape=(28, 28, 1))
28
  x = Conv2D(32, (3, 3), activation="relu", padding="same")(inp)
29
  x = MaxPooling2D((2, 2), padding="same")(x)
 
88
  return jsonify({"error": "not available"}), 404
89
 
90
 
91
+ @app.route("/api/samples")
92
+ def get_samples():
93
+ """Return list of sample images as base64 thumbnails"""
94
+ samples = []
95
+ if os.path.exists(SAMPLES_DIR):
96
+ for fname in sorted(os.listdir(SAMPLES_DIR)):
97
+ if fname.lower().endswith((".png", ".jpg", ".jpeg")):
98
+ fpath = os.path.join(SAMPLES_DIR, fname)
99
+ with open(fpath, "rb") as f:
100
+ b64 = base64.b64encode(f.read()).decode()
101
+ # Parse label from filename e.g. noisy_digit_2_8.png -> Digit 2
102
+ parts = fname.replace(".png", "").split("_")
103
+ label = f"Digit {parts[2]}" if len(parts) >= 3 else fname
104
+ samples.append({
105
+ "filename": fname,
106
+ "label": label,
107
+ "thumbnail": f"data:image/png;base64,{b64}"
108
+ })
109
+ return jsonify(samples)
110
+
111
+
112
+ @app.route("/api/denoise-sample", methods=["POST"])
113
+ def denoise_sample():
114
+ """Denoise a built-in sample image by filename"""
115
+ if model is None:
116
+ return jsonify({"error": "Model not loaded"}), 500
117
+ data = request.get_json()
118
+ filename = data.get("filename", "")
119
+ # Sanitize: only allow filenames, no path traversal
120
+ filename = os.path.basename(filename)
121
+ fpath = os.path.join(SAMPLES_DIR, filename)
122
+ if not os.path.exists(fpath):
123
+ return jsonify({"error": "Sample not found"}), 404
124
+ try:
125
+ image = Image.open(fpath)
126
+ proc = preprocess_image(image)
127
+ denoised = model.predict(proc, verbose=0)
128
+ return jsonify({"original": array_to_base64(proc), "denoised": array_to_base64(denoised)})
129
+ except Exception as e:
130
+ return jsonify({"error": str(e)}), 500
131
+
132
+
133
  @app.route("/denoise", methods=["POST"])
134
  def denoise():
135
  if model is None:
static/script.js CHANGED
@@ -7,112 +7,133 @@ const loading = document.getElementById('loading');
7
  const error = document.getElementById('error');
8
  const originalImg = document.getElementById('originalImg');
9
  const denoisedImg = document.getElementById('denoisedImg');
 
10
 
11
  let selectedFile = null;
12
 
13
- // Click to upload
14
- uploadBox.addEventListener('click', () => {
15
- imageInput.click();
16
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- // File selection
19
- imageInput.addEventListener('change', (e) => {
20
- handleFile(e.target.files[0]);
21
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- // Drag and drop
24
  uploadBox.addEventListener('dragover', (e) => {
25
  e.preventDefault();
26
  uploadBox.classList.add('dragover');
27
  });
28
-
29
- uploadBox.addEventListener('dragleave', () => {
30
- uploadBox.classList.remove('dragover');
31
- });
32
-
33
  uploadBox.addEventListener('drop', (e) => {
34
  e.preventDefault();
35
  uploadBox.classList.remove('dragover');
36
  handleFile(e.dataTransfer.files[0]);
37
  });
38
 
39
- // Handle file selection
40
  function handleFile(file) {
41
  if (!file) return;
42
-
43
- if (!file.type.startsWith('image/')) {
44
- showError('Please upload an image file');
45
- return;
46
- }
47
-
48
  selectedFile = file;
49
  denoiseBtn.disabled = false;
50
-
51
- // Update upload box to show file name
52
  const uploadContent = uploadBox.querySelector('.upload-content');
53
  uploadContent.innerHTML = `
54
  <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
55
  <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
56
  <polyline points="13 2 13 9 20 9"></polyline>
57
  </svg>
58
- <p style="color: #667eea; font-weight: 600;">${file.name}</p>
59
  <span>Click to change file</span>
60
  `;
61
-
62
  hideError();
63
  resultsSection.style.display = 'none';
64
  }
65
 
66
- // Denoise button click
67
  denoiseBtn.addEventListener('click', async () => {
68
  if (!selectedFile) return;
69
-
70
- // Show loading
71
- loading.style.display = 'block';
72
- resultsSection.style.display = 'none';
73
  hideError();
74
  denoiseBtn.disabled = true;
75
-
76
- // Create form data
77
  const formData = new FormData();
78
  formData.append('image', selectedFile);
79
-
80
  try {
81
- const response = await fetch('/denoise', {
82
- method: 'POST',
83
- body: formData
84
- });
85
-
86
- const data = await response.json();
87
-
88
- if (!response.ok) {
89
- throw new Error(data.error || 'Failed to denoise image');
90
- }
91
-
92
- // Display results
93
- originalImg.src = data.original;
94
- denoisedImg.src = data.denoised;
95
-
96
- loading.style.display = 'none';
97
- resultsSection.style.display = 'block';
98
  denoiseBtn.disabled = false;
99
-
100
  } catch (err) {
101
- loading.style.display = 'none';
102
  showError(err.message);
103
  denoiseBtn.disabled = false;
104
  }
105
  });
106
 
107
- // Error handling
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  function showError(message) {
109
  error.textContent = message;
110
  error.style.display = 'block';
111
- setTimeout(() => {
112
- hideError();
113
- }, 5000);
114
  }
115
 
116
- function hideError() {
117
- error.style.display = 'none';
118
- }
 
 
7
  const error = document.getElementById('error');
8
  const originalImg = document.getElementById('originalImg');
9
  const denoisedImg = document.getElementById('denoisedImg');
10
+ const samplesGrid = document.getElementById('samplesGrid');
11
 
12
  let selectedFile = null;
13
 
14
+ // ── Load sample images on page load ──────────────────────────────────────────
15
+ async function loadSamples() {
16
+ try {
17
+ const res = await fetch('/api/samples');
18
+ const samples = await res.json();
19
+ samplesGrid.innerHTML = '';
20
+ samples.forEach(s => {
21
+ const card = document.createElement('div');
22
+ card.className = 'sample-card';
23
+ card.innerHTML = `
24
+ <img src="${s.thumbnail}" alt="${s.label}" title="Click to denoise">
25
+ <span>${s.label}</span>
26
+ `;
27
+ card.addEventListener('click', () => denoiseSample(s.filename, card));
28
+ samplesGrid.appendChild(card);
29
+ });
30
+ } catch (e) {
31
+ samplesGrid.innerHTML = '<p style="color:#999">Could not load samples.</p>';
32
+ }
33
+ }
34
 
35
+ async function denoiseSample(filename, card) {
36
+ // Highlight selected card
37
+ document.querySelectorAll('.sample-card').forEach(c => c.classList.remove('active'));
38
+ card.classList.add('active');
39
+
40
+ showLoading();
41
+ hideError();
42
+
43
+ try {
44
+ const res = await fetch('/api/denoise-sample', {
45
+ method: 'POST',
46
+ headers: { 'Content-Type': 'application/json' },
47
+ body: JSON.stringify({ filename })
48
+ });
49
+ const data = await res.json();
50
+ if (!res.ok) throw new Error(data.error || 'Failed');
51
+ showResults(data.original, data.denoised);
52
+ } catch (err) {
53
+ hideLoading();
54
+ showError(err.message);
55
+ }
56
+ }
57
+
58
+ // ── Upload flow ───────────────────────────────────────────────────────────────
59
+ uploadBox.addEventListener('click', () => imageInput.click());
60
+
61
+ imageInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
62
 
 
63
  uploadBox.addEventListener('dragover', (e) => {
64
  e.preventDefault();
65
  uploadBox.classList.add('dragover');
66
  });
67
+ uploadBox.addEventListener('dragleave', () => uploadBox.classList.remove('dragover'));
 
 
 
 
68
  uploadBox.addEventListener('drop', (e) => {
69
  e.preventDefault();
70
  uploadBox.classList.remove('dragover');
71
  handleFile(e.dataTransfer.files[0]);
72
  });
73
 
 
74
  function handleFile(file) {
75
  if (!file) return;
76
+ if (!file.type.startsWith('image/')) { showError('Please upload an image file'); return; }
 
 
 
 
 
77
  selectedFile = file;
78
  denoiseBtn.disabled = false;
 
 
79
  const uploadContent = uploadBox.querySelector('.upload-content');
80
  uploadContent.innerHTML = `
81
  <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
82
  <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
83
  <polyline points="13 2 13 9 20 9"></polyline>
84
  </svg>
85
+ <p style="color:#667eea;font-weight:600;">${file.name}</p>
86
  <span>Click to change file</span>
87
  `;
 
88
  hideError();
89
  resultsSection.style.display = 'none';
90
  }
91
 
 
92
  denoiseBtn.addEventListener('click', async () => {
93
  if (!selectedFile) return;
94
+ showLoading();
 
 
 
95
  hideError();
96
  denoiseBtn.disabled = true;
97
+
 
98
  const formData = new FormData();
99
  formData.append('image', selectedFile);
100
+
101
  try {
102
+ const res = await fetch('/denoise', { method: 'POST', body: formData });
103
+ const data = await res.json();
104
+ if (!res.ok) throw new Error(data.error || 'Failed to denoise image');
105
+ showResults(data.original, data.denoised);
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  denoiseBtn.disabled = false;
 
107
  } catch (err) {
108
+ hideLoading();
109
  showError(err.message);
110
  denoiseBtn.disabled = false;
111
  }
112
  });
113
 
114
+ // ── Helpers ───────────────────────────────────────────────────────────────────
115
+ function showResults(original, denoised) {
116
+ originalImg.src = original;
117
+ denoisedImg.src = denoised;
118
+ hideLoading();
119
+ resultsSection.style.display = 'block';
120
+ resultsSection.scrollIntoView({ behavior: 'smooth' });
121
+ }
122
+
123
+ function showLoading() {
124
+ loading.style.display = 'block';
125
+ resultsSection.style.display = 'none';
126
+ }
127
+
128
+ function hideLoading() { loading.style.display = 'none'; }
129
+
130
  function showError(message) {
131
  error.textContent = message;
132
  error.style.display = 'block';
133
+ setTimeout(hideError, 5000);
 
 
134
  }
135
 
136
+ function hideError() { error.style.display = 'none'; }
137
+
138
+ // Init
139
+ loadSamples();
static/style.css CHANGED
@@ -304,3 +304,87 @@ header p {
304
  padding: 20px;
305
  }
306
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  padding: 20px;
305
  }
306
  }
307
+
308
+ /* ── Sample images section ─────────────────────────────────────── */
309
+ .samples-section {
310
+ background: white;
311
+ border-radius: 20px;
312
+ padding: 40px;
313
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
314
+ margin-bottom: 30px;
315
+ }
316
+
317
+ .samples-section h2 {
318
+ color: #333;
319
+ font-size: 1.8rem;
320
+ font-weight: 700;
321
+ margin-bottom: 8px;
322
+ text-align: center;
323
+ }
324
+
325
+ .samples-subtitle {
326
+ text-align: center;
327
+ color: #666;
328
+ margin-bottom: 24px;
329
+ font-size: 1rem;
330
+ }
331
+
332
+ .samples-grid {
333
+ display: flex;
334
+ flex-wrap: wrap;
335
+ gap: 16px;
336
+ justify-content: center;
337
+ }
338
+
339
+ .samples-loading {
340
+ color: #999;
341
+ font-size: 1rem;
342
+ }
343
+
344
+ .sample-card {
345
+ display: flex;
346
+ flex-direction: column;
347
+ align-items: center;
348
+ gap: 8px;
349
+ cursor: pointer;
350
+ padding: 12px;
351
+ border-radius: 12px;
352
+ border: 2px solid #e8e9ff;
353
+ background: #f8f9ff;
354
+ transition: all 0.2s ease;
355
+ width: 100px;
356
+ }
357
+
358
+ .sample-card:hover {
359
+ border-color: #667eea;
360
+ transform: translateY(-4px);
361
+ box-shadow: 0 6px 16px rgba(102,126,234,0.25);
362
+ }
363
+
364
+ .sample-card.active {
365
+ border-color: #764ba2;
366
+ background: #f0eaff;
367
+ box-shadow: 0 6px 16px rgba(118,75,162,0.3);
368
+ }
369
+
370
+ .sample-card img {
371
+ width: 64px;
372
+ height: 64px;
373
+ image-rendering: pixelated;
374
+ border-radius: 6px;
375
+ }
376
+
377
+ .sample-card span {
378
+ font-size: 0.8rem;
379
+ font-weight: 600;
380
+ color: #555;
381
+ }
382
+
383
+ .upload-section h2,
384
+ .results-section h2 {
385
+ color: #333;
386
+ font-size: 1.5rem;
387
+ font-weight: 700;
388
+ margin-bottom: 20px;
389
+ text-align: center;
390
+ }
templates/index.html CHANGED
@@ -3,14 +3,14 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Image Denoiser - AI Powered</title>
7
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
  </head>
9
  <body>
10
  <div class="container">
11
  <header>
12
- <h1>🎨 Image Denoiser</h1>
13
- <p>Upload a noisy image and let AI clean it up</p>
14
  </header>
15
 
16
  <!-- Model Info Section -->
@@ -18,7 +18,6 @@
18
  <div class="info-header">
19
  <h2>πŸ“Š Model Performance</h2>
20
  </div>
21
-
22
  <div class="metrics-grid">
23
  <div class="metric-box">
24
  <div class="metric-icon">🎯</div>
@@ -27,13 +26,10 @@
27
  <div class="metric-value">
28
  {% if model_info and model_info.test_accuracy != 'N/A' %}
29
  {{ "%.2f"|format(model_info.test_accuracy * 100) }}%
30
- {% else %}
31
- N/A
32
- {% endif %}
33
  </div>
34
  </div>
35
  </div>
36
-
37
  <div class="metric-box">
38
  <div class="metric-icon">πŸ“ˆ</div>
39
  <div class="metric-content">
@@ -41,13 +37,10 @@
41
  <div class="metric-value">
42
  {% if model_info and model_info.test_f1_score != 'N/A' %}
43
  {{ "%.4f"|format(model_info.test_f1_score) }}
44
- {% else %}
45
- N/A
46
- {% endif %}
47
  </div>
48
  </div>
49
  </div>
50
-
51
  <div class="metric-box">
52
  <div class="metric-icon">πŸ“‰</div>
53
  <div class="metric-content">
@@ -55,14 +48,11 @@
55
  <div class="metric-value">
56
  {% if model_info and model_info.test_loss != 'N/A' %}
57
  {{ "%.4f"|format(model_info.test_loss) }}
58
- {% else %}
59
- N/A
60
- {% endif %}
61
  </div>
62
  </div>
63
  </div>
64
  </div>
65
-
66
  <div class="dataset-section">
67
  <h3>πŸ“š Training Information</h3>
68
  <div class="dataset-grid">
@@ -98,7 +88,18 @@
98
  </div>
99
  </div>
100
 
 
 
 
 
 
 
 
 
 
 
101
  <div class="upload-section">
 
102
  <div class="upload-box" id="uploadBox">
103
  <input type="file" id="imageInput" accept="image/*" hidden>
104
  <div class="upload-content">
@@ -114,7 +115,9 @@
114
  <button id="denoiseBtn" class="btn-primary" disabled>Denoise Image</button>
115
  </div>
116
 
 
117
  <div class="results-section" id="resultsSection" style="display: none;">
 
118
  <div class="image-comparison">
119
  <div class="image-box">
120
  <h3>Original (Noisy)</h3>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DeepClean - Image Denoiser</title>
7
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
  </head>
9
  <body>
10
  <div class="container">
11
  <header>
12
+ <h1>🎨 DeepClean</h1>
13
+ <p>CNN Autoencoder β€” Upload a noisy image or try a sample below</p>
14
  </header>
15
 
16
  <!-- Model Info Section -->
 
18
  <div class="info-header">
19
  <h2>πŸ“Š Model Performance</h2>
20
  </div>
 
21
  <div class="metrics-grid">
22
  <div class="metric-box">
23
  <div class="metric-icon">🎯</div>
 
26
  <div class="metric-value">
27
  {% if model_info and model_info.test_accuracy != 'N/A' %}
28
  {{ "%.2f"|format(model_info.test_accuracy * 100) }}%
29
+ {% else %}N/A{% endif %}
 
 
30
  </div>
31
  </div>
32
  </div>
 
33
  <div class="metric-box">
34
  <div class="metric-icon">πŸ“ˆ</div>
35
  <div class="metric-content">
 
37
  <div class="metric-value">
38
  {% if model_info and model_info.test_f1_score != 'N/A' %}
39
  {{ "%.4f"|format(model_info.test_f1_score) }}
40
+ {% else %}N/A{% endif %}
 
 
41
  </div>
42
  </div>
43
  </div>
 
44
  <div class="metric-box">
45
  <div class="metric-icon">πŸ“‰</div>
46
  <div class="metric-content">
 
48
  <div class="metric-value">
49
  {% if model_info and model_info.test_loss != 'N/A' %}
50
  {{ "%.4f"|format(model_info.test_loss) }}
51
+ {% else %}N/A{% endif %}
 
 
52
  </div>
53
  </div>
54
  </div>
55
  </div>
 
56
  <div class="dataset-section">
57
  <h3>πŸ“š Training Information</h3>
58
  <div class="dataset-grid">
 
88
  </div>
89
  </div>
90
 
91
+ <!-- Sample Images Section -->
92
+ <div class="samples-section">
93
+ <h2>πŸ–ΌοΈ Try a Sample Image</h2>
94
+ <p class="samples-subtitle">Click any noisy digit below to instantly denoise it</p>
95
+ <div class="samples-grid" id="samplesGrid">
96
+ <div class="samples-loading">Loading samples...</div>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- Upload Section -->
101
  <div class="upload-section">
102
+ <h2>πŸ“€ Or Upload Your Own</h2>
103
  <div class="upload-box" id="uploadBox">
104
  <input type="file" id="imageInput" accept="image/*" hidden>
105
  <div class="upload-content">
 
115
  <button id="denoiseBtn" class="btn-primary" disabled>Denoise Image</button>
116
  </div>
117
 
118
+ <!-- Results Section -->
119
  <div class="results-section" id="resultsSection" style="display: none;">
120
+ <h2>✨ Result</h2>
121
  <div class="image-comparison">
122
  <div class="image-box">
123
  <h3>Original (Noisy)</h3>
test_images/noisy_digit_2_8.png ADDED
test_images/noisy_digit_3_1.png ADDED
test_images/noisy_digit_3_4.png ADDED
test_images/noisy_digit_4_2.png ADDED
test_images/noisy_digit_4_6.png ADDED
test_images/noisy_digit_5_10.png ADDED
test_images/noisy_digit_5_5.png ADDED
test_images/noisy_digit_5_7.png ADDED
test_images/noisy_digit_5_9.png ADDED
test_images/noisy_digit_8_3.png ADDED