diff --git a/croppa/main.py b/croppa/main.py index 44b2664..163a384 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -876,6 +876,13 @@ class VideoEditor: # Optical flow tracking self.optical_flow_enabled = False self.previous_frame_for_flow = None + + # Template matching tracking + self.template_matching_enabled = False + self.tracking_template = None + self.template_region = None # (x, y, w, h) in rotated frame coordinates + self.template_selection_start = None + self.template_selection_rect = None # Project view mode self.project_view_mode = False @@ -1620,7 +1627,32 @@ class VideoEditor: def _get_interpolated_tracking_position(self, frame_number): """Linear interpolation in ROTATED frame coords. Returns (rx, ry) or None.""" - # First try feature tracking if enabled - but use smooth interpolation instead of averaging + # First try template matching if enabled (much better than optical flow) + if self.template_matching_enabled and self.tracking_template is not None: + if self.current_display_frame is not None: + # Apply transformations to get the display frame + display_frame = self.apply_crop_zoom_and_rotation(self.current_display_frame) + if display_frame is not None: + # Track template in display frame + result = self.track_template(display_frame) + if result: + center_x, center_y, confidence = result + print(f"DEBUG: Template match found at ({center_x}, {center_y}) with confidence {confidence:.2f}") + + # Map from display frame coordinates to rotated frame coordinates + frame_height, frame_width = display_frame.shape[:2] + available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT) + start_y = (available_height - frame_height) // 2 + start_x = (self.window_width - frame_width) // 2 + + screen_x = center_x + start_x + screen_y = center_y + start_y + + # Map from screen coordinates to rotated frame coordinates + rx, ry = self._map_screen_to_rotated(screen_x, screen_y) + return (rx, ry) + + # Fall back to feature tracking if enabled - but use smooth interpolation instead of averaging if self.feature_tracker.tracking_enabled: # Get the nearest frames with features for smooth interpolation feature_frames = sorted(self.feature_tracker.features.keys()) @@ -2079,6 +2111,91 @@ class VideoEditor: interp_y = start_center[1] + alpha * (end_center[1] - start_center[1]) return (interp_x, interp_y) + + def set_tracking_template(self, frame, region): + """Set a template region for tracking (much better than optical flow)""" + try: + x, y, w, h = region + self.tracking_template = frame[y:y+h, x:x+w].copy() + self.template_region = region + print(f"DEBUG: Set tracking template with region {region}") + return True + except Exception as e: + print(f"Error setting tracking template: {e}") + return False + + def track_template(self, frame): + """Track the template in the current frame""" + if self.tracking_template is None: + return None + + try: + # Convert to grayscale + if len(frame.shape) == 3: + gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + else: + gray_frame = frame + + if len(self.tracking_template.shape) == 3: + gray_template = cv2.cvtColor(self.tracking_template, cv2.COLOR_BGR2GRAY) + else: + gray_template = self.tracking_template + + # Template matching + result = cv2.matchTemplate(gray_frame, gray_template, cv2.TM_CCOEFF_NORMED) + min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) + + # Only accept matches above threshold + if max_val > 0.6: # Adjust threshold as needed + # Get template center + template_h, template_w = gray_template.shape + center_x = max_loc[0] + template_w // 2 + center_y = max_loc[1] + template_h // 2 + return (center_x, center_y, max_val) + else: + return None + + except Exception as e: + print(f"Error in template tracking: {e}") + return None + + def _set_template_from_region(self, screen_rect): + """Set template from selected region""" + x, y, w, h = screen_rect + print(f"DEBUG: Setting template from region ({x}, {y}, {w}, {h})") + + if self.current_display_frame is not None: + # Apply transformations to get the display frame + display_frame = self.apply_crop_zoom_and_rotation(self.current_display_frame) + if display_frame is not None: + # Map screen coordinates to display frame coordinates + frame_height, frame_width = display_frame.shape[:2] + available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT) + start_y = (available_height - frame_height) // 2 + start_x = (self.window_width - frame_width) // 2 + + # Convert screen coordinates to display frame coordinates + display_x = x - start_x + display_y = y - start_y + display_w = w + display_h = h + + # Ensure region is within frame bounds + if (display_x >= 0 and display_y >= 0 and + display_x + display_w <= frame_width and + display_y + display_h <= frame_height): + + # Extract template from display frame + template = display_frame[display_y:display_y+display_h, display_x:display_x+display_w] + if template.size > 0: + self.tracking_template = template.copy() + self.template_region = (display_x, display_y, display_w, display_h) + self.show_feedback_message(f"Template set from region ({display_w}x{display_h})") + print(f"DEBUG: Template set with size {template.shape}") + else: + self.show_feedback_message("Template region too small") + else: + self.show_feedback_message("Template region outside frame bounds") def apply_rotation(self, frame): @@ -2938,6 +3055,26 @@ class VideoEditor: self.selective_feature_deletion_start = None self.selective_feature_deletion_rect = None + # Handle Alt+Right-click+drag for template region selection + if event == cv2.EVENT_RBUTTONDOWN and (flags & cv2.EVENT_FLAG_ALTKEY): + if not self.is_image_mode: + self.template_selection_start = (x, y) + self.template_selection_rect = None + print(f"DEBUG: Started template selection at ({x}, {y})") + + # Handle Alt+Right-click+drag for template region selection + if event == cv2.EVENT_MOUSEMOVE and (flags & cv2.EVENT_FLAG_ALTKEY) and self.template_selection_start: + if not self.is_image_mode: + start_x, start_y = self.template_selection_start + self.template_selection_rect = (min(start_x, x), min(start_y, y), abs(x - start_x), abs(y - start_y)) + + # Handle Alt+Right-click release for template region selection + if event == cv2.EVENT_RBUTTONUP and (flags & cv2.EVENT_FLAG_ALTKEY) and self.template_selection_start: + if not self.is_image_mode and self.template_selection_rect: + self._set_template_from_region(self.template_selection_rect) + self.template_selection_start = None + self.template_selection_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: @@ -3787,8 +3924,10 @@ class VideoEditor: print(" G: Clear all feature data") print(" H: Switch detector (SIFT/ORB)") print(" o: Toggle optical flow tracking") + print(" m: Toggle template matching tracking") print(" Shift+Right-click+drag: Extract features from selected region") print(" Ctrl+Right-click+drag: Delete features from selected region") + print(" Alt+Right-click+drag: Set template region for tracking") if len(self.video_files) > 1: print(" N: Next video") print(" n: Previous video") @@ -4124,6 +4263,12 @@ class VideoEditor: self.show_feedback_message(f"Optical flow {'ON' if self.optical_flow_enabled else 'OFF'}") self.save_state() + elif key == ord("m"): + # Toggle template matching tracking + self.template_matching_enabled = not self.template_matching_enabled + print(f"DEBUG: Template matching toggled to {self.template_matching_enabled}") + self.show_feedback_message(f"Template matching {'ON' if self.template_matching_enabled else 'OFF'}") + self.save_state() elif key == ord("t"): # Marker looping only for videos if not self.is_image_mode: