Compare commits

...

16 Commits

Author SHA1 Message Date
10284dad81 Implement template matching position retrieval in VideoEditor
This commit introduces a new method to get the template matching position and confidence for a specific frame. It optimizes the tracking process by utilizing cropped regions for faster template matching while maintaining functionality for full frame matching when no crop is set. Additionally, it updates the rendering logic to visualize the template matching point on the canvas, enhancing user feedback with confidence levels displayed. This update improves the overall accuracy and efficiency of the template matching feature.
2025-09-26 14:51:04 +02:00
a2dc4a2186 Refactor tracking position calculation in VideoEditor to compute the average of all available positions from manual, template, and feature tracking methods. This change simplifies the logic by aggregating positions instead of calculating weighted offsets, enhancing accuracy and clarity in tracking results. Debug messages have been updated to reflect the new averaging process. 2025-09-26 14:48:17 +02:00
5d76681ded Refactor tracking position calculation in VideoEditor to combine manual, template, and feature tracking methods. This update introduces a more robust approach to determine the final tracking position by calculating offsets from both template matching and feature tracking, weighted appropriately against a base position derived from manual tracking points. The logic ensures smoother transitions and improved accuracy in tracking results, with debug messages added for better insight into the combined tracking process. 2025-09-26 14:44:19 +02:00
f8acef2da4 Update VideoEditor to set current display frame during rendering for improved motion tracking
This commit adds functionality to store the current display frame and update the current frame index during the rendering process. This enhancement supports better motion tracking by ensuring the correct frame is referenced, contributing to more accurate processing of video frames.
2025-09-26 14:37:23 +02:00
65b80034cb Enhance template matching in VideoEditor by utilizing cropped regions for improved speed and accuracy. This update modifies the tracking logic to prioritize cropped areas for faster processing, while maintaining functionality for full frame matching when no crop is set. Debug messages have been refined to provide clearer insights into tracking results and confidence levels. 2025-09-26 14:35:51 +02:00
5400592afd Refactor template matching in VideoEditor to use raw frame coordinates for improved accuracy. This update simplifies the tracking logic by directly utilizing the raw display frame, eliminating unnecessary transformations. Additionally, it enhances the template setting process by ensuring the selected region is correctly mapped to raw frame coordinates, improving usability and feedback during template selection. Debug messages have been updated to reflect these changes. 2025-09-26 14:34:48 +02:00
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}")
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]]:
"""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
positions = self.features[frame_number]['positions']
if not positions:
return None
# Calculate average position
avg_x = sum(pos[0] for pos in positions) / len(positions)
avg_y = sum(pos[1] for pos in positions) / len(positions)
return (avg_x, avg_y)
@@ -834,6 +873,17 @@ class VideoEditor:
self.selective_feature_deletion_start = 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
self.project_view_mode = False
self.project_view = None
@@ -879,7 +929,9 @@ class VideoEditor:
'is_playing': getattr(self, 'is_playing', False),
'tracking_enabled': self.tracking_enabled,
'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:
@@ -967,6 +1019,14 @@ class VideoEditor:
self.feature_tracker.load_state_dict(state['feature_tracker'])
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
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")
@@ -1297,9 +1357,20 @@ class VideoEditor:
def seek_to_frame(self, frame_number: int):
"""Seek to specific frame"""
old_frame = self.current_frame
self.current_frame = max(0, min(frame_number, self.total_frames - 1))
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
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:
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):
"""Jump to the previous tracking marker (frame with tracking points)."""
if self.is_image_mode:
@@ -1552,15 +1637,100 @@ 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
if self.feature_tracker.tracking_enabled:
feature_pos = self.feature_tracker.get_tracking_position(frame_number)
if feature_pos:
# Features are stored in rotated frame coordinates (like existing motion tracking)
# We can use them directly for the tracking system
return (feature_pos[0], feature_pos[1])
# Get base position from manual tracking points
base_pos = self._get_manual_tracking_position(frame_number)
# Fall back to manual tracking points
# Calculate offset from template matching if enabled
template_offset = None
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 from raw frame
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 raw frame coordinates
# Add crop offset back
raw_x = center_x + crop_x
raw_y = center_y + crop_y
template_offset = (raw_x, raw_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}")
# Template matching returns coordinates in raw frame space
template_offset = (center_x, center_y)
# Calculate offset from feature tracking if enabled
feature_offset = None
if self.feature_tracker.tracking_enabled:
# Get the nearest frames with features for smooth interpolation
feature_frames = sorted(self.feature_tracker.features.keys())
if feature_frames:
# Find the two nearest frames for interpolation
if frame_number <= feature_frames[0]:
# Before first feature frame - use first frame
feature_offset = self._get_feature_center(feature_frames[0])
elif frame_number >= feature_frames[-1]:
# After last feature frame - use last frame
feature_offset = 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]:
feature_offset = self._interpolate_feature_positions(
feature_frames[i], feature_frames[i + 1], frame_number
)
break
# Combine tracking methods: average of all available positions
positions = []
# Add manual tracking position
if base_pos:
positions.append(base_pos)
print(f"DEBUG: Manual tracking: ({base_pos[0]:.1f}, {base_pos[1]:.1f})")
# Add template matching position
if template_offset:
positions.append(template_offset)
print(f"DEBUG: Template matching: ({template_offset[0]:.1f}, {template_offset[1]:.1f})")
# Add feature tracking position
if feature_offset:
positions.append(feature_offset)
print(f"DEBUG: Feature tracking: ({feature_offset[0]:.1f}, {feature_offset[1]:.1f})")
# Calculate average of all available positions
if positions:
avg_x = sum(pos[0] for pos in positions) / len(positions)
avg_y = sum(pos[1] for pos in positions) / len(positions)
print(f"DEBUG: Average of {len(positions)} positions: ({avg_x:.1f}, {avg_y:.1f})")
return (avg_x, avg_y)
# Fall back to individual tracking methods if no base position
if template_offset:
return template_offset
elif feature_offset:
return feature_offset
else:
return None
def _get_manual_tracking_position(self, frame_number):
"""Get manual tracking position for a frame"""
if not self.tracking_points:
return None
frames = sorted(self.tracking_points.keys())
@@ -1590,6 +1760,38 @@ class VideoEditor:
return (x1 + t * (x2 - x1), y1 + t * (y2 - y1))
return None
def _get_template_matching_position(self, frame_number):
"""Get template matching position and confidence for a frame"""
if not self.template_matching_enabled or self.tracking_template is None:
return 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 from raw frame
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
# Map from cropped frame coordinates to raw frame coordinates
# Add crop offset back
raw_x = center_x + crop_x
raw_y = center_y + crop_y
return (raw_x, raw_y, confidence)
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
return (center_x, center_y, confidence)
return None
def _get_display_params(self):
"""Unified display transform parameters for current frame in rotated space."""
eff_x, eff_y, eff_w, eff_h = self._get_effective_crop_rect_for_frame(getattr(self, 'current_frame', 0))
@@ -1719,8 +1921,9 @@ class VideoEditor:
orig_x + orig_w <= self.frame_width and
orig_y + orig_h <= self.frame_height):
if self.current_display_frame is not None:
region_frame = self.current_display_frame[orig_y:orig_y+orig_h, orig_x:orig_x+orig_w]
if region_frame.size > 0:
if region_frame is not None and 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
@@ -1786,6 +1989,338 @@ class VideoEditor:
else:
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:
# Map screen coordinates to rotated frame coordinates (raw frame)
# This is what we need for template matching during rendering
rot_x, rot_y = self._map_screen_to_rotated(x, y)
rot_x2, rot_y2 = self._map_screen_to_rotated(x + w, y + h)
# Calculate region in rotated frame coordinates
raw_x = min(rot_x, rot_x2)
raw_y = min(rot_y, rot_y2)
raw_w = abs(rot_x2 - rot_x)
raw_h = abs(rot_y2 - rot_y)
print(f"DEBUG: Mapped to raw frame coordinates ({raw_x}, {raw_y}, {raw_w}, {raw_h})")
# Ensure region is within raw frame bounds
if (raw_x >= 0 and raw_y >= 0 and
raw_x + raw_w <= self.frame_width and
raw_y + raw_h <= self.frame_height):
# Extract template from raw frame
template = self.current_display_frame[raw_y:raw_y+raw_h, raw_x:raw_x+raw_w]
if template.size > 0:
self.tracking_template = template.copy()
self.template_region = (raw_x, raw_y, raw_w, raw_h)
self.show_feedback_message(f"Template set from region ({raw_w}x{raw_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):
"""Apply rotation to frame"""
@@ -2341,6 +2876,8 @@ class VideoEditor:
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_text = f" | Features: {feature_count} pts"
if self.optical_flow_enabled:
feature_text += " (OPTICAL FLOW)"
autorepeat_text = (
f" | Loop: ON" if self.looping_between_markers else ""
)
@@ -2452,6 +2989,23 @@ 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 template matching point (blue circle with confidence)
if (not self.is_image_mode and
self.template_matching_enabled and
self.tracking_template is not None):
# Get template matching position for current frame
template_pos = self._get_template_matching_position(self.current_frame)
if template_pos:
tx, ty, confidence = template_pos
# Map to screen coordinates
sx, sy = self._map_rotated_to_screen(tx, ty)
# Draw blue circle for template matching
cv2.circle(canvas, (sx, sy), 8, (255, 0, 255), -1) # Magenta circle for template
cv2.circle(canvas, (sx, sy), 8, (255, 255, 255), 2)
# Draw confidence text
conf_text = f"{confidence:.2f}"
cv2.putText(canvas, conf_text, (sx + 10, sy - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (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
@@ -2461,6 +3015,11 @@ class VideoEditor:
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 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
if not self.is_image_mode and self.tracking_points:
prev_result = self._get_previous_tracking_point()
@@ -2642,6 +3201,26 @@ class VideoEditor:
self.selective_feature_deletion_start = 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)
if event == cv2.EVENT_RBUTTONDOWN and not (flags & (cv2.EVENT_FLAG_CTRLKEY | cv2.EVENT_FLAG_SHIFTKEY)):
if not self.is_image_mode:
@@ -3346,6 +3925,10 @@ class VideoEditor:
if not ret:
break
# Set current display frame for motion tracking during rendering
self.current_display_frame = frame.copy()
self.current_frame = start_frame + i
processed_frame = self._process_frame_for_render(frame, output_width, output_height, start_frame + i)
if processed_frame is not None:
if i == 0:
@@ -3490,8 +4073,11 @@ class VideoEditor:
print(" g: Toggle auto feature extraction")
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(" Ctrl+Left-click+drag: Set template region for tracking")
if len(self.video_files) > 1:
print(" N: Next video")
print(" n: Previous video")
@@ -3816,6 +4402,23 @@ class VideoEditor:
self.feature_tracker.set_detector_type(new_type)
self.show_feedback_message(f"Detector switched to {new_type}")
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"):
# Marker looping only for videos
if not self.is_image_mode: