minh9972t12 commited on
Commit
c4d55e5
·
1 Parent(s): 11f621a

Update src/detection.py

Browse files
Files changed (1) hide show
  1. src/detection.py +303 -204
src/detection.py CHANGED
@@ -1,204 +1,303 @@
1
- import numpy as np
2
- from typing import List, Dict, Tuple
3
- import cv2
4
- from pathlib import Path
5
- import yaml
6
-
7
- class YOLOv11Detector:
8
- """YOLOv11 detector for car damage detection"""
9
-
10
- def __init__(self, config_path: str = "config.yaml"):
11
- """Initialize YOLOv11 detector with configuration"""
12
- with open(config_path, 'r') as f:
13
- self.config = yaml.safe_load(f)
14
-
15
- model_path = self.config['model']['path']
16
-
17
- # Check which model file exists
18
- if not Path(model_path).exists():
19
- # Try to find available model files
20
- model_dir = Path("models")
21
- if (model_dir / "best.pt").exists():
22
- model_path = str(model_dir / "best.pt")
23
- print(f"Using best.pt model from training")
24
- elif (model_dir / "last.pt").exists():
25
- model_path = str(model_dir / "last.pt")
26
- print(f"Using last.pt checkpoint model")
27
- elif (model_dir / "best.onnx").exists():
28
- model_path = str(model_dir / "best.onnx")
29
- print(f"Using best.onnx model")
30
- else:
31
- raise FileNotFoundError(f"No model files found in models/ directory!")
32
-
33
- self.model_path = model_path
34
- self.device = self.config['model']['device']
35
- self.confidence = self.config['model']['confidence']
36
- self.iou_threshold = self.config['model']['iou_threshold']
37
- self.classes = self.config['detection']['classes']
38
-
39
- # Load model based on format
40
- if model_path.endswith('.onnx'):
41
- self._load_onnx_model()
42
- else: # .pt format
43
- self._load_pytorch_model()
44
-
45
- def _load_pytorch_model(self):
46
- """Load PyTorch model using Ultralytics"""
47
- from ultralytics import YOLO
48
- self.model = YOLO(self.model_path)
49
-
50
- # Set model to appropriate device
51
- if self.device == 'cuda:0':
52
- self.model.to('cuda')
53
-
54
- print(f"Loaded PyTorch model: {self.model_path}")
55
-
56
- def _load_onnx_model(self):
57
- """Load ONNX model using OpenCV DNN"""
58
- self.net = cv2.dnn.readNet(self.model_path)
59
-
60
- # Set backend based on device
61
- if self.device == 'cuda:0':
62
- self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
63
- self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
64
- else:
65
- self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
66
- self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
67
-
68
- print(f"Loaded ONNX model: {self.model_path}")
69
-
70
- def detect(self, image: np.ndarray) -> Dict:
71
- """
72
- Perform detection on image
73
-
74
- Args:
75
- image: Input image as numpy array (BGR format)
76
-
77
- Returns:
78
- Dictionary containing detection results
79
- """
80
- if self.model_path.endswith('.onnx'):
81
- return self._detect_onnx(image)
82
- else:
83
- return self._detect_pytorch(image)
84
-
85
- def _detect_pytorch(self, image: np.ndarray) -> Dict:
86
- """Detection using PyTorch model"""
87
- # Run YOLO inference
88
- results = self.model(
89
- image,
90
- conf=self.confidence,
91
- iou=self.iou_threshold,
92
- device=self.device,
93
- verbose=False
94
- )
95
-
96
- # Parse results
97
- detections = {
98
- 'boxes': [],
99
- 'confidences': [],
100
- 'classes': [],
101
- 'class_ids': []
102
- }
103
-
104
- if len(results) > 0 and results[0].boxes is not None:
105
- boxes = results[0].boxes
106
-
107
- for box in boxes:
108
- # Get box coordinates (xyxy format)
109
- x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
110
-
111
- # Get confidence and class
112
- conf = float(box.conf[0].cpu().numpy())
113
- cls_id = int(box.cls[0].cpu().numpy())
114
-
115
- # Map class ID to class name
116
- if cls_id < len(self.classes):
117
- cls_name = self.classes[cls_id]
118
- else:
119
- cls_name = f"class_{cls_id}"
120
-
121
- detections['boxes'].append([int(x1), int(y1), int(x2), int(y2)])
122
- detections['confidences'].append(conf)
123
- detections['classes'].append(cls_name)
124
- detections['class_ids'].append(cls_id)
125
-
126
- return detections
127
-
128
- def _detect_onnx(self, image: np.ndarray) -> Dict:
129
- """Detection using ONNX model (compatible with original code)"""
130
- height, width = image.shape[:2]
131
-
132
- # Preprocess image for ONNX
133
- blob = cv2.dnn.blobFromImage(
134
- image, 1/255.0, (640, 640),
135
- swapRB=True, crop=False
136
- )
137
-
138
- self.net.setInput(blob)
139
- preds = self.net.forward()
140
- preds = preds.transpose((0, 2, 1))
141
-
142
- # Extract outputs
143
- detections = self._extract_onnx_output(
144
- preds=preds,
145
- image_shape=(height, width),
146
- input_shape=(640, 640)
147
- )
148
-
149
- return detections
150
-
151
- def _extract_onnx_output(self, preds: np.ndarray, image_shape: Tuple[int, int],
152
- input_shape: Tuple[int, int]) -> Dict:
153
- """Extract detection results from ONNX model output"""
154
- class_ids, confs, boxes = [], [], []
155
-
156
- image_height, image_width = image_shape
157
- input_height, input_width = input_shape
158
- x_factor = image_width / input_width
159
- y_factor = image_height / input_height
160
-
161
- rows = preds[0].shape[0]
162
- for i in range(rows):
163
- row = preds[0][i]
164
- conf = row[4]
165
-
166
- classes_score = row[4:]
167
- _, _, _, max_idx = cv2.minMaxLoc(classes_score)
168
- class_id = max_idx[1]
169
-
170
- if classes_score[class_id] > self.confidence:
171
- confs.append(float(conf))
172
- label = self.classes[int(class_id)] if int(class_id) < len(self.classes) else f"class_{class_id}"
173
- class_ids.append(label)
174
-
175
- # Extract boxes
176
- x, y, w, h = row[0].item(), row[1].item(), row[2].item(), row[3].item()
177
- left = int((x - 0.5 * w) * x_factor)
178
- top = int((y - 0.5 * h) * y_factor)
179
- width = int(w * x_factor)
180
- height = int(h * y_factor)
181
- box = [left, top, left + width, top + height]
182
- boxes.append(box)
183
-
184
- # Apply NMS
185
- if len(boxes) > 0:
186
- indices = cv2.dnn.NMSBoxes(
187
- [[b[0], b[1], b[2]-b[0], b[3]-b[1]] for b in boxes],
188
- confs, self.confidence, self.iou_threshold
189
- )
190
-
191
- if len(indices) > 0:
192
- indices = indices.flatten()
193
- return {
194
- 'boxes': [boxes[i] for i in indices],
195
- 'confidences': [confs[i] for i in indices],
196
- 'classes': [class_ids[i] for i in indices],
197
- 'class_ids': list(range(len(indices)))
198
- }
199
-
200
- return {'boxes': [], 'confidences': [], 'classes': [], 'class_ids': []}
201
-
202
- def detect_batch(self, images: List[np.ndarray]) -> List[Dict]:
203
- """Detect on multiple images"""
204
- return [self.detect(img) for img in images]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from typing import List, Dict, Tuple
3
+ import cv2
4
+ from pathlib import Path
5
+ import yaml
6
+ import torch
7
+ import random
8
+ import os
9
+
10
+ class YOLOv11Detector:
11
+ """YOLOv11 detector for car damage detection with deterministic inference"""
12
+
13
+ def __init__(self, config_path: str = "config.yaml", deterministic: bool = True):
14
+ """Initialize YOLOv11 detector with configuration"""
15
+
16
+ # Enable deterministic behavior
17
+ if deterministic:
18
+ self._set_deterministic()
19
+
20
+ with open(config_path, 'r') as f:
21
+ self.config = yaml.safe_load(f)
22
+
23
+ model_path = self.config['model']['path']
24
+
25
+ # Check which model file exists
26
+ if not Path(model_path).exists():
27
+ # Try to find available model files
28
+ model_dir = Path("models")
29
+ if (model_dir / "best.pt").exists():
30
+ model_path = str(model_dir / "best.pt")
31
+ print(f"Using best.pt model from training")
32
+ elif (model_dir / "last.pt").exists():
33
+ model_path = str(model_dir / "last.pt")
34
+ print(f"Using last.pt checkpoint model")
35
+ elif (model_dir / "best.onnx").exists():
36
+ model_path = str(model_dir / "best.onnx")
37
+ print(f"Using best.onnx model")
38
+ else:
39
+ raise FileNotFoundError(f"No model files found in models/ directory!")
40
+
41
+ self.model_path = model_path
42
+ self.device = self.config['model']['device']
43
+ self.confidence = self.config['model']['confidence']
44
+ self.iou_threshold = self.config['model']['iou_threshold']
45
+ self.classes = self.config['detection']['classes']
46
+ self.deterministic = deterministic
47
+
48
+ # Load model based on format
49
+ if model_path.endswith('.onnx'):
50
+ self._load_onnx_model()
51
+ else: # .pt format
52
+ self._load_pytorch_model()
53
+
54
+ def _set_deterministic(self, seed: int = 42):
55
+ """Set deterministic behavior for reproducible results"""
56
+ print(f"Setting deterministic mode with seed: {seed}")
57
+
58
+ # Set random seeds
59
+ random.seed(seed)
60
+ np.random.seed(seed)
61
+ torch.manual_seed(seed)
62
+
63
+ # Set CUDA deterministic settings
64
+ if torch.cuda.is_available():
65
+ torch.cuda.manual_seed(seed)
66
+ torch.cuda.manual_seed_all(seed)
67
+ torch.backends.cudnn.deterministic = True
68
+ torch.backends.cudnn.benchmark = False
69
+
70
+ # Additional CUDA deterministic settings
71
+ os.environ['CUDA_LAUNCH_BLOCKING'] = '1'
72
+ os.environ['CUBLAS_WORKSPACE_CONFIG'] = ':4096:8'
73
+
74
+ # Set PyTorch deterministic operations
75
+ torch.use_deterministic_algorithms(True, warn_only=True)
76
+
77
+ # Set OpenCV random seed for ONNX inference
78
+ cv2.setRNGSeed(seed)
79
+
80
+ def _load_pytorch_model(self):
81
+ """Load PyTorch model using Ultralytics"""
82
+ from ultralytics import YOLO
83
+
84
+ # Ensure deterministic loading
85
+ if self.deterministic:
86
+ torch.manual_seed(42)
87
+
88
+ self.model = YOLO(self.model_path)
89
+
90
+ # Set model to appropriate device
91
+ if self.device == 'cuda:0' and torch.cuda.is_available():
92
+ self.model.to('cuda')
93
+ else:
94
+ self.model.to('cpu')
95
+
96
+ # Set model to evaluation mode for consistent inference
97
+ if hasattr(self.model.model, 'eval'):
98
+ self.model.model.eval()
99
+
100
+ print(f"Loaded PyTorch model: {self.model_path}")
101
+ print(f"Model device: {next(self.model.model.parameters()).device}")
102
+
103
+ def _load_onnx_model(self):
104
+ """Load ONNX model using OpenCV DNN"""
105
+ self.net = cv2.dnn.readNet(self.model_path)
106
+
107
+ # Set backend based on device
108
+ if self.device == 'cuda:0':
109
+ self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
110
+ self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
111
+ else:
112
+ self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
113
+ self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
114
+
115
+ print(f"Loaded ONNX model: {self.model_path}")
116
+
117
+ def detect(self, image: np.ndarray) -> Dict:
118
+ """
119
+ Perform detection on image with deterministic behavior
120
+
121
+ Args:
122
+ image: Input image as numpy array (BGR format)
123
+
124
+ Returns:
125
+ Dictionary containing detection results
126
+ """
127
+ # Ensure deterministic preprocessing
128
+ if self.deterministic:
129
+ # Reset random seeds before each inference for consistency
130
+ if hasattr(torch, 'manual_seed'):
131
+ torch.manual_seed(42)
132
+ if torch.cuda.is_available():
133
+ torch.cuda.manual_seed(42)
134
+
135
+ if self.model_path.endswith('.onnx'):
136
+ return self._detect_onnx(image)
137
+ else:
138
+ return self._detect_pytorch(image)
139
+
140
+ def _detect_pytorch(self, image: np.ndarray) -> Dict:
141
+ """Detection using PyTorch model with deterministic settings"""
142
+
143
+ # Ensure model is in eval mode
144
+ if hasattr(self.model.model, 'eval'):
145
+ self.model.model.eval()
146
+
147
+ # Disable gradients for inference
148
+ with torch.no_grad():
149
+ # Run YOLO inference with deterministic settings
150
+ results = self.model(
151
+ image,
152
+ conf=self.confidence,
153
+ iou=self.iou_threshold,
154
+ device=self.device,
155
+ verbose=False,
156
+ # Add deterministic parameters
157
+ augment=False, # Disable test-time augmentation
158
+ half=False, # Disable FP16 for consistency
159
+ max_det=1000 # Set consistent max detections
160
+ )
161
+
162
+ # Parse results
163
+ detections = {
164
+ 'boxes': [],
165
+ 'confidences': [],
166
+ 'classes': [],
167
+ 'class_ids': []
168
+ }
169
+
170
+ if len(results) > 0 and results[0].boxes is not None:
171
+ boxes = results[0].boxes
172
+
173
+ for box in boxes:
174
+ # Get box coordinates (xyxy format)
175
+ x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
176
+
177
+ # Get confidence and class
178
+ conf = float(box.conf[0].cpu().numpy())
179
+ cls_id = int(box.cls[0].cpu().numpy())
180
+
181
+ # Map class ID to class name
182
+ if cls_id < len(self.classes):
183
+ cls_name = self.classes[cls_id]
184
+ else:
185
+ cls_name = f"class_{cls_id}"
186
+
187
+ detections['boxes'].append([int(x1), int(y1), int(x2), int(y2)])
188
+ detections['confidences'].append(conf)
189
+ detections['classes'].append(cls_name)
190
+ detections['class_ids'].append(cls_id)
191
+
192
+ # Sort results by confidence for consistency
193
+ if len(detections['boxes']) > 0:
194
+ # Create indices sorted by confidence (descending)
195
+ sorted_indices = sorted(range(len(detections['confidences'])),
196
+ key=lambda i: detections['confidences'][i], reverse=True)
197
+
198
+ # Reorder all detection lists
199
+ detections['boxes'] = [detections['boxes'][i] for i in sorted_indices]
200
+ detections['confidences'] = [detections['confidences'][i] for i in sorted_indices]
201
+ detections['classes'] = [detections['classes'][i] for i in sorted_indices]
202
+ detections['class_ids'] = [detections['class_ids'][i] for i in sorted_indices]
203
+
204
+ return detections
205
+
206
+ def _detect_onnx(self, image: np.ndarray) -> Dict:
207
+ """Detection using ONNX model with deterministic preprocessing"""
208
+ height, width = image.shape[:2]
209
+
210
+ # Deterministic preprocessing for ONNX
211
+ blob = cv2.dnn.blobFromImage(
212
+ image, 1/255.0, (640, 640),
213
+ swapRB=True, crop=False
214
+ )
215
+
216
+ self.net.setInput(blob)
217
+ preds = self.net.forward()
218
+ preds = preds.transpose((0, 2, 1))
219
+
220
+ # Extract outputs
221
+ detections = self._extract_onnx_output(
222
+ preds=preds,
223
+ image_shape=(height, width),
224
+ input_shape=(640, 640)
225
+ )
226
+
227
+ return detections
228
+
229
+ def _extract_onnx_output(self, preds: np.ndarray, image_shape: Tuple[int, int],
230
+ input_shape: Tuple[int, int]) -> Dict:
231
+ """Extract detection results from ONNX model output"""
232
+ class_ids, confs, boxes = [], [], []
233
+
234
+ image_height, image_width = image_shape
235
+ input_height, input_width = input_shape
236
+ x_factor = image_width / input_width
237
+ y_factor = image_height / input_height
238
+
239
+ rows = preds[0].shape[0]
240
+ for i in range(rows):
241
+ row = preds[0][i]
242
+ conf = row[4]
243
+
244
+ classes_score = row[4:]
245
+ _, _, _, max_idx = cv2.minMaxLoc(classes_score)
246
+ class_id = max_idx[1]
247
+
248
+ if classes_score[class_id] > self.confidence:
249
+ confs.append(float(conf))
250
+ label = self.classes[int(class_id)] if int(class_id) < len(self.classes) else f"class_{class_id}"
251
+ class_ids.append(label)
252
+
253
+ # Extract boxes
254
+ x, y, w, h = row[0].item(), row[1].item(), row[2].item(), row[3].item()
255
+ left = int((x - 0.5 * w) * x_factor)
256
+ top = int((y - 0.5 * h) * y_factor)
257
+ width = int(w * x_factor)
258
+ height = int(h * y_factor)
259
+ box = [left, top, left + width, top + height]
260
+ boxes.append(box)
261
+
262
+ # Apply NMS with deterministic ordering
263
+ if len(boxes) > 0:
264
+ # Convert to proper format for NMS
265
+ nms_boxes = [[b[0], b[1], b[2]-b[0], b[3]-b[1]] for b in boxes]
266
+
267
+ indices = cv2.dnn.NMSBoxes(
268
+ nms_boxes,
269
+ confs,
270
+ self.confidence,
271
+ self.iou_threshold
272
+ )
273
+
274
+ if len(indices) > 0:
275
+ indices = indices.flatten()
276
+
277
+ # Create detection results
278
+ final_boxes = [boxes[i] for i in indices]
279
+ final_confs = [confs[i] for i in indices]
280
+ final_classes = [class_ids[i] for i in indices]
281
+
282
+ # Sort by confidence for consistency
283
+ sorted_data = sorted(zip(final_boxes, final_confs, final_classes, range(len(indices))),
284
+ key=lambda x: x[1], reverse=True)
285
+
286
+ return {
287
+ 'boxes': [item[0] for item in sorted_data],
288
+ 'confidences': [item[1] for item in sorted_data],
289
+ 'classes': [item[2] for item in sorted_data],
290
+ 'class_ids': [item[3] for item in sorted_data]
291
+ }
292
+
293
+ return {'boxes': [], 'confidences': [], 'classes': [], 'class_ids': []}
294
+
295
+ def detect_batch(self, images: List[np.ndarray]) -> List[Dict]:
296
+ """Detect on multiple images with consistent ordering"""
297
+ return [self.detect(img) for img in images]
298
+
299
+ def reset_deterministic_state(self):
300
+ """Reset deterministic state - call this between different sessions"""
301
+ if self.deterministic:
302
+ self._set_deterministic(42)
303
+ print("Deterministic state reset")