Compare commits
17 Commits
c3bf49f301
...
2013ccf627
Author | SHA1 | Date | |
---|---|---|---|
2013ccf627 | |||
e1d94f2b24 | |||
9df6d73db8 | |||
01340a0a81 | |||
44ed4220b9 | |||
151744d144 | |||
e823a11929 | |||
c1c01e86ca | |||
184aceeee3 | |||
db2aa57ce5 | |||
92c2e62166 | |||
86c31a49d9 | |||
f5b8656bc2 | |||
b9c60ffc25 | |||
b6c7863b77 | |||
612d024161 | |||
840440eb1a |
604
croppa/main.py
604
croppa/main.py
@@ -4,7 +4,7 @@ import cv2
|
||||
import argparse
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
from typing import List, Dict, Any
|
||||
import time
|
||||
import re
|
||||
import threading
|
||||
@@ -200,24 +200,6 @@ class FeatureTracker:
|
||||
|
||||
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
|
||||
def clear_features(self):
|
||||
@@ -878,14 +860,19 @@ class VideoEditor:
|
||||
self.previous_frame_for_flow = None
|
||||
|
||||
# Template matching tracking
|
||||
self.template_matching_enabled = False
|
||||
self.tracking_template = None
|
||||
self.template_region = None
|
||||
self.template_match_history = [] # Store recent match confidences for adaptive thresholding
|
||||
self.multi_scale_template_matching = False # Disable multi-scale by default # (x, y, w, h) in rotated frame coordinates
|
||||
# (x, y, w, h) in rotated frame coordinates
|
||||
self.template_selection_start = None
|
||||
self.template_selection_rect = None
|
||||
|
||||
# Multiple templates system
|
||||
self.templates = {} # {template_id: {'template': image, 'region': (x,y,w,h), 'start_frame': int, 'end_frame': int}}
|
||||
self.current_template_id = None
|
||||
self.template_id_counter = 0
|
||||
|
||||
# Template matching modes
|
||||
self.template_matching_full_frame = False # Toggle for full frame vs cropped template matching
|
||||
|
||||
# Project view mode
|
||||
self.project_view_mode = False
|
||||
self.project_view = None
|
||||
@@ -932,9 +919,15 @@ class VideoEditor:
|
||||
'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(),
|
||||
'template_matching_enabled': self.template_matching_enabled,
|
||||
'template_region': self.template_region,
|
||||
'multi_scale_template_matching': self.multi_scale_template_matching
|
||||
'template_matching_full_frame': self.template_matching_full_frame,
|
||||
'templates': {str(k): {
|
||||
'template': None, # Don't save template images (too large)
|
||||
'region': v['region'],
|
||||
'start_frame': v['start_frame'],
|
||||
'end_frame': v['end_frame']
|
||||
} for k, v in self.templates.items()},
|
||||
'current_template_id': self.current_template_id,
|
||||
'template_id_counter': self.template_id_counter
|
||||
}
|
||||
|
||||
with open(state_file, 'w') as f:
|
||||
@@ -1023,14 +1016,25 @@ class VideoEditor:
|
||||
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
|
||||
if 'multi_scale_template_matching' in state:
|
||||
self.multi_scale_template_matching = state['multi_scale_template_matching'] # Will be recreated on first use
|
||||
if 'template_matching_full_frame' in state:
|
||||
self.template_matching_full_frame = state['template_matching_full_frame']
|
||||
|
||||
# Load multiple templates state
|
||||
if 'templates' in state:
|
||||
self.templates = {}
|
||||
for template_id_str, template_data in state['templates'].items():
|
||||
template_id = int(template_id_str)
|
||||
self.templates[template_id] = {
|
||||
'template': None, # Will be recreated when needed
|
||||
'region': template_data['region'],
|
||||
'start_frame': template_data['start_frame'],
|
||||
'end_frame': template_data['end_frame']
|
||||
}
|
||||
print(f"Loaded {len(self.templates)} templates")
|
||||
if 'current_template_id' in state:
|
||||
self.current_template_id = state['current_template_id']
|
||||
if 'template_id_counter' in state:
|
||||
self.template_id_counter = state['template_id_counter']
|
||||
|
||||
# Validate cut markers against current video length
|
||||
if self.cut_start_frame is not None and self.cut_start_frame >= self.total_frames:
|
||||
@@ -1376,6 +1380,10 @@ class VideoEditor:
|
||||
else:
|
||||
print(f"DEBUG: Frame {self.current_frame} has NO features")
|
||||
|
||||
# Select the best template for the new frame
|
||||
if self.templates:
|
||||
self._select_best_template_for_frame(self.current_frame)
|
||||
|
||||
# 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}")
|
||||
|
||||
@@ -1647,37 +1655,44 @@ class VideoEditor:
|
||||
|
||||
# Calculate offset from template matching if enabled
|
||||
template_offset = None
|
||||
if self.template_matching_enabled and self.tracking_template is not None:
|
||||
if self.templates and self.current_template_id 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)
|
||||
if self.template_matching_full_frame:
|
||||
# Full frame mode - use the entire original frame
|
||||
result = self.track_template(self.current_display_frame)
|
||||
if result:
|
||||
center_x, center_y, confidence = result
|
||||
print(f"DEBUG: Template match found at ({center_x}, {center_y}) with confidence {confidence:.2f}")
|
||||
template_offset = (center_x, center_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}")
|
||||
# Cropped mode - use only the cropped region for 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:
|
||||
# Apply motion tracking offset to the cropped frame
|
||||
offset_frame = self._apply_motion_tracking_offset(cropped_frame, base_pos)
|
||||
if offset_frame is not None:
|
||||
# Track template in cropped and offset frame (much faster!)
|
||||
result = self.track_template(offset_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)
|
||||
# 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 with offset
|
||||
offset_frame = self._apply_motion_tracking_offset(self.current_display_frame, base_pos)
|
||||
if offset_frame is not None:
|
||||
result = self.track_template(offset_frame)
|
||||
if result:
|
||||
center_x, center_y, confidence = result
|
||||
template_offset = (center_x, center_y)
|
||||
|
||||
# Calculate offset from feature tracking if enabled
|
||||
feature_offset = None
|
||||
@@ -1765,35 +1780,91 @@ class VideoEditor:
|
||||
return (x1 + t * (x2 - x1), y1 + t * (y2 - y1))
|
||||
return None
|
||||
|
||||
def _apply_motion_tracking_offset(self, frame, base_pos):
|
||||
"""Apply motion tracking offset to frame for template matching"""
|
||||
if base_pos is None:
|
||||
return frame
|
||||
|
||||
try:
|
||||
# Get the motion tracking offset
|
||||
offset_x, offset_y = base_pos
|
||||
|
||||
# Create offset frame by shifting the content
|
||||
h, w = frame.shape[:2]
|
||||
offset_frame = np.zeros_like(frame)
|
||||
|
||||
# Calculate the shift
|
||||
shift_x = int(offset_x)
|
||||
shift_y = int(offset_y)
|
||||
|
||||
# Apply the offset
|
||||
if shift_x != 0 or shift_y != 0:
|
||||
# Calculate source and destination regions
|
||||
src_x1 = max(0, -shift_x)
|
||||
src_y1 = max(0, -shift_y)
|
||||
src_x2 = min(w, w - shift_x)
|
||||
src_y2 = min(h, h - shift_y)
|
||||
|
||||
dst_x1 = max(0, shift_x)
|
||||
dst_y1 = max(0, shift_y)
|
||||
dst_x2 = min(w, w + shift_x)
|
||||
dst_y2 = min(h, h + shift_y)
|
||||
|
||||
if src_x2 > src_x1 and src_y2 > src_y1 and dst_x2 > dst_x1 and dst_y2 > dst_y1:
|
||||
offset_frame[dst_y1:dst_y2, dst_x1:dst_x2] = frame[src_y1:src_y2, src_x1:src_x2]
|
||||
else:
|
||||
offset_frame = frame.copy()
|
||||
else:
|
||||
offset_frame = frame.copy()
|
||||
|
||||
return offset_frame
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error applying motion tracking offset: {e}")
|
||||
return frame
|
||||
|
||||
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:
|
||||
if not self.templates or self.current_template_id 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)
|
||||
# Get base position for motion tracking offset
|
||||
base_pos = self._get_manual_tracking_position(frame_number)
|
||||
|
||||
if self.template_matching_full_frame:
|
||||
# Full frame mode - use the entire original frame
|
||||
result = self.track_template(self.current_display_frame)
|
||||
if result:
|
||||
center_x, center_y, confidence = result
|
||||
return (center_x, center_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)
|
||||
# Cropped mode - use only the cropped region for 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:
|
||||
# Apply motion tracking offset to the cropped frame
|
||||
offset_frame = self._apply_motion_tracking_offset(cropped_frame, base_pos)
|
||||
if offset_frame is not None:
|
||||
# Track template in cropped and offset frame (much faster!)
|
||||
result = self.track_template(offset_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 with offset
|
||||
offset_frame = self._apply_motion_tracking_offset(self.current_display_frame, base_pos)
|
||||
if offset_frame is not None:
|
||||
result = self.track_template(offset_frame)
|
||||
if result:
|
||||
center_x, center_y, confidence = result
|
||||
return (center_x, center_y, confidence)
|
||||
|
||||
return None
|
||||
|
||||
@@ -1866,8 +1937,6 @@ class VideoEditor:
|
||||
|
||||
def _map_screen_to_rotated(self, sx, sy):
|
||||
"""Map a point on canvas screen coords back to ROTATED frame coords (pre-crop)."""
|
||||
frame_number = getattr(self, 'current_frame', 0)
|
||||
angle = self.rotation_angle
|
||||
# Use unified display params
|
||||
params = self._get_display_params()
|
||||
# Back to processed (zoomed+cropped) space
|
||||
@@ -2208,74 +2277,36 @@ class VideoEditor:
|
||||
|
||||
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
|
||||
if self.current_template_id is None or self.current_template_id not in self.templates:
|
||||
return None
|
||||
|
||||
template_data = self.templates[self.current_template_id]
|
||||
tracking_template = template_data['template']
|
||||
|
||||
if tracking_template is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Apply image preprocessing for better template matching
|
||||
gray_frame, gray_template = self._improve_template_matching(frame, self.tracking_template)
|
||||
gray_frame, gray_template = self._improve_template_matching(frame, tracking_template)
|
||||
|
||||
# Multi-scale template matching for better tracking (if enabled)
|
||||
if self.multi_scale_template_matching:
|
||||
scales = [0.8, 0.9, 1.0, 1.1, 1.2] # Different scales to try
|
||||
# Single-scale template matching (faster)
|
||||
result = cv2.matchTemplate(gray_frame, gray_template, cv2.TM_CCOEFF_NORMED)
|
||||
_, max_val, _, max_loc = cv2.minMaxLoc(result)
|
||||
|
||||
if max_val > 0.6: # Higher threshold for single-scale
|
||||
template_h, template_w = gray_template.shape
|
||||
center_x = max_loc[0] + template_w // 2
|
||||
center_y = max_loc[1] + template_h // 2
|
||||
best_match = (center_x, center_y, max_val)
|
||||
best_confidence = max_val
|
||||
else:
|
||||
best_match = None
|
||||
best_confidence = 0.0
|
||||
|
||||
for scale in scales:
|
||||
# Resize template
|
||||
template_h, template_w = gray_template.shape
|
||||
new_w = int(template_w * scale)
|
||||
new_h = int(template_h * scale)
|
||||
|
||||
if new_w <= 0 or new_h <= 0 or new_w > gray_frame.shape[1] or new_h > gray_frame.shape[0]:
|
||||
continue
|
||||
|
||||
scaled_template = cv2.resize(gray_template, (new_w, new_h))
|
||||
|
||||
# Perform template matching
|
||||
result = cv2.matchTemplate(gray_frame, scaled_template, cv2.TM_CCOEFF_NORMED)
|
||||
_, max_val, _, max_loc = cv2.minMaxLoc(result)
|
||||
|
||||
# Check if this is the best match so far
|
||||
if max_val > best_confidence:
|
||||
best_confidence = max_val
|
||||
# Get center of template
|
||||
center_x = max_loc[0] + new_w // 2
|
||||
center_y = max_loc[1] + new_h // 2
|
||||
best_match = (center_x, center_y, max_val)
|
||||
else:
|
||||
# Single-scale template matching (faster)
|
||||
result = cv2.matchTemplate(gray_frame, gray_template, cv2.TM_CCOEFF_NORMED)
|
||||
_, max_val, _, max_loc = cv2.minMaxLoc(result)
|
||||
|
||||
if max_val > 0.6: # Higher threshold for single-scale
|
||||
template_h, template_w = gray_template.shape
|
||||
center_x = max_loc[0] + template_w // 2
|
||||
center_y = max_loc[1] + template_h // 2
|
||||
best_match = (center_x, center_y, max_val)
|
||||
best_confidence = max_val
|
||||
else:
|
||||
best_match = None
|
||||
best_confidence = 0.0
|
||||
|
||||
# Adaptive thresholding based on recent match history
|
||||
if len(self.template_match_history) > 0:
|
||||
# Use average of recent matches as baseline
|
||||
@@ -2298,36 +2329,6 @@ class VideoEditor:
|
||||
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 _improve_template_matching(self, frame, template):
|
||||
"""Apply image preprocessing to improve template matching"""
|
||||
@@ -2383,15 +2384,166 @@ class VideoEditor:
|
||||
# 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}")
|
||||
# Add template to collection
|
||||
template_id = self.add_template(template, (raw_x, raw_y, raw_w, raw_h))
|
||||
self.show_feedback_message(f"Template {template_id} set from region ({raw_w}x{raw_h})")
|
||||
print(f"DEBUG: Template {template_id} 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 add_template(self, template, region, start_frame=None, end_frame=None):
|
||||
"""Add a new template to the collection"""
|
||||
template_id = self.template_id_counter
|
||||
self.template_id_counter += 1
|
||||
|
||||
if start_frame is None:
|
||||
start_frame = self.current_frame
|
||||
if end_frame is None:
|
||||
end_frame = self.total_frames - 1
|
||||
|
||||
# Only end the current template if it exists and starts at or before the current frame
|
||||
if self.current_template_id is not None and self.current_template_id in self.templates:
|
||||
current_template = self.templates[self.current_template_id]
|
||||
if current_template['start_frame'] <= self.current_frame:
|
||||
# End the current template at the previous frame
|
||||
self.templates[self.current_template_id]['end_frame'] = self.current_frame - 1
|
||||
print(f"DEBUG: Ended current template {self.current_template_id} at frame {self.current_frame - 1}")
|
||||
|
||||
self.templates[template_id] = {
|
||||
'template': template.copy(),
|
||||
'region': region,
|
||||
'start_frame': start_frame,
|
||||
'end_frame': end_frame
|
||||
}
|
||||
|
||||
# Set as current template
|
||||
self.current_template_id = template_id
|
||||
|
||||
self.show_feedback_message(f"Template {template_id} added (frames {start_frame}-{end_frame})")
|
||||
return template_id
|
||||
|
||||
def remove_template(self, template_id):
|
||||
"""Remove a template from the collection"""
|
||||
if template_id in self.templates:
|
||||
del self.templates[template_id]
|
||||
|
||||
# If we removed the current template, find a new one
|
||||
if self.current_template_id == template_id:
|
||||
self._select_best_template_for_frame(self.current_frame)
|
||||
|
||||
# If no templates left, clear current template
|
||||
if not self.templates:
|
||||
self.current_template_id = None
|
||||
|
||||
self.show_feedback_message(f"Template {template_id} removed")
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_template_for_frame(self, frame_number):
|
||||
"""Get the most recent template that covers this frame"""
|
||||
# Find the most recent template (highest ID) that covers this frame
|
||||
best_template_id = None
|
||||
|
||||
for template_id, template_data in self.templates.items():
|
||||
start_frame = template_data['start_frame']
|
||||
end_frame = template_data['end_frame']
|
||||
|
||||
# Check if template covers this frame
|
||||
if start_frame <= frame_number <= end_frame:
|
||||
# Use the template with the highest ID (most recent)
|
||||
if best_template_id is None or template_id > best_template_id:
|
||||
best_template_id = template_id
|
||||
|
||||
return best_template_id
|
||||
|
||||
def _select_best_template_for_frame(self, frame_number):
|
||||
"""Select the most recent template for the current frame"""
|
||||
best_template_id = self.get_template_for_frame(frame_number)
|
||||
|
||||
if best_template_id is not None:
|
||||
self.current_template_id = best_template_id
|
||||
template_data = self.templates[best_template_id]
|
||||
|
||||
# Recreate template if it's None (loaded from state)
|
||||
if template_data['template'] is None:
|
||||
if self.current_display_frame is not None:
|
||||
success = self._recreate_template_from_template_data(best_template_id, self.current_display_frame)
|
||||
if not success:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
# Template is already stored in the templates dict
|
||||
return True
|
||||
else:
|
||||
self.current_template_id = None
|
||||
return False
|
||||
|
||||
def _recreate_template_from_template_data(self, template_id, frame):
|
||||
"""Recreate template from template data region"""
|
||||
try:
|
||||
template_data = self.templates[template_id]
|
||||
x, y, w, h = template_data['region']
|
||||
|
||||
# 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.templates[template_id]['template'] = template.copy()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error recreating template {template_id}: {e}")
|
||||
return False
|
||||
|
||||
def jump_to_previous_template(self):
|
||||
"""Jump to the previous template marker (frame where template was created)."""
|
||||
if self.is_image_mode:
|
||||
return
|
||||
self.stop_auto_repeat_seek()
|
||||
template_frames = sorted([data['start_frame'] for data in self.templates.values()])
|
||||
if not template_frames:
|
||||
print("DEBUG: No template markers; prev jump ignored")
|
||||
return
|
||||
current = self.current_frame
|
||||
candidates = [f for f in template_frames if f < current]
|
||||
if candidates:
|
||||
target = candidates[-1]
|
||||
print(f"DEBUG: Jump prev template from {current} -> {target}; template_frames={template_frames}")
|
||||
self.seek_to_frame(target)
|
||||
else:
|
||||
target = template_frames[0]
|
||||
print(f"DEBUG: Jump prev template to first marker from {current} -> {target}; template_frames={template_frames}")
|
||||
self.seek_to_frame(target)
|
||||
|
||||
def jump_to_next_template(self):
|
||||
"""Jump to the next template marker (frame where template was created)."""
|
||||
if self.is_image_mode:
|
||||
return
|
||||
self.stop_auto_repeat_seek()
|
||||
template_frames = sorted([data['start_frame'] for data in self.templates.values()])
|
||||
if not template_frames:
|
||||
print("DEBUG: No template markers; next jump ignored")
|
||||
return
|
||||
current = self.current_frame
|
||||
for f in template_frames:
|
||||
if f > current:
|
||||
print(f"DEBUG: Jump next template from {current} -> {f}; template_frames={template_frames}")
|
||||
self.seek_to_frame(f)
|
||||
return
|
||||
target = template_frames[-1]
|
||||
print(f"DEBUG: Jump next template to last marker from {current} -> {target}; template_frames={template_frames}")
|
||||
self.seek_to_frame(target)
|
||||
|
||||
def apply_rotation(self, frame):
|
||||
"""Apply rotation to frame"""
|
||||
@@ -2847,6 +2999,41 @@ class VideoEditor:
|
||||
1,
|
||||
)
|
||||
|
||||
# Draw template markers
|
||||
for template_id, template_data in self.templates.items():
|
||||
start_frame = template_data['start_frame']
|
||||
end_frame = template_data['end_frame']
|
||||
|
||||
# Draw template range
|
||||
start_progress = start_frame / max(1, self.total_frames - 1)
|
||||
end_progress = end_frame / max(1, self.total_frames - 1)
|
||||
start_x = bar_x_start + int(bar_width * start_progress)
|
||||
end_x = bar_x_start + int(bar_width * end_progress)
|
||||
|
||||
# Template range color (green for active, blue for inactive)
|
||||
is_active = (self.current_template_id == template_id)
|
||||
template_color = (0, 255, 0) if is_active else (255, 0, 0) # Green if active, red if inactive
|
||||
|
||||
# Draw template range bar
|
||||
cv2.rectangle(
|
||||
frame,
|
||||
(start_x, bar_y + 2),
|
||||
(end_x, bar_y + self.TIMELINE_BAR_HEIGHT - 2),
|
||||
template_color,
|
||||
-1,
|
||||
)
|
||||
|
||||
# Draw template ID number
|
||||
cv2.putText(
|
||||
frame,
|
||||
str(template_id),
|
||||
(start_x + 2, bar_y + 10),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.3,
|
||||
(255, 255, 255),
|
||||
1,
|
||||
)
|
||||
|
||||
def display_current_frame(self):
|
||||
"""Display the current frame with all overlays"""
|
||||
if self.current_display_frame is None:
|
||||
@@ -2961,11 +3148,10 @@ class VideoEditor:
|
||||
feature_text = f" | Features: {feature_count} pts"
|
||||
if self.optical_flow_enabled:
|
||||
feature_text += " (OPTICAL FLOW)"
|
||||
template_text = (
|
||||
f" | Template: {self.template_matching_enabled}" if self.template_matching_enabled else ""
|
||||
)
|
||||
if self.template_matching_enabled and self.multi_scale_template_matching:
|
||||
template_text += " (MULTI-SCALE)"
|
||||
template_text = ""
|
||||
if self.templates:
|
||||
mode = "Full Frame" if self.template_matching_full_frame else "Cropped"
|
||||
template_text = f" | Template: {mode}"
|
||||
autorepeat_text = (
|
||||
f" | Loop: ON" if self.looping_between_markers else ""
|
||||
)
|
||||
@@ -3079,8 +3265,8 @@ class VideoEditor:
|
||||
|
||||
# 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):
|
||||
self.templates and
|
||||
self.current_template_id is not None):
|
||||
# Get template matching position for current frame
|
||||
template_pos = self._get_template_matching_position(self.current_frame)
|
||||
if template_pos:
|
||||
@@ -3094,6 +3280,7 @@ class VideoEditor:
|
||||
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
|
||||
@@ -3312,6 +3499,25 @@ class VideoEditor:
|
||||
# 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:
|
||||
# First check for template removal (like motion tracking points)
|
||||
if self.templates:
|
||||
screen_x, screen_y = x, y
|
||||
raw_x, raw_y = self._map_screen_to_rotated(screen_x, screen_y)
|
||||
|
||||
for template_id, template_data in self.templates.items():
|
||||
# Only check templates that cover current frame
|
||||
if (template_data['start_frame'] <= self.current_frame <= template_data['end_frame']):
|
||||
tx, ty, tw, th = template_data['region']
|
||||
center_x = tx + tw // 2
|
||||
center_y = ty + th // 2
|
||||
|
||||
# Check if click is within 10px of template center
|
||||
distance = ((raw_x - center_x) ** 2 + (raw_y - center_y) ** 2) ** 0.5
|
||||
if distance <= 10:
|
||||
self.remove_template(template_id)
|
||||
self.save_state()
|
||||
return
|
||||
|
||||
# Store tracking points in ROTATED frame coordinates (pre-crop)
|
||||
rx, ry = self._map_screen_to_rotated(x, y)
|
||||
threshold = self.TRACKING_POINT_THRESHOLD
|
||||
@@ -3686,13 +3892,10 @@ class VideoEditor:
|
||||
|
||||
def _render_video_worker(self, output_path: str):
|
||||
"""Worker method that runs in the render thread"""
|
||||
render_cap = None
|
||||
try:
|
||||
if not output_path.endswith(".mp4"):
|
||||
output_path += ".mp4"
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Send progress update to main thread
|
||||
self.render_progress_queue.put(("init", "Initializing render...", 0.0, 0.0))
|
||||
|
||||
@@ -4503,16 +4706,25 @@ class VideoEditor:
|
||||
self.show_feedback_message(f"Optical flow {'ON' if self.optical_flow_enabled else 'OFF'}")
|
||||
self.save_state()
|
||||
elif key == ord("m"):
|
||||
# Toggle template matching tracking
|
||||
self.template_matching_enabled = not self.template_matching_enabled
|
||||
print(f"DEBUG: Template matching toggled to {self.template_matching_enabled}")
|
||||
self.show_feedback_message(f"Template matching {'ON' if self.template_matching_enabled else 'OFF'}")
|
||||
# Clear all templates
|
||||
if self.templates:
|
||||
self.templates.clear()
|
||||
self.current_template_id = None
|
||||
print("DEBUG: All templates cleared")
|
||||
self.show_feedback_message("All templates cleared")
|
||||
else:
|
||||
print("DEBUG: No templates to clear")
|
||||
self.show_feedback_message("No templates to clear")
|
||||
self.save_state()
|
||||
elif key == ord("M"): # Shift+M - Toggle multi-scale template matching
|
||||
self.multi_scale_template_matching = not self.multi_scale_template_matching
|
||||
print(f"DEBUG: Multi-scale template matching toggled to {self.multi_scale_template_matching}")
|
||||
self.show_feedback_message(f"Multi-scale template matching {'ON' if self.multi_scale_template_matching else 'OFF'}")
|
||||
self.template_matching_full_frame = not self.template_matching_full_frame
|
||||
print(f"DEBUG: Template matching full frame toggled to {self.template_matching_full_frame}")
|
||||
self.show_feedback_message(f"Template matching: {'Full Frame' if self.template_matching_full_frame else 'Cropped'}")
|
||||
self.save_state()
|
||||
elif key == ord(";"): # Semicolon - Jump to previous template marker
|
||||
self.jump_to_previous_template()
|
||||
elif key == ord(":"): # Colon - Jump to next template marker
|
||||
self.jump_to_next_template()
|
||||
elif key == ord("t"):
|
||||
# Marker looping only for videos
|
||||
if not self.is_image_mode:
|
||||
|
Reference in New Issue
Block a user