Abs6187 commited on
Commit
82ae22b
·
verified ·
1 Parent(s): fdf832f

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +421 -417
main.py CHANGED
@@ -1,417 +1,421 @@
1
- """
2
- Vehicle Detection, Tracking, Counting, and Speed Estimation System
3
- ===================================================================
4
-
5
- A comprehensive computer vision pipeline for analyzing traffic videos,
6
- detecting vehicles, tracking their movement, counting them, and estimating
7
- their speeds using YOLO object detection and perspective transformation.
8
-
9
- Authors:
10
- - Abhay Gupta (0205CC221005)
11
- - Aditi Lakhera (0205CC221011)
12
- - Balraj Patel (0205CC221049)
13
- - Bhumika Patel (0205CC221050)
14
-
15
- Technical Approach:
16
- - YOLO for real-time object detection
17
- - ByteTrack for multi-object tracking
18
- - Perspective transformation for speed calculation
19
- - Line zones for vehicle counting
20
- """
21
-
22
- import sys
23
- import logging
24
- from pathlib import Path
25
- from typing import Dict, Optional, Callable
26
- from time import time
27
-
28
- import cv2
29
- import numpy as np
30
- import supervision as sv
31
- from ultralytics import YOLO
32
-
33
- from src import FrameAnnotator, VehicleSpeedEstimator, PerspectiveTransformer
34
- from src.exceptions import (
35
- VideoProcessingError,
36
- ModelLoadError,
37
- ConfigurationError
38
- )
39
- from config import VehicleDetectionConfig
40
-
41
- # Configure logging
42
- logging.basicConfig(
43
- level=logging.INFO,
44
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
45
- )
46
- logger = logging.getLogger(__name__)
47
-
48
-
49
- class VehicleDetectionPipeline:
50
- """
51
- Main pipeline for vehicle detection, tracking, counting, and speed estimation.
52
-
53
- This class orchestrates the entire processing workflow, from loading the model
54
- to processing each frame and generating the output video.
55
- """
56
-
57
- def __init__(self, config: VehicleDetectionConfig):
58
- """
59
- Initialize the detection pipeline.
60
-
61
- Args:
62
- config: Configuration object with all parameters
63
-
64
- Raises:
65
- ModelLoadError: If model cannot be loaded
66
- ConfigurationError: If configuration is invalid
67
- """
68
- self.config = config
69
- self.model = None
70
- self.tracker = None
71
- self.line_zone = None
72
- self.speed_estimator = None
73
- self.annotator = None
74
- self.video_info = None
75
-
76
- logger.info(f"Initializing pipeline with config: {config}")
77
- self._initialize_components()
78
-
79
- def _initialize_components(self) -> None:
80
- """Initialize all pipeline components."""
81
- try:
82
- # Load YOLO model
83
- logger.info(f"Loading YOLO model: {self.config.model_path}")
84
- self.model = YOLO(self.config.model_path)
85
- self.model.conf = self.config.confidence_threshold
86
- self.model.iou = self.config.iou_threshold
87
- logger.info("Model loaded successfully")
88
-
89
- except Exception as e:
90
- logger.error(f"Failed to load model: {e}")
91
- raise ModelLoadError(f"Could not load YOLO model from {self.config.model_path}: {e}")
92
-
93
- def _setup_video_components(self, video_path: str) -> None:
94
- """
95
- Set up video-specific components.
96
-
97
- Args:
98
- video_path: Path to input video
99
-
100
- Raises:
101
- VideoProcessingError: If video cannot be opened
102
- """
103
- try:
104
- # Get video information
105
- self.video_info = sv.VideoInfo.from_video_path(video_path)
106
- logger.info(f"Video info: {self.video_info.width}x{self.video_info.height} @ {self.video_info.fps}fps")
107
-
108
- # Initialize ByteTrack tracker
109
- self.tracker = sv.ByteTrack(
110
- frame_rate=self.video_info.fps,
111
- track_activation_threshold=self.config.confidence_threshold
112
- )
113
- logger.info("Tracker initialized")
114
-
115
- # Set up counting line zone
116
- line_start = sv.Point(
117
- x=self.config.line_offset,
118
- y=self.config.line_y
119
- )
120
- line_end = sv.Point(
121
- x=self.video_info.width - self.config.line_offset,
122
- y=self.config.line_y
123
- )
124
-
125
- self.line_zone = sv.LineZone(
126
- start=line_start,
127
- end=line_end,
128
- triggering_anchors=(sv.Position.BOTTOM_CENTER,)
129
- )
130
- logger.info(f"Line zone created at y={self.config.line_y}")
131
-
132
- # Initialize perspective transformer
133
- source_pts = np.array(self.config.source_points, dtype=np.float32)
134
- target_pts = np.array(self.config.target_points, dtype=np.float32)
135
-
136
- transformer = PerspectiveTransformer(
137
- source_points=source_pts,
138
- target_points=target_pts
139
- )
140
- logger.info("Perspective transformer initialized")
141
-
142
- # Initialize speed estimator
143
- self.speed_estimator = VehicleSpeedEstimator(
144
- fps=self.video_info.fps,
145
- transformer=transformer,
146
- history_duration=self.config.speed_history_seconds,
147
- speed_unit=self.config.speed_unit
148
- )
149
- logger.info("Speed estimator initialized")
150
-
151
- # Initialize frame annotator
152
- self.annotator = FrameAnnotator(
153
- video_resolution=(self.video_info.width, self.video_info.height),
154
- show_boxes=self.config.enable_boxes,
155
- show_labels=self.config.enable_labels,
156
- show_traces=self.config.enable_traces,
157
- show_line_zones=self.config.enable_line_zones,
158
- trace_length=self.config.trace_length,
159
- zone_polygon=source_pts
160
- )
161
- logger.info("Frame annotator initialized")
162
-
163
- except Exception as e:
164
- logger.error(f"Failed to setup video components: {e}")
165
- raise VideoProcessingError(f"Error setting up video processing: {e}")
166
-
167
- def _process_single_frame(self, frame: np.ndarray) -> tuple:
168
- """
169
- Process a single video frame.
170
-
171
- Args:
172
- frame: Input video frame
173
-
174
- Returns:
175
- Tuple of (annotated_frame, detections)
176
- """
177
- # Run YOLO detection
178
- results = self.model(frame, verbose=False)[0]
179
- detections = sv.Detections.from_ultralytics(results)
180
-
181
- # Update tracker
182
- detections = self.tracker.update_with_detections(detections)
183
-
184
- # Trigger line zone counting
185
- self.line_zone.trigger(detections)
186
-
187
- # Estimate speeds
188
- detections = self.speed_estimator.estimate(detections)
189
-
190
- # Generate labels
191
- labels = self._create_labels(detections)
192
-
193
- # Annotate frame
194
- annotated_frame = self.annotator.draw_annotations(
195
- frame=frame,
196
- detections=detections,
197
- labels=labels,
198
- line_zones=[self.line_zone]
199
- )
200
-
201
- return annotated_frame, detections
202
-
203
- def _create_labels(self, detections: sv.Detections) -> list:
204
- """
205
- Create display labels for detected vehicles.
206
-
207
- Args:
208
- detections: Detection results
209
-
210
- Returns:
211
- List of label strings
212
- """
213
- labels = []
214
-
215
- if not hasattr(detections, 'tracker_id') or detections.tracker_id is None:
216
- return labels
217
-
218
- for idx, tracker_id in enumerate(detections.tracker_id):
219
- # Get class name
220
- class_name = "Vehicle"
221
- if "class_name" in detections.data:
222
- class_name = detections.data["class_name"][idx]
223
-
224
- # Get speed
225
- speed_text = ""
226
- if "speed" in detections.data:
227
- speed = detections.data["speed"][idx]
228
- if speed > 0:
229
- speed_text = f" {speed:.0f}{self.config.speed_unit}"
230
-
231
- # Create label
232
- label = f"{class_name} #{tracker_id}{speed_text}"
233
- labels.append(label)
234
-
235
- return labels
236
-
237
- def process_video(
238
- self,
239
- progress_callback: Optional[Callable[[float], None]] = None
240
- ) -> Dict:
241
- """
242
- Process the entire video.
243
-
244
- Args:
245
- progress_callback: Optional callback for progress updates
246
-
247
- Returns:
248
- Dictionary with processing statistics
249
-
250
- Raises:
251
- VideoProcessingError: If video processing fails
252
- """
253
- start_time = time()
254
-
255
- try:
256
- # Validate input video
257
- if not Path(self.config.input_video).exists():
258
- raise VideoProcessingError(f"Input video not found: {self.config.input_video}")
259
-
260
- # Setup components
261
- self._setup_video_components(self.config.input_video)
262
-
263
- # Create output directory if needed
264
- output_path = Path(self.config.output_video)
265
- output_path.parent.mkdir(parents=True, exist_ok=True)
266
-
267
- # Initialize statistics
268
- frame_count = 0
269
- total_frames = self.video_info.total_frames or 0
270
- all_speeds = []
271
-
272
- # Setup display window if enabled
273
- if self.config.display_enabled:
274
- cv2.namedWindow(self.config.window_name, cv2.WINDOW_NORMAL)
275
- cv2.resizeWindow(
276
- self.config.window_name,
277
- self.video_info.width,
278
- self.video_info.height
279
- )
280
-
281
- # Process video
282
- logger.info("Starting video processing...")
283
- frame_generator = sv.get_video_frames_generator(self.config.input_video)
284
-
285
- with sv.VideoSink(self.config.output_video, self.video_info) as sink:
286
- for frame in frame_generator:
287
- try:
288
- # Process frame
289
- annotated_frame, detections = self._process_single_frame(frame)
290
-
291
- # Collect speed statistics
292
- if "speed" in detections.data:
293
- speeds = detections.data["speed"]
294
- all_speeds.extend([s for s in speeds if s > 0])
295
-
296
- # Write to output
297
- sink.write_frame(annotated_frame)
298
-
299
- # Display if enabled
300
- if self.config.display_enabled:
301
- cv2.imshow(self.config.window_name, annotated_frame)
302
-
303
- # Check for quit
304
- if cv2.waitKey(1) & 0xFF == ord('q'):
305
- logger.info("Processing interrupted by user")
306
- break
307
-
308
- # Check if window was closed
309
- if cv2.getWindowProperty(
310
- self.config.window_name,
311
- cv2.WND_PROP_VISIBLE
312
- ) < 1:
313
- logger.info("Window closed by user")
314
- break
315
-
316
- # Update progress
317
- frame_count += 1
318
- if progress_callback and total_frames > 0:
319
- progress = frame_count / total_frames
320
- progress_callback(progress)
321
-
322
- except Exception as e:
323
- logger.warning(f"Error processing frame {frame_count}: {e}")
324
- continue
325
-
326
- # Cleanup
327
- if self.config.display_enabled:
328
- cv2.destroyAllWindows()
329
-
330
- # Calculate statistics
331
- processing_time = time() - start_time
332
- stats = {
333
- 'total_count': self.line_zone.in_count + self.line_zone.out_count,
334
- 'in_count': self.line_zone.in_count,
335
- 'out_count': self.line_zone.out_count,
336
- 'avg_speed': np.mean(all_speeds) if all_speeds else 0.0,
337
- 'max_speed': np.max(all_speeds) if all_speeds else 0.0,
338
- 'min_speed': np.min(all_speeds) if all_speeds else 0.0,
339
- 'frames_processed': frame_count,
340
- 'processing_time': processing_time,
341
- 'fps': frame_count / processing_time if processing_time > 0 else 0
342
- }
343
-
344
- logger.info(f"Processing complete: {frame_count} frames in {processing_time:.2f}s")
345
- logger.info(f"Vehicles counted: {stats['total_count']} (In: {stats['in_count']}, Out: {stats['out_count']})")
346
-
347
- return stats
348
-
349
- except Exception as e:
350
- logger.error(f"Video processing failed: {e}", exc_info=True)
351
- raise VideoProcessingError(f"Failed to process video: {e}")
352
-
353
-
354
- def process_video(
355
- config: VehicleDetectionConfig,
356
- progress_callback: Optional[Callable[[float], None]] = None
357
- ) -> Dict:
358
- """
359
- Convenience function to process a video with given configuration.
360
-
361
- Args:
362
- config: Configuration object
363
- progress_callback: Optional progress callback
364
-
365
- Returns:
366
- Processing statistics dictionary
367
- """
368
- pipeline = VehicleDetectionPipeline(config)
369
- return pipeline.process_video(progress_callback)
370
-
371
-
372
- def main():
373
- """Main entry point for CLI usage."""
374
- try:
375
- logger.info("=" * 60)
376
- logger.info("Vehicle Speed Estimation & Counting System")
377
- logger.info("=" * 60)
378
-
379
- # Load configuration
380
- config = VehicleDetectionConfig()
381
- logger.info(f"Configuration: {config}")
382
-
383
- # Process video
384
- stats = process_video(config)
385
-
386
- # Display results
387
- print("\n" + "=" * 60)
388
- print("PROCESSING RESULTS")
389
- print("=" * 60)
390
- print(f"Output saved to: {config.output_video}")
391
- print(f"\nVehicle Count:")
392
- print(f" Total: {stats['total_count']}")
393
- print(f" In: {stats['in_count']}")
394
- print(f" Out: {stats['out_count']}")
395
- print(f"\nSpeed Statistics ({config.speed_unit}):")
396
- print(f" Average: {stats['avg_speed']:.1f}")
397
- print(f" Maximum: {stats['max_speed']:.1f}")
398
- print(f" Minimum: {stats['min_speed']:.1f}")
399
- print(f"\nProcessing Info:")
400
- print(f" Frames: {stats['frames_processed']}")
401
- print(f" Time: {stats['processing_time']:.2f}s")
402
- print(f" FPS: {stats['fps']:.1f}")
403
- print("=" * 60)
404
-
405
- return 0
406
-
407
- except KeyboardInterrupt:
408
- logger.info("Processing interrupted by user")
409
- return 1
410
- except Exception as e:
411
- logger.error(f"Fatal error: {e}", exc_info=True)
412
- print(f"\n❌ Error: {e}", file=sys.stderr)
413
- return 1
414
-
415
-
416
- if __name__ == "__main__":
417
- sys.exit(main())
 
 
 
 
 
1
+ """
2
+ Vehicle Detection, Tracking, Counting, and Speed Estimation System
3
+ ===================================================================
4
+
5
+ A comprehensive computer vision pipeline for analyzing traffic videos,
6
+ detecting vehicles, tracking their movement, counting them, and estimating
7
+ their speeds using YOLO object detection and perspective transformation.
8
+
9
+ Authors:
10
+ - Abhay Gupta (0205CC221005)
11
+ - Aditi Lakhera (0205CC221011)
12
+ - Balraj Patel (0205CC221049)
13
+ - Bhumika Patel (0205CC221050)
14
+
15
+ Technical Approach:
16
+ - YOLO for real-time object detection
17
+ - ByteTrack for multi-object tracking
18
+ - Perspective transformation for speed calculation
19
+ - Line zones for vehicle counting
20
+ """
21
+
22
+ import sys
23
+ import logging
24
+ from pathlib import Path
25
+ from typing import Dict, Optional, Callable
26
+ from time import time
27
+
28
+ import cv2
29
+ import numpy as np
30
+ import supervision as sv
31
+ from ultralytics import YOLO
32
+
33
+ from src import FrameAnnotator, VehicleSpeedEstimator, PerspectiveTransformer
34
+ from src.exceptions import (
35
+ VideoProcessingError,
36
+ ModelLoadError,
37
+ ConfigurationError
38
+ )
39
+ from config import VehicleDetectionConfig
40
+
41
+ # Configure logging
42
+ logging.basicConfig(
43
+ level=logging.INFO,
44
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
45
+ )
46
+ logger = logging.getLogger(__name__)
47
+
48
+
49
+ class VehicleDetectionPipeline:
50
+ """
51
+ Main pipeline for vehicle detection, tracking, counting, and speed estimation.
52
+
53
+ This class orchestrates the entire processing workflow, from loading the model
54
+ to processing each frame and generating the output video.
55
+ """
56
+
57
+ def __init__(self, config: VehicleDetectionConfig):
58
+ """
59
+ Initialize the detection pipeline.
60
+
61
+ Args:
62
+ config: Configuration object with all parameters
63
+
64
+ Raises:
65
+ ModelLoadError: If model cannot be loaded
66
+ ConfigurationError: If configuration is invalid
67
+ """
68
+ self.config = config
69
+ self.model = None
70
+ self.tracker = None
71
+ self.line_zone = None
72
+ self.speed_estimator = None
73
+ self.annotator = None
74
+ self.video_info = None
75
+
76
+ logger.info(f"Initializing pipeline with config: {config}")
77
+ self._initialize_components()
78
+
79
+ def _initialize_components(self) -> None:
80
+ """Initialize all pipeline components."""
81
+ try:
82
+ # Load YOLO model
83
+ logger.info(f"Loading YOLO model: {self.config.model_path}")
84
+ self.model = YOLO(self.config.model_path)
85
+ self.model.conf = self.config.confidence_threshold
86
+ self.model.iou = self.config.iou_threshold
87
+ logger.info("Model loaded successfully")
88
+
89
+ except Exception as e:
90
+ logger.error(f"Failed to load model: {e}")
91
+ raise ModelLoadError(f"Could not load YOLO model from {self.config.model_path}: {e}")
92
+
93
+ def _setup_video_components(self, video_path: str) -> None:
94
+ """
95
+ Set up video-specific components.
96
+
97
+ Args:
98
+ video_path: Path to input video
99
+
100
+ Raises:
101
+ VideoProcessingError: If video cannot be opened
102
+ """
103
+ try:
104
+ # Get video information
105
+ self.video_info = sv.VideoInfo.from_video_path(video_path)
106
+ logger.info(f"Video info: {self.video_info.width}x{self.video_info.height} @ {self.video_info.fps}fps")
107
+
108
+ # Initialize ByteTrack tracker
109
+ self.tracker = sv.ByteTrack(
110
+ frame_rate=self.video_info.fps,
111
+ track_activation_threshold=self.config.confidence_threshold
112
+ )
113
+ logger.info("Tracker initialized")
114
+
115
+ # Set up counting line zone
116
+ line_start = sv.Point(
117
+ x=self.config.line_offset,
118
+ y=self.config.line_y
119
+ )
120
+ line_end = sv.Point(
121
+ x=self.video_info.width - self.config.line_offset,
122
+ y=self.config.line_y
123
+ )
124
+
125
+ self.line_zone = sv.LineZone(
126
+ start=line_start,
127
+ end=line_end,
128
+ triggering_anchors=(sv.Position.BOTTOM_CENTER,)
129
+ )
130
+ logger.info(f"Line zone created at y={self.config.line_y}")
131
+
132
+ # Initialize perspective transformer
133
+ source_pts = np.array(self.config.source_points, dtype=np.float32)
134
+ target_pts = np.array(self.config.target_points, dtype=np.float32)
135
+
136
+ transformer = PerspectiveTransformer(
137
+ source_points=source_pts,
138
+ target_points=target_pts
139
+ )
140
+ logger.info("Perspective transformer initialized")
141
+
142
+ # Initialize speed estimator
143
+ self.speed_estimator = VehicleSpeedEstimator(
144
+ fps=self.video_info.fps,
145
+ transformer=transformer,
146
+ history_duration=self.config.speed_history_seconds,
147
+ speed_unit=self.config.speed_unit
148
+ )
149
+ logger.info("Speed estimator initialized")
150
+
151
+ # Initialize frame annotator
152
+ self.annotator = FrameAnnotator(
153
+ video_resolution=(self.video_info.width, self.video_info.height),
154
+ show_boxes=self.config.enable_boxes,
155
+ show_labels=self.config.enable_labels,
156
+ show_traces=self.config.enable_traces,
157
+ show_line_zones=self.config.enable_line_zones,
158
+ trace_length=self.config.trace_length,
159
+ zone_polygon=source_pts
160
+ )
161
+ logger.info("Frame annotator initialized")
162
+
163
+ except Exception as e:
164
+ logger.error(f"Failed to setup video components: {e}")
165
+ raise VideoProcessingError(f"Error setting up video processing: {e}")
166
+
167
+ def _process_single_frame(self, frame: np.ndarray) -> tuple:
168
+ """
169
+ Process a single video frame.
170
+
171
+ Args:
172
+ frame: Input video frame
173
+
174
+ Returns:
175
+ Tuple of (annotated_frame, detections)
176
+ """
177
+ # Run YOLO detection
178
+ results = self.model(frame, verbose=False)[0]
179
+ detections = sv.Detections.from_ultralytics(results)
180
+
181
+ # Update tracker
182
+ detections = self.tracker.update_with_detections(detections)
183
+
184
+ # Trigger line zone counting
185
+ self.line_zone.trigger(detections)
186
+
187
+ # Estimate speeds
188
+ detections = self.speed_estimator.estimate(detections)
189
+
190
+ # Generate labels
191
+ labels = self._create_labels(detections)
192
+
193
+ # Annotate frame
194
+ annotated_frame = self.annotator.draw_annotations(
195
+ frame=frame,
196
+ detections=detections,
197
+ labels=labels,
198
+ line_zones=[self.line_zone]
199
+ )
200
+
201
+ return annotated_frame, detections
202
+
203
+ def _create_labels(self, detections: sv.Detections) -> list:
204
+ """
205
+ Create display labels for detected vehicles.
206
+
207
+ Args:
208
+ detections: Detection results
209
+
210
+ Returns:
211
+ List of label strings
212
+ """
213
+ labels = []
214
+
215
+ if not hasattr(detections, 'tracker_id') or detections.tracker_id is None:
216
+ return labels
217
+
218
+ for idx, tracker_id in enumerate(detections.tracker_id):
219
+ # Get class name
220
+ class_name = "Vehicle"
221
+ if "class_name" in detections.data:
222
+ class_name = detections.data["class_name"][idx]
223
+
224
+ # Get speed
225
+ speed_text = ""
226
+ if "speed" in detections.data:
227
+ speed = detections.data["speed"][idx]
228
+ if speed > 0:
229
+ speed_text = f" {speed:.0f}{self.config.speed_unit}"
230
+
231
+ # Create label
232
+ label = f"{class_name} #{tracker_id}{speed_text}"
233
+ labels.append(label)
234
+
235
+ return labels
236
+
237
+ def process_video(
238
+ self,
239
+ progress_callback: Optional[Callable[[float], None]] = None
240
+ ) -> Dict:
241
+ """
242
+ Process the entire video.
243
+
244
+ Args:
245
+ progress_callback: Optional callback for progress updates
246
+
247
+ Returns:
248
+ Dictionary with processing statistics
249
+
250
+ Raises:
251
+ VideoProcessingError: If video processing fails
252
+ """
253
+ start_time = time()
254
+
255
+ try:
256
+ # Validate input video
257
+ if not Path(self.config.input_video).exists():
258
+ raise VideoProcessingError(f"Input video not found: {self.config.input_video}")
259
+
260
+ # Setup components
261
+ self._setup_video_components(self.config.input_video)
262
+
263
+ # Create output directory if needed
264
+ output_path = Path(self.config.output_video)
265
+ output_path.parent.mkdir(parents=True, exist_ok=True)
266
+
267
+ # Initialize statistics
268
+ frame_count = 0
269
+ total_frames = self.video_info.total_frames or 0
270
+ all_speeds = []
271
+
272
+ # Setup display window if enabled (disabled in headless environments like HF Spaces)
273
+ if self.config.display_enabled:
274
+ try:
275
+ cv2.namedWindow(self.config.window_name, cv2.WINDOW_NORMAL)
276
+ cv2.resizeWindow(
277
+ self.config.window_name,
278
+ self.video_info.width,
279
+ self.video_info.height
280
+ )
281
+ except Exception as e:
282
+ logger.warning(f"Could not create display window (headless environment?): {e}")
283
+ self.config.display_enabled = False
284
+
285
+ # Process video
286
+ logger.info("Starting video processing...")
287
+ frame_generator = sv.get_video_frames_generator(self.config.input_video)
288
+
289
+ with sv.VideoSink(self.config.output_video, self.video_info) as sink:
290
+ for frame in frame_generator:
291
+ try:
292
+ # Process frame
293
+ annotated_frame, detections = self._process_single_frame(frame)
294
+
295
+ # Collect speed statistics
296
+ if "speed" in detections.data:
297
+ speeds = detections.data["speed"]
298
+ all_speeds.extend([s for s in speeds if s > 0])
299
+
300
+ # Write to output
301
+ sink.write_frame(annotated_frame)
302
+
303
+ # Display if enabled
304
+ if self.config.display_enabled:
305
+ cv2.imshow(self.config.window_name, annotated_frame)
306
+
307
+ # Check for quit
308
+ if cv2.waitKey(1) & 0xFF == ord('q'):
309
+ logger.info("Processing interrupted by user")
310
+ break
311
+
312
+ # Check if window was closed
313
+ if cv2.getWindowProperty(
314
+ self.config.window_name,
315
+ cv2.WND_PROP_VISIBLE
316
+ ) < 1:
317
+ logger.info("Window closed by user")
318
+ break
319
+
320
+ # Update progress
321
+ frame_count += 1
322
+ if progress_callback and total_frames > 0:
323
+ progress = frame_count / total_frames
324
+ progress_callback(progress)
325
+
326
+ except Exception as e:
327
+ logger.warning(f"Error processing frame {frame_count}: {e}")
328
+ continue
329
+
330
+ # Cleanup
331
+ if self.config.display_enabled:
332
+ cv2.destroyAllWindows()
333
+
334
+ # Calculate statistics
335
+ processing_time = time() - start_time
336
+ stats = {
337
+ 'total_count': self.line_zone.in_count + self.line_zone.out_count,
338
+ 'in_count': self.line_zone.in_count,
339
+ 'out_count': self.line_zone.out_count,
340
+ 'avg_speed': np.mean(all_speeds) if all_speeds else 0.0,
341
+ 'max_speed': np.max(all_speeds) if all_speeds else 0.0,
342
+ 'min_speed': np.min(all_speeds) if all_speeds else 0.0,
343
+ 'frames_processed': frame_count,
344
+ 'processing_time': processing_time,
345
+ 'fps': frame_count / processing_time if processing_time > 0 else 0
346
+ }
347
+
348
+ logger.info(f"Processing complete: {frame_count} frames in {processing_time:.2f}s")
349
+ logger.info(f"Vehicles counted: {stats['total_count']} (In: {stats['in_count']}, Out: {stats['out_count']})")
350
+
351
+ return stats
352
+
353
+ except Exception as e:
354
+ logger.error(f"Video processing failed: {e}", exc_info=True)
355
+ raise VideoProcessingError(f"Failed to process video: {e}")
356
+
357
+
358
+ def process_video(
359
+ config: VehicleDetectionConfig,
360
+ progress_callback: Optional[Callable[[float], None]] = None
361
+ ) -> Dict:
362
+ """
363
+ Convenience function to process a video with given configuration.
364
+
365
+ Args:
366
+ config: Configuration object
367
+ progress_callback: Optional progress callback
368
+
369
+ Returns:
370
+ Processing statistics dictionary
371
+ """
372
+ pipeline = VehicleDetectionPipeline(config)
373
+ return pipeline.process_video(progress_callback)
374
+
375
+
376
+ def main():
377
+ """Main entry point for CLI usage."""
378
+ try:
379
+ logger.info("=" * 60)
380
+ logger.info("Vehicle Speed Estimation & Counting System")
381
+ logger.info("=" * 60)
382
+
383
+ # Load configuration
384
+ config = VehicleDetectionConfig()
385
+ logger.info(f"Configuration: {config}")
386
+
387
+ # Process video
388
+ stats = process_video(config)
389
+
390
+ # Display results
391
+ print("\n" + "=" * 60)
392
+ print("PROCESSING RESULTS")
393
+ print("=" * 60)
394
+ print(f"Output saved to: {config.output_video}")
395
+ print(f"\nVehicle Count:")
396
+ print(f" Total: {stats['total_count']}")
397
+ print(f" In: {stats['in_count']}")
398
+ print(f" Out: {stats['out_count']}")
399
+ print(f"\nSpeed Statistics ({config.speed_unit}):")
400
+ print(f" Average: {stats['avg_speed']:.1f}")
401
+ print(f" Maximum: {stats['max_speed']:.1f}")
402
+ print(f" Minimum: {stats['min_speed']:.1f}")
403
+ print(f"\nProcessing Info:")
404
+ print(f" Frames: {stats['frames_processed']}")
405
+ print(f" Time: {stats['processing_time']:.2f}s")
406
+ print(f" FPS: {stats['fps']:.1f}")
407
+ print("=" * 60)
408
+
409
+ return 0
410
+
411
+ except KeyboardInterrupt:
412
+ logger.info("Processing interrupted by user")
413
+ return 1
414
+ except Exception as e:
415
+ logger.error(f"Fatal error: {e}", exc_info=True)
416
+ print(f"\n❌ Error: {e}", file=sys.stderr)
417
+ return 1
418
+
419
+
420
+ if __name__ == "__main__":
421
+ sys.exit(main())