Manvikk commited on
Commit
dd623ec
ยท
verified ยท
1 Parent(s): 95ee92b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +382 -445
app.py CHANGED
@@ -1,8 +1,11 @@
1
  import os
 
 
 
2
  import cv2
3
  import numpy as np
4
  import base64
5
- from flask import Flask, render_template_string, request, redirect, flash, url_for
6
  import roboflow
7
  import torch
8
  from collections import Counter
@@ -10,6 +13,9 @@ from collections import Counter
10
  app = Flask(__name__)
11
  app.secret_key = 'your_secret_key' # Replace with a secure secret key
12
 
 
 
 
13
  #########################################
14
  # 1. Initialize the Models
15
  #########################################
@@ -69,109 +75,108 @@ def custom_nms(preds, iou_threshold=0.3):
69
  filtered_preds.append(pred)
70
  return filtered_preds
71
 
72
- def process_image(image_path, object_type):
73
- # Load image
74
- image = cv2.imread(image_path)
75
- if image is None:
76
- print("DEBUG: cv2.imread failed to load the image from", image_path)
77
- return None, "Could not read the image."
78
-
79
- img_height, img_width = image.shape[:2]
80
- detection_info = [] # List to hold detection results for display
81
-
82
- if object_type == "box":
83
- # --- Roboflow Box Detection & Measurement ---
84
- if box_model is None:
85
- print("DEBUG: Roboflow model is not initialized.")
86
- return None, "Roboflow model is not available."
87
- try:
88
- results = box_model.predict(image_path, confidence=50, overlap=30).json()
89
- predictions = results.get("predictions", [])
90
- except Exception as e:
91
- print("DEBUG: Error during Roboflow prediction:", e)
92
- return None, "Error during Roboflow prediction."
93
-
94
- processed_preds = []
95
- for prediction in predictions:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  try:
97
- x, y, width, height = prediction["x"], prediction["y"], prediction["width"], prediction["height"]
98
- x1 = int(round(x - width / 2))
99
- y1 = int(round(y - height / 2))
100
- x2 = int(round(x + width / 2))
101
- y2 = int(round(y + height / 2))
102
- # Clamp coordinates to image dimensions
103
- x1 = max(0, min(x1, img_width - 1))
104
- y1 = max(0, min(y1, img_height - 1))
105
- x2 = max(0, min(x2, img_width - 1))
106
- y2 = max(0, min(y2, img_height - 1))
107
- processed_preds.append({
108
- "box": (x1, y1, x2, y2),
109
- "class": prediction["class"],
110
- "confidence": prediction["confidence"]
111
- })
112
  except Exception as e:
113
- print("DEBUG: Error processing a prediction:", e)
114
- continue
115
-
116
- box_detections = custom_nms(processed_preds, iou_threshold=0.3)
117
-
118
- # Detect ArUco marker for measurement (only applicable for boxes)
119
- marker_real_width_cm = 10.0 # The marker is 10cm x 10cm
120
- try:
121
- gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
122
- aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_6X6_250)
123
- aruco_params = cv2.aruco.DetectorParameters()
124
- corners, ids, _ = cv2.aruco.detectMarkers(gray, aruco_dict, parameters=aruco_params)
125
- if ids is not None and len(corners) > 0:
126
- marker_corners = corners[0].reshape((4, 2))
127
- cv2.aruco.drawDetectedMarkers(image, corners, ids)
128
- marker_width_pixels = np.linalg.norm(marker_corners[0] - marker_corners[1])
129
- marker_height_pixels = np.linalg.norm(marker_corners[1] - marker_corners[2])
130
- marker_pixel_size = (marker_width_pixels + marker_height_pixels) / 2.0
131
- conversion_factor = marker_real_width_cm / marker_pixel_size
132
- else:
133
  conversion_factor = None
134
- except Exception as e:
135
- print("DEBUG: Error during ArUco detection:", e)
136
- conversion_factor = None
137
-
138
- # Draw box detections and record measurement info
139
- for pred in box_detections:
140
- x1, y1, x2, y2 = pred["box"]
141
- label = pred["class"]
142
- confidence = pred["confidence"]
143
- cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
144
- if conversion_factor is not None:
145
- box_width_pixels = x2 - x1
146
- box_height_pixels = y2 - y1
147
- box_width_cm = box_width_pixels * conversion_factor
148
- box_height_cm = box_height_pixels * conversion_factor
149
- size_text = f"{box_width_cm:.1f}x{box_height_cm:.1f} cm"
150
- detection_info.append({
151
- "class": label,
152
- "confidence": f"{confidence:.2f}",
153
- "width_cm": f"{box_width_cm:.1f}",
154
- "height_cm": f"{box_height_cm:.1f}"
155
- })
156
- else:
157
- size_text = ""
158
- detection_info.append({
159
- "class": label,
160
- "confidence": f"{confidence:.2f}",
161
- "width_cm": "N/A",
162
- "height_cm": "N/A"
163
- })
164
- text = f"{label} ({confidence:.2f}) {size_text}"
165
- (text_width, text_height), baseline = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
166
- cv2.rectangle(image, (x1, y1 - text_height - baseline - 5), (x1 + text_width, y1 - 5), (0, 255, 0), -1)
167
- cv2.putText(image, text, (x1, y1 - 5 - baseline), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
168
-
169
- elif object_type in {"person", "car"}:
170
- # --- YOLOv5 for Persons or Cars (filtering for the selected type) ---
171
- if yolov5_model is None:
172
- print("DEBUG: YOLOv5 model is not initialized.")
173
- return None, "YOLOv5 model is not available."
174
- else:
175
  try:
176
  img_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
177
  yolo_results = yolov5_model(img_rgb)
@@ -196,28 +201,30 @@ def process_image(image_path, object_type):
196
  "height_cm": "N/A"
197
  })
198
  except Exception as e:
199
- print("DEBUG: Error during YOLOv5 inference:", e)
200
- return None, "Error during YOLOv5 inference."
201
- else:
202
- err_msg = f"Unrecognized object type: {object_type}"
203
- print("DEBUG:", err_msg)
204
- return None, err_msg
205
-
206
- # --- Build Top Summary Text ---
207
- detection_counts = Counter(det["class"] for det in detection_info)
208
- if detection_counts:
209
- top_text = ", ".join(f"{cls}: {count}" for cls, count in detection_counts.items())
210
- (info_width, info_height), info_baseline = cv2.getTextSize(top_text, cv2.FONT_HERSHEY_SIMPLEX, 1, 2)
211
- cv2.rectangle(image, (5, 5), (5 + info_width, 5 + info_height + info_baseline), (0, 255, 0), -1)
212
- cv2.putText(image, top_text, (5, 5 + info_height), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
213
-
214
- return image, detection_info
 
 
 
215
 
216
  #########################################
217
  # 3. HTML Templates
218
  #########################################
219
 
220
- # --- Landing Page Template (New) ---
221
  landing_template = '''
222
  <!DOCTYPE html>
223
  <html lang="en">
@@ -228,42 +235,15 @@ landing_template = '''
228
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
229
  <style>
230
  @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
231
- body {
232
- background-color: #fff;
233
- color: #000;
234
- font-family: "Share Tech Mono", monospace;
235
- text-align: center;
236
- display: flex;
237
- flex-direction: column;
238
- justify-content: center;
239
- align-items: center;
240
- min-height: 100vh;
241
- padding: 20px;
242
- overflow: auto;
243
- }
244
- h1 {
245
- font-size: 2.5rem;
246
- margin-bottom: 20px;
247
- }
248
- p {
249
- font-size: 1.5rem;
250
- margin-bottom: 40px;
251
- }
252
- .btn {
253
- display: inline-block;
254
- margin: 10px;
255
- padding: 15px 30px;
256
- font-size: 1.2rem;
257
- text-decoration: none;
258
- border: 2px solid #000;
259
- color: #000;
260
- cursor: pointer;
261
- transition: background-color 0.3s, color 0.3s;
262
- }
263
- .btn:hover {
264
- background-color: #000;
265
- color: #fff;
266
- }
267
  </style>
268
  </head>
269
  <body>
@@ -278,267 +258,211 @@ landing_template = '''
278
  </html>
279
  '''
280
 
281
- # --- Upload & Result Page Template (Original Design with Home Button and Progress Bar Overlay) ---
282
  upload_template = '''
283
  <!DOCTYPE html>
284
  <html lang="en">
285
  <head>
286
- <meta charset="UTF-8">
287
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
288
- <title>MathLens - AI Detection & Measurement</title>
289
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
290
- <style>
291
- @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
292
- body {
293
- background-color: #fff;
294
- color: #000;
295
- font-family: "Share Tech Mono", monospace;
296
- text-align: center;
297
- display: flex;
298
- flex-direction: column;
299
- justify-content: center;
300
- align-items: center;
301
- min-height: 100vh;
302
- padding: 20px;
303
- overflow: auto;
304
- }
305
- .typing-effect {
306
- font-size: 2rem;
307
- font-weight: bold;
308
- margin-bottom: 20px;
309
- height: 50px;
310
- white-space: nowrap;
311
- }
312
- form {
313
- margin-bottom: 20px;
314
- }
315
- input[type="file"], button {
316
- display: block;
317
- margin: 10px auto;
318
- padding: 10px;
319
- background: none;
320
- border: 2px solid #000;
321
- color: #000;
322
- font-size: 1rem;
323
- font-family: "Share Tech Mono", monospace;
324
- cursor: pointer;
325
- }
326
- input[type="file"]::file-selector-button {
327
- background: none;
328
- border: none;
329
- color: #000;
330
- font-family: "Share Tech Mono", monospace;
331
- }
332
- .home-btn {
333
- display: inline-block;
334
- margin: 10px auto 20px;
335
- padding: 10px 20px;
336
- border: 2px solid #000;
337
- color: #000;
338
- text-decoration: none;
339
- font-family: "Share Tech Mono", monospace;
340
- cursor: pointer;
341
- transition: background-color 0.3s, color 0.3s;
342
- }
343
- .home-btn:hover {
344
- background-color: #000;
345
- color: #fff;
346
- }
347
- /* Progress overlay styles */
348
- #progressOverlay {
349
- position: fixed;
350
- top: 0;
351
- left: 0;
352
- width: 100%;
353
- height: 100%;
354
- background: rgba(255,255,255,0.9);
355
- display: none;
356
- align-items: center;
357
- justify-content: center;
358
- flex-direction: column;
359
- z-index: 9999;
360
- }
361
- #progressContainer {
362
- width: 80%;
363
- max-width: 400px;
364
- }
365
- #progressBar {
366
- height: 20px;
367
- width: 0;
368
- background-color: #000;
369
- border-radius: 10px;
370
- -webkit-transition: width 0.05s linear;
371
- transition: width 0.05s linear;
372
- }
373
- #progressText {
374
- margin-top: 10px;
375
- font-size: 1.2rem;
376
- font-family: "Share Tech Mono", monospace;
377
- }
378
- .content-wrapper {
379
- display: flex;
380
- flex-direction: row;
381
- align-items: center;
382
- justify-content: space-evenly;
383
- width: 100%;
384
- max-width: 1200px;
385
- flex-wrap: wrap;
386
- gap: 20px;
387
- }
388
- .result-img {
389
- max-width: 100%;
390
- border: 2px solid #000;
391
- }
392
- table {
393
- width: 100%;
394
- max-width: 600px;
395
- border-collapse: collapse;
396
- }
397
- th, td {
398
- border: 1px solid #000;
399
- padding: 5px;
400
- text-align: center;
401
- }
402
- .footer {
403
- margin-top: 20px;
404
- font-size: 0.9em;
405
- color: #000;
406
- }
407
- @media (max-width: 768px) {
408
- .typing-effect {
409
- font-size: 1.5rem;
410
- }
411
- .content-wrapper {
412
- flex-direction: column;
413
- align-items: center;
414
- justify-content: center;
415
- }
416
- .result-img {
417
- max-width: 90%;
418
- }
419
- table {
420
- max-width: 100%;
421
- }
422
- }
423
- </style>
424
  </head>
425
  <body>
426
- <!-- Home button -->
427
- <a href="{{ url_for('landing') }}" class="home-btn">Home</a>
428
- <!-- Progress overlay with progress bar and dynamic text -->
429
- <div id="progressOverlay">
430
- <div id="progressContainer">
431
- <div id="progressBar"></div>
432
- <div id="progressText">Starting up... ๐Ÿ› ๏ธ</div>
433
- </div>
434
  </div>
435
- <div class="typing-effect" id="typing"></div>
436
- <!-- The form now has an id and an onsubmit handler -->
437
- <form id="uploadForm" method="post" enctype="multipart/form-data" onsubmit="handleFormSubmit(event)">
438
- <input type="file" name="file" accept="image/*" required>
439
- <!-- Hidden field to pass the selected object type -->
440
- <input type="hidden" name="object_type" value="{{ object_type }}">
441
- <button type="submit">Analyze Image</button>
442
- </form>
 
 
443
  {% if image_data or detection_info %}
444
- <div class="content-wrapper">
445
  <img src="data:image/jpeg;base64,{{ image_data }}" alt="Processed Image" class="result-img">
446
  <table>
447
- <thead>
448
- <tr>
449
- <th>#</th>
450
- <th>Class</th>
451
- <th>Confidence</th>
452
- <th>Width (cm)</th>
453
- <th>Height (cm)</th>
454
- </tr>
455
- </thead>
456
- <tbody>
457
- {% for det in detection_info %}
458
- <tr>
459
- <td>{{ loop.index }}</td>
460
- <td>{{ det.class }}</td>
461
- <td>{{ det.confidence }}</td>
462
- <td>{{ det.width_cm }}</td>
463
- <td>{{ det.height_cm }}</td>
464
- </tr>
465
- {% endfor %}
466
- </tbody>
467
  </table>
468
- </div>
469
  {% endif %}
470
- <div class="footer">&copy; 2024 MathLens AI Detection App. All rights reserved.</div>
471
- <script>
472
- // Intercept form submission and simulate a progress bar animation
473
- function handleFormSubmit(e) {
474
- e.preventDefault();
475
- // Determine if the visitor is on mobile (adjust delay accordingly)
476
- var isMobile = /Mobi|Android/i.test(navigator.userAgent);
477
- var totalDelay = isMobile ? 5000 : 2000; // 5 seconds on mobile, 2 seconds on desktop
478
- startProgressAndSubmit(totalDelay);
479
- }
480
-
481
- // Animate the progress overlay and then submit the form after the simulated delay
482
- function startProgressAndSubmit(totalTime) {
483
- var progressOverlay = document.getElementById("progressOverlay");
484
- progressOverlay.style.display = "flex";
485
- var progressBar = document.getElementById("progressBar");
486
- var progressText = document.getElementById("progressText");
487
- var progress = 0;
488
- var updateInterval = totalTime / 100; // time (ms) per 1% increment
489
- var phrases = [
490
- { threshold: 0, text: "Starting up... ๐Ÿ› ๏ธ" },
491
- { threshold: 10, text: "Writing scripts... ๐Ÿค–" },
492
- { threshold: 30, text: "Calculating formulas... ๐Ÿงฎ" },
493
- { threshold: 50, text: "Crunching numbers... ๐Ÿ”ข" },
494
- { threshold: 70, text: "Almost there... ๐Ÿš€" },
495
- { threshold: 90, text: "Finalizing... ๐Ÿ" },
496
- { threshold: 100, text: "Done! โœ…" }
497
- ];
498
- var interval = setInterval(function() {
499
- progress++;
500
- if (progress > 100) progress = 100;
501
- progressBar.style.width = progress + "%";
502
- for (var i = phrases.length - 1; i >= 0; i--) {
503
- if (progress >= phrases[i].threshold) {
504
- progressText.textContent = phrases[i].text;
505
- break;
506
- }
507
- }
508
- if (progress === 100) {
509
- clearInterval(interval);
510
- document.getElementById("uploadForm").submit();
511
- }
512
- }, updateInterval);
513
- }
514
-
515
- // Existing typing effect code
516
- const textArray = ["MathLens", "Smart Counting with Maths"];
517
- let textIndex = 0;
518
- let charIndex = 0;
519
- let isDeleting = false;
520
- const typingElement = document.getElementById("typing");
521
- function typeEffect() {
522
- let currentText = textArray[textIndex];
523
- if (isDeleting) {
524
- typingElement.textContent = currentText.substring(0, charIndex--);
 
 
 
 
 
 
525
  } else {
526
- typingElement.textContent = currentText.substring(0, charIndex++);
527
  }
528
- if (!isDeleting && charIndex === currentText.length) {
529
- setTimeout(() => { isDeleting = true; typeEffect(); }, 3000);
530
- } else if (isDeleting && charIndex === 0) {
531
- isDeleting = false;
532
- textIndex = (textIndex + 1) % textArray.length;
533
- setTimeout(typeEffect, 500);
534
- } else {
535
- setTimeout(typeEffect, isDeleting ? 50 : 100);
 
 
 
 
 
 
536
  }
537
- }
538
- document.addEventListener("DOMContentLoaded", () => {
539
- setTimeout(typeEffect, 500);
540
- });
541
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
  </body>
543
  </html>
544
  '''
@@ -547,59 +471,72 @@ upload_template = '''
547
  # 4. Flask Routes
548
  #########################################
549
 
550
- # Landing page: displays only the title and three buttons
551
  @app.route('/')
552
  def landing():
553
  return render_template_string(landing_template)
554
 
555
- # Upload page: uses the original upload page design and processes the image.
556
- @app.route('/upload', methods=['GET', 'POST'])
557
  def upload():
558
- if request.method == 'GET':
559
- # Get the selected object type from the query parameter
560
- object_type = request.args.get('object_type', '').lower()
561
- if object_type not in {"person", "car", "box"}:
562
- flash("Please select a valid object type.")
563
- return redirect(url_for('landing'))
564
- return render_template_string(upload_template, object_type=object_type)
565
- else:
566
- # POST: process the uploaded image
567
- if 'file' not in request.files:
568
- flash('No file part')
569
- return redirect(request.url)
570
- file = request.files['file']
571
- if file.filename == '':
572
- flash('No selected file')
573
- return redirect(request.url)
574
- object_type = request.form.get('object_type', '').lower()
575
- if object_type not in {"person", "car", "box"}:
576
- flash("Invalid object type selected.")
577
- return redirect(url_for('landing'))
578
- upload_path = "uploaded.jpg"
579
- try:
580
- file.save(upload_path)
581
- except Exception as e:
582
- print("DEBUG: Error saving uploaded file:", e)
583
- flash("Error saving uploaded file.")
584
- return redirect(request.url)
585
- processed_image, detection_info = process_image(upload_path, object_type)
586
- if processed_image is None:
587
- flash("Error Processing Image: " + detection_info)
588
- return redirect(request.url)
589
- else:
590
- retval, buffer = cv2.imencode('.jpg', processed_image)
591
- image_data = base64.b64encode(buffer).decode('utf-8')
592
- try:
593
- os.remove(upload_path)
594
- except Exception as e:
595
- print("DEBUG: Error removing uploaded file:", e)
596
- return render_template_string(upload_template, object_type=object_type,
597
- image_data=image_data, detection_info=detection_info)
 
 
 
 
 
 
 
 
 
 
 
 
 
598
 
599
  #########################################
600
  # 5. Run the App
601
  #########################################
602
 
603
  if __name__ == '__main__':
604
- # Runs on host 0.0.0.0 and port 7860 (adjust as needed)
605
- app.run(host="0.0.0.0", port=7860)
 
1
  import os
2
+ import uuid
3
+ import threading
4
+ import time
5
  import cv2
6
  import numpy as np
7
  import base64
8
+ from flask import Flask, render_template_string, request, redirect, flash, url_for, jsonify
9
  import roboflow
10
  import torch
11
  from collections import Counter
 
13
  app = Flask(__name__)
14
  app.secret_key = 'your_secret_key' # Replace with a secure secret key
15
 
16
+ # Global dictionary to hold job progress and results
17
+ jobs = {} # jobs[job_id] = {"progress": int, "result": {โ€ฆ}}
18
+
19
  #########################################
20
  # 1. Initialize the Models
21
  #########################################
 
75
  filtered_preds.append(pred)
76
  return filtered_preds
77
 
78
+ def process_image(job_id, image_path, object_type):
79
+ try:
80
+ jobs[job_id]['progress'] = 10
81
+ # Load image
82
+ image = cv2.imread(image_path)
83
+ if image is None:
84
+ jobs[job_id]['progress'] = 100
85
+ jobs[job_id]['result'] = {"error": "Could not read the image."}
86
+ return
87
+
88
+ jobs[job_id]['progress'] = 20
89
+ img_height, img_width = image.shape[:2]
90
+ detection_info = []
91
+
92
+ if object_type == "box":
93
+ if box_model is None:
94
+ jobs[job_id]['progress'] = 100
95
+ jobs[job_id]['result'] = {"error": "Roboflow model not available."}
96
+ return
97
+ try:
98
+ results = box_model.predict(image_path, confidence=50, overlap=30).json()
99
+ predictions = results.get("predictions", [])
100
+ except Exception as e:
101
+ jobs[job_id]['progress'] = 100
102
+ jobs[job_id]['result'] = {"error": "Error during Roboflow prediction."}
103
+ return
104
+
105
+ processed_preds = []
106
+ for prediction in predictions:
107
+ try:
108
+ x, y, width, height = prediction["x"], prediction["y"], prediction["width"], prediction["height"]
109
+ x1 = int(round(x - width / 2))
110
+ y1 = int(round(y - height / 2))
111
+ x2 = int(round(x + width / 2))
112
+ y2 = int(round(y + height / 2))
113
+ x1 = max(0, min(x1, img_width - 1))
114
+ y1 = max(0, min(y1, img_height - 1))
115
+ x2 = max(0, min(x2, img_width - 1))
116
+ y2 = max(0, min(y2, img_height - 1))
117
+ processed_preds.append({
118
+ "box": (x1, y1, x2, y2),
119
+ "class": prediction["class"],
120
+ "confidence": prediction["confidence"]
121
+ })
122
+ except Exception as e:
123
+ continue
124
+
125
+ box_detections = custom_nms(processed_preds, iou_threshold=0.3)
126
+ jobs[job_id]['progress'] = 60
127
+
128
+ # (Optional) Detect ArUco marker for measurement
129
+ marker_real_width_cm = 10.0
130
  try:
131
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
132
+ aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_6X6_250)
133
+ aruco_params = cv2.aruco.DetectorParameters()
134
+ corners, ids, _ = cv2.aruco.detectMarkers(gray, aruco_dict, parameters=aruco_params)
135
+ if ids is not None and len(corners) > 0:
136
+ marker_corners = corners[0].reshape((4, 2))
137
+ cv2.aruco.drawDetectedMarkers(image, corners, ids)
138
+ marker_width_pixels = np.linalg.norm(marker_corners[0] - marker_corners[1])
139
+ marker_height_pixels = np.linalg.norm(marker_corners[1] - marker_corners[2])
140
+ marker_pixel_size = (marker_width_pixels + marker_height_pixels) / 2.0
141
+ conversion_factor = marker_real_width_cm / marker_pixel_size
142
+ else:
143
+ conversion_factor = None
 
 
144
  except Exception as e:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  conversion_factor = None
146
+
147
+ for pred in box_detections:
148
+ x1, y1, x2, y2 = pred["box"]
149
+ label = pred["class"]
150
+ confidence = pred["confidence"]
151
+ cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
152
+ if conversion_factor is not None:
153
+ box_width_pixels = x2 - x1
154
+ box_height_pixels = y2 - y1
155
+ box_width_cm = box_width_pixels * conversion_factor
156
+ box_height_cm = box_height_pixels * conversion_factor
157
+ size_text = f"{box_width_cm:.1f}x{box_height_cm:.1f} cm"
158
+ detection_info.append({
159
+ "class": label,
160
+ "confidence": f"{confidence:.2f}",
161
+ "width_cm": f"{box_width_cm:.1f}",
162
+ "height_cm": f"{box_height_cm:.1f}"
163
+ })
164
+ else:
165
+ detection_info.append({
166
+ "class": label,
167
+ "confidence": f"{confidence:.2f}",
168
+ "width_cm": "N/A",
169
+ "height_cm": "N/A"
170
+ })
171
+ text = f"{label} ({confidence:.2f})"
172
+ (text_width, text_height), baseline = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
173
+ cv2.rectangle(image, (x1, y1 - text_height - baseline - 5), (x1 + text_width, y1 - 5), (0, 255, 0), -1)
174
+ cv2.putText(image, text, (x1, y1 - 5 - baseline), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
175
+ elif object_type in {"person", "car"}:
176
+ if yolov5_model is None:
177
+ jobs[job_id]['progress'] = 100
178
+ jobs[job_id]['result'] = {"error": "YOLOv5 model not available."}
179
+ return
 
 
 
 
 
 
 
180
  try:
181
  img_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
182
  yolo_results = yolov5_model(img_rgb)
 
201
  "height_cm": "N/A"
202
  })
203
  except Exception as e:
204
+ jobs[job_id]['progress'] = 100
205
+ jobs[job_id]['result'] = {"error": "Error during YOLOv5 inference."}
206
+ return
207
+
208
+ # Build summary text (drawn on image)
209
+ detection_counts = Counter(det["class"] for det in detection_info)
210
+ if detection_counts:
211
+ top_text = ", ".join(f"{cls}: {count}" for cls, count in detection_counts.items())
212
+ (info_width, info_height), info_baseline = cv2.getTextSize(top_text, cv2.FONT_HERSHEY_SIMPLEX, 1, 2)
213
+ cv2.rectangle(image, (5, 5), (5 + info_width, 5 + info_height + info_baseline), (0, 255, 0), -1)
214
+ cv2.putText(image, top_text, (5, 5 + info_height), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
215
+
216
+ jobs[job_id]['progress'] = 100
217
+ retval, buffer = cv2.imencode('.jpg', image)
218
+ image_data = base64.b64encode(buffer).decode('utf-8')
219
+ jobs[job_id]['result'] = {"image_data": image_data, "detection_info": detection_info}
220
+ except Exception as e:
221
+ jobs[job_id]['progress'] = 100
222
+ jobs[job_id]['result'] = {"error": "Unexpected error during processing."}
223
 
224
  #########################################
225
  # 3. HTML Templates
226
  #########################################
227
 
 
228
  landing_template = '''
229
  <!DOCTYPE html>
230
  <html lang="en">
 
235
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
236
  <style>
237
  @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
238
+ body { background-color: #fff; color: #000; font-family: "Share Tech Mono", monospace;
239
+ text-align: center; display: flex; flex-direction: column; justify-content: center;
240
+ align-items: center; min-height: 100vh; padding: 20px; }
241
+ h1 { font-size: 2.5rem; margin-bottom: 20px; }
242
+ p { font-size: 1.5rem; margin-bottom: 40px; }
243
+ .btn { display: inline-block; margin: 10px; padding: 15px 30px;
244
+ font-size: 1.2rem; text-decoration: none; border: 2px solid #000;
245
+ color: #000; transition: background-color 0.3s, color 0.3s; }
246
+ .btn:hover { background-color: #000; color: #fff; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  </style>
248
  </head>
249
  <body>
 
258
  </html>
259
  '''
260
 
 
261
  upload_template = '''
262
  <!DOCTYPE html>
263
  <html lang="en">
264
  <head>
265
+ <meta charset="UTF-8">
266
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
267
+ <title>MathLens - AI Detection & Measurement</title>
268
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
269
+ <style>
270
+ @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
271
+ body { background-color: #fff; color: #000; font-family: "Share Tech Mono", monospace;
272
+ text-align: center; display: flex; flex-direction: column; justify-content: center;
273
+ align-items: center; min-height: 100vh; padding: 20px; }
274
+ .typing-effect { font-size: 2rem; font-weight: bold; margin-bottom: 20px;
275
+ height: 50px; white-space: nowrap; }
276
+ form { margin-bottom: 20px; }
277
+ input[type="file"], button { display: block; margin: 10px auto; padding: 10px;
278
+ background: none; border: 2px solid #000; color: #000;
279
+ font-size: 1rem; font-family: "Share Tech Mono", monospace;
280
+ cursor: pointer; }
281
+ input[type="file"]::file-selector-button { background: none; border: none; color: #000; }
282
+ .home-btn { display: inline-block; margin: 10px auto 20px; padding: 10px 20px;
283
+ border: 2px solid #000; color: #000; text-decoration: none;
284
+ font-family: "Share Tech Mono", monospace; transition: background-color 0.3s, color 0.3s; }
285
+ .home-btn:hover { background-color: #000; color: #fff; }
286
+ /* Progress overlay styles */
287
+ #progressOverlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%;
288
+ background: rgba(255,255,255,0.9); display: none;
289
+ align-items: center; justify-content: center; flex-direction: column;
290
+ z-index: 9999; }
291
+ #progressContainer { width: 80%; max-width: 400px; }
292
+ #progressBar { height: 20px; width: 0; background-color: #000; border-radius: 10px;
293
+ transition: width 0.2s linear; }
294
+ #progressText { margin-top: 10px; font-size: 1.2rem; }
295
+ .content-wrapper { display: flex; flex-direction: row; align-items: center;
296
+ justify-content: space-evenly; width: 100%; max-width: 1200px;
297
+ flex-wrap: wrap; gap: 20px; }
298
+ .result-img { max-width: 100%; border: 2px solid #000; }
299
+ table { width: 100%; max-width: 600px; border-collapse: collapse; }
300
+ th, td { border: 1px solid #000; padding: 5px; text-align: center; }
301
+ .footer { margin-top: 20px; font-size: 0.9em; color: #000; }
302
+ @media (max-width: 768px) {
303
+ .typing-effect { font-size: 1.5rem; }
304
+ .content-wrapper { flex-direction: column; align-items: center; }
305
+ .result-img { max-width: 90%; }
306
+ table { max-width: 100%; }
307
+ }
308
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  </head>
310
  <body>
311
+ <!-- Home button -->
312
+ <a href="{{ url_for('landing') }}" class="home-btn">Home</a>
313
+ <!-- Progress overlay -->
314
+ <div id="progressOverlay">
315
+ <div id="progressContainer">
316
+ <div id="progressBar"></div>
317
+ <div id="progressText">Starting up... ๐Ÿ› ๏ธ</div>
 
318
  </div>
319
+ </div>
320
+ <div class="typing-effect" id="typing"></div>
321
+ <!-- The file upload form (submission handled via AJAX) -->
322
+ <form id="uploadForm">
323
+ <input type="file" name="file" accept="image/*" required>
324
+ <!-- Hidden field to pass the selected object type -->
325
+ <input type="hidden" name="object_type" value="{{ object_type }}">
326
+ <button type="submit">Analyze Image</button>
327
+ </form>
328
+ <div id="resultContainer">
329
  {% if image_data or detection_info %}
330
+ <div class="content-wrapper">
331
  <img src="data:image/jpeg;base64,{{ image_data }}" alt="Processed Image" class="result-img">
332
  <table>
333
+ <thead>
334
+ <tr>
335
+ <th>#</th>
336
+ <th>Class</th>
337
+ <th>Confidence</th>
338
+ <th>Width (cm)</th>
339
+ <th>Height (cm)</th>
340
+ </tr>
341
+ </thead>
342
+ <tbody>
343
+ {% for det in detection_info %}
344
+ <tr>
345
+ <td>{{ loop.index }}</td>
346
+ <td>{{ det.class }}</td>
347
+ <td>{{ det.confidence }}</td>
348
+ <td>{{ det.width_cm }}</td>
349
+ <td>{{ det.height_cm }}</td>
350
+ </tr>
351
+ {% endfor %}
352
+ </tbody>
353
  </table>
354
+ </div>
355
  {% endif %}
356
+ </div>
357
+ <div class="footer">&copy; 2024 MathLens AI Detection App. All rights reserved.</div>
358
+ <script>
359
+ // Handle typing effect (unchanged)
360
+ const textArray = ["MathLens", "Smart Counting with Maths"];
361
+ let textIndex = 0, charIndex = 0, isDeleting = false;
362
+ const typingElement = document.getElementById("typing");
363
+ function typeEffect() {
364
+ let currentText = textArray[textIndex];
365
+ if (isDeleting) {
366
+ typingElement.textContent = currentText.substring(0, charIndex--);
367
+ } else {
368
+ typingElement.textContent = currentText.substring(0, charIndex++);
369
+ }
370
+ if (!isDeleting && charIndex === currentText.length) {
371
+ setTimeout(() => { isDeleting = true; typeEffect(); }, 3000);
372
+ } else if (isDeleting && charIndex === 0) {
373
+ isDeleting = false;
374
+ textIndex = (textIndex + 1) % textArray.length;
375
+ setTimeout(typeEffect, 500);
376
+ } else {
377
+ setTimeout(typeEffect, isDeleting ? 50 : 100);
378
+ }
379
+ }
380
+ document.addEventListener("DOMContentLoaded", () => {
381
+ setTimeout(typeEffect, 500);
382
+ });
383
+
384
+ // AJAX-based submission and progress polling
385
+ const uploadForm = document.getElementById("uploadForm");
386
+ uploadForm.addEventListener("submit", function(e) {
387
+ e.preventDefault();
388
+ const formData = new FormData(uploadForm);
389
+ // Show progress overlay
390
+ document.getElementById("progressOverlay").style.display = "flex";
391
+ // Start the analysis job
392
+ fetch("{{ url_for('analyze') }}", {
393
+ method: "POST",
394
+ body: formData
395
+ })
396
+ .then(response => response.json())
397
+ .then(data => {
398
+ const jobId = data.job_id;
399
+ // Start polling progress every 500ms
400
+ const progressInterval = setInterval(() => {
401
+ fetch("{{ url_for('progress') }}?job_id=" + jobId)
402
+ .then(response => response.json())
403
+ .then(progData => {
404
+ const progress = progData.progress;
405
+ document.getElementById("progressBar").style.width = progress + "%";
406
+ // Update dynamic phrases based on progress
407
+ if (progress < 10) {
408
+ document.getElementById("progressText").textContent = "Starting up... ๐Ÿ› ๏ธ";
409
+ } else if (progress < 30) {
410
+ document.getElementById("progressText").textContent = "Writing scripts... ๐Ÿค–";
411
+ } else if (progress < 50) {
412
+ document.getElementById("progressText").textContent = "Calculating formulas... ๐Ÿงฎ";
413
+ } else if (progress < 70) {
414
+ document.getElementById("progressText").textContent = "Crunching numbers... ๐Ÿ”ข";
415
+ } else if (progress < 90) {
416
+ document.getElementById("progressText").textContent = "Almost there... ๐Ÿš€";
417
  } else {
418
+ document.getElementById("progressText").textContent = "Finalizing... ๐Ÿ";
419
  }
420
+ if (progress >= 100) {
421
+ clearInterval(progressInterval);
422
+ // Once done, fetch the result and update the page
423
+ fetch("{{ url_for('result') }}?job_id=" + jobId)
424
+ .then(response => response.json())
425
+ .then(resultData => {
426
+ document.getElementById("progressOverlay").style.display = "none";
427
+ // Replace the result container with the new result
428
+ document.getElementById("resultContainer").innerHTML = `
429
+ <div class="content-wrapper">
430
+ <img src="data:image/jpeg;base64,${resultData.image_data}" alt="Processed Image" class="result-img">
431
+ ${buildTableHTML(resultData.detection_info)}
432
+ </div>`;
433
+ });
434
  }
435
+ });
436
+ }, 500);
437
+ });
438
+ });
439
+
440
+ function buildTableHTML(detectionInfo) {
441
+ if (!detectionInfo || detectionInfo.length === 0) return "";
442
+ let tableHTML = `<table>
443
+ <thead>
444
+ <tr>
445
+ <th>#</th>
446
+ <th>Class</th>
447
+ <th>Confidence</th>
448
+ <th>Width (cm)</th>
449
+ <th>Height (cm)</th>
450
+ </tr>
451
+ </thead>
452
+ <tbody>`;
453
+ detectionInfo.forEach((det, index) => {
454
+ tableHTML += `<tr>
455
+ <td>${index+1}</td>
456
+ <td>${det.class}</td>
457
+ <td>${det.confidence}</td>
458
+ <td>${det.width_cm}</td>
459
+ <td>${det.height_cm}</td>
460
+ </tr>`;
461
+ });
462
+ tableHTML += `</tbody></table>`;
463
+ return tableHTML;
464
+ }
465
+ </script>
466
  </body>
467
  </html>
468
  '''
 
471
  # 4. Flask Routes
472
  #########################################
473
 
474
+ # Landing page (unchanged)
475
  @app.route('/')
476
  def landing():
477
  return render_template_string(landing_template)
478
 
479
+ # Traditional upload page (in case you want to support non-AJAX)
480
+ @app.route('/upload', methods=['GET'])
481
  def upload():
482
+ object_type = request.args.get('object_type', '').lower()
483
+ if object_type not in {"person", "car", "box"}:
484
+ flash("Please select a valid object type.")
485
+ return redirect(url_for('landing'))
486
+ # In this version the AJAX version is preferred.
487
+ return render_template_string(upload_template, object_type=object_type)
488
+
489
+ # Endpoint to start analysis (called via AJAX)
490
+ @app.route('/analyze', methods=['POST'])
491
+ def analyze():
492
+ # Save uploaded file
493
+ if 'file' not in request.files:
494
+ return jsonify({"error": "No file provided."}), 400
495
+ file = request.files['file']
496
+ if file.filename == '':
497
+ return jsonify({"error": "No selected file."}), 400
498
+ object_type = request.form.get('object_type', '').lower()
499
+ if object_type not in {"person", "car", "box"}:
500
+ return jsonify({"error": "Invalid object type."}), 400
501
+ upload_path = "uploaded.jpg"
502
+ try:
503
+ file.save(upload_path)
504
+ except Exception as e:
505
+ return jsonify({"error": "Error saving file."}), 500
506
+
507
+ # Create a unique job id and initialize job progress
508
+ job_id = str(uuid.uuid4())
509
+ jobs[job_id] = {"progress": 0, "result": None}
510
+ # Start background thread to process image
511
+ thread = threading.Thread(target=process_image, args=(job_id, upload_path, object_type))
512
+ thread.start()
513
+ # Optionally remove the uploaded file after starting the thread
514
+ # (In a real system, you might want to keep it until processing is done.)
515
+ return jsonify({"job_id": job_id})
516
+
517
+ # Endpoint for client to poll progress
518
+ @app.route('/progress', methods=['GET'])
519
+ def progress():
520
+ job_id = request.args.get('job_id', '')
521
+ if job_id not in jobs:
522
+ return jsonify({"progress": 0})
523
+ return jsonify({"progress": jobs[job_id].get("progress", 0)})
524
+
525
+ # Endpoint for client to fetch final result
526
+ @app.route('/result', methods=['GET'])
527
+ def result():
528
+ job_id = request.args.get('job_id', '')
529
+ if job_id not in jobs or jobs[job_id].get("result") is None:
530
+ return jsonify({"error": "Result not available."}), 404
531
+ result = jobs[job_id]["result"]
532
+ # Clean up job if desired
533
+ del jobs[job_id]
534
+ return jsonify(result)
535
 
536
  #########################################
537
  # 5. Run the App
538
  #########################################
539
 
540
  if __name__ == '__main__':
541
+ # Run in threaded mode so background threads can work
542
+ app.run(host="0.0.0.0", port=7860, threaded=True)