From ea008ba23c1ef24b9e907817645140fc7974cdfb Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Fri, 26 Sep 2025 13:57:00 +0200 Subject: [PATCH] Implement shift-right click and ctrl-right click for feature extraction --- croppa/main.py | 207 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 162 insertions(+), 45 deletions(-) diff --git a/croppa/main.py b/croppa/main.py index a8fc325..d0090bc 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -769,6 +769,12 @@ class VideoEditor: # Feature tracking system self.feature_tracker = FeatureTracker() + + # Initialize selective feature extraction/deletion + self.selective_feature_extraction_start = None + self.selective_feature_extraction_rect = None + self.selective_feature_deletion_start = None + self.selective_feature_deletion_rect = None # Project view mode self.project_view_mode = False @@ -1479,50 +1485,6 @@ class VideoEditor: h = min(h, rot_h - y) return (x, y, w, h) - def _map_transformed_to_original_coords(self, x, y): - """Map coordinates from transformed frame back to original frame coordinates.""" - # The transformed frame is the result of apply_crop_zoom_and_rotation - # We need to reverse the transformations to get back to original frame coordinates - - # First, reverse the crop transformation - if self.crop_rect: - crop_x, crop_y, crop_w, crop_h = self.crop_rect - # Add crop offset back - orig_x = x + crop_x - orig_y = y + crop_y - else: - orig_x, orig_y = x, y - - # Then reverse the rotation - if self.rotation_angle == 90: - # 90° clockwise -> 270° counterclockwise - orig_x, orig_y = self.frame_height - orig_y, orig_x - elif self.rotation_angle == 180: - # 180° -> flip both axes - orig_x = self.frame_width - orig_x - orig_y = self.frame_height - orig_y - elif self.rotation_angle == 270: - # 270° clockwise -> 90° counterclockwise - orig_x, orig_y = orig_y, self.frame_width - orig_x - - return (int(orig_x), int(orig_y)) - - def _map_original_to_rotated_coords(self, x, y): - """Map coordinates from original frame to rotated frame coordinates.""" - # Apply rotation (same as existing system) - if self.rotation_angle == 90: - # 90° clockwise - rot_x, rot_y = self.frame_height - y, x - elif self.rotation_angle == 180: - # 180° -> flip both axes - rot_x, rot_y = self.frame_width - x, self.frame_height - y - elif self.rotation_angle == 270: - # 270° clockwise - rot_x, rot_y = y, self.frame_width - x - else: - rot_x, rot_y = x, y - - return (int(rot_x), int(rot_y)) def _get_interpolated_tracking_position(self, frame_number): """Linear interpolation in ROTATED frame coords. Returns (rx, ry) or None.""" @@ -1655,6 +1617,109 @@ class VideoEditor: self.cached_transformed_frame = None self.cached_frame_number = None self.cached_transform_hash = None + + def _extract_features_from_region(self, screen_rect): + """Extract features from a specific screen region""" + x, y, w, h = screen_rect + print(f"DEBUG: Extracting features from region ({x}, {y}, {w}, {h})") + + # Map screen coordinates to rotated frame coordinates + rx1, ry1 = self._map_screen_to_rotated(x, y) + rx2, ry2 = self._map_screen_to_rotated(x + w, y + h) + + # Get the region in rotated frame coordinates + left_r = min(rx1, rx2) + top_r = min(ry1, ry2) + right_r = max(rx1, rx2) + bottom_r = max(ry1, ry2) + + # Extract features from this region of the original frame + if self.rotation_angle in (90, 270): + # For rotated frames, we need to map back to original frame coordinates + if self.rotation_angle == 90: + orig_x = top_r + orig_y = self.frame_height - right_r + orig_w = bottom_r - top_r + orig_h = right_r - left_r + else: # 270 + orig_x = self.frame_width - bottom_r + orig_y = left_r + orig_w = bottom_r - top_r + orig_h = right_r - left_r + else: + orig_x, orig_y = left_r, top_r + orig_w, orig_h = right_r - left_r, bottom_r - top_r + + # Extract features from this region + if (orig_x >= 0 and orig_y >= 0 and + orig_x + orig_w <= self.frame_width and + orig_y + orig_h <= self.frame_height): + + region_frame = self.current_display_frame[orig_y:orig_y+orig_h, orig_x:orig_x+orig_w] + if region_frame.size > 0: + # Map coordinates from region to rotated frame coordinates + def coord_mapper(px, py): + # Map from region coordinates to rotated frame coordinates + if self.rotation_angle == 90: + rot_x = orig_x + py + rot_y = self.frame_height - (orig_y + px) + elif self.rotation_angle == 270: + rot_x = self.frame_width - (orig_y + py) + rot_y = orig_x + px + else: + rot_x = orig_x + px + 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) + if success: + count = self.feature_tracker.get_feature_count(self.current_frame) + self.show_feedback_message(f"Extracted {count} features from selected region") + else: + self.show_feedback_message("Failed to extract features from region") + else: + self.show_feedback_message("Region too small") + else: + self.show_feedback_message("Region outside frame bounds") + + def _delete_features_from_region(self, screen_rect): + """Delete features from a specific screen region""" + x, y, w, h = screen_rect + print(f"DEBUG: Deleting features from region ({x}, {y}, {w}, {h})") + + if self.current_frame not in self.feature_tracker.features: + self.show_feedback_message("No features to delete") + return + + # Map screen coordinates to rotated frame coordinates + rx1, ry1 = self._map_screen_to_rotated(x, y) + rx2, ry2 = self._map_screen_to_rotated(x + w, y + h) + + # Get the region in rotated frame coordinates + left_r = min(rx1, rx2) + top_r = min(ry1, ry2) + right_r = max(rx1, rx2) + bottom_r = max(ry1, ry2) + + # Remove features within this region + features = self.feature_tracker.features[self.current_frame] + original_count = len(features['positions']) + + # Filter out features within the region + filtered_positions = [] + for fx, fy in features['positions']: + if not (left_r <= fx <= right_r and top_r <= fy <= bottom_r): + filtered_positions.append((fx, fy)) + + # Update the features + features['positions'] = filtered_positions + removed_count = original_count - len(filtered_positions) + + if removed_count > 0: + self.show_feedback_message(f"Removed {removed_count} features from selected region") + self.save_state() + else: + self.show_feedback_message("No features found in selected region") def apply_rotation(self, frame): @@ -2322,6 +2387,15 @@ class VideoEditor: cv2.circle(canvas, (sx, sy), 4, (0, 255, 0), -1) # Green circles for features cv2.circle(canvas, (sx, sy), 4, (255, 255, 255), 1) + # Draw selection rectangles for feature extraction/deletion + if self.selective_feature_extraction_rect: + x, y, w, h = self.selective_feature_extraction_rect + cv2.rectangle(canvas, (x, y), (x + w, y + h), (0, 255, 255), 2) # Yellow for extraction + + if self.selective_feature_deletion_rect: + x, y, w, h = self.selective_feature_deletion_rect + cv2.rectangle(canvas, (x, y), (x + w, y + h), (0, 0, 255), 2) # Red for deletion + # Draw previous and next tracking points with motion path visualization if not self.is_image_mode and self.tracking_points: prev_result = self._get_previous_tracking_point() @@ -2459,6 +2533,46 @@ class VideoEditor: if flags & cv2.EVENT_FLAG_CTRLKEY and event == cv2.EVENT_LBUTTONDOWN: self.zoom_center = (x, y) + # Handle Shift+Right-click+drag for selective feature extraction + if event == cv2.EVENT_RBUTTONDOWN and (flags & cv2.EVENT_FLAG_SHIFTKEY): + if not self.is_image_mode and self.feature_tracker.tracking_enabled: + self.selective_feature_extraction_start = (x, y) + self.selective_feature_extraction_rect = None + print(f"DEBUG: Started selective feature extraction at ({x}, {y})") + + # Handle Shift+Right-click+drag for selective feature extraction + if event == cv2.EVENT_MOUSEMOVE and (flags & cv2.EVENT_FLAG_SHIFTKEY) and self.selective_feature_extraction_start: + if not self.is_image_mode: + start_x, start_y = self.selective_feature_extraction_start + self.selective_feature_extraction_rect = (min(start_x, x), min(start_y, y), abs(x - start_x), abs(y - start_y)) + + # Handle Shift+Right-click release for selective feature extraction + if event == cv2.EVENT_RBUTTONUP and (flags & cv2.EVENT_FLAG_SHIFTKEY) and self.selective_feature_extraction_start: + if not self.is_image_mode and self.feature_tracker.tracking_enabled and self.selective_feature_extraction_rect: + self._extract_features_from_region(self.selective_feature_extraction_rect) + self.selective_feature_extraction_start = None + self.selective_feature_extraction_rect = None + + # Handle Ctrl+Right-click+drag for selective feature deletion + if event == cv2.EVENT_RBUTTONDOWN and (flags & cv2.EVENT_FLAG_CTRLKEY): + if not self.is_image_mode and self.feature_tracker.tracking_enabled: + self.selective_feature_deletion_start = (x, y) + self.selective_feature_deletion_rect = None + print(f"DEBUG: Started selective feature deletion at ({x}, {y})") + + # Handle Ctrl+Right-click+drag for selective feature deletion + if event == cv2.EVENT_MOUSEMOVE and (flags & cv2.EVENT_FLAG_CTRLKEY) and self.selective_feature_deletion_start: + if not self.is_image_mode: + start_x, start_y = self.selective_feature_deletion_start + self.selective_feature_deletion_rect = (min(start_x, x), min(start_y, y), abs(x - start_x), abs(y - start_y)) + + # Handle Ctrl+Right-click release for selective feature deletion + if event == cv2.EVENT_RBUTTONUP and (flags & cv2.EVENT_FLAG_CTRLKEY) and self.selective_feature_deletion_start: + if not self.is_image_mode and self.feature_tracker.tracking_enabled and self.selective_feature_deletion_rect: + self._delete_features_from_region(self.selective_feature_deletion_rect) + self.selective_feature_deletion_start = None + self.selective_feature_deletion_rect = None + # Handle right-click for tracking points (no modifiers) if event == cv2.EVENT_RBUTTONDOWN and not (flags & (cv2.EVENT_FLAG_CTRLKEY | cv2.EVENT_FLAG_SHIFTKEY)): if not self.is_image_mode: @@ -3307,6 +3421,8 @@ class VideoEditor: print(" g: Toggle auto feature extraction") print(" G: Clear all feature data") print(" H: Switch detector (SIFT/SURF/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: print(" N: Next video") print(" n: Previous video") @@ -3621,11 +3737,12 @@ class VideoEditor: elif key == ord("H"): # Switch detector type (SIFT -> SURF -> ORB -> SIFT) current_type = self.feature_tracker.detector_type - print(f"Current detector type: {current_type}") if current_type == 'SIFT': new_type = 'SURF' elif current_type == 'SURF': new_type = 'ORB' + elif current_type == 'ORB': + new_type = 'SIFT' else: new_type = 'SIFT' self.feature_tracker.set_detector_type(new_type)