Manvikk commited on
Commit
0ce5c73
·
verified ·
1 Parent(s): 9f6b36d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +67 -96
app.py CHANGED
@@ -1,6 +1,7 @@
1
  import os
2
  import uuid
3
  import threading
 
4
  import cv2
5
  import numpy as np
6
  import base64
@@ -74,10 +75,6 @@ def custom_nms(preds, iou_threshold=0.3):
74
  filtered_preds.append(pred)
75
  return filtered_preds
76
 
77
- #########################################
78
- # 3. Processing Function with Pose-based Measurement
79
- #########################################
80
-
81
  def process_image(job_id, image_path, object_type):
82
  try:
83
  jobs[job_id]['progress'] = 10
@@ -109,12 +106,10 @@ def process_image(job_id, image_path, object_type):
109
  for prediction in predictions:
110
  try:
111
  x, y, width, height = prediction["x"], prediction["y"], prediction["width"], prediction["height"]
112
- # Convert from center (x, y) and width, height to top-left and bottom-right coordinates
113
  x1 = int(round(x - width / 2))
114
  y1 = int(round(y - height / 2))
115
  x2 = int(round(x + width / 2))
116
  y2 = int(round(y + height / 2))
117
- # Clamp to image boundaries
118
  x1 = max(0, min(x1, img_width - 1))
119
  y1 = max(0, min(y1, img_height - 1))
120
  x2 = max(0, min(x2, img_width - 1))
@@ -128,105 +123,64 @@ def process_image(job_id, image_path, object_type):
128
  continue
129
 
130
  box_detections = custom_nms(processed_preds, iou_threshold=0.3)
131
- jobs[job_id]['progress'] = 40
132
 
133
- # -------------------------------
134
- # Use ArUco pose estimation to compute a conversion factor.
135
- # Try multiple dictionaries in case your marker was created using a different one.
136
- # IMPORTANT: For accurate measurement you need proper camera calibration.
137
- # Replace the cameraMatrix and distCoeffs below with your actual parameters if available.
138
- # -------------------------------
139
- marker_conversion_factor = None
140
  try:
141
  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
142
- # Use detector parameters
 
143
  if hasattr(cv2.aruco, 'DetectorParameters_create'):
144
  aruco_params = cv2.aruco.DetectorParameters_create()
145
  else:
146
  aruco_params = cv2.aruco.DetectorParameters()
147
- # Try several common dictionaries
148
- dictionaries = [
149
- cv2.aruco.DICT_6X6_250,
150
- cv2.aruco.DICT_4X4_50,
151
- cv2.aruco.DICT_4X4_100,
152
- cv2.aruco.DICT_5X5_100
153
- ]
154
- marker_found = False
155
- for d in dictionaries:
156
- aruco_dict = cv2.aruco.getPredefinedDictionary(d)
157
- corners, ids, _ = cv2.aruco.detectMarkers(gray, aruco_dict, parameters=aruco_params)
158
- if ids is not None and len(corners) > 0:
159
- marker_found = True
160
- print("Detected marker using dictionary:", d)
161
- break
162
- if marker_found:
163
- # Use approximate calibration parameters (adjust these if you have real calibration data)
164
- fx = 1.2 * img_width
165
- fy = 1.2 * img_height
166
- cx = img_width / 2
167
- cy = img_height / 2
168
- cameraMatrix = np.array([[fx, 0, cx],
169
- [0, fy, cy],
170
- [0, 0, 1]], dtype=np.float32)
171
- distCoeffs = np.zeros((5, 1), dtype=np.float32)
172
- # Marker length in meters (10 cm = 0.10 m)
173
- marker_length = 0.10
174
- rvecs, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(corners, marker_length, cameraMatrix, distCoeffs)
175
- rvec = rvecs[0]
176
- tvec = tvecs[0]
177
- # Define 3D coordinates for the marker's corners in its coordinate system
178
- obj_points = np.array([
179
- [-marker_length/2, marker_length/2, 0],
180
- [ marker_length/2, marker_length/2, 0],
181
- [ marker_length/2, -marker_length/2, 0],
182
- [-marker_length/2, -marker_length/2, 0]
183
- ], dtype=np.float32)
184
- projected_points, _ = cv2.projectPoints(obj_points, rvec, tvec, cameraMatrix, distCoeffs)
185
- projected_points = projected_points.reshape((4,2))
186
- # Compute the pixel distance between two adjacent corners (e.g., top edge)
187
- pixel_distance = np.linalg.norm(projected_points[0] - projected_points[1])
188
- # Conversion factor: 10 cm (marker real width) divided by measured pixel distance
189
- marker_conversion_factor = 10.0 / pixel_distance # cm per pixel
190
- # Optionally, draw the marker axis for visualization:
191
- cv2.aruco.drawAxis(image, cameraMatrix, distCoeffs, rvec, tvec, 0.05)
192
  else:
193
- marker_conversion_factor = None
194
  except Exception as e:
195
- marker_conversion_factor = None
196
-
197
- jobs[job_id]['progress'] = 60
198
 
199
- # Process each detected box using the conversion factor from pose estimation.
200
  for pred in box_detections:
201
  x1, y1, x2, y2 = pred["box"]
202
  label = pred["class"]
203
  confidence = pred["confidence"]
204
  cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
205
- if marker_conversion_factor is not None:
206
  box_width_pixels = x2 - x1
207
  box_height_pixels = y2 - y1
208
- box_width_cm = box_width_pixels * marker_conversion_factor
209
- box_height_cm = box_height_pixels * marker_conversion_factor
210
- width_str = f"{box_width_cm:.1f}"
211
- height_str = f"{box_height_cm:.1f}"
 
 
 
 
212
  else:
213
- width_str = "N/A"
214
- height_str = "N/A"
215
-
216
- detection_info.append({
217
- "class": label,
218
- "confidence": f"{confidence:.2f}",
219
- "width_cm": width_str,
220
- "height_cm": height_str
221
- })
222
-
223
  text = f"{label} ({confidence:.2f})"
224
  (text_width, text_height), baseline = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
225
- cv2.rectangle(image, (x1, y1 - text_height - baseline - 5),
226
- (x1 + text_width, y1 - 5), (0, 255, 0), -1)
227
- cv2.putText(image, text, (x1, y1 - 5 - baseline),
228
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
229
-
230
  elif object_type in {"person", "car"}:
231
  if yolov5_model is None:
232
  jobs[job_id]['progress'] = 100
@@ -247,10 +201,8 @@ def process_image(job_id, image_path, object_type):
247
  cv2.rectangle(image, (xmin, ymin), (xmax, ymax), (255, 0, 0), 2)
248
  text = f"{label} ({conf:.2f})"
249
  (text_width, text_height), baseline = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
250
- cv2.rectangle(image, (xmin, ymin - text_height - baseline - 5),
251
- (xmin + text_width, ymin - 5), (255, 0, 0), -1)
252
- cv2.putText(image, text, (xmin, ymin - 5 - baseline),
253
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
254
  detection_info.append({
255
  "class": label,
256
  "confidence": f"{conf:.2f}",
@@ -262,7 +214,7 @@ def process_image(job_id, image_path, object_type):
262
  jobs[job_id]['result'] = {"error": "Error during YOLOv5 inference."}
263
  return
264
 
265
- # Draw a summary of detection counts on the image
266
  detection_counts = Counter(det["class"] for det in detection_info)
267
  if detection_counts:
268
  top_text = ", ".join(f"{cls}: {count}" for cls, count in detection_counts.items())
@@ -279,7 +231,7 @@ def process_image(job_id, image_path, object_type):
279
  jobs[job_id]['result'] = {"error": "Unexpected error during processing."}
280
 
281
  #########################################
282
- # 4. HTML Templates
283
  #########################################
284
 
285
  landing_template = '''
@@ -375,7 +327,7 @@ upload_template = '''
375
  </div>
376
  </div>
377
  <div class="typing-effect" id="typing"></div>
378
- <!-- File upload form (submission via AJAX) -->
379
  <form id="uploadForm">
380
  <input type="file" name="file" accept="image/*" required>
381
  <!-- Hidden field to pass the selected object type -->
@@ -413,7 +365,7 @@ upload_template = '''
413
  </div>
414
  <div class="footer">&copy; 2024 MathLens AI Detection App. All rights reserved.</div>
415
  <script>
416
- // Typing effect
417
  const textArray = ["MathLens", "Smart Counting with Maths"];
418
  let textIndex = 0, charIndex = 0, isDeleting = false;
419
  const typingElement = document.getElementById("typing");
@@ -437,12 +389,14 @@ upload_template = '''
437
  document.addEventListener("DOMContentLoaded", () => {
438
  setTimeout(typeEffect, 500);
439
  });
440
- // AJAX submission and progress polling
441
  const uploadForm = document.getElementById("uploadForm");
442
  uploadForm.addEventListener("submit", function(e) {
443
  e.preventDefault();
444
  const formData = new FormData(uploadForm);
 
445
  document.getElementById("progressOverlay").style.display = "flex";
 
446
  fetch("{{ url_for('analyze') }}", {
447
  method: "POST",
448
  body: formData
@@ -450,12 +404,14 @@ upload_template = '''
450
  .then(response => response.json())
451
  .then(data => {
452
  const jobId = data.job_id;
 
453
  const progressInterval = setInterval(() => {
454
  fetch("{{ url_for('progress') }}?job_id=" + jobId)
455
  .then(response => response.json())
456
  .then(progData => {
457
  const progress = progData.progress;
458
  document.getElementById("progressBar").style.width = progress + "%";
 
459
  if (progress < 10) {
460
  document.getElementById("progressText").textContent = "Starting up... 🛠️";
461
  } else if (progress < 30) {
@@ -471,10 +427,12 @@ upload_template = '''
471
  }
472
  if (progress >= 100) {
473
  clearInterval(progressInterval);
 
474
  fetch("{{ url_for('result') }}?job_id=" + jobId)
475
  .then(response => response.json())
476
  .then(resultData => {
477
  document.getElementById("progressOverlay").style.display = "none";
 
478
  document.getElementById("resultContainer").innerHTML = `
479
  <div class="content-wrapper">
480
  <img src="data:image/jpeg;base64,${resultData.image_data}" alt="Processed Image" class="result-img">
@@ -517,23 +475,28 @@ upload_template = '''
517
  '''
518
 
519
  #########################################
520
- # 5. Flask Routes
521
  #########################################
522
 
 
523
  @app.route('/')
524
  def landing():
525
  return render_template_string(landing_template)
526
 
 
527
  @app.route('/upload', methods=['GET'])
528
  def upload():
529
  object_type = request.args.get('object_type', '').lower()
530
  if object_type not in {"person", "car", "box"}:
531
  flash("Please select a valid object type.")
532
  return redirect(url_for('landing'))
 
533
  return render_template_string(upload_template, object_type=object_type)
534
 
 
535
  @app.route('/analyze', methods=['POST'])
536
  def analyze():
 
537
  if 'file' not in request.files:
538
  return jsonify({"error": "No file provided."}), 400
539
  file = request.files['file']
@@ -548,12 +511,17 @@ def analyze():
548
  except Exception as e:
549
  return jsonify({"error": "Error saving file."}), 500
550
 
 
551
  job_id = str(uuid.uuid4())
552
  jobs[job_id] = {"progress": 0, "result": None}
 
553
  thread = threading.Thread(target=process_image, args=(job_id, upload_path, object_type))
554
  thread.start()
 
 
555
  return jsonify({"job_id": job_id})
556
 
 
557
  @app.route('/progress', methods=['GET'])
558
  def progress():
559
  job_id = request.args.get('job_id', '')
@@ -561,18 +529,21 @@ def progress():
561
  return jsonify({"progress": 0})
562
  return jsonify({"progress": jobs[job_id].get("progress", 0)})
563
 
 
564
  @app.route('/result', methods=['GET'])
565
  def result():
566
  job_id = request.args.get('job_id', '')
567
  if job_id not in jobs or jobs[job_id].get("result") is None:
568
  return jsonify({"error": "Result not available."}), 404
569
  result = jobs[job_id]["result"]
 
570
  del jobs[job_id]
571
  return jsonify(result)
572
 
573
  #########################################
574
- # 6. Run the App
575
  #########################################
576
 
577
  if __name__ == '__main__':
 
578
  app.run(host="0.0.0.0", port=7860, threaded=True)
 
1
  import os
2
  import uuid
3
  import threading
4
+ import time
5
  import cv2
6
  import numpy as np
7
  import base64
 
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
 
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))
 
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
+ # IMPORTANT: Set this to your marker's real-world side length in centimeters.
130
+ marker_real_width_cm = 10.0
 
 
 
 
131
  try:
132
  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
133
+ aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_6X6_250)
134
+ # Use the newer DetectorParameters_create if available
135
  if hasattr(cv2.aruco, 'DetectorParameters_create'):
136
  aruco_params = cv2.aruco.DetectorParameters_create()
137
  else:
138
  aruco_params = cv2.aruco.DetectorParameters()
139
+ corners, ids, _ = cv2.aruco.detectMarkers(gray, aruco_dict, parameters=aruco_params)
140
+ if ids is not None and len(corners) > 0:
141
+ # Use the first detected marker as reference
142
+ marker_corners = corners[0].reshape((4, 2))
143
+ cv2.aruco.drawDetectedMarkers(image, corners, ids)
144
+ # Compute the distance between consecutive corners and take the average
145
+ side_lengths = []
146
+ for i in range(4):
147
+ pt1 = marker_corners[i]
148
+ pt2 = marker_corners[(i + 1) % 4]
149
+ side_lengths.append(np.linalg.norm(pt1 - pt2))
150
+ avg_side_length = np.mean(side_lengths)
151
+ conversion_factor = marker_real_width_cm / avg_side_length
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  else:
153
+ conversion_factor = None
154
  except Exception as e:
155
+ conversion_factor = None
 
 
156
 
 
157
  for pred in box_detections:
158
  x1, y1, x2, y2 = pred["box"]
159
  label = pred["class"]
160
  confidence = pred["confidence"]
161
  cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
162
+ if conversion_factor is not None:
163
  box_width_pixels = x2 - x1
164
  box_height_pixels = y2 - y1
165
+ box_width_cm = box_width_pixels * conversion_factor
166
+ box_height_cm = box_height_pixels * conversion_factor
167
+ detection_info.append({
168
+ "class": label,
169
+ "confidence": f"{confidence:.2f}",
170
+ "width_cm": f"{box_width_cm:.1f}",
171
+ "height_cm": f"{box_height_cm:.1f}"
172
+ })
173
  else:
174
+ detection_info.append({
175
+ "class": label,
176
+ "confidence": f"{confidence:.2f}",
177
+ "width_cm": "N/A",
178
+ "height_cm": "N/A"
179
+ })
 
 
 
 
180
  text = f"{label} ({confidence:.2f})"
181
  (text_width, text_height), baseline = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
182
+ cv2.rectangle(image, (x1, y1 - text_height - baseline - 5), (x1 + text_width, y1 - 5), (0, 255, 0), -1)
183
+ cv2.putText(image, text, (x1, y1 - 5 - baseline), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
 
 
 
184
  elif object_type in {"person", "car"}:
185
  if yolov5_model is None:
186
  jobs[job_id]['progress'] = 100
 
201
  cv2.rectangle(image, (xmin, ymin), (xmax, ymax), (255, 0, 0), 2)
202
  text = f"{label} ({conf:.2f})"
203
  (text_width, text_height), baseline = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
204
+ cv2.rectangle(image, (xmin, ymin - text_height - baseline - 5), (xmin + text_width, ymin - 5), (255, 0, 0), -1)
205
+ cv2.putText(image, text, (xmin, ymin - 5 - baseline), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
 
 
206
  detection_info.append({
207
  "class": label,
208
  "confidence": f"{conf:.2f}",
 
214
  jobs[job_id]['result'] = {"error": "Error during YOLOv5 inference."}
215
  return
216
 
217
+ # Build summary text (drawn on image)
218
  detection_counts = Counter(det["class"] for det in detection_info)
219
  if detection_counts:
220
  top_text = ", ".join(f"{cls}: {count}" for cls, count in detection_counts.items())
 
231
  jobs[job_id]['result'] = {"error": "Unexpected error during processing."}
232
 
233
  #########################################
234
+ # 3. HTML Templates
235
  #########################################
236
 
237
  landing_template = '''
 
327
  </div>
328
  </div>
329
  <div class="typing-effect" id="typing"></div>
330
+ <!-- The file upload form (submission handled via AJAX) -->
331
  <form id="uploadForm">
332
  <input type="file" name="file" accept="image/*" required>
333
  <!-- Hidden field to pass the selected object type -->
 
365
  </div>
366
  <div class="footer">&copy; 2024 MathLens AI Detection App. All rights reserved.</div>
367
  <script>
368
+ // Handle typing effect
369
  const textArray = ["MathLens", "Smart Counting with Maths"];
370
  let textIndex = 0, charIndex = 0, isDeleting = false;
371
  const typingElement = document.getElementById("typing");
 
389
  document.addEventListener("DOMContentLoaded", () => {
390
  setTimeout(typeEffect, 500);
391
  });
392
+ // AJAX-based submission and progress polling
393
  const uploadForm = document.getElementById("uploadForm");
394
  uploadForm.addEventListener("submit", function(e) {
395
  e.preventDefault();
396
  const formData = new FormData(uploadForm);
397
+ // Show progress overlay
398
  document.getElementById("progressOverlay").style.display = "flex";
399
+ // Start the analysis job
400
  fetch("{{ url_for('analyze') }}", {
401
  method: "POST",
402
  body: formData
 
404
  .then(response => response.json())
405
  .then(data => {
406
  const jobId = data.job_id;
407
+ // Start polling progress every 500ms
408
  const progressInterval = setInterval(() => {
409
  fetch("{{ url_for('progress') }}?job_id=" + jobId)
410
  .then(response => response.json())
411
  .then(progData => {
412
  const progress = progData.progress;
413
  document.getElementById("progressBar").style.width = progress + "%";
414
+ // Update dynamic phrases based on progress
415
  if (progress < 10) {
416
  document.getElementById("progressText").textContent = "Starting up... 🛠️";
417
  } else if (progress < 30) {
 
427
  }
428
  if (progress >= 100) {
429
  clearInterval(progressInterval);
430
+ // Once done, fetch the result and update the page
431
  fetch("{{ url_for('result') }}?job_id=" + jobId)
432
  .then(response => response.json())
433
  .then(resultData => {
434
  document.getElementById("progressOverlay").style.display = "none";
435
+ // Replace the result container with the new result
436
  document.getElementById("resultContainer").innerHTML = `
437
  <div class="content-wrapper">
438
  <img src="data:image/jpeg;base64,${resultData.image_data}" alt="Processed Image" class="result-img">
 
475
  '''
476
 
477
  #########################################
478
+ # 4. Flask Routes
479
  #########################################
480
 
481
+ # Landing page
482
  @app.route('/')
483
  def landing():
484
  return render_template_string(landing_template)
485
 
486
+ # Traditional upload page (in case you want to support non-AJAX)
487
  @app.route('/upload', methods=['GET'])
488
  def upload():
489
  object_type = request.args.get('object_type', '').lower()
490
  if object_type not in {"person", "car", "box"}:
491
  flash("Please select a valid object type.")
492
  return redirect(url_for('landing'))
493
+ # In this version the AJAX version is preferred.
494
  return render_template_string(upload_template, object_type=object_type)
495
 
496
+ # Endpoint to start analysis (called via AJAX)
497
  @app.route('/analyze', methods=['POST'])
498
  def analyze():
499
+ # Save uploaded file
500
  if 'file' not in request.files:
501
  return jsonify({"error": "No file provided."}), 400
502
  file = request.files['file']
 
511
  except Exception as e:
512
  return jsonify({"error": "Error saving file."}), 500
513
 
514
+ # Create a unique job id and initialize job progress
515
  job_id = str(uuid.uuid4())
516
  jobs[job_id] = {"progress": 0, "result": None}
517
+ # Start background thread to process image
518
  thread = threading.Thread(target=process_image, args=(job_id, upload_path, object_type))
519
  thread.start()
520
+ # Optionally remove the uploaded file after starting the thread
521
+ # (In a real system, you might want to keep it until processing is done.)
522
  return jsonify({"job_id": job_id})
523
 
524
+ # Endpoint for client to poll progress
525
  @app.route('/progress', methods=['GET'])
526
  def progress():
527
  job_id = request.args.get('job_id', '')
 
529
  return jsonify({"progress": 0})
530
  return jsonify({"progress": jobs[job_id].get("progress", 0)})
531
 
532
+ # Endpoint for client to fetch final result
533
  @app.route('/result', methods=['GET'])
534
  def result():
535
  job_id = request.args.get('job_id', '')
536
  if job_id not in jobs or jobs[job_id].get("result") is None:
537
  return jsonify({"error": "Result not available."}), 404
538
  result = jobs[job_id]["result"]
539
+ # Clean up job if desired
540
  del jobs[job_id]
541
  return jsonify(result)
542
 
543
  #########################################
544
+ # 5. Run the App
545
  #########################################
546
 
547
  if __name__ == '__main__':
548
+ # Run in threaded mode so background threads can work
549
  app.run(host="0.0.0.0", port=7860, threaded=True)