intisarhasnain commited on
Commit
45c47b0
·
verified ·
1 Parent(s): ac6dfad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +288 -10
app.py CHANGED
@@ -1,9 +1,10 @@
1
  from fastapi import FastAPI, File, UploadFile, Form
2
- from fastapi.responses import JSONResponse
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from ultralytics import YOLO
5
  import numpy as np
6
  import cv2
 
7
 
8
  app = FastAPI()
9
 
@@ -58,6 +59,11 @@ async def detect(
58
  }
59
  })
60
 
 
 
 
 
 
61
  return JSONResponse({
62
  "detections": detections,
63
  "count": len(detections),
@@ -68,15 +74,287 @@ async def detect(
68
  "settings": {
69
  "confidence": confidence,
70
  "overlap": overlap,
71
- }
 
72
  })
73
 
74
 
75
- @app.get("/")
76
- def root():
77
- return {
78
- "status": "ok",
79
- "model": "floor-jaaps YOLOv8s",
80
- "classes": list(model.names.values()),
81
- "usage": "POST /detect with form-data: file (image), confidence (0-1), overlap (0-1)"
82
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from fastapi import FastAPI, File, UploadFile, Form
2
+ from fastapi.responses import JSONResponse, HTMLResponse
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from ultralytics import YOLO
5
  import numpy as np
6
  import cv2
7
+ import base64
8
 
9
  app = FastAPI()
10
 
 
59
  }
60
  })
61
 
62
+ # Draw boxes on image and return as base64
63
+ annotated = results.plot()
64
+ _, buffer = cv2.imencode(".jpg", annotated)
65
+ annotated_b64 = base64.b64encode(buffer).decode("utf-8")
66
+
67
  return JSONResponse({
68
  "detections": detections,
69
  "count": len(detections),
 
74
  "settings": {
75
  "confidence": confidence,
76
  "overlap": overlap,
77
+ },
78
+ "annotated_image": f"data:image/jpeg;base64,{annotated_b64}"
79
  })
80
 
81
 
82
+ @app.get("/", response_class=HTMLResponse)
83
+ def ui():
84
+ classes = list(model.names.values())
85
+ classes_str = ", ".join(f'<span class="tag">{c}</span>' for c in classes)
86
+ return f"""<!DOCTYPE html>
87
+ <html lang="en">
88
+ <head>
89
+ <meta charset="UTF-8">
90
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
91
+ <title>Floor Plan Detector</title>
92
+ <style>
93
+ *, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
94
+ body {{
95
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
96
+ background: #f0f2f5;
97
+ min-height: 100vh;
98
+ display: flex;
99
+ flex-direction: column;
100
+ align-items: center;
101
+ padding: 32px 16px;
102
+ color: #1a1a2e;
103
+ }}
104
+ h1 {{ font-size: 1.6rem; font-weight: 700; margin-bottom: 4px; }}
105
+ .subtitle {{ color: #666; font-size: 0.9rem; margin-bottom: 24px; }}
106
+ .card {{
107
+ background: white;
108
+ border-radius: 16px;
109
+ padding: 28px;
110
+ width: 100%;
111
+ max-width: 720px;
112
+ box-shadow: 0 4px 24px rgba(0,0,0,0.08);
113
+ margin-bottom: 20px;
114
+ }}
115
+ .card h2 {{ font-size: 1rem; font-weight: 600; margin-bottom: 16px; color: #444; text-transform: uppercase; letter-spacing: 0.05em; }}
116
+ .drop-zone {{
117
+ border: 2px dashed #c5c9d6;
118
+ border-radius: 12px;
119
+ padding: 40px 20px;
120
+ text-align: center;
121
+ cursor: pointer;
122
+ transition: all 0.2s;
123
+ background: #fafbfc;
124
+ position: relative;
125
+ }}
126
+ .drop-zone:hover, .drop-zone.drag-over {{ border-color: #4f46e5; background: #f5f3ff; }}
127
+ .drop-zone input {{ position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }}
128
+ .drop-icon {{ font-size: 2rem; margin-bottom: 8px; }}
129
+ .drop-text {{ color: #666; font-size: 0.95rem; }}
130
+ .drop-text strong {{ color: #4f46e5; }}
131
+ #preview {{ max-width: 100%; border-radius: 8px; margin-top: 16px; display: none; }}
132
+ .slider-row {{
133
+ display: flex;
134
+ align-items: center;
135
+ gap: 12px;
136
+ margin-bottom: 14px;
137
+ }}
138
+ .slider-label {{ width: 110px; font-size: 0.9rem; color: #555; font-weight: 500; }}
139
+ input[type=range] {{ flex: 1; accent-color: #4f46e5; cursor: pointer; }}
140
+ .slider-val {{
141
+ width: 42px;
142
+ text-align: right;
143
+ font-size: 0.9rem;
144
+ font-weight: 600;
145
+ color: #4f46e5;
146
+ }}
147
+ .btn {{
148
+ width: 100%;
149
+ padding: 14px;
150
+ background: #4f46e5;
151
+ color: white;
152
+ border: none;
153
+ border-radius: 10px;
154
+ font-size: 1rem;
155
+ font-weight: 600;
156
+ cursor: pointer;
157
+ transition: background 0.2s;
158
+ margin-top: 8px;
159
+ }}
160
+ .btn:hover {{ background: #4338ca; }}
161
+ .btn:disabled {{ background: #a5b4fc; cursor: not-allowed; }}
162
+ #result-img {{ max-width: 100%; border-radius: 10px; display: none; }}
163
+ .stats {{
164
+ display: flex;
165
+ gap: 16px;
166
+ margin-bottom: 16px;
167
+ flex-wrap: wrap;
168
+ }}
169
+ .stat {{
170
+ background: #f5f3ff;
171
+ border-radius: 8px;
172
+ padding: 10px 18px;
173
+ text-align: center;
174
+ }}
175
+ .stat-val {{ font-size: 1.5rem; font-weight: 700; color: #4f46e5; }}
176
+ .stat-lbl {{ font-size: 0.75rem; color: #888; margin-top: 2px; }}
177
+ .detections {{ display: flex; flex-direction: column; gap: 8px; }}
178
+ .det-row {{
179
+ display: flex;
180
+ align-items: center;
181
+ justify-content: space-between;
182
+ background: #f9fafb;
183
+ border-radius: 8px;
184
+ padding: 10px 14px;
185
+ font-size: 0.9rem;
186
+ }}
187
+ .det-class {{ font-weight: 600; }}
188
+ .det-conf {{
189
+ background: #4f46e5;
190
+ color: white;
191
+ border-radius: 20px;
192
+ padding: 2px 10px;
193
+ font-size: 0.8rem;
194
+ font-weight: 600;
195
+ }}
196
+ .tags {{ display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }}
197
+ .tag {{
198
+ background: #ede9fe;
199
+ color: #5b21b6;
200
+ border-radius: 20px;
201
+ padding: 3px 10px;
202
+ font-size: 0.8rem;
203
+ font-weight: 500;
204
+ }}
205
+ .error {{ color: #dc2626; background: #fef2f2; border-radius: 8px; padding: 12px 16px; font-size: 0.9rem; }}
206
+ .hidden {{ display: none; }}
207
+ #result-section {{ display: none; }}
208
+ .spinner {{
209
+ width: 20px; height: 20px;
210
+ border: 3px solid rgba(255,255,255,0.4);
211
+ border-top-color: white;
212
+ border-radius: 50%;
213
+ animation: spin 0.8s linear infinite;
214
+ display: inline-block;
215
+ vertical-align: middle;
216
+ margin-right: 8px;
217
+ }}
218
+ @keyframes spin {{ to {{ transform: rotate(360deg); }} }}
219
+ </style>
220
+ </head>
221
+ <body>
222
+ <h1>🏠 Floor Plan Detector</h1>
223
+ <p class="subtitle">YOLOv8s · Trained on 14,274 floor plan images</p>
224
+
225
+ <div class="card">
226
+ <h2>Upload Image</h2>
227
+ <div class="drop-zone" id="dropZone">
228
+ <input type="file" id="fileInput" accept="image/*">
229
+ <div class="drop-icon">📐</div>
230
+ <div class="drop-text">Drop a floor plan image or <strong>click to browse</strong></div>
231
+ </div>
232
+ <img id="preview">
233
+ </div>
234
+
235
+ <div class="card">
236
+ <h2>Settings</h2>
237
+ <div class="slider-row">
238
+ <span class="slider-label">Confidence</span>
239
+ <input type="range" id="conf" min="1" max="99" value="50">
240
+ <span class="slider-val" id="confVal">0.50</span>
241
+ </div>
242
+ <div class="slider-row">
243
+ <span class="slider-label">Overlap (IoU)</span>
244
+ <input type="range" id="overlap" min="1" max="99" value="50">
245
+ <span class="slider-val" id="overlapVal">0.50</span>
246
+ </div>
247
+ <div style="margin-top:8px">
248
+ <span style="font-size:0.85rem;color:#888;">Detectable classes: </span>
249
+ <div class="tags" style="margin-top:6px">{classes_str}</div>
250
+ </div>
251
+ <button class="btn" id="detectBtn" onclick="detect()" disabled>Select an image first</button>
252
+ </div>
253
+
254
+ <div id="result-section">
255
+ <div class="card">
256
+ <h2>Annotated Result</h2>
257
+ <img id="result-img">
258
+ </div>
259
+ <div class="card">
260
+ <h2>Detections</h2>
261
+ <div class="stats">
262
+ <div class="stat"><div class="stat-val" id="countVal">0</div><div class="stat-lbl">Objects Found</div></div>
263
+ <div class="stat"><div class="stat-val" id="confSetting">0.50</div><div class="stat-lbl">Confidence Used</div></div>
264
+ </div>
265
+ <div class="detections" id="detList"></div>
266
+ <div class="error hidden" id="errBox"></div>
267
+ </div>
268
+ </div>
269
+
270
+ <script>
271
+ const confSlider = document.getElementById('conf');
272
+ const overlapSlider = document.getElementById('overlap');
273
+ const confVal = document.getElementById('confVal');
274
+ const overlapVal = document.getElementById('overlapVal');
275
+ const fileInput = document.getElementById('fileInput');
276
+ const preview = document.getElementById('preview');
277
+ const btn = document.getElementById('detectBtn');
278
+ const dropZone = document.getElementById('dropZone');
279
+
280
+ confSlider.oninput = () => confVal.textContent = (confSlider.value / 100).toFixed(2);
281
+ overlapSlider.oninput = () => overlapVal.textContent = (overlapSlider.value / 100).toFixed(2);
282
+
283
+ dropZone.addEventListener('dragover', e => {{ e.preventDefault(); dropZone.classList.add('drag-over'); }});
284
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
285
+ dropZone.addEventListener('drop', e => {{
286
+ e.preventDefault();
287
+ dropZone.classList.remove('drag-over');
288
+ if (e.dataTransfer.files[0]) loadFile(e.dataTransfer.files[0]);
289
+ }});
290
+
291
+ fileInput.onchange = () => {{ if (fileInput.files[0]) loadFile(fileInput.files[0]); }};
292
+
293
+ function loadFile(file) {{
294
+ const reader = new FileReader();
295
+ reader.onload = e => {{
296
+ preview.src = e.target.result;
297
+ preview.style.display = 'block';
298
+ btn.disabled = false;
299
+ btn.textContent = 'Run Detection';
300
+ }};
301
+ reader.readAsDataURL(file);
302
+ }}
303
+
304
+ async function detect() {{
305
+ const file = fileInput.files[0];
306
+ if (!file) return;
307
+
308
+ btn.disabled = true;
309
+ btn.innerHTML = '<span class="spinner"></span>Detecting...';
310
+ document.getElementById('result-section').style.display = 'none';
311
+ document.getElementById('errBox').classList.add('hidden');
312
+
313
+ const fd = new FormData();
314
+ fd.append('file', file);
315
+ fd.append('confidence', confSlider.value / 100);
316
+ fd.append('overlap', overlapSlider.value / 100);
317
+
318
+ try {{
319
+ const res = await fetch('/detect', {{ method: 'POST', body: fd }});
320
+ const data = await res.json();
321
+
322
+ if (data.error) throw new Error(data.error);
323
+
324
+ // Show annotated image
325
+ document.getElementById('result-img').src = data.annotated_image;
326
+ document.getElementById('result-img').style.display = 'block';
327
+
328
+ // Stats
329
+ document.getElementById('countVal').textContent = data.count;
330
+ document.getElementById('confSetting').textContent = data.settings.confidence.toFixed(2);
331
+
332
+ // Detection list
333
+ const list = document.getElementById('detList');
334
+ if (data.detections.length === 0) {{
335
+ list.innerHTML = '<div class="det-row" style="color:#888">No objects detected — try lowering the confidence threshold</div>';
336
+ }} else {{
337
+ list.innerHTML = data.detections
338
+ .sort((a, b) => b.confidence - a.confidence)
339
+ .map(d => `
340
+ <div class="det-row">
341
+ <span class="det-class">${{d.class}}</span>
342
+ <span class="det-conf">${{(d.confidence * 100).toFixed(1)}}%</span>
343
+ </div>`)
344
+ .join('');
345
+ }}
346
+
347
+ document.getElementById('result-section').style.display = 'block';
348
+ }} catch (err) {{
349
+ const errBox = document.getElementById('errBox');
350
+ errBox.textContent = 'Error: ' + err.message;
351
+ errBox.classList.remove('hidden');
352
+ document.getElementById('result-section').style.display = 'block';
353
+ }} finally {{
354
+ btn.disabled = false;
355
+ btn.textContent = 'Run Detection';
356
+ }}
357
+ }}
358
+ </script>
359
+ </body>
360
+ </html>"""