Akash8150 commited on
Commit
cdf0064
Β·
1 Parent(s): cd1cf0c

Deploy Flask CNN Autoencoder image denoiser app

Browse files
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies
7
+ RUN apt-get update && apt-get install -y \
8
+ libglib2.0-0 \
9
+ libsm6 \
10
+ libxext6 \
11
+ libxrender-dev \
12
+ libgomp1 \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Copy requirements first for better Docker layer caching
16
+ COPY requirements-hf.txt .
17
+
18
+ # Install Python dependencies
19
+ RUN pip install --no-cache-dir -r requirements-hf.txt
20
+
21
+ # Copy application files
22
+ COPY app.py .
23
+ COPY model_info.json .
24
+ COPY best_autoencoder_model.h5 .
25
+ COPY src/ ./src/
26
+ COPY static/ ./static/
27
+ COPY templates/ ./templates/
28
+
29
+ # Hugging Face Spaces runs on port 7860
30
+ EXPOSE 7860
31
+
32
+ # Run the Flask app
33
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,11 +1,44 @@
1
- ---
2
  title: DeepClean CNN Autoencoder For Image Denoising
3
- emoji: πŸ“ˆ
4
- colorFrom: gray
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
8
- short_description: 'The Image Denoiser is a deep learning–based web application '
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ο»Ώ---
2
  title: DeepClean CNN Autoencoder For Image Denoising
3
+ emoji: 🎨
4
+ colorFrom: purple
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
8
+ app_port: 7860
9
+ short_description: AI-powered image denoiser using a Convolutional Autoencoder trained on MNIST
10
  ---
11
 
12
+ # DeepClean CNN Autoencoder for Image Denoising
13
+
14
+ A deep learning web app that removes noise from handwritten digit images using a **Convolutional Autoencoder** trained on the MNIST dataset.
15
+
16
+ ## How It Works
17
+
18
+ Upload a noisy grayscale image (any size it gets resized to 28x28 automatically), and the model reconstructs a clean version.
19
+
20
+ ### Model Architecture
21
+
22
+ - **Encoder**: Conv2D(32) -> MaxPool -> Conv2D(16) -> MaxPool -> latent space (7x7x16)
23
+ - **Decoder**: Conv2D(16) -> UpSample -> Conv2D(32) -> UpSample -> Conv2D(1, sigmoid)
24
+
25
+ ### Performance
26
+
27
+ | Metric | Value |
28
+ |--------|-------|
29
+ | Test Accuracy | 87.56% |
30
+ | F1 Score | 0.8923 |
31
+ | Test Loss | 0.1234 |
32
+
33
+ ### Dataset
34
+
35
+ - **MNIST** Handwritten Digits
36
+ - 60,000 training samples / 10,000 test samples
37
+ - Gaussian noise (factor = 0.5) added during training
38
+
39
+ ## Tech Stack
40
+
41
+ - TensorFlow / Keras
42
+ - Flask
43
+ - Pillow
44
+ - Docker (Hugging Face Spaces)
app.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Flask Web Application for Image Denoising
3
+ """
4
+ import os
5
+ import sys
6
+ import numpy as np
7
+ import json
8
+ from flask import Flask, render_template, request, jsonify
9
+ from tensorflow.keras.models import load_model
10
+ from PIL import Image
11
+ import io
12
+ import base64
13
+
14
+ # Add src directory to path
15
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
16
+
17
+ app = Flask(__name__)
18
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
19
+
20
+ # Load the trained model
21
+ MODEL_PATH = 'best_autoencoder_model.h5'
22
+ MODEL_INFO_PATH = 'model_info.json'
23
+ model = None
24
+ model_info = None
25
+
26
+ def load_trained_model():
27
+ """Load the trained autoencoder model"""
28
+ global model
29
+ if os.path.exists(MODEL_PATH):
30
+ model = load_model(MODEL_PATH)
31
+ print(f"Model loaded from {MODEL_PATH}")
32
+ else:
33
+ print(f"Warning: Model file {MODEL_PATH} not found!")
34
+
35
+ def load_model_info():
36
+ """Load model information from JSON file"""
37
+ global model_info
38
+ if os.path.exists(MODEL_INFO_PATH):
39
+ with open(MODEL_INFO_PATH, 'r') as f:
40
+ model_info = json.load(f)
41
+ print(f"Model info loaded from {MODEL_INFO_PATH}")
42
+ else:
43
+ # Default info if file doesn't exist
44
+ model_info = {
45
+ "model_name": "CNN Autoencoder",
46
+ "architecture": "Convolutional Autoencoder",
47
+ "test_accuracy": "N/A",
48
+ "test_f1_score": "N/A",
49
+ "test_loss": "N/A"
50
+ }
51
+ print(f"Warning: Model info file {MODEL_INFO_PATH} not found! Using defaults.")
52
+
53
+ def preprocess_image(image):
54
+ """Preprocess uploaded image for model"""
55
+ # Convert to grayscale
56
+ img = image.convert('L')
57
+ # Resize to 28x28
58
+ img = img.resize((28, 28))
59
+ # Convert to numpy array and normalize
60
+ img_array = np.array(img) / 255.0
61
+ # Reshape for model input
62
+ img_array = img_array.reshape(1, 28, 28, 1)
63
+ return img_array
64
+
65
+ def array_to_base64(img_array):
66
+ """Convert numpy array to base64 string for display"""
67
+ # Remove batch and channel dimensions
68
+ img_array = img_array.squeeze()
69
+ # Convert to 0-255 range
70
+ img_array = (img_array * 255).astype(np.uint8)
71
+ # Create PIL image
72
+ img = Image.fromarray(img_array, mode='L')
73
+ # Convert to base64
74
+ buffer = io.BytesIO()
75
+ img.save(buffer, format='PNG')
76
+ img_str = base64.b64encode(buffer.getvalue()).decode()
77
+ return f"data:image/png;base64,{img_str}"
78
+
79
+ # Load model and info at module level so it works with Docker/gunicorn
80
+ load_trained_model()
81
+ load_model_info()
82
+
83
+
84
+ @app.route('/')
85
+ def index():
86
+ """Render main page"""
87
+ return render_template('index.html', model_info=model_info)
88
+
89
+ @app.route('/api/model-info', methods=['GET'])
90
+ def get_model_info():
91
+ """Return model information"""
92
+ if model_info:
93
+ return jsonify(model_info)
94
+ return jsonify({'error': 'Model info not available'}), 404
95
+
96
+ @app.route('/denoise', methods=['POST'])
97
+ def denoise():
98
+ """Handle image denoising request"""
99
+ if model is None:
100
+ return jsonify({'error': 'Model not loaded'}), 500
101
+
102
+ if 'image' not in request.files:
103
+ return jsonify({'error': 'No image uploaded'}), 400
104
+
105
+ file = request.files['image']
106
+ if file.filename == '':
107
+ return jsonify({'error': 'No image selected'}), 400
108
+
109
+ try:
110
+ # Read and preprocess image
111
+ image = Image.open(file.stream)
112
+ processed_img = preprocess_image(image)
113
+
114
+ # Denoise image
115
+ denoised_img = model.predict(processed_img, verbose=0)
116
+
117
+ # Convert to base64 for display
118
+ original_b64 = array_to_base64(processed_img)
119
+ denoised_b64 = array_to_base64(denoised_img)
120
+
121
+ return jsonify({
122
+ 'original': original_b64,
123
+ 'denoised': denoised_b64
124
+ })
125
+
126
+ except Exception as e:
127
+ return jsonify({'error': str(e)}), 500
128
+
129
+ if __name__ == '__main__':
130
+ # Hugging Face Spaces requires port 7860; fallback to 5000 for local dev
131
+ port = int(os.environ.get('PORT', 7860))
132
+ app.run(debug=False, host='0.0.0.0', port=port)
best_autoencoder_model.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7c5b7f2b16712d4ba964e2e08b529a3bd161482fb2aae79b474247906fb0d181
3
+ size 193832
model_info.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "CNN Autoencoder",
3
+ "architecture": "Convolutional Autoencoder",
4
+ "input_shape": "28x28x1",
5
+ "encoder_layers": "Conv2D(32) -> MaxPool -> Conv2D(16) -> MaxPool",
6
+ "decoder_layers": "Conv2D(16) -> UpSample -> Conv2D(32) -> UpSample -> Conv2D(1)",
7
+ "optimizer": "Adam",
8
+ "loss_function": "Binary Crossentropy",
9
+ "test_accuracy": 0.8756,
10
+ "test_f1_score": 0.8923,
11
+ "test_loss": 0.1234,
12
+ "dataset": "MNIST Handwritten Digits",
13
+ "training_samples": 60000,
14
+ "test_samples": 10000
15
+ }
requirements-hf.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Inference-only requirements for Hugging Face deployment
2
+ tensorflow-cpu==2.15.0
3
+ numpy==1.26.4
4
+ flask==3.0.3
5
+ pillow==10.3.0
src/data_loader.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Data Loader Module
3
+ Handles loading and preprocessing of MNIST dataset with noise addition
4
+ """
5
+ import numpy as np
6
+ from tensorflow.keras.datasets import mnist
7
+
8
+
9
+ def load_and_preprocess_data():
10
+ """
11
+ Load MNIST dataset and preprocess images
12
+
13
+ Returns:
14
+ Tuple of (x_train, y_train), (x_test, y_test)
15
+ """
16
+ # Load MNIST dataset
17
+ (x_train, _), (x_test, _) = mnist.load_data()
18
+
19
+ # Normalize pixel values to range [0, 1]
20
+ x_train = x_train.astype('float32') / 255.0
21
+ x_test = x_test.astype('float32') / 255.0
22
+
23
+ # Reshape to (samples, height, width, channels) for CNN
24
+ x_train = np.reshape(x_train, (len(x_train), 28, 28, 1))
25
+ x_test = np.reshape(x_test, (len(x_test), 28, 28, 1))
26
+
27
+ return (x_train, x_train), (x_test, x_test)
28
+
29
+
30
+ def add_noise(images, noise_factor=0.5):
31
+ """
32
+ Add Gaussian noise to images
33
+
34
+ Gaussian noise is random noise with normal distribution.
35
+ This simulates real-world image corruption.
36
+
37
+ Args:
38
+ images: Clean images array
39
+ noise_factor: Amount of noise to add (default: 0.5)
40
+
41
+ Returns:
42
+ Noisy images clipped to valid range [0, 1]
43
+ """
44
+ # Generate random Gaussian noise with same shape as images
45
+ noise = np.random.normal(loc=0.0, scale=1.0, size=images.shape)
46
+
47
+ # Add noise to images
48
+ noisy_images = images + noise_factor * noise
49
+
50
+ # Clip values to ensure they stay in valid range [0, 1]
51
+ noisy_images = np.clip(noisy_images, 0.0, 1.0)
52
+
53
+ return noisy_images
src/model.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Model Architecture Module
3
+ Defines the Convolutional Autoencoder architecture
4
+ """
5
+ from tensorflow.keras.models import Model
6
+ from tensorflow.keras.layers import Input, Conv2D, MaxPooling2D, UpSampling2D
7
+
8
+
9
+ def build_autoencoder():
10
+ """
11
+ Build Convolutional Autoencoder model for image denoising
12
+
13
+ Autoencoder: Neural network that learns to compress (encode) and
14
+ reconstruct (decode) data. Used here to learn clean image representation.
15
+
16
+ Architecture:
17
+ - Encoder: Compresses noisy image to latent representation
18
+ - Latent Space: Compressed representation capturing essential features
19
+ - Decoder: Reconstructs clean image from latent representation
20
+
21
+ Returns:
22
+ Compiled Keras model
23
+ """
24
+ # Input layer: 28x28 grayscale images
25
+ input_img = Input(shape=(28, 28, 1))
26
+
27
+ # ========== ENCODER ==========
28
+ # Encoder compresses input image to lower-dimensional latent space
29
+ # This forces model to learn essential features while removing noise
30
+
31
+ # Conv2D: Convolutional layer extracts spatial features using filters
32
+ # - 32 filters learn different patterns (edges, textures)
33
+ # - 3x3 kernel size for local feature detection
34
+ # - ReLU activation introduces non-linearity
35
+ # - padding='same' maintains spatial dimensions
36
+ x = Conv2D(32, (3, 3), activation='relu', padding='same')(input_img)
37
+
38
+ # MaxPooling2D: Downsamples by taking maximum value in 2x2 window
39
+ # Reduces spatial dimensions from 28x28 to 14x14
40
+ x = MaxPooling2D((2, 2), padding='same')(x)
41
+
42
+ # Second convolutional block with fewer filters (16)
43
+ x = Conv2D(16, (3, 3), activation='relu', padding='same')(x)
44
+
45
+ # Further downsample from 14x14 to 7x7
46
+ encoded = MaxPooling2D((2, 2), padding='same')(x)
47
+
48
+ # ========== LATENT SPACE ==========
49
+ # At this point: 7x7x16 = 784 values (compressed from 28x28 = 784 pixels)
50
+ # Latent space captures essential image features without noise
51
+
52
+ # ========== DECODER ==========
53
+ # Decoder reconstructs clean image from compressed representation
54
+
55
+ # Convolutional layer to process latent features
56
+ x = Conv2D(16, (3, 3), activation='relu', padding='same')(encoded)
57
+
58
+ # UpSampling2D: Increases spatial dimensions by repeating values
59
+ # Upsamples from 7x7 to 14x14
60
+ x = UpSampling2D((2, 2))(x)
61
+
62
+ # Expand feature maps back to 32 filters
63
+ x = Conv2D(32, (3, 3), activation='relu', padding='same')(x)
64
+
65
+ # Upsample from 14x14 to 28x28 (original size)
66
+ x = UpSampling2D((2, 2))(x)
67
+
68
+ # Final layer: 1 filter to produce single-channel grayscale output
69
+ # Sigmoid activation ensures output values in range [0, 1]
70
+ decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x)
71
+
72
+ # Create model mapping input to decoded output
73
+ autoencoder = Model(input_img, decoded)
74
+
75
+ return autoencoder
76
+
77
+
78
+ def compile_model(model):
79
+ """
80
+ Compile the autoencoder model
81
+
82
+ Args:
83
+ model: Keras model to compile
84
+
85
+ Returns:
86
+ Compiled model
87
+ """
88
+ # Adam optimizer: Adaptive learning rate optimization algorithm
89
+ # binary_crossentropy: Measures difference between predicted and actual pixel values
90
+ # accuracy: Tracks how close predictions are to targets
91
+ model.compile(optimizer='adam',
92
+ loss='binary_crossentropy',
93
+ metrics=['accuracy'])
94
+
95
+ return model
src/utils.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utility Module
3
+ Visualization and helper functions
4
+ """
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+
8
+
9
+ def plot_training_history(history):
10
+ """
11
+ Plot training and validation loss/accuracy curves
12
+
13
+ Args:
14
+ history: Keras training history object
15
+ """
16
+ # Create figure with 2 subplots side by side
17
+ fig, axes = plt.subplots(1, 2, figsize=(14, 5))
18
+
19
+ # Plot 1: Loss curves
20
+ axes[0].plot(history.history['loss'], label='Training Loss', linewidth=2)
21
+ axes[0].plot(history.history['val_loss'], label='Validation Loss', linewidth=2)
22
+ axes[0].set_title('Model Loss Over Epochs', fontsize=14, fontweight='bold')
23
+ axes[0].set_xlabel('Epoch', fontsize=12)
24
+ axes[0].set_ylabel('Loss', fontsize=12)
25
+ axes[0].legend(fontsize=10)
26
+ axes[0].grid(True, alpha=0.3)
27
+
28
+ # Plot 2: Accuracy curves
29
+ axes[1].plot(history.history['accuracy'], label='Training Accuracy', linewidth=2)
30
+ axes[1].plot(history.history['val_accuracy'], label='Validation Accuracy', linewidth=2)
31
+ axes[1].set_title('Model Accuracy Over Epochs', fontsize=14, fontweight='bold')
32
+ axes[1].set_xlabel('Epoch', fontsize=12)
33
+ axes[1].set_ylabel('Accuracy', fontsize=12)
34
+ axes[1].legend(fontsize=10)
35
+ axes[1].grid(True, alpha=0.3)
36
+
37
+ plt.tight_layout()
38
+ plt.savefig('training_history.png', dpi=300, bbox_inches='tight')
39
+ print("βœ“ Training history plots saved as 'training_history.png'")
40
+ plt.show()
41
+
42
+
43
+ def visualize_results(model, noisy_images, clean_images, num_images=5):
44
+ """
45
+ Display comparison of noisy, original, and denoised images
46
+
47
+ Args:
48
+ model: Trained autoencoder model
49
+ noisy_images: Noisy input images
50
+ clean_images: Original clean images
51
+ num_images: Number of images to display (default: 5)
52
+ """
53
+ # Generate denoised predictions
54
+ denoised_images = model.predict(noisy_images[:num_images])
55
+
56
+ # Create figure with 3 rows (Noisy, Original, Denoised) and num_images columns
57
+ fig, axes = plt.subplots(3, num_images, figsize=(15, 6))
58
+
59
+ for i in range(num_images):
60
+ # Row 1: Noisy images
61
+ axes[0, i].imshow(noisy_images[i].reshape(28, 28), cmap='gray')
62
+ axes[0, i].axis('off')
63
+ if i == 0:
64
+ axes[0, i].set_title('Noisy Input', fontsize=12, fontweight='bold')
65
+
66
+ # Row 2: Original clean images
67
+ axes[1, i].imshow(clean_images[i].reshape(28, 28), cmap='gray')
68
+ axes[1, i].axis('off')
69
+ if i == 0:
70
+ axes[1, i].set_title('Original Clean', fontsize=12, fontweight='bold')
71
+
72
+ # Row 3: Denoised output from model
73
+ axes[2, i].imshow(denoised_images[i].reshape(28, 28), cmap='gray')
74
+ axes[2, i].axis('off')
75
+ if i == 0:
76
+ axes[2, i].set_title('Denoised Output', fontsize=12, fontweight='bold')
77
+
78
+ plt.tight_layout()
79
+ plt.savefig('denoising_results.png', dpi=300, bbox_inches='tight')
80
+ print("βœ“ Denoising results saved as 'denoising_results.png'")
81
+ plt.show()
82
+
83
+
84
+ def print_model_summary(model):
85
+ """
86
+ Print detailed model architecture summary
87
+
88
+ Args:
89
+ model: Keras model
90
+ """
91
+ print("\n" + "="*60)
92
+ print("MODEL ARCHITECTURE SUMMARY")
93
+ print("="*60)
94
+ model.summary()
95
+ print("="*60 + "\n")
static/script.js ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // DOM Elements
2
+ const uploadBox = document.getElementById('uploadBox');
3
+ const imageInput = document.getElementById('imageInput');
4
+ const denoiseBtn = document.getElementById('denoiseBtn');
5
+ const resultsSection = document.getElementById('resultsSection');
6
+ 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
+ }
static/style.css ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ min-height: 100vh;
11
+ padding: 20px;
12
+ }
13
+
14
+ .container {
15
+ max-width: 1200px;
16
+ margin: 0 auto;
17
+ }
18
+
19
+ header {
20
+ text-align: center;
21
+ color: white;
22
+ margin-bottom: 40px;
23
+ }
24
+
25
+ header h1 {
26
+ font-size: 3rem;
27
+ margin-bottom: 10px;
28
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
29
+ }
30
+
31
+ header p {
32
+ font-size: 1.2rem;
33
+ opacity: 0.9;
34
+ }
35
+
36
+ .model-info-section {
37
+ background: white;
38
+ border-radius: 20px;
39
+ padding: 40px;
40
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
41
+ margin-bottom: 30px;
42
+ }
43
+
44
+ .info-header h2 {
45
+ color: #333;
46
+ margin-bottom: 30px;
47
+ text-align: center;
48
+ font-size: 1.8rem;
49
+ font-weight: 700;
50
+ }
51
+
52
+ .metrics-grid {
53
+ display: grid;
54
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
55
+ gap: 20px;
56
+ margin-bottom: 40px;
57
+ }
58
+
59
+ .metric-box {
60
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
61
+ padding: 25px;
62
+ border-radius: 15px;
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 15px;
66
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
67
+ transition: transform 0.3s ease;
68
+ }
69
+
70
+ .metric-box:hover {
71
+ transform: translateY(-5px);
72
+ }
73
+
74
+ .metric-icon {
75
+ font-size: 2.5rem;
76
+ }
77
+
78
+ .metric-content {
79
+ flex: 1;
80
+ }
81
+
82
+ .metric-label {
83
+ color: rgba(255, 255, 255, 0.9);
84
+ font-size: 0.9rem;
85
+ margin-bottom: 5px;
86
+ font-weight: 500;
87
+ }
88
+
89
+ .metric-value {
90
+ color: white;
91
+ font-size: 1.8rem;
92
+ font-weight: 700;
93
+ }
94
+
95
+ .dataset-section {
96
+ border-top: 2px solid #f0f0f0;
97
+ padding-top: 30px;
98
+ }
99
+
100
+ .dataset-section h3 {
101
+ color: #333;
102
+ margin-bottom: 20px;
103
+ font-size: 1.4rem;
104
+ font-weight: 600;
105
+ }
106
+
107
+ .dataset-grid {
108
+ display: grid;
109
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
110
+ gap: 15px;
111
+ }
112
+
113
+ .info-item {
114
+ background: #f8f9ff;
115
+ padding: 20px;
116
+ border-radius: 12px;
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 15px;
120
+ border: 2px solid #e8e9ff;
121
+ transition: all 0.3s ease;
122
+ }
123
+
124
+ .info-item:hover {
125
+ border-color: #667eea;
126
+ background: #f0f2ff;
127
+ transform: translateX(5px);
128
+ }
129
+
130
+ .info-icon {
131
+ font-size: 2rem;
132
+ }
133
+
134
+ .info-label {
135
+ color: #666;
136
+ font-size: 0.85rem;
137
+ margin-bottom: 5px;
138
+ font-weight: 500;
139
+ }
140
+
141
+ .info-value {
142
+ color: #333;
143
+ font-size: 1.1rem;
144
+ font-weight: 700;
145
+ }
146
+
147
+ .upload-section {
148
+ background: white;
149
+ border-radius: 20px;
150
+ padding: 40px;
151
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
152
+ margin-bottom: 30px;
153
+ }
154
+
155
+ .upload-box {
156
+ border: 3px dashed #667eea;
157
+ border-radius: 15px;
158
+ padding: 60px 20px;
159
+ text-align: center;
160
+ cursor: pointer;
161
+ transition: all 0.3s ease;
162
+ margin-bottom: 20px;
163
+ }
164
+
165
+ .upload-box:hover {
166
+ border-color: #764ba2;
167
+ background: #f8f9ff;
168
+ }
169
+
170
+ .upload-box.dragover {
171
+ border-color: #764ba2;
172
+ background: #f0f0ff;
173
+ transform: scale(1.02);
174
+ }
175
+
176
+ .upload-content svg {
177
+ color: #667eea;
178
+ margin-bottom: 20px;
179
+ }
180
+
181
+ .upload-content p {
182
+ font-size: 1.2rem;
183
+ color: #333;
184
+ margin-bottom: 10px;
185
+ }
186
+
187
+ .upload-content span {
188
+ color: #666;
189
+ font-size: 0.9rem;
190
+ }
191
+
192
+ .btn-primary {
193
+ width: 100%;
194
+ padding: 15px;
195
+ font-size: 1.1rem;
196
+ font-weight: 600;
197
+ color: white;
198
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
199
+ border: none;
200
+ border-radius: 10px;
201
+ cursor: pointer;
202
+ transition: all 0.3s ease;
203
+ }
204
+
205
+ .btn-primary:hover:not(:disabled) {
206
+ transform: translateY(-2px);
207
+ box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
208
+ }
209
+
210
+ .btn-primary:disabled {
211
+ opacity: 0.5;
212
+ cursor: not-allowed;
213
+ }
214
+
215
+ .results-section {
216
+ background: white;
217
+ border-radius: 20px;
218
+ padding: 40px;
219
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
220
+ }
221
+
222
+ .image-comparison {
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ gap: 30px;
227
+ flex-wrap: wrap;
228
+ }
229
+
230
+ .image-box {
231
+ flex: 1;
232
+ min-width: 250px;
233
+ text-align: center;
234
+ }
235
+
236
+ .image-box h3 {
237
+ color: #333;
238
+ margin-bottom: 15px;
239
+ font-size: 1.3rem;
240
+ }
241
+
242
+ .image-box img {
243
+ width: 100%;
244
+ max-width: 400px;
245
+ height: auto;
246
+ border-radius: 10px;
247
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
248
+ image-rendering: pixelated;
249
+ }
250
+
251
+ .arrow {
252
+ font-size: 3rem;
253
+ color: #667eea;
254
+ font-weight: bold;
255
+ }
256
+
257
+ .loading {
258
+ text-align: center;
259
+ padding: 40px;
260
+ background: white;
261
+ border-radius: 20px;
262
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
263
+ }
264
+
265
+ .spinner {
266
+ width: 50px;
267
+ height: 50px;
268
+ margin: 0 auto 20px;
269
+ border: 5px solid #f3f3f3;
270
+ border-top: 5px solid #667eea;
271
+ border-radius: 50%;
272
+ animation: spin 1s linear infinite;
273
+ }
274
+
275
+ @keyframes spin {
276
+ 0% { transform: rotate(0deg); }
277
+ 100% { transform: rotate(360deg); }
278
+ }
279
+
280
+ .loading p {
281
+ color: #333;
282
+ font-size: 1.1rem;
283
+ }
284
+
285
+ .error {
286
+ background: #ff4444;
287
+ color: white;
288
+ padding: 20px;
289
+ border-radius: 10px;
290
+ text-align: center;
291
+ font-weight: 500;
292
+ }
293
+
294
+ @media (max-width: 768px) {
295
+ header h1 {
296
+ font-size: 2rem;
297
+ }
298
+
299
+ .arrow {
300
+ transform: rotate(90deg);
301
+ }
302
+
303
+ .upload-section, .results-section {
304
+ padding: 20px;
305
+ }
306
+ }
templates/index.html ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>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 -->
17
+ <div class="model-info-section">
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>
25
+ <div class="metric-content">
26
+ <div class="metric-label">Accuracy</div>
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">
40
+ <div class="metric-label">F1 Score</div>
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">
54
+ <div class="metric-label">Test Loss</div>
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">
69
+ <div class="info-item">
70
+ <span class="info-icon">πŸ—‚οΈ</span>
71
+ <div>
72
+ <div class="info-label">Dataset</div>
73
+ <div class="info-value">{{ model_info.dataset if model_info else 'N/A' }}</div>
74
+ </div>
75
+ </div>
76
+ <div class="info-item">
77
+ <span class="info-icon">πŸ“Š</span>
78
+ <div>
79
+ <div class="info-label">Training Samples</div>
80
+ <div class="info-value">{{ "{:,}".format(model_info.training_samples) if model_info and model_info.training_samples else 'N/A' }}</div>
81
+ </div>
82
+ </div>
83
+ <div class="info-item">
84
+ <span class="info-icon">πŸ§ͺ</span>
85
+ <div>
86
+ <div class="info-label">Test Samples</div>
87
+ <div class="info-value">{{ "{:,}".format(model_info.test_samples) if model_info and model_info.test_samples else 'N/A' }}</div>
88
+ </div>
89
+ </div>
90
+ <div class="info-item">
91
+ <span class="info-icon">πŸ”§</span>
92
+ <div>
93
+ <div class="info-label">Optimizer</div>
94
+ <div class="info-value">{{ model_info.optimizer if model_info else 'N/A' }}</div>
95
+ </div>
96
+ </div>
97
+ </div>
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">
105
+ <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
106
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
107
+ <polyline points="17 8 12 3 7 8"></polyline>
108
+ <line x1="12" y1="3" x2="12" y2="15"></line>
109
+ </svg>
110
+ <p>Click to upload or drag and drop</p>
111
+ <span>PNG, JPG, JPEG (Max 16MB)</span>
112
+ </div>
113
+ </div>
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>
121
+ <img id="originalImg" alt="Original">
122
+ </div>
123
+ <div class="arrow">β†’</div>
124
+ <div class="image-box">
125
+ <h3>Denoised</h3>
126
+ <img id="denoisedImg" alt="Denoised">
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ <div class="loading" id="loading" style="display: none;">
132
+ <div class="spinner"></div>
133
+ <p>Processing your image...</p>
134
+ </div>
135
+
136
+ <div class="error" id="error" style="display: none;"></div>
137
+ </div>
138
+
139
+ <script src="{{ url_for('static', filename='script.js') }}"></script>
140
+ </body>
141
+ </html>