DariusGiannoli
fix: 11 bugs β confusion matrix, multi-class localization, dedup RCE/NMS, validation guards
a2b92f9 | import cv2 | |
| import numpy as np | |
| import time | |
| import joblib | |
| from pathlib import Path | |
| from src.config import MODEL_PATHS | |
| class ORBDetector: | |
| """ | |
| The Ancestor: ORB (Oriented FAST and Rotated BRIEF). | |
| ROBUST VERSION: Tuned for smooth/synthetic data. | |
| """ | |
| def __init__(self): | |
| print("ποΈ Initializing Classical ORB Detector (Aggressive Mode)...") | |
| # KEY CHANGE: fastThreshold=0 ensures we detect even faint corners | |
| # nfeatures=2000 allows us to capture more context | |
| self.orb = cv2.ORB_create( | |
| nfeatures=500, | |
| scaleFactor=1.2, | |
| nlevels=8, | |
| edgeThreshold=25, # Reduced from 31 to allow features near edges | |
| fastThreshold=10 # Reduced from 20 to 0 (Max Sensitivity) | |
| ) | |
| self.bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True) | |
| self.reference_descriptors = None | |
| self.model_path = MODEL_PATHS.get('orb_ref') | |
| self.load_reference() | |
| def load_reference(self): | |
| if self.model_path and Path(self.model_path).exists(): | |
| self.reference_descriptors = joblib.load(self.model_path) | |
| print(f"β Loaded ORB Reference from {self.model_path}") | |
| else: | |
| print(f"β οΈ Reference not found. Run training/train_orb.py") | |
| def train(self, images, labels): | |
| print(f"ποΈ Training ORB on {len(images)} images...") | |
| best_num_features = 0 | |
| best_descriptors = None | |
| bird_images = [img for img, lbl in zip(images, labels) if lbl == 'bird'] | |
| if not bird_images: | |
| print("β No bird images found.") | |
| return | |
| print(f" -> Scanning {len(bird_images)} bird images with High Sensitivity...") | |
| for i, img in enumerate(bird_images): | |
| if img is None: continue | |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) | |
| # CLAHE: Enhance contrast to help ORB see details in smooth images | |
| clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) | |
| gray = clahe.apply(gray) | |
| kp, des = self.orb.detectAndCompute(gray, None) | |
| count = 0 if des is None else len(des) | |
| # print(f" [Img {i}] Features: {count}") # Uncomment if you need to debug | |
| if des is not None and count > best_num_features: | |
| best_num_features = count | |
| best_descriptors = des | |
| if best_descriptors is None: | |
| raise RuntimeError( | |
| "ORB training failed: no features detected. " | |
| "Images may be too smooth, too small, or solid colors.") | |
| else: | |
| self.reference_descriptors = best_descriptors | |
| if self.model_path: | |
| joblib.dump(self.reference_descriptors, self.model_path) | |
| print(f"πΎ Success! Saved Reference with {best_num_features} features.") | |
| def predict(self, image): | |
| if self.reference_descriptors is None: | |
| return "Untrained", 0.0, 0.0 | |
| t0 = time.perf_counter() | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
| # Apply same contrast enhancement | |
| clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) | |
| gray = clahe.apply(gray) | |
| kp, des = self.orb.detectAndCompute(gray, None) | |
| if des is None: | |
| return "background", 0.0, (time.perf_counter() - t0) * 1000 | |
| matches = self.bf.match(self.reference_descriptors, des) | |
| matches = sorted(matches, key=lambda x: x.distance) | |
| # Relaxed matching for smooth images | |
| good_matches = [m for m in matches if m.distance < 70] | |
| score = len(good_matches) | |
| # Lower threshold for detection | |
| label = "bird" if score > 5 else "background" | |
| confidence = min(score / 10.0, 1.0) | |
| t1 = time.perf_counter() | |
| return label, confidence, (t1 - t0) * 1000 |