koesan commited on
Commit
986ff76
·
1 Parent(s): 7787f7f

Add application file

Browse files
Files changed (4) hide show
  1. Dockerfile +32 -0
  2. app.py +220 -0
  3. requirements.txt +6 -0
  4. templates/index.html +553 -0
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies for OpenCV
6
+ RUN apt-get update && apt-get install -y \
7
+ libglib2.0-0 \
8
+ libsm6 \
9
+ libxext6 \
10
+ libxrender-dev \
11
+ libgomp1 \
12
+ libgl1-mesa-glx \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Copy requirements and install Python dependencies
16
+ COPY requirements.txt .
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy application files
20
+ COPY app.py .
21
+ COPY satellite_standard_unet_100epochs_7May2023.hdf5 .
22
+ COPY templates/ templates/
23
+ COPY image/ image/
24
+
25
+ # Create uploads directory
26
+ RUN mkdir -p uploads && chmod 777 uploads
27
+
28
+ # Expose port
29
+ EXPOSE 7860
30
+
31
+ # Run the application
32
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import numpy as np
4
+ import base64
5
+ from flask import Flask, render_template, request, jsonify
6
+ from werkzeug.utils import secure_filename
7
+ from io import BytesIO
8
+ from PIL import Image
9
+ import tensorflow as tf
10
+ from tensorflow.keras.models import load_model
11
+
12
+ # Suppress warnings
13
+ os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
14
+ os.environ['MPLCONFIGDIR'] = '/tmp/matplotlib'
15
+
16
+ app = Flask(__name__)
17
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max
18
+ app.config['UPLOAD_FOLDER'] = 'uploads'
19
+ app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg'}
20
+
21
+ os.makedirs(app.config['UPLOAD_FOLDER'], mode=0o777, exist_ok=True)
22
+
23
+ # Load U-Net model
24
+ print("Loading U-Net model...")
25
+ model = load_model('satellite_standard_unet_100epochs_7May2023.hdf5', compile=False)
26
+ model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
27
+ print("✓ Model loaded successfully!")
28
+
29
+ # Define color map for segmentation classes
30
+ CLASS_COLORS = {
31
+ 0: [60, 16, 152], # Class 0 - Purple
32
+ 1: [132, 41, 246], # Class 1 - Light Purple
33
+ 2: [110, 193, 228], # Class 2 - Light Blue
34
+ 3: [254, 221, 58], # Class 3 - Yellow
35
+ }
36
+
37
+ def allowed_file(filename):
38
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
39
+
40
+ def preprocess_image(image_path):
41
+ """Preprocess image for U-Net model (256x256 grayscale)"""
42
+ try:
43
+ # Read image
44
+ img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
45
+
46
+ if img is None:
47
+ raise ValueError("Could not read image")
48
+
49
+ print(f"Original image shape: {img.shape}")
50
+
51
+ # Resize to 256x256
52
+ img_resized = cv2.resize(img, (256, 256))
53
+
54
+ # Normalize to [0, 1]
55
+ img_normalized = img_resized / 255.0
56
+
57
+ # Add dimensions: (1, 256, 256, 1)
58
+ img_input = np.expand_dims(img_normalized, axis=0)
59
+ img_input = np.expand_dims(img_input, axis=-1)
60
+
61
+ print(f"Model input shape: {img_input.shape}")
62
+
63
+ return img_input, img_resized
64
+
65
+ except Exception as e:
66
+ raise ValueError(f"Failed to preprocess image: {str(e)}")
67
+
68
+ def create_colored_mask(prediction, original_shape):
69
+ """Create colored segmentation mask from prediction"""
70
+ # Get class with highest probability for each pixel
71
+ pred_mask = np.argmax(prediction, axis=-1)[0] # (256, 256)
72
+
73
+ # Create RGB mask
74
+ colored_mask = np.zeros((256, 256, 3), dtype=np.uint8)
75
+
76
+ for class_id, color in CLASS_COLORS.items():
77
+ colored_mask[pred_mask == class_id] = color
78
+
79
+ return colored_mask
80
+
81
+ def create_overlay(original, mask, alpha=0.5):
82
+ """Create overlay of original image and segmentation mask"""
83
+ # Convert grayscale to RGB
84
+ if len(original.shape) == 2:
85
+ original_rgb = cv2.cvtColor(original, cv2.COLOR_GRAY2RGB)
86
+ else:
87
+ original_rgb = original
88
+
89
+ # Blend
90
+ overlay = cv2.addWeighted(original_rgb, 1 - alpha, mask, alpha, 0)
91
+
92
+ return overlay
93
+
94
+ def img_to_base64(img):
95
+ """Convert numpy image to base64 string"""
96
+ if len(img.shape) == 2:
97
+ img_pil = Image.fromarray(img)
98
+ else:
99
+ img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
100
+
101
+ buf = BytesIO()
102
+ img_pil.save(buf, format='PNG')
103
+ buf.seek(0)
104
+ img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')
105
+
106
+ return f'data:image/png;base64,{img_base64}'
107
+
108
+ @app.route('/')
109
+ def index():
110
+ return render_template('index.html')
111
+
112
+ @app.route('/predict', methods=['POST'])
113
+ def predict():
114
+ try:
115
+ if 'file' not in request.files:
116
+ return jsonify({'error': 'No file uploaded'}), 400
117
+
118
+ file = request.files['file']
119
+
120
+ if file.filename == '':
121
+ return jsonify({'error': 'No file selected'}), 400
122
+
123
+ if not allowed_file(file.filename):
124
+ return jsonify({'error': 'Invalid file type. Please upload PNG, JPG, or JPEG'}), 400
125
+
126
+ # Save file
127
+ filename = secure_filename(file.filename)
128
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
129
+ file.save(filepath)
130
+
131
+ print(f"Processing: {filename}")
132
+
133
+ # Preprocess
134
+ img_input, original_resized = preprocess_image(filepath)
135
+
136
+ # Predict
137
+ print("Making prediction...")
138
+ prediction = model.predict(img_input, verbose=0)
139
+
140
+ # Create colored mask
141
+ colored_mask = create_colored_mask(prediction, original_resized.shape)
142
+
143
+ # Create overlay
144
+ overlay = create_overlay(original_resized, colored_mask, alpha=0.6)
145
+
146
+ # Convert to base64
147
+ original_base64 = img_to_base64(original_resized)
148
+ mask_base64 = img_to_base64(colored_mask)
149
+ overlay_base64 = img_to_base64(overlay)
150
+
151
+ # Clean up
152
+ os.remove(filepath)
153
+
154
+ result = {
155
+ 'original': original_base64,
156
+ 'mask': mask_base64,
157
+ 'overlay': overlay_base64,
158
+ 'image_size': '256x256'
159
+ }
160
+
161
+ print(f"✓ Prediction completed successfully")
162
+
163
+ return jsonify(result)
164
+
165
+ except Exception as e:
166
+ print(f"Error during prediction: {e}")
167
+ import traceback
168
+ traceback.print_exc()
169
+ if os.path.exists(filepath):
170
+ os.remove(filepath)
171
+ return jsonify({'error': str(e)}), 500
172
+
173
+ @app.route('/test-example', methods=['POST'])
174
+ def test_example():
175
+ """Test with example image"""
176
+ try:
177
+ example_path = 'image/045.png'
178
+
179
+ if not os.path.exists(example_path):
180
+ return jsonify({'error': 'Example image not found'}), 404
181
+
182
+ print(f"Testing with example: {example_path}")
183
+
184
+ # Preprocess
185
+ img_input, original_resized = preprocess_image(example_path)
186
+
187
+ # Predict
188
+ print("Making prediction on example...")
189
+ prediction = model.predict(img_input, verbose=0)
190
+
191
+ # Create colored mask
192
+ colored_mask = create_colored_mask(prediction, original_resized.shape)
193
+
194
+ # Create overlay
195
+ overlay = create_overlay(original_resized, colored_mask, alpha=0.6)
196
+
197
+ # Convert to base64
198
+ original_base64 = img_to_base64(original_resized)
199
+ mask_base64 = img_to_base64(colored_mask)
200
+ overlay_base64 = img_to_base64(overlay)
201
+
202
+ result = {
203
+ 'original': original_base64,
204
+ 'mask': mask_base64,
205
+ 'overlay': overlay_base64,
206
+ 'image_size': '256x256'
207
+ }
208
+
209
+ print(f"✓ Example prediction completed")
210
+
211
+ return jsonify(result)
212
+
213
+ except Exception as e:
214
+ print(f"Error during example prediction: {e}")
215
+ import traceback
216
+ traceback.print_exc()
217
+ return jsonify({'error': str(e)}), 500
218
+
219
+ if __name__ == '__main__':
220
+ app.run(host='0.0.0.0', port=7860, debug=False)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ flask==2.3.0
2
+ tensorflow==2.13.0
3
+ opencv-python-headless==4.8.0.74
4
+ numpy==1.24.3
5
+ pillow==10.0.0
6
+ werkzeug==2.3.0
templates/index.html ADDED
@@ -0,0 +1,553 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>U-Net Semantic Segmentation</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1200px;
23
+ margin: 0 auto;
24
+ background: white;
25
+ border-radius: 20px;
26
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
27
+ overflow: hidden;
28
+ }
29
+
30
+ .header {
31
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
32
+ color: white;
33
+ padding: 40px;
34
+ text-align: center;
35
+ }
36
+
37
+ .header h1 {
38
+ font-size: 2.5em;
39
+ margin-bottom: 10px;
40
+ }
41
+
42
+ .header p {
43
+ font-size: 1.2em;
44
+ opacity: 0.9;
45
+ margin-bottom: 20px;
46
+ }
47
+
48
+ .github-link {
49
+ display: inline-flex;
50
+ align-items: center;
51
+ gap: 8px;
52
+ color: white;
53
+ text-decoration: none;
54
+ font-weight: 600;
55
+ font-size: 1.1em;
56
+ padding: 12px 24px;
57
+ background: rgba(255, 255, 255, 0.2);
58
+ border: 2px solid white;
59
+ border-radius: 10px;
60
+ transition: all 0.3s;
61
+ }
62
+
63
+ .github-link:hover {
64
+ background: white;
65
+ color: #667eea;
66
+ transform: translateY(-2px);
67
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
68
+ }
69
+
70
+ .content {
71
+ padding: 40px;
72
+ }
73
+
74
+ .info-section {
75
+ background: #f8f9fa;
76
+ border-radius: 15px;
77
+ padding: 30px;
78
+ margin-bottom: 30px;
79
+ border-left: 5px solid #667eea;
80
+ }
81
+
82
+ .info-section h2 {
83
+ color: #667eea;
84
+ margin-bottom: 15px;
85
+ font-size: 1.8em;
86
+ }
87
+
88
+ .info-section p {
89
+ color: #495057;
90
+ line-height: 1.8;
91
+ font-size: 1.05em;
92
+ margin-bottom: 15px;
93
+ }
94
+
95
+ .features {
96
+ display: grid;
97
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
98
+ gap: 15px;
99
+ margin-top: 20px;
100
+ }
101
+
102
+ .feature-item {
103
+ background: white;
104
+ padding: 15px;
105
+ border-radius: 10px;
106
+ border: 2px solid #e9ecef;
107
+ text-align: center;
108
+ }
109
+
110
+ .feature-item strong {
111
+ color: #667eea;
112
+ display: block;
113
+ margin-bottom: 5px;
114
+ font-size: 1.1em;
115
+ }
116
+
117
+ .upload-section {
118
+ background: #f8f9fa;
119
+ border-radius: 15px;
120
+ padding: 40px;
121
+ text-align: center;
122
+ margin-bottom: 30px;
123
+ border: 3px dashed #667eea;
124
+ transition: all 0.3s;
125
+ }
126
+
127
+ .upload-section:hover {
128
+ border-color: #764ba2;
129
+ background: #f0f2ff;
130
+ }
131
+
132
+ .upload-icon {
133
+ font-size: 4em;
134
+ color: #667eea;
135
+ margin-bottom: 20px;
136
+ }
137
+
138
+ .file-input {
139
+ display: none;
140
+ }
141
+
142
+ .upload-button {
143
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
144
+ color: white;
145
+ padding: 15px 40px;
146
+ border: none;
147
+ border-radius: 30px;
148
+ font-size: 1.2em;
149
+ font-weight: bold;
150
+ cursor: pointer;
151
+ transition: transform 0.2s;
152
+ margin: 10px;
153
+ }
154
+
155
+ .upload-button:hover {
156
+ transform: scale(1.05);
157
+ }
158
+
159
+ .test-button {
160
+ background: #28a745;
161
+ color: white;
162
+ padding: 15px 40px;
163
+ border: none;
164
+ border-radius: 30px;
165
+ font-size: 1.2em;
166
+ font-weight: bold;
167
+ cursor: pointer;
168
+ transition: transform 0.2s;
169
+ margin: 10px;
170
+ }
171
+
172
+ .test-button:hover {
173
+ background: #218838;
174
+ transform: scale(1.05);
175
+ }
176
+
177
+ .preview-section {
178
+ display: none;
179
+ text-align: center;
180
+ margin-bottom: 30px;
181
+ }
182
+
183
+ .preview-image {
184
+ max-width: 100%;
185
+ max-height: 400px;
186
+ border-radius: 10px;
187
+ box-shadow: 0 5px 20px rgba(0,0,0,0.1);
188
+ }
189
+
190
+ .results-grid {
191
+ display: none;
192
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
193
+ gap: 30px;
194
+ margin-top: 30px;
195
+ }
196
+
197
+ .result-card {
198
+ background: white;
199
+ border-radius: 15px;
200
+ padding: 20px;
201
+ box-shadow: 0 5px 20px rgba(0,0,0,0.1);
202
+ text-align: center;
203
+ }
204
+
205
+ .result-card h3 {
206
+ color: #667eea;
207
+ margin-bottom: 15px;
208
+ font-size: 1.5em;
209
+ }
210
+
211
+ .result-image {
212
+ width: 100%;
213
+ border-radius: 10px;
214
+ box-shadow: 0 3px 10px rgba(0,0,0,0.1);
215
+ }
216
+
217
+ .loading {
218
+ display: none;
219
+ text-align: center;
220
+ padding: 40px;
221
+ }
222
+
223
+ .spinner {
224
+ border: 5px solid #f3f3f3;
225
+ border-top: 5px solid #667eea;
226
+ border-radius: 50%;
227
+ width: 60px;
228
+ height: 60px;
229
+ animation: spin 1s linear infinite;
230
+ margin: 0 auto 20px;
231
+ }
232
+
233
+ @keyframes spin {
234
+ 0% { transform: rotate(0deg); }
235
+ 100% { transform: rotate(360deg); }
236
+ }
237
+
238
+ .error {
239
+ display: none;
240
+ background: #f8d7da;
241
+ color: #721c24;
242
+ padding: 20px;
243
+ border-radius: 10px;
244
+ border-left: 5px solid #dc3545;
245
+ margin-bottom: 20px;
246
+ }
247
+
248
+ .legend {
249
+ background: white;
250
+ padding: 20px;
251
+ border-radius: 10px;
252
+ margin-top: 30px;
253
+ }
254
+
255
+ .legend h3 {
256
+ color: #667eea;
257
+ margin-bottom: 15px;
258
+ }
259
+
260
+ .legend-items {
261
+ display: grid;
262
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
263
+ gap: 15px;
264
+ }
265
+
266
+ .legend-item {
267
+ display: flex;
268
+ align-items: center;
269
+ gap: 10px;
270
+ }
271
+
272
+ .legend-color {
273
+ width: 40px;
274
+ height: 40px;
275
+ border-radius: 5px;
276
+ border: 2px solid #dee2e6;
277
+ }
278
+
279
+ .reset-button {
280
+ background: #6c757d;
281
+ color: white;
282
+ padding: 12px 30px;
283
+ border: none;
284
+ border-radius: 25px;
285
+ font-size: 1.1em;
286
+ cursor: pointer;
287
+ margin-top: 20px;
288
+ transition: transform 0.2s;
289
+ }
290
+
291
+ .reset-button:hover {
292
+ background: #5a6268;
293
+ transform: scale(1.05);
294
+ }
295
+ </style>
296
+ </head>
297
+ <body>
298
+ <div class="container">
299
+ <div class="header">
300
+ <h1>🛰️ U-Net Semantic Segmentation</h1>
301
+ <p>AI-Powered Drone Imagery Segmentation</p>
302
+ <a href="https://github.com/koesan/U-Net" target="_blank" class="github-link">
303
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
304
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
305
+ </svg>
306
+ View on GitHub
307
+ </a>
308
+ </div>
309
+
310
+ <div class="content">
311
+ <!-- Info Section -->
312
+ <div class="info-section">
313
+ <h2>📖 About This Project</h2>
314
+ <p>
315
+ This application uses a <strong>U-Net deep learning model</strong> trained on aerial drone imagery
316
+ to perform <strong>semantic segmentation</strong>. The model identifies and classifies different
317
+ regions in satellite/drone images into 4 distinct classes, enabling precise pixel-level analysis
318
+ of geographic features.
319
+ </p>
320
+ <p>
321
+ <strong>🎯 How it works:</strong> Upload a drone or satellite image (PNG, JPG, or JPEG),
322
+ and the U-Net model will automatically segment it into different semantic classes,
323
+ displaying the original image, segmentation mask, and an overlay visualization.
324
+ </p>
325
+
326
+ <div class="features">
327
+ <div class="feature-item">
328
+ <strong>🖼️ Input</strong>
329
+ <span>256×256 pixels</span>
330
+ </div>
331
+ <div class="feature-item">
332
+ <strong>🧠 Architecture</strong>
333
+ <span>U-Net (5 levels)</span>
334
+ </div>
335
+ <div class="feature-item">
336
+ <strong>��� Classes</strong>
337
+ <span>4 Semantic Categories</span>
338
+ </div>
339
+ <div class="feature-item">
340
+ <strong>⚡ Speed</strong>
341
+ <span>~1-2 seconds</span>
342
+ </div>
343
+ </div>
344
+ </div>
345
+
346
+ <!-- Error Message -->
347
+ <div class="error" id="errorMessage"></div>
348
+
349
+ <!-- Upload Section -->
350
+ <div class="upload-section" id="uploadSection">
351
+ <div class="upload-icon">📤</div>
352
+ <h2 style="color: #667eea; margin-bottom: 15px;">Upload Drone/Satellite Image</h2>
353
+ <p style="color: #6c757d; margin-bottom: 20px;">
354
+ Supported formats: PNG, JPG, JPEG<br>
355
+ Images will be automatically resized to 256×256 pixels
356
+ </p>
357
+ <input type="file" id="fileInput" class="file-input" accept="image/*">
358
+ <button class="upload-button" onclick="document.getElementById('fileInput').click()">
359
+ Choose Image
360
+ </button>
361
+ <button class="test-button" onclick="testExample()">
362
+ 🧪 Test Example
363
+ </button>
364
+ </div>
365
+
366
+ <!-- Preview Section -->
367
+ <div class="preview-section" id="previewSection">
368
+ <h3 style="margin-bottom: 15px; color: #667eea;">Selected Image:</h3>
369
+ <img id="previewImage" class="preview-image" alt="Preview">
370
+ <div style="margin-top: 20px;">
371
+ <button class="upload-button" onclick="analyzeImage()">
372
+ 🔍 Analyze Image
373
+ </button>
374
+ </div>
375
+ </div>
376
+
377
+ <!-- Loading -->
378
+ <div class="loading" id="loading">
379
+ <div class="spinner"></div>
380
+ <p style="color: #667eea; font-size: 1.2em;">Analyzing image with U-Net...</p>
381
+ </div>
382
+
383
+ <!-- Results -->
384
+ <div class="results-grid" id="resultsGrid">
385
+ <div class="result-card">
386
+ <h3>📷 Original Image</h3>
387
+ <img id="originalImage" class="result-image" alt="Original">
388
+ </div>
389
+ <div class="result-card">
390
+ <h3>🎨 Segmentation Mask</h3>
391
+ <img id="maskImage" class="result-image" alt="Mask">
392
+ </div>
393
+ <div class="result-card">
394
+ <h3>✨ Overlay</h3>
395
+ <img id="overlayImage" class="result-image" alt="Overlay">
396
+ </div>
397
+ </div>
398
+
399
+ <!-- Legend -->
400
+ <div class="legend" id="legend" style="display: none;">
401
+ <h3>🏷️ Segmentation Classes</h3>
402
+ <div class="legend-items">
403
+ <div class="legend-item">
404
+ <div class="legend-color" style="background-color: rgb(60, 16, 152);"></div>
405
+ <span><strong>Class 0</strong></span>
406
+ </div>
407
+ <div class="legend-item">
408
+ <div class="legend-color" style="background-color: rgb(132, 41, 246);"></div>
409
+ <span><strong>Class 1</strong></span>
410
+ </div>
411
+ <div class="legend-item">
412
+ <div class="legend-color" style="background-color: rgb(110, 193, 228);"></div>
413
+ <span><strong>Class 2</strong></span>
414
+ </div>
415
+ <div class="legend-item">
416
+ <div class="legend-color" style="background-color: rgb(254, 221, 58);"></div>
417
+ <span><strong>Class 3</strong></span>
418
+ </div>
419
+ </div>
420
+ </div>
421
+
422
+ <!-- Reset Button -->
423
+ <div style="text-align: center; margin-top: 30px;" id="resetSection" style="display: none;">
424
+ <button class="reset-button" onclick="resetAnalysis()">
425
+ 🔄 Analyze Another Image
426
+ </button>
427
+ </div>
428
+ </div>
429
+ </div>
430
+
431
+ <script>
432
+ let selectedFile = null;
433
+
434
+ // File input handler
435
+ document.getElementById('fileInput').addEventListener('change', function(e) {
436
+ const file = e.target.files[0];
437
+ if (file) {
438
+ handleFile(file);
439
+ }
440
+ });
441
+
442
+ function handleFile(file) {
443
+ if (!file.type.startsWith('image/')) {
444
+ showError('Please upload a valid image file (PNG, JPG, JPEG)');
445
+ return;
446
+ }
447
+
448
+ selectedFile = file;
449
+
450
+ // Show preview
451
+ const reader = new FileReader();
452
+ reader.onload = function(e) {
453
+ document.getElementById('previewImage').src = e.target.result;
454
+ document.getElementById('uploadSection').style.display = 'none';
455
+ document.getElementById('previewSection').style.display = 'block';
456
+ document.getElementById('errorMessage').style.display = 'none';
457
+ };
458
+ reader.readAsDataURL(file);
459
+ }
460
+
461
+ async function analyzeImage() {
462
+ if (!selectedFile) return;
463
+
464
+ // Show loading
465
+ document.getElementById('loading').style.display = 'block';
466
+ document.getElementById('previewSection').style.display = 'none';
467
+ document.getElementById('resultsGrid').style.display = 'none';
468
+ document.getElementById('legend').style.display = 'none';
469
+
470
+ const formData = new FormData();
471
+ formData.append('file', selectedFile);
472
+
473
+ try {
474
+ const response = await fetch('/predict', {
475
+ method: 'POST',
476
+ body: formData
477
+ });
478
+
479
+ const data = await response.json();
480
+
481
+ document.getElementById('loading').style.display = 'none';
482
+
483
+ if (data.error) {
484
+ showError(data.error);
485
+ } else {
486
+ showResults(data);
487
+ }
488
+ } catch (error) {
489
+ document.getElementById('loading').style.display = 'none';
490
+ showError('An error occurred: ' + error.message);
491
+ }
492
+ }
493
+
494
+ async function testExample() {
495
+ // Show loading
496
+ document.getElementById('loading').style.display = 'block';
497
+ document.getElementById('uploadSection').style.display = 'none';
498
+ document.getElementById('previewSection').style.display = 'none';
499
+ document.getElementById('resultsGrid').style.display = 'none';
500
+ document.getElementById('legend').style.display = 'none';
501
+ document.getElementById('errorMessage').style.display = 'none';
502
+
503
+ try {
504
+ const response = await fetch('/test-example', {
505
+ method: 'POST'
506
+ });
507
+
508
+ const data = await response.json();
509
+
510
+ document.getElementById('loading').style.display = 'none';
511
+
512
+ if (data.error) {
513
+ showError(data.error);
514
+ } else {
515
+ showResults(data);
516
+ }
517
+ } catch (error) {
518
+ document.getElementById('loading').style.display = 'none';
519
+ showError('An error occurred: ' + error.message);
520
+ }
521
+ }
522
+
523
+ function showResults(data) {
524
+ document.getElementById('originalImage').src = data.original;
525
+ document.getElementById('maskImage').src = data.mask;
526
+ document.getElementById('overlayImage').src = data.overlay;
527
+
528
+ document.getElementById('resultsGrid').style.display = 'grid';
529
+ document.getElementById('legend').style.display = 'block';
530
+ document.getElementById('resetSection').style.display = 'block';
531
+ }
532
+
533
+ function showError(message) {
534
+ const errorElement = document.getElementById('errorMessage');
535
+ errorElement.textContent = '❌ Error: ' + message;
536
+ errorElement.style.display = 'block';
537
+ document.getElementById('uploadSection').style.display = 'block';
538
+ }
539
+
540
+ function resetAnalysis() {
541
+ selectedFile = null;
542
+ document.getElementById('fileInput').value = '';
543
+ document.getElementById('uploadSection').style.display = 'block';
544
+ document.getElementById('previewSection').style.display = 'none';
545
+ document.getElementById('resultsGrid').style.display = 'none';
546
+ document.getElementById('legend').style.display = 'none';
547
+ document.getElementById('resetSection').style.display = 'none';
548
+ document.getElementById('errorMessage').style.display = 'none';
549
+ document.getElementById('loading').style.display = 'none';
550
+ }
551
+ </script>
552
+ </body>
553
+ </html>