diff --git a/croppa/main.py b/croppa/main.py index d0090bc..37239c9 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -49,7 +49,10 @@ class FeatureTracker: if self.detector_type == 'SIFT': self.detector = cv2.SIFT_create(nfeatures=self.max_features) elif self.detector_type == 'SURF': - self.detector = cv2.xfeatures2d.SURF_create(hessianThreshold=400) + # SURF requires opencv-contrib-python, fallback to SIFT + print("Warning: SURF requires opencv-contrib-python package. Using SIFT instead.") + self.detector = cv2.SIFT_create(nfeatures=self.max_features) + self.detector_type = 'SIFT' elif self.detector_type == 'ORB': self.detector = cv2.ORB_create(nfeatures=self.max_features) else: @@ -107,6 +110,53 @@ class FeatureTracker: print(f"Error extracting features from frame {frame_number}: {e}") return False + def extract_features_from_region(self, frame: np.ndarray, frame_number: int, coord_mapper=None) -> bool: + """Extract features from a frame and ADD them to existing features""" + try: + # Convert to grayscale if needed + if len(frame.shape) == 3: + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + else: + gray = frame + + # Extract keypoints and descriptors + keypoints, descriptors = self.detector.detectAndCompute(gray, None) + + if keypoints is None or descriptors is None: + return False + + # Map coordinates back to original frame space if mapper provided + if coord_mapper: + mapped_positions = [] + for kp in keypoints: + orig_x, orig_y = coord_mapper(kp.pt[0], kp.pt[1]) + mapped_positions.append((int(orig_x), int(orig_y))) + else: + mapped_positions = [(int(kp.pt[0]), int(kp.pt[1])) for kp in keypoints] + + # Add to existing features or create new entry + if frame_number in self.features: + # Append to existing features + existing_features = self.features[frame_number] + existing_features['keypoints'] = np.concatenate([existing_features['keypoints'], keypoints]) + existing_features['descriptors'] = np.concatenate([existing_features['descriptors'], descriptors]) + existing_features['positions'].extend(mapped_positions) + print(f"Added {len(keypoints)} features to frame {frame_number} (total: {len(existing_features['positions'])})") + else: + # Create new features entry + self.features[frame_number] = { + 'keypoints': keypoints, + 'descriptors': descriptors, + 'positions': mapped_positions + } + print(f"Extracted {len(keypoints)} features from frame {frame_number}") + + return True + + except Exception as e: + print(f"Error extracting features from frame {frame_number}: {e}") + return False + def get_tracking_position(self, frame_number: int) -> Optional[Tuple[float, float]]: @@ -1248,8 +1298,11 @@ class VideoEditor: self.feature_tracker.auto_tracking and self.current_display_frame is not None): + print(f"DEBUG: Auto-tracking conditions met for frame {self.current_frame}") + print(f"DEBUG: tracking_enabled={self.feature_tracker.tracking_enabled}, auto_tracking={self.feature_tracker.auto_tracking}") # Only extract if we don't already have features for this frame if self.current_frame not in self.feature_tracker.features: + print(f"DEBUG: Extracting features for frame {self.current_frame}") # Extract features from the transformed frame (what user sees) # This handles all transformations (crop, zoom, rotation) correctly display_frame = self.apply_crop_zoom_and_rotation(self.current_display_frame) @@ -1271,6 +1324,8 @@ class VideoEditor: return self._map_screen_to_rotated(screen_x, screen_y) self.feature_tracker.extract_features(display_frame, self.current_frame, coord_mapper) + else: + print(f"DEBUG: Frame {self.current_frame} already has features, skipping") def jump_to_previous_marker(self): """Jump to the previous tracking marker (frame with tracking points).""" @@ -1671,10 +1726,11 @@ class VideoEditor: rot_y = orig_y + py return (int(rot_x), int(rot_y)) - success = self.feature_tracker.extract_features(region_frame, self.current_frame, coord_mapper) + # Extract features and add them to existing features + success = self.feature_tracker.extract_features_from_region(region_frame, self.current_frame, coord_mapper) if success: count = self.feature_tracker.get_feature_count(self.current_frame) - self.show_feedback_message(f"Extracted {count} features from selected region") + self.show_feedback_message(f"Added features from selected region (total: {count})") else: self.show_feedback_message("Failed to extract features from region") else: @@ -3420,7 +3476,7 @@ class VideoEditor: print(" Shift+T: Extract features from current frame") print(" g: Toggle auto feature extraction") print(" G: Clear all feature data") - print(" H: Switch detector (SIFT/SURF/ORB)") + print(" H: Switch detector (SIFT/ORB)") print(" Shift+Right-click+drag: Extract features from selected region") print(" Ctrl+Right-click+drag: Delete features from selected region") if len(self.video_files) > 1: @@ -3727,6 +3783,7 @@ class VideoEditor: elif key == ord("g"): # Toggle auto tracking self.feature_tracker.auto_tracking = not self.feature_tracker.auto_tracking + print(f"DEBUG: Auto tracking toggled to {self.feature_tracker.auto_tracking}") self.show_feedback_message(f"Auto tracking {'ON' if self.feature_tracker.auto_tracking else 'OFF'}") self.save_state() elif key == ord("G"): @@ -3735,11 +3792,9 @@ class VideoEditor: self.show_feedback_message("Feature tracking data cleared") self.save_state() elif key == ord("H"): - # Switch detector type (SIFT -> SURF -> ORB -> SIFT) + # Switch detector type (SIFT -> ORB -> SIFT) - SURF not available current_type = self.feature_tracker.detector_type if current_type == 'SIFT': - new_type = 'SURF' - elif current_type == 'SURF': new_type = 'ORB' elif current_type == 'ORB': new_type = 'SIFT'