Compare commits
16 Commits
463228baf5
...
10284dad81
Author | SHA1 | Date | |
---|---|---|---|
10284dad81 | |||
a2dc4a2186 | |||
5d76681ded | |||
f8acef2da4 | |||
65b80034cb | |||
5400592afd | |||
e6616ed1b1 | |||
048e8ef033 | |||
c08d5c5999 | |||
8c1efb1b05 | |||
f942392fb3 | |||
c749d9af80 | |||
71e5870306 | |||
e813be2890 | |||
80fb35cced | |||
d8b4439382 |
625
croppa/main.py
625
croppa/main.py
@@ -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:
|
||||
|
Reference in New Issue
Block a user