nagasurendra commited on
Commit
cc741a6
·
verified ·
1 Parent(s): f86c839

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +50 -58
app.py CHANGED
@@ -42,20 +42,6 @@ model = YOLO('./data/best.pt').to(device)
42
  if device == "cuda":
43
  model.half()
44
 
45
- def zip_directory(folder_path: str, zip_path: str) -> str:
46
- try:
47
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
48
- for root, _, files in os.walk(folder_path):
49
- for file in files:
50
- file_path = os.path.join(root, file)
51
- arcname = os.path.relpath(file_path, folder_path)
52
- zipf.write(file_path, os.path.join(os.path.basename(folder_path), arcname))
53
- return zip_path
54
- except Exception as e:
55
- logging.error(f"Failed to zip {folder_path}: {str(e)}")
56
- log_entries.append(f"Error: Failed to zip {folder_path}: {str(e)}")
57
- return ""
58
-
59
  def zip_all_outputs(report_path: str, video_path: str, chart_path: str, map_path: str) -> str:
60
  zip_path = os.path.join(OUTPUT_DIR, f"drone_analysis_outputs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip")
61
  try:
@@ -63,27 +49,26 @@ def zip_all_outputs(report_path: str, video_path: str, chart_path: str, map_path
63
  if os.path.exists(report_path):
64
  zipf.write(report_path, os.path.basename(report_path))
65
  if os.path.exists(video_path):
66
- zipf.write(video_path, os.path.basename(video_path))
67
  if os.path.exists(chart_path):
68
- zipf.write(chart_path, os.path.basename(chart_path))
69
  if os.path.exists(map_path):
70
- zipf.write(map_path, os.path.basename(map_path))
71
- for root, _, files in os.walk(CAPTURED_FRAMES_DIR):
72
- for file in files:
73
- file_path = os.path.join(root, file)
74
- zipf.write(file_path, os.path.join("captured_frames", file))
75
  for root, _, files in os.walk(FLIGHT_LOG_DIR):
76
  for file in files:
77
  file_path = os.path.join(root, file)
78
  zipf.write(file_path, os.path.join("flight_logs", file))
 
79
  return zip_path
80
  except Exception as e:
81
- logging.error(f"Failed to create output ZIP: {str(e)}")
82
- log_entries.append(f"Error: Failed to create output ZIP: {str(e)}")
83
  return ""
84
 
85
  def generate_map(gps_coords: List[List[float]], items: List[Dict[str, Any]]) -> str:
86
- map_path = os.path.join(OUTPUT_DIR, "map_temp.png")
87
  plt.figure(figsize=(4, 4))
88
  plt.scatter([x[1] for x in gps_coords], [x[0] for x in gps_coords], c='blue', label='GPS Points')
89
  plt.title("Issue Locations Map")
@@ -110,7 +95,6 @@ def write_geotag(image_path: str, gps_coord: List[float]) -> bool:
110
  piexif.insert(piexif.dump(exif_dict), image_path)
111
  return True
112
  except Exception as e:
113
- logging.error(f"Failed to geotag {image_path}: {str(e)}")
114
  log_entries.append(f"Error: Failed to geotag {image_path}: {str(e)}")
115
  return False
116
 
@@ -123,7 +107,6 @@ def write_flight_log(frame_count: int, gps_coord: List[float], timestamp: str) -
123
  writer.writerow([frame_count, timestamp, gps_coord[0], gps_coord[1], 5.0, 12, 60])
124
  return log_path
125
  except Exception as e:
126
- logging.error(f"Failed to write flight log {log_path}: {str(e)}")
127
  log_entries.append(f"Error: Failed to write flight log {log_path}: {str(e)}")
128
  return ""
129
 
@@ -156,7 +139,7 @@ def generate_line_chart() -> Optional[str]:
156
  plt.ylabel("Count")
157
  plt.grid(True)
158
  plt.tight_layout()
159
- chart_path = os.path.join(OUTPUT_DIR, "chart_temp.png")
160
  plt.savefig(chart_path)
161
  plt.close()
162
  return chart_path
@@ -179,7 +162,9 @@ def generate_report(
179
  inference_times: List[float],
180
  io_times: List[float]
181
  ) -> str:
 
182
  report_path = os.path.join(OUTPUT_DIR, f"drone_analysis_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md")
 
183
  report_content = [
184
  "# NHAI Drone Survey Analysis Report",
185
  "",
@@ -229,14 +214,24 @@ def generate_report(
229
  report_content.append(f" - {item['type']}: {item['count']} ({percentage:.2f}%)")
230
  report_content.extend([
231
  f"- Processing Time: {total_time:.2f} seconds",
232
- f"- Average Frame Time: {sum(frame_times)/len(frame_times):.2f} ms",
233
- f"- Average Resize Time: {sum(resize_times)/len(resize_times):.2f} ms",
234
- f"- Average Inference Time: {sum(inference_times)/len(inference_times):.2f} ms",
235
- f"- Average I/O Time: {sum(io_times)/len(io_times):.2f} ms",
236
  f"- Timestamp: {metrics.get('timestamp', 'N/A')}",
237
  "- Summary: Potholes and cracks detected in high-traffic segments.",
238
  "",
239
- "## 5. Geotagged Images",
 
 
 
 
 
 
 
 
 
 
240
  f"- Total Images: {len(detected_issues)}",
241
  f"- Storage: Data Lake `/project_xyz/images/{datetime.now().strftime('%Y-%m-%d')}`",
242
  "",
@@ -244,14 +239,14 @@ def generate_report(
244
  "|-------|------------|----------------|-----------|------------|------------|"
245
  ])
246
 
247
- for detection in all_detections:
248
  report_content.append(
249
- f"| {detection['frame']:06d} | {detection['label']} | ({detection['gps'][0]:.6f}, {detection['gps'][1]:.6f}) | {detection['timestamp']} | {detection['conf']:.2f} | {detection['path']} |"
250
  )
251
 
252
  report_content.extend([
253
  "",
254
- "## 6. Flight Logs",
255
  f"- Total Logs: {len(detected_issues)}",
256
  f"- Storage: Data Lake `/project_xyz/flight_logs/{datetime.now().strftime('%Y-%m-%d')}`",
257
  "",
@@ -259,27 +254,27 @@ def generate_report(
259
  "|-------|-----------|----------|-----------|-------------|------------|--------------|----------|"
260
  ])
261
 
262
- for detection in all_detections:
263
- log_path = os.path.join(FLIGHT_LOG_DIR, f"flight_log_{detection['frame']:06d}.csv")
264
  report_content.append(
265
  f"| {detection['frame']:06d} | {detection['timestamp']} | {detection['gps'][0]:.6f} | {detection['gps'][1]:.6f} | 5.0 | 12 | 60 | {log_path} |"
266
  )
267
 
268
  report_content.extend([
269
  "",
270
- "## 7. Processed Video",
271
- f"- Path: `/project_xyz/videos/processed_output_{datetime.now().strftime('%Y%m%d')}.mp4`",
272
  f"- Frames: {output_frames}",
273
  f"- FPS: {output_fps:.2f}",
274
  f"- Duration: {output_duration:.2f} seconds",
275
  "",
276
- "## 8. Visualizations",
277
- f"- Detection Trend Chart: `/project_xyz/charts/chart_temp_{datetime.now().strftime('%Y%m%d')}.png`",
278
- f"- Issue Locations Map: `/project_xyz/maps/map_temp_{datetime.now().strftime('%Y%m%d')}.png`",
279
  "",
280
- "## 9. Processing Timestamps",
281
  f"- Total Processing Time: {total_time:.2f} seconds",
282
- f"- Log Entries (Last 10):"
283
  ])
284
 
285
  for entry in log_entries[-10:]:
@@ -287,16 +282,16 @@ def generate_report(
287
 
288
  report_content.extend([
289
  "",
290
- "## 10. Stakeholder Validation",
291
  "- AE/IE Comments: [Pending]",
292
  "- PD/RO Comments: [Pending]",
293
  "",
294
- "## 11. Recommendations",
295
  "- Repair potholes in high-traffic segments.",
296
  "- Seal cracks to prevent degradation.",
297
  "- Schedule follow-up survey.",
298
  "",
299
- "## 12. Data Lake References",
300
  f"- Images: `/project_xyz/images/{datetime.now().strftime('%Y-%m-%d')}`",
301
  f"- Flight Logs: `/project_xyz/flight_logs/{datetime.now().strftime('%Y-%m-%d')}`",
302
  f"- Video: `/project_xyz/videos/processed_output_{datetime.now().strftime('%Y%m%d')}.mp4`",
@@ -306,10 +301,9 @@ def generate_report(
306
  try:
307
  with open(report_path, 'w') as f:
308
  f.write("\n".join(report_content))
309
- logging.info(f"Report saved: {report_path}")
310
  return report_path
311
  except Exception as e:
312
- logging.error(f"Failed to save report: {str(e)}")
313
  log_entries.append(f"Error: Failed to save report: {str(e)}")
314
  return ""
315
 
@@ -324,14 +318,13 @@ def process_video(video, resize_width=4000, resize_height=3000, frame_skip=5):
324
 
325
  if video is None:
326
  log_entries.append("Error: No video uploaded")
327
- logging.error("No video uploaded")
328
  return None, json.dumps({"error": "No video uploaded"}, indent=2), "\n".join(log_entries), [], None, None, None
329
 
 
330
  start_time = time.time()
331
  cap = cv2.VideoCapture(video)
332
  if not cap.isOpened():
333
  log_entries.append("Error: Could not open video file")
334
- logging.error("Could not open video file")
335
  return None, json.dumps({"error": "Could not open video file"}, indent=2), "\n".join(log_entries), [], None, None, None
336
 
337
  frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
@@ -339,13 +332,13 @@ def process_video(video, resize_width=4000, resize_height=3000, frame_skip=5):
339
  input_resolution = frame_width * frame_height
340
  fps = cap.get(cv2.CAP_PROP_FPS)
341
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
 
342
 
343
  out_width, out_height = resize_width, resize_height
344
  output_path = os.path.join(OUTPUT_DIR, "processed_output.mp4")
345
  out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (out_width, out_height))
346
  if not out.isOpened():
347
  log_entries.append("Error: Failed to initialize mp4v codec")
348
- logging.error("Failed to initialize mp4v codec")
349
  cap.release()
350
  return None, json.dumps({"error": "mp4v codec failed"}, indent=2), "\n".join(log_entries), [], None, None, None
351
 
@@ -404,7 +397,6 @@ def process_video(video, resize_width=4000, resize_height=3000, frame_skip=5):
404
  "path": os.path.join(CAPTURED_FRAMES_DIR, f"detected_{frame_count:06d}.jpg")
405
  })
406
  log_entries.append(f"Frame {frame_count} at {timestamp_str}: Detected {label} with confidence {conf:.2f}")
407
- logging.info(f"Frame {frame_count} at {timestamp_str}: Detected {label} with confidence {conf:.2f}")
408
 
409
  if frame_detections:
410
  detection_frame_count += 1
@@ -413,15 +405,14 @@ def process_video(video, resize_width=4000, resize_height=3000, frame_skip=5):
413
  if cv2.imwrite(captured_frame_path, annotated_frame):
414
  if write_geotag(captured_frame_path, gps_coord):
415
  detected_issues.append(captured_frame_path)
416
- if len(detected_issues) > 100:
417
  detected_issues.pop(0)
418
  else:
419
  log_entries.append(f"Frame {frame_count}: Geotagging failed")
420
  else:
421
  log_entries.append(f"Error: Failed to save {captured_frame_path}")
422
- logging.error(f"Failed to save {captured_frame_path}")
423
 
424
- flight_log_path = write_flight_log(frame_count, gps_coord, timestamp_str)
425
  io_times.append((time.time() - io_start) * 1000)
426
 
427
  out.write(annotated_frame)
@@ -441,7 +432,6 @@ def process_video(video, resize_width=4000, resize_height=3000, frame_skip=5):
441
 
442
  if time.time() - start_time > 600:
443
  log_entries.append("Error: Processing timeout after 600 seconds")
444
- logging.error("Processing timeout after 600 seconds")
445
  break
446
 
447
  while output_frame_count < total_frames and last_annotated_frame is not None:
@@ -461,8 +451,8 @@ def process_video(video, resize_width=4000, resize_height=3000, frame_skip=5):
461
 
462
  total_time = time.time() - start_time
463
  log_entries.append(f"Output video: {output_frames} frames, {output_fps:.2f} FPS, {output_duration:.2f} seconds")
464
- logging.info(f"Output video: {output_frames} frames, {output_fps:.2f} FPS, {output_duration:.2f} seconds")
465
 
 
466
  chart_path = generate_line_chart()
467
  map_path = generate_map(gps_coordinates[-5:], all_detections)
468
 
@@ -485,8 +475,10 @@ def process_video(video, resize_width=4000, resize_height=3000, frame_skip=5):
485
  io_times
486
  )
487
 
 
488
  output_zip_path = zip_all_outputs(report_path, output_path, chart_path, map_path)
489
 
 
490
  return (
491
  output_path,
492
  json.dumps(last_metrics, indent=2),
 
42
  if device == "cuda":
43
  model.half()
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  def zip_all_outputs(report_path: str, video_path: str, chart_path: str, map_path: str) -> str:
46
  zip_path = os.path.join(OUTPUT_DIR, f"drone_analysis_outputs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip")
47
  try:
 
49
  if os.path.exists(report_path):
50
  zipf.write(report_path, os.path.basename(report_path))
51
  if os.path.exists(video_path):
52
+ zipf.write(video_path, os.path.join("outputs", os.path.basename(video_path)))
53
  if os.path.exists(chart_path):
54
+ zipf.write(chart_path, os.path.join("outputs", os.path.basename(chart_path)))
55
  if os.path.exists(map_path):
56
+ zipf.write(map_path, os.path.join("outputs", os.path.basename(map_path)))
57
+ for file in detected_issues:
58
+ if os.path.exists(file):
59
+ zipf.write(file, os.path.join("captured_frames", os.path.basename(file)))
 
60
  for root, _, files in os.walk(FLIGHT_LOG_DIR):
61
  for file in files:
62
  file_path = os.path.join(root, file)
63
  zipf.write(file_path, os.path.join("flight_logs", file))
64
+ log_entries.append(f"Created ZIP: {zip_path}")
65
  return zip_path
66
  except Exception as e:
67
+ log_entries.append(f"Error: Failed to create ZIP: {str(e)}")
 
68
  return ""
69
 
70
  def generate_map(gps_coords: List[List[float]], items: List[Dict[str, Any]]) -> str:
71
+ map_path = os.path.join(OUTPUT_DIR, f"map_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png")
72
  plt.figure(figsize=(4, 4))
73
  plt.scatter([x[1] for x in gps_coords], [x[0] for x in gps_coords], c='blue', label='GPS Points')
74
  plt.title("Issue Locations Map")
 
95
  piexif.insert(piexif.dump(exif_dict), image_path)
96
  return True
97
  except Exception as e:
 
98
  log_entries.append(f"Error: Failed to geotag {image_path}: {str(e)}")
99
  return False
100
 
 
107
  writer.writerow([frame_count, timestamp, gps_coord[0], gps_coord[1], 5.0, 12, 60])
108
  return log_path
109
  except Exception as e:
 
110
  log_entries.append(f"Error: Failed to write flight log {log_path}: {str(e)}")
111
  return ""
112
 
 
139
  plt.ylabel("Count")
140
  plt.grid(True)
141
  plt.tight_layout()
142
+ chart_path = os.path.join(OUTPUT_DIR, f"chart_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png")
143
  plt.savefig(chart_path)
144
  plt.close()
145
  return chart_path
 
162
  inference_times: List[float],
163
  io_times: List[float]
164
  ) -> str:
165
+ log_entries.append("Generating report...")
166
  report_path = os.path.join(OUTPUT_DIR, f"drone_analysis_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md")
167
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
168
  report_content = [
169
  "# NHAI Drone Survey Analysis Report",
170
  "",
 
214
  report_content.append(f" - {item['type']}: {item['count']} ({percentage:.2f}%)")
215
  report_content.extend([
216
  f"- Processing Time: {total_time:.2f} seconds",
217
+ f"- Average Frame Time: {sum(frame_times)/len(frame_times):.2f} ms" if frame_times else "- Average Frame Time: N/A",
218
+ f"- Average Resize Time: {sum(resize_times)/len(resize_times):.2f} ms" if resize_times else "- Average Resize Time: N/A",
219
+ f"- Average Inference Time: {sum(inference_times)/len(inference_times):.2f} ms" if inference_times else "- Average Inference Time: N/A",
220
+ f"- Average I/O Time: {sum(io_times)/len(io_times):.2f} ms" if io_times else "- Average I/O Time: N/A",
221
  f"- Timestamp: {metrics.get('timestamp', 'N/A')}",
222
  "- Summary: Potholes and cracks detected in high-traffic segments.",
223
  "",
224
+ "## 5. Output File Structure",
225
+ "- ZIP file contains:",
226
+ " - `drone_analysis_report_<timestamp>.md`: This report",
227
+ " - `outputs/processed_output.mp4`: Processed video with annotations",
228
+ " - `outputs/chart_<timestamp>.png`: Detection trend chart",
229
+ " - `outputs/map_<timestamp>.png`: Issue locations map",
230
+ " - `captured_frames/detected_<frame>.jpg`: Geotagged images for detected issues",
231
+ " - `flight_logs/flight_log_<frame>.csv`: Flight logs matching image frames",
232
+ "- Note: Images and logs share frame numbers (e.g., `detected_000001.jpg` corresponds to `flight_log_000001.csv`).",
233
+ "",
234
+ "## 6. Geotagged Images",
235
  f"- Total Images: {len(detected_issues)}",
236
  f"- Storage: Data Lake `/project_xyz/images/{datetime.now().strftime('%Y-%m-%d')}`",
237
  "",
 
239
  "|-------|------------|----------------|-----------|------------|------------|"
240
  ])
241
 
242
+ for detection in all_detections[:100]:
243
  report_content.append(
244
+ f"| {detection['frame']:06d} | {detection['label']} | ({detection['gps'][0]:.6f}, {detection['gps'][1]:.6f}) | {detection['timestamp']} | {detection['conf']:.2f} | captured_frames/{os.path.basename(detection['path'])} |"
245
  )
246
 
247
  report_content.extend([
248
  "",
249
+ "## 7. Flight Logs",
250
  f"- Total Logs: {len(detected_issues)}",
251
  f"- Storage: Data Lake `/project_xyz/flight_logs/{datetime.now().strftime('%Y-%m-%d')}`",
252
  "",
 
254
  "|-------|-----------|----------|-----------|-------------|------------|--------------|----------|"
255
  ])
256
 
257
+ for detection in all_detections[:100]:
258
+ log_path = f"flight_logs/flight_log_{detection['frame']:06d}.csv"
259
  report_content.append(
260
  f"| {detection['frame']:06d} | {detection['timestamp']} | {detection['gps'][0]:.6f} | {detection['gps'][1]:.6f} | 5.0 | 12 | 60 | {log_path} |"
261
  )
262
 
263
  report_content.extend([
264
  "",
265
+ "## 8. Processed Video",
266
+ f"- Path: outputs/processed_output.mp4",
267
  f"- Frames: {output_frames}",
268
  f"- FPS: {output_fps:.2f}",
269
  f"- Duration: {output_duration:.2f} seconds",
270
  "",
271
+ "## 9. Visualizations",
272
+ f"- Detection Trend Chart: outputs/chart_{timestamp}.png",
273
+ f"- Issue Locations Map: outputs/map_{timestamp}.png",
274
  "",
275
+ "## 10. Processing Timestamps",
276
  f"- Total Processing Time: {total_time:.2f} seconds",
277
+ "- Log Entries (Last 10):"
278
  ])
279
 
280
  for entry in log_entries[-10:]:
 
282
 
283
  report_content.extend([
284
  "",
285
+ "## 11. Stakeholder Validation",
286
  "- AE/IE Comments: [Pending]",
287
  "- PD/RO Comments: [Pending]",
288
  "",
289
+ "## 12. Recommendations",
290
  "- Repair potholes in high-traffic segments.",
291
  "- Seal cracks to prevent degradation.",
292
  "- Schedule follow-up survey.",
293
  "",
294
+ "## 13. Data Lake References",
295
  f"- Images: `/project_xyz/images/{datetime.now().strftime('%Y-%m-%d')}`",
296
  f"- Flight Logs: `/project_xyz/flight_logs/{datetime.now().strftime('%Y-%m-%d')}`",
297
  f"- Video: `/project_xyz/videos/processed_output_{datetime.now().strftime('%Y%m%d')}.mp4`",
 
301
  try:
302
  with open(report_path, 'w') as f:
303
  f.write("\n".join(report_content))
304
+ log_entries.append(f"Report saved: {report_path}")
305
  return report_path
306
  except Exception as e:
 
307
  log_entries.append(f"Error: Failed to save report: {str(e)}")
308
  return ""
309
 
 
318
 
319
  if video is None:
320
  log_entries.append("Error: No video uploaded")
 
321
  return None, json.dumps({"error": "No video uploaded"}, indent=2), "\n".join(log_entries), [], None, None, None
322
 
323
+ log_entries.append("Starting video processing...")
324
  start_time = time.time()
325
  cap = cv2.VideoCapture(video)
326
  if not cap.isOpened():
327
  log_entries.append("Error: Could not open video file")
 
328
  return None, json.dumps({"error": "Could not open video file"}, indent=2), "\n".join(log_entries), [], None, None, None
329
 
330
  frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
 
332
  input_resolution = frame_width * frame_height
333
  fps = cap.get(cv2.CAP_PROP_FPS)
334
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
335
+ log_entries.append(f"Input video: {frame_width}x{frame_height}, {fps} FPS, {total_frames} frames")
336
 
337
  out_width, out_height = resize_width, resize_height
338
  output_path = os.path.join(OUTPUT_DIR, "processed_output.mp4")
339
  out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (out_width, out_height))
340
  if not out.isOpened():
341
  log_entries.append("Error: Failed to initialize mp4v codec")
 
342
  cap.release()
343
  return None, json.dumps({"error": "mp4v codec failed"}, indent=2), "\n".join(log_entries), [], None, None, None
344
 
 
397
  "path": os.path.join(CAPTURED_FRAMES_DIR, f"detected_{frame_count:06d}.jpg")
398
  })
399
  log_entries.append(f"Frame {frame_count} at {timestamp_str}: Detected {label} with confidence {conf:.2f}")
 
400
 
401
  if frame_detections:
402
  detection_frame_count += 1
 
405
  if cv2.imwrite(captured_frame_path, annotated_frame):
406
  if write_geotag(captured_frame_path, gps_coord):
407
  detected_issues.append(captured_frame_path)
408
+ if len(detected_issues) > 1000: # Limit to 1000 images
409
  detected_issues.pop(0)
410
  else:
411
  log_entries.append(f"Frame {frame_count}: Geotagging failed")
412
  else:
413
  log_entries.append(f"Error: Failed to save {captured_frame_path}")
414
+ flight_log_path = write_flight_log(frame_count, gps_coord, timestamp_str)
415
 
 
416
  io_times.append((time.time() - io_start) * 1000)
417
 
418
  out.write(annotated_frame)
 
432
 
433
  if time.time() - start_time > 600:
434
  log_entries.append("Error: Processing timeout after 600 seconds")
 
435
  break
436
 
437
  while output_frame_count < total_frames and last_annotated_frame is not None:
 
451
 
452
  total_time = time.time() - start_time
453
  log_entries.append(f"Output video: {output_frames} frames, {output_fps:.2f} FPS, {output_duration:.2f} seconds")
 
454
 
455
+ log_entries.append("Generating chart and map...")
456
  chart_path = generate_line_chart()
457
  map_path = generate_map(gps_coordinates[-5:], all_detections)
458
 
 
475
  io_times
476
  )
477
 
478
+ log_entries.append("Creating output ZIP...")
479
  output_zip_path = zip_all_outputs(report_path, output_path, chart_path, map_path)
480
 
481
+ log_entries.append(f"Processing completed in {total_time:.2f} seconds")
482
  return (
483
  output_path,
484
  json.dumps(last_metrics, indent=2),