Add template matching tracking to VideoEditor
This commit introduces a new tracking method using template matching, enhancing the tracking capabilities of the VideoEditor. It includes the ability to set a tracking template from a selected region, track the template in the current frame, and toggle template matching on and off. Additionally, debug messages have been added to provide insights during the template tracking process, improving user experience and functionality.
This commit is contained in:
147
croppa/main.py
147
croppa/main.py
@@ -877,6 +877,13 @@ class VideoEditor:
|
|||||||
self.optical_flow_enabled = False
|
self.optical_flow_enabled = False
|
||||||
self.previous_frame_for_flow = None
|
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
|
# Project view mode
|
||||||
self.project_view_mode = False
|
self.project_view_mode = False
|
||||||
self.project_view = None
|
self.project_view = None
|
||||||
@@ -1620,7 +1627,32 @@ class VideoEditor:
|
|||||||
|
|
||||||
def _get_interpolated_tracking_position(self, frame_number):
|
def _get_interpolated_tracking_position(self, frame_number):
|
||||||
"""Linear interpolation in ROTATED frame coords. Returns (rx, ry) or None."""
|
"""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:
|
if self.feature_tracker.tracking_enabled:
|
||||||
# Get the nearest frames with features for smooth interpolation
|
# Get the nearest frames with features for smooth interpolation
|
||||||
feature_frames = sorted(self.feature_tracker.features.keys())
|
feature_frames = sorted(self.feature_tracker.features.keys())
|
||||||
@@ -2080,6 +2112,91 @@ class VideoEditor:
|
|||||||
|
|
||||||
return (interp_x, interp_y)
|
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):
|
def apply_rotation(self, frame):
|
||||||
"""Apply rotation to frame"""
|
"""Apply rotation to frame"""
|
||||||
@@ -2938,6 +3055,26 @@ class VideoEditor:
|
|||||||
self.selective_feature_deletion_start = None
|
self.selective_feature_deletion_start = None
|
||||||
self.selective_feature_deletion_rect = 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)
|
# 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 event == cv2.EVENT_RBUTTONDOWN and not (flags & (cv2.EVENT_FLAG_CTRLKEY | cv2.EVENT_FLAG_SHIFTKEY)):
|
||||||
if not self.is_image_mode:
|
if not self.is_image_mode:
|
||||||
@@ -3787,8 +3924,10 @@ class VideoEditor:
|
|||||||
print(" G: Clear all feature data")
|
print(" G: Clear all feature data")
|
||||||
print(" H: Switch detector (SIFT/ORB)")
|
print(" H: Switch detector (SIFT/ORB)")
|
||||||
print(" o: Toggle optical flow tracking")
|
print(" o: Toggle optical flow tracking")
|
||||||
|
print(" m: Toggle template matching tracking")
|
||||||
print(" Shift+Right-click+drag: Extract features from selected region")
|
print(" Shift+Right-click+drag: Extract features from selected region")
|
||||||
print(" Ctrl+Right-click+drag: Delete 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:
|
if len(self.video_files) > 1:
|
||||||
print(" N: Next video")
|
print(" N: Next video")
|
||||||
print(" n: Previous 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.show_feedback_message(f"Optical flow {'ON' if self.optical_flow_enabled else 'OFF'}")
|
||||||
self.save_state()
|
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"):
|
elif key == ord("t"):
|
||||||
# Marker looping only for videos
|
# Marker looping only for videos
|
||||||
if not self.is_image_mode:
|
if not self.is_image_mode:
|
||||||
|
Reference in New Issue
Block a user