Compare commits

..

10 Commits

Author SHA1 Message Date
e6616ed1b1 Optimize template matching in VideoEditor by utilizing cropped regions for faster processing. This update modifies the tracking logic to first check for a defined crop rectangle, allowing for quicker template matching on smaller frames. If no crop is set, the full frame is used, maintaining the previous functionality. Debug messages remain to assist in tracking accuracy and confidence levels. 2025-09-26 14:33:36 +02:00
048e8ef033 Refactor template management in VideoEditor to improve tracking functionality
This commit removes the direct storage of the tracking template and introduces a method to recreate the template from the specified region when needed. It enhances the template tracking logic by ensuring that the template is dynamically generated based on the current frame and region, improving accuracy and usability. Debug messages have been added to assist in tracking the template recreation process.
2025-09-26 14:31:35 +02:00
c08d5c5999 Refactor template tracking in VideoEditor to use raw display frame
This commit modifies the template tracking logic in the VideoEditor to utilize the raw display frame instead of applying transformations, improving accuracy in tracking. It updates the coordinate mapping to account for various rotation angles, ensuring correct positioning of the tracked template. This enhancement streamlines the tracking process and enhances overall functionality.
2025-09-26 14:29:50 +02:00
8c1efb1b05 Add template selection rectangle visualization in VideoEditor
This commit introduces a new feature in the VideoEditor that allows users to visualize the template selection rectangle on the canvas. The rectangle is drawn in magenta, enhancing the user interface and providing clearer feedback during template selection. This update complements the existing functionality for selective feature deletion and improves overall usability.
2025-09-26 14:27:40 +02:00
f942392fb3 Enhance VideoEditor with template matching state management and user interaction updates
This commit adds functionality to manage the state of template matching in the VideoEditor, including loading and saving template matching settings. It also updates user interaction for selecting template regions, changing the control scheme from Alt+Right-click to Ctrl+Left-click for better usability. Additionally, it improves the handling of the current display frame during feature extraction, ensuring robustness in the tracking process.
2025-09-26 14:26:42 +02:00
c749d9af80 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.
2025-09-26 14:24:31 +02:00
71e5870306 Enhance feature tracking with smooth interpolation and center calculation
This commit improves the feature tracking logic in the VideoEditor by implementing smooth interpolation between feature positions across frames. It introduces methods to calculate the center of features for a given frame, ensuring a more fluid tracking experience. These enhancements provide better continuity in feature tracking, particularly when features are sparse, and improve overall user experience.
2025-09-26 14:21:14 +02:00
e813be2890 Improve feature tracking logic and debugging in VideoEditor
This commit refines the feature tracking process by enhancing the get_tracking_position method to handle cases where frame features are missing. It also adds detailed debug messages throughout the VideoEditor class, particularly during frame seeking and feature interpolation, to provide better insights into the feature availability and interpolation process. These changes improve the robustness and user experience of the feature tracking functionality.
2025-09-26 14:19:42 +02:00
80fb35cced Implement feature interpolation and gap filling in optical flow tracking
This commit introduces methods for interpolating features between frames and filling gaps in feature tracking using linear interpolation. It enhances the optical flow tracking capabilities by ensuring continuity of features across frames. Debug messages have been added to provide insights during the interpolation process, improving the overall functionality and user experience in the VideoEditor.
2025-09-26 14:14:15 +02:00
d8b4439382 Add optical flow tracking for feature tracking in VideoEditor
This commit introduces a new method for tracking features using Lucas-Kanade optical flow, enhancing the feature tracking capabilities. It includes logic to toggle optical flow tracking, store previous frames for flow calculations, and update feature positions based on optical flow results. Debug messages have been added to provide insights during the tracking process, improving user experience and functionality.
2025-09-26 14:11:05 +02:00

View File

@@ -165,19 +165,58 @@ class FeatureTracker:
print(f"Error extracting features from frame {frame_number}: {e}") print(f"Error extracting features from frame {frame_number}: {e}")
return False return False
def track_features_optical_flow(self, prev_frame, curr_frame, prev_points):
"""Track features using Lucas-Kanade optical flow"""
try:
# Convert to grayscale if needed
if len(prev_frame.shape) == 3:
prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
else:
prev_gray = prev_frame
if len(curr_frame.shape) == 3:
curr_gray = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2GRAY)
else:
curr_gray = curr_frame
# Parameters for Lucas-Kanade optical flow
lk_params = dict(winSize=(15, 15),
maxLevel=2,
criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))
# Calculate optical flow
new_points, status, _ = cv2.calcOpticalFlowPyrLK(prev_gray, curr_gray, prev_points, None, **lk_params)
# Filter out bad tracks
good_new = new_points[status == 1]
good_old = prev_points[status == 1]
return good_new, good_old, status
except Exception as e:
print(f"Error in optical flow tracking: {e}")
return None, None, None
def get_tracking_position(self, frame_number: int) -> Optional[Tuple[float, float]]: def get_tracking_position(self, frame_number: int) -> Optional[Tuple[float, float]]:
"""Get the average tracking position for a frame""" """Get the average tracking position for a frame"""
if frame_number not in self.features or not self.features[frame_number]['positions']: if frame_number not in self.features:
return None
if not self.features[frame_number]['positions']:
return None return None
positions = self.features[frame_number]['positions'] positions = self.features[frame_number]['positions']
if not positions: if not positions:
return None return None
# Calculate average position
avg_x = sum(pos[0] for pos in positions) / len(positions) avg_x = sum(pos[0] for pos in positions) / len(positions)
avg_y = sum(pos[1] for pos in positions) / len(positions) avg_y = sum(pos[1] for pos in positions) / len(positions)
return (avg_x, avg_y) return (avg_x, avg_y)
@@ -834,6 +873,17 @@ 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
# 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 # Project view mode
self.project_view_mode = False self.project_view_mode = False
self.project_view = None self.project_view = None
@@ -879,7 +929,9 @@ class VideoEditor:
'is_playing': getattr(self, 'is_playing', False), 'is_playing': getattr(self, 'is_playing', False),
'tracking_enabled': self.tracking_enabled, 'tracking_enabled': self.tracking_enabled,
'tracking_points': {str(k): v for k, v in self.tracking_points.items()}, 'tracking_points': {str(k): v for k, v in self.tracking_points.items()},
'feature_tracker': self.feature_tracker.get_state_dict() 'feature_tracker': self.feature_tracker.get_state_dict(),
'template_matching_enabled': self.template_matching_enabled,
'template_region': self.template_region
} }
with open(state_file, 'w') as f: with open(state_file, 'w') as f:
@@ -967,6 +1019,14 @@ class VideoEditor:
self.feature_tracker.load_state_dict(state['feature_tracker']) self.feature_tracker.load_state_dict(state['feature_tracker'])
print(f"Loaded feature tracker state") print(f"Loaded feature tracker state")
# Load template matching state
if 'template_matching_enabled' in state:
self.template_matching_enabled = state['template_matching_enabled']
if 'template_region' in state and state['template_region'] is not None:
self.template_region = state['template_region']
# Recreate template from region when needed
self.tracking_template = None # Will be recreated on first use
# Validate cut markers against current video length # Validate cut markers against current video length
if self.cut_start_frame is not None and self.cut_start_frame >= self.total_frames: if self.cut_start_frame is not None and self.cut_start_frame >= self.total_frames:
print(f"DEBUG: cut_start_frame {self.cut_start_frame} is beyond video length {self.total_frames}, clearing") print(f"DEBUG: cut_start_frame {self.cut_start_frame} is beyond video length {self.total_frames}, clearing")
@@ -1297,9 +1357,20 @@ class VideoEditor:
def seek_to_frame(self, frame_number: int): def seek_to_frame(self, frame_number: int):
"""Seek to specific frame""" """Seek to specific frame"""
old_frame = self.current_frame
self.current_frame = max(0, min(frame_number, self.total_frames - 1)) self.current_frame = max(0, min(frame_number, self.total_frames - 1))
self.load_current_frame() self.load_current_frame()
# Only log when we actually change frames
if old_frame != self.current_frame:
print(f"DEBUG: === LOADED NEW FRAME {self.current_frame} ===")
print(f"DEBUG: Features available for frames: {sorted(self.feature_tracker.features.keys())}")
if self.current_frame in self.feature_tracker.features:
feature_count = len(self.feature_tracker.features[self.current_frame]['positions'])
print(f"DEBUG: Frame {self.current_frame} has {feature_count} features")
else:
print(f"DEBUG: Frame {self.current_frame} has NO features")
# Auto-extract features if feature tracking is enabled and auto-tracking is on # Auto-extract features if feature tracking is enabled and auto-tracking is on
print(f"DEBUG: seek_to_frame {frame_number}: is_image_mode={self.is_image_mode}, tracking_enabled={self.feature_tracker.tracking_enabled}, auto_tracking={self.feature_tracker.auto_tracking}, display_frame={self.current_display_frame is not None}") print(f"DEBUG: seek_to_frame {frame_number}: is_image_mode={self.is_image_mode}, tracking_enabled={self.feature_tracker.tracking_enabled}, auto_tracking={self.feature_tracker.auto_tracking}, display_frame={self.current_display_frame is not None}")
@@ -1336,6 +1407,20 @@ class VideoEditor:
else: else:
print(f"DEBUG: Frame {self.current_frame} already has features, skipping") print(f"DEBUG: Frame {self.current_frame} already has features, skipping")
# Optical flow tracking - track features from previous frame
if (not self.is_image_mode and
self.optical_flow_enabled and
self.feature_tracker.tracking_enabled and
self.previous_frame_for_flow is not None and
self.current_display_frame is not None):
self._track_with_optical_flow()
# Store current frame for next optical flow iteration
if not self.is_image_mode and self.current_display_frame is not None:
self.previous_frame_for_flow = self.current_display_frame.copy()
def jump_to_previous_marker(self): def jump_to_previous_marker(self):
"""Jump to the previous tracking marker (frame with tracking points).""" """Jump to the previous tracking marker (frame with tracking points)."""
if self.is_image_mode: if self.is_image_mode:
@@ -1552,13 +1637,70 @@ 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 # 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:
# Use only the cropped region for much faster template matching
if self.crop_rect:
crop_x, crop_y, crop_w, crop_h = self.crop_rect
# Extract only the cropped region
cropped_frame = self.current_display_frame[crop_y:crop_y+crop_h, crop_x:crop_x+crop_w]
if cropped_frame is not None and cropped_frame.size > 0:
# Track template in cropped frame (much faster!)
result = self.track_template(cropped_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 cropped frame coordinates to rotated frame coordinates
# Add crop offset back
rot_x = center_x + crop_x
rot_y = center_y + crop_y
return (rot_x, rot_y)
else:
# No crop - use full frame
raw_frame = self.current_display_frame.copy()
if raw_frame is not None:
result = self.track_template(raw_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 raw frame coordinates to rotated frame coordinates
if self.rotation_angle == 90:
rot_x = self.frame_height - center_y
rot_y = center_x
elif self.rotation_angle == 180:
rot_x = self.frame_width - center_x
rot_y = self.frame_height - center_y
elif self.rotation_angle == 270:
rot_x = center_y
rot_y = self.frame_width - center_x
else:
rot_x, rot_y = center_x, center_y
return (rot_x, rot_y)
# 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:
feature_pos = self.feature_tracker.get_tracking_position(frame_number) # Get the nearest frames with features for smooth interpolation
if feature_pos: feature_frames = sorted(self.feature_tracker.features.keys())
# Features are stored in rotated frame coordinates (like existing motion tracking) if feature_frames:
# We can use them directly for the tracking system # Find the two nearest frames for interpolation
return (feature_pos[0], feature_pos[1]) if frame_number <= feature_frames[0]:
# Before first feature frame - use first frame
return self._get_feature_center(feature_frames[0])
elif frame_number >= feature_frames[-1]:
# After last feature frame - use last frame
return self._get_feature_center(feature_frames[-1])
else:
# Between two feature frames - interpolate smoothly
for i in range(len(feature_frames) - 1):
if feature_frames[i] <= frame_number <= feature_frames[i + 1]:
return self._interpolate_feature_positions(
feature_frames[i], feature_frames[i + 1], frame_number
)
# Fall back to manual tracking points # Fall back to manual tracking points
if not self.tracking_points: if not self.tracking_points:
@@ -1719,21 +1861,22 @@ class VideoEditor:
orig_x + orig_w <= self.frame_width and orig_x + orig_w <= self.frame_width and
orig_y + orig_h <= self.frame_height): 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 self.current_display_frame is not None:
if region_frame.size > 0: region_frame = self.current_display_frame[orig_y:orig_y+orig_h, orig_x:orig_x+orig_w]
# Map coordinates from region to rotated frame coordinates if region_frame is not None and region_frame.size > 0:
def coord_mapper(px, py): # Map coordinates from region to rotated frame coordinates
# Map from region coordinates to rotated frame coordinates def coord_mapper(px, py):
if self.rotation_angle == 90: # Map from region coordinates to rotated frame coordinates
rot_x = orig_x + py if self.rotation_angle == 90:
rot_y = self.frame_height - (orig_y + px) rot_x = orig_x + py
elif self.rotation_angle == 270: rot_y = self.frame_height - (orig_y + px)
rot_x = self.frame_width - (orig_y + py) elif self.rotation_angle == 270:
rot_y = orig_x + px rot_x = self.frame_width - (orig_y + py)
else: rot_y = orig_x + px
rot_x = orig_x + px else:
rot_y = orig_y + py rot_x = orig_x + px
return (int(rot_x), int(rot_y)) rot_y = orig_y + py
return (int(rot_x), int(rot_y))
# Extract features and add them to existing features # Extract features and add them to existing features
success = self.feature_tracker.extract_features_from_region(region_frame, self.current_frame, coord_mapper) success = self.feature_tracker.extract_features_from_region(region_frame, self.current_frame, coord_mapper)
@@ -1786,6 +1929,340 @@ class VideoEditor:
else: else:
self.show_feedback_message("No features found in selected region") self.show_feedback_message("No features found in selected region")
def _track_with_optical_flow(self):
"""Track features using optical flow from previous frame"""
try:
# Get previous frame features
prev_frame_number = self.current_frame - 1
if prev_frame_number not in self.feature_tracker.features:
print(f"DEBUG: No features on previous frame {prev_frame_number} for optical flow")
return
prev_features = self.feature_tracker.features[prev_frame_number]
prev_positions = np.array(prev_features['positions'], dtype=np.float32).reshape(-1, 1, 2)
if len(prev_positions) == 0:
print(f"DEBUG: No positions on previous frame {prev_frame_number} for optical flow")
return
print(f"DEBUG: Optical flow tracking from frame {prev_frame_number} to {self.current_frame}")
# Apply transformations to get the display frames
prev_display_frame = self.apply_crop_zoom_and_rotation(self.previous_frame_for_flow)
curr_display_frame = self.apply_crop_zoom_and_rotation(self.current_display_frame)
if prev_display_frame is None or curr_display_frame is None:
print("DEBUG: Could not get display frames for optical flow")
return
# Map previous positions to display frame coordinates
display_prev_positions = []
for px, py in prev_positions.reshape(-1, 2):
# Map from rotated frame coordinates to screen coordinates
sx, sy = self._map_rotated_to_screen(px, py)
# Map from screen coordinates to display frame coordinates
frame_height, frame_width = prev_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
display_x = sx - start_x
display_y = sy - start_y
if 0 <= display_x < frame_width and 0 <= display_y < frame_height:
display_prev_positions.append([display_x, display_y])
if len(display_prev_positions) == 0:
print("DEBUG: No valid display positions for optical flow")
return
display_prev_positions = np.array(display_prev_positions, dtype=np.float32).reshape(-1, 1, 2)
print(f"DEBUG: Tracking {len(display_prev_positions)} points with optical flow")
# Track using optical flow
new_points, good_old, status = self.feature_tracker.track_features_optical_flow(
prev_display_frame, curr_display_frame, display_prev_positions
)
if new_points is not None and len(new_points) > 0:
print(f"DEBUG: Optical flow found {len(new_points)} tracked points")
# Map new positions back to rotated frame coordinates
mapped_positions = []
for point in new_points.reshape(-1, 2):
# Map from display frame coordinates to screen coordinates
frame_height, frame_width = curr_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 = point[0] + start_x
screen_y = point[1] + start_y
# Map from screen coordinates to rotated frame coordinates
rx, ry = self._map_screen_to_rotated(screen_x, screen_y)
mapped_positions.append((int(rx), int(ry)))
# Store tracked features
self.feature_tracker.features[self.current_frame] = {
'keypoints': [], # Optical flow doesn't use keypoints
'descriptors': np.array([]), # Optical flow doesn't use descriptors
'positions': mapped_positions
}
print(f"Optical flow tracked {len(mapped_positions)} features to frame {self.current_frame}")
else:
print("DEBUG: Optical flow failed to track any points")
except Exception as e:
print(f"Error in optical flow tracking: {e}")
def _interpolate_features_between_frames(self, start_frame, end_frame):
"""Interpolate features between two frames using linear interpolation"""
try:
print(f"DEBUG: Starting interpolation between frame {start_frame} and {end_frame}")
if start_frame not in self.feature_tracker.features or end_frame not in self.feature_tracker.features:
print(f"DEBUG: Missing features on start_frame={start_frame} or end_frame={end_frame}")
return
start_features = self.feature_tracker.features[start_frame]['positions']
end_features = self.feature_tracker.features[end_frame]['positions']
print(f"DEBUG: Start frame {start_frame} has {len(start_features)} features")
print(f"DEBUG: End frame {end_frame} has {len(end_features)} features")
if len(start_features) != len(end_features):
print(f"DEBUG: Feature count mismatch between frames {start_frame} and {end_frame} ({len(start_features)} vs {len(end_features)})")
print(f"DEBUG: Using minimum count for interpolation")
# Use the minimum count to avoid index errors
min_count = min(len(start_features), len(end_features))
start_features = start_features[:min_count]
end_features = end_features[:min_count]
# Interpolate for all frames between start and end
frames_to_interpolate = []
for frame_num in range(start_frame + 1, end_frame):
if frame_num in self.feature_tracker.features:
print(f"DEBUG: Frame {frame_num} already has features, skipping")
continue # Skip if already has features
frames_to_interpolate.append(frame_num)
print(f"DEBUG: Will interpolate {len(frames_to_interpolate)} frames: {frames_to_interpolate}")
for frame_num in frames_to_interpolate:
# Linear interpolation
alpha = (frame_num - start_frame) / (end_frame - start_frame)
interpolated_positions = []
for i in range(len(start_features)):
start_x, start_y = start_features[i]
end_x, end_y = end_features[i]
interp_x = start_x + alpha * (end_x - start_x)
interp_y = start_y + alpha * (end_y - start_y)
interpolated_positions.append((int(interp_x), int(interp_y)))
# Store interpolated features
self.feature_tracker.features[frame_num] = {
'keypoints': [],
'descriptors': np.array([]),
'positions': interpolated_positions
}
print(f"DEBUG: Interpolated {len(interpolated_positions)} features for frame {frame_num}")
print(f"DEBUG: Finished interpolation between frame {start_frame} and {end_frame}")
except Exception as e:
print(f"Error interpolating features: {e}")
def _fill_all_gaps_with_interpolation(self):
"""Fill all gaps between existing features with linear interpolation"""
try:
print("=== FILLING ALL GAPS WITH INTERPOLATION ===")
print(f"DEBUG: Total features stored: {len(self.feature_tracker.features)}")
if not self.feature_tracker.features:
print("DEBUG: No features to interpolate between")
return
# Get all frames with features, sorted
frames_with_features = sorted(self.feature_tracker.features.keys())
print(f"DEBUG: Frames with features: {frames_with_features}")
if len(frames_with_features) < 2:
print("DEBUG: Need at least 2 frames with features to interpolate")
return
# Fill gaps between each pair of consecutive frames with features
for i in range(len(frames_with_features) - 1):
start_frame = frames_with_features[i]
end_frame = frames_with_features[i + 1]
print(f"DEBUG: Interpolating between frame {start_frame} and {end_frame}")
self._interpolate_features_between_frames(start_frame, end_frame)
print(f"DEBUG: After interpolation, total features stored: {len(self.feature_tracker.features)}")
print("=== FINISHED FILLING ALL GAPS ===")
except Exception as e:
print(f"Error filling all gaps: {e}")
def _get_feature_center(self, frame_number):
"""Get the center of features for a frame (smooth, not jarring)"""
if frame_number not in self.feature_tracker.features:
return None
positions = self.feature_tracker.features[frame_number]['positions']
if not positions:
return None
# Calculate center of mass (smoother than average)
center_x = sum(pos[0] for pos in positions) / len(positions)
center_y = sum(pos[1] for pos in positions) / len(positions)
return (center_x, center_y)
def _interpolate_feature_positions(self, start_frame, end_frame, target_frame):
"""Smoothly interpolate between feature centers of two frames"""
start_center = self._get_feature_center(start_frame)
end_center = self._get_feature_center(end_frame)
if not start_center or not end_center:
return None
# Linear interpolation between centers
alpha = (target_frame - start_frame) / (end_frame - start_frame)
interp_x = start_center[0] + alpha * (end_center[0] - start_center[0])
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:
# Try to recreate template from saved region
if self.template_region is not None:
self._recreate_template_from_region(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)
_, max_val, _, 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 _recreate_template_from_region(self, frame):
"""Recreate template from saved region coordinates"""
try:
if self.template_region is None:
return False
x, y, w, h = self.template_region
print(f"DEBUG: Recreating template from region ({x}, {y}, {w}, {h})")
# Ensure region is within frame bounds
if (x >= 0 and y >= 0 and
x + w <= frame.shape[1] and
y + h <= frame.shape[0]):
# Extract template from frame
template = frame[y:y+h, x:x+w]
if template.size > 0:
self.tracking_template = template.copy()
print(f"DEBUG: Template recreated with size {template.shape}")
return True
else:
print("DEBUG: Template region too small")
return False
else:
print("DEBUG: Template region outside frame bounds")
return False
except Exception as e:
print(f"Error recreating template: {e}")
return False
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"""
@@ -2341,6 +2818,8 @@ class VideoEditor:
if self.feature_tracker.tracking_enabled and self.current_frame in self.feature_tracker.features: if self.feature_tracker.tracking_enabled and self.current_frame in self.feature_tracker.features:
feature_count = self.feature_tracker.get_feature_count(self.current_frame) feature_count = self.feature_tracker.get_feature_count(self.current_frame)
feature_text = f" | Features: {feature_count} pts" feature_text = f" | Features: {feature_count} pts"
if self.optical_flow_enabled:
feature_text += " (OPTICAL FLOW)"
autorepeat_text = ( autorepeat_text = (
f" | Loop: ON" if self.looping_between_markers else "" f" | Loop: ON" if self.looping_between_markers else ""
) )
@@ -2461,6 +2940,11 @@ class VideoEditor:
x, y, w, h = 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 cv2.rectangle(canvas, (x, y), (x + w, y + h), (0, 0, 255), 2) # Red for deletion
# Draw template selection rectangle
if self.template_selection_rect:
x, y, w, h = self.template_selection_rect
cv2.rectangle(canvas, (x, y), (x + w, y + h), (255, 0, 255), 2) # Magenta for template selection
# Draw previous and next tracking points with motion path visualization # Draw previous and next tracking points with motion path visualization
if not self.is_image_mode and self.tracking_points: if not self.is_image_mode and self.tracking_points:
prev_result = self._get_previous_tracking_point() prev_result = self._get_previous_tracking_point()
@@ -2642,6 +3126,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 Ctrl+Left-click+drag for template region selection
if event == cv2.EVENT_LBUTTONDOWN and (flags & cv2.EVENT_FLAG_CTRLKEY):
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 Ctrl+Left-click+drag for template region selection
if event == cv2.EVENT_MOUSEMOVE and (flags & cv2.EVENT_FLAG_CTRLKEY) 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 Ctrl+Left-click release for template region selection
if event == cv2.EVENT_LBUTTONUP and (flags & cv2.EVENT_FLAG_CTRLKEY) 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:
@@ -3490,8 +3994,11 @@ class VideoEditor:
print(" g: Toggle auto feature extraction") print(" g: Toggle auto feature extraction")
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(" 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(" Ctrl+Left-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")
@@ -3816,6 +4323,23 @@ class VideoEditor:
self.feature_tracker.set_detector_type(new_type) self.feature_tracker.set_detector_type(new_type)
self.show_feedback_message(f"Detector switched to {new_type}") self.show_feedback_message(f"Detector switched to {new_type}")
self.save_state() self.save_state()
elif key == ord("o"):
# Toggle optical flow tracking
self.optical_flow_enabled = not self.optical_flow_enabled
print(f"DEBUG: Optical flow toggled to {self.optical_flow_enabled}")
# If enabling optical flow, fill all gaps between existing features
if self.optical_flow_enabled:
self._fill_all_gaps_with_interpolation()
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"): 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: