Compare commits

...

17 Commits

Author SHA1 Message Date
2013ccf627 Remove logic for future template management in VideoEditor
This commit eliminates the code that removes templates starting after the current frame, streamlining the template handling process. The changes enhance clarity and efficiency in template management during video editing sessions. Debug messages related to template removal have also been removed to reflect this update.
2025-09-26 18:53:10 +02:00
e1d94f2b24 Refactor template management in VideoEditor to streamline template handling
This commit removes unused attributes and logic related to template matching, enhancing the clarity and efficiency of the template management system. The changes include the removal of the template matching enabled flag and associated state management, focusing on the current template ID and its associated data. Debug messages have been updated to reflect these changes, ensuring better tracking and feedback during video editing sessions.
2025-09-26 18:42:35 +02:00
9df6d73db8 Enhance template management in VideoEditor by removing future templates and ending current ones
This commit updates the template handling logic in the VideoEditor to remove any templates that start after the current frame and properly end templates that start at the current frame. This change improves the clarity and efficiency of template management during video editing sessions, ensuring that only relevant templates are active. Debug messages have been added to provide insights into the template removal and ending process.
2025-09-26 18:31:10 +02:00
01340a0a81 Refactor and streamline template matching logic in VideoEditor
This commit removes unused methods and simplifies the template matching process by eliminating multi-scale matching in favor of a single-scale approach. The changes enhance performance and clarity in the tracking logic, ensuring a more efficient template matching experience. Debug messages have been updated to reflect the current state of the template matching process.
2025-09-26 18:30:19 +02:00
44ed4220b9 Refactor template matching logic in VideoEditor to support full frame and cropped modes
This commit introduces a toggle for template matching modes in the VideoEditor, allowing users to choose between full frame and cropped region matching. The logic has been updated to handle both modes effectively, improving flexibility and performance during template tracking. Additionally, the multi-scale template matching feature has been removed, streamlining the template matching process. Debug messages and feedback have been enhanced to reflect the current mode, ensuring better user experience during video editing sessions.
2025-09-26 18:20:12 +02:00
151744d144 Update 2025-09-26 17:56:28 +02:00
e823a11929 Enhance template matching in VideoEditor by applying motion tracking offsets
This commit updates the template matching logic in the VideoEditor to apply motion tracking offsets to the current frame before performing template matching. The new method, _apply_motion_tracking_offset, creates an offset frame based on the base position, improving tracking accuracy. This change simplifies the process by removing the previous cropping logic and ensures that template matching works effectively on the adjusted frame, enhancing overall performance during video editing sessions.
2025-09-26 17:54:56 +02:00
c1c01e86ca Optimize template matching in VideoEditor to utilize cropped regions for improved performance
This commit modifies the template matching logic in the VideoEditor to use only the cropped region of the frame when available, significantly enhancing the speed of template tracking. If no crop is applied, the original frame is used for tracking. This change improves the efficiency of the template matching process while maintaining accuracy in locating objects during video editing sessions.
2025-09-26 17:52:09 +02:00
184aceeee3 Enhance template selection and tracking logic in VideoEditor
This commit introduces a mechanism to select the best template for the current frame if templates are available. Additionally, it updates the template matching process to apply tracking directly on the original frame, avoiding infinite recursion and improving tracking accuracy. These changes enhance the overall functionality and reliability of the template management system during video editing sessions.
2025-09-26 17:50:07 +02:00
db2aa57ce5 Refactor template matching logic in VideoEditor to utilize transformed frames
This commit updates the template matching process in the VideoEditor to apply tracking on the already transformed frame, which includes crop, zoom, and rotation adjustments. This change enhances tracking accuracy by ensuring that the template can locate the object in its modified position, while also improving code clarity and performance during video editing sessions.
2025-09-26 17:49:29 +02:00
92c2e62166 Refactor template matching logic in VideoEditor to use original frame for tracking
This commit updates the template matching process in the VideoEditor to apply tracking directly on the original frame instead of a cropped region. This change ensures that the template can accurately locate the object in its original position, improving tracking reliability. The code has been simplified by removing the previous cropping logic, enhancing overall clarity and performance during video editing sessions.
2025-09-26 17:45:19 +02:00
86c31a49d9 Refactor template management in VideoEditor to enhance tracking functionality
This commit removes the initial template selection logic from the tracking method and introduces a mechanism to disable template matching when no templates are available. Additionally, it updates the comments to clarify the enabling of template matching upon template creation, improving the overall management of templates during video editing sessions.
2025-09-26 17:42:30 +02:00
f5b8656bc2 Refactor template navigation methods in VideoEditor for improved functionality
This commit renames and updates the template navigation methods in the VideoEditor to enhance user experience. The methods now allow users to jump directly to the previous or next template markers, improving frame handling and feedback. Debug messages have been added to provide clearer insights during navigation, ensuring users are informed about the current actions and template positions.
2025-09-26 17:39:02 +02:00
b9c60ffc25 Refactor template dot rendering and right-click functionality in VideoEditor
This commit simplifies the rendering of template dots in the VideoEditor by standardizing their appearance to match motion tracking points. Additionally, it enhances the right-click functionality to allow for template removal when clicking near template centers, improving user interaction and feedback during video editing sessions.
2025-09-26 17:37:35 +02:00
b6c7863b77 Refactor template navigation in VideoEditor for improved frame handling and user feedback
This commit updates the navigation methods for templates in the VideoEditor, enhancing the logic to jump to the start frame of the selected template. The feedback messages have been improved to provide clearer information about the selected template and its corresponding frame. Additionally, visual indicators for active templates have been added, allowing users to easily identify which templates are currently in use during video editing sessions.
2025-09-26 17:34:27 +02:00
612d024161 Update VideoEditor to manage template lifecycle and improve frame handling
This commit enhances the VideoEditor by implementing logic to end the previous template at the current frame when a new template is added. It also updates the method for retrieving the best template for a given frame to prioritize the most recent template. Additionally, a new method is introduced to recreate templates from their data region, ensuring templates are correctly initialized when needed. These changes improve template management and user feedback during video editing sessions.
2025-09-26 17:31:34 +02:00
840440eb1a Add multiple templates management to VideoEditor
This commit introduces a system for managing multiple templates within the VideoEditor. Users can now add, remove, and navigate through templates, enhancing the flexibility of template tracking. The state management has been updated to save and load multiple templates, including their regions and frame ranges. Additionally, visual indicators for active templates have been implemented in the frame display, improving user feedback during editing sessions.
2025-09-26 17:23:04 +02:00

View File

@@ -4,7 +4,7 @@ import cv2
import argparse import argparse
import numpy as np import numpy as np
from pathlib import Path from pathlib import Path
from typing import List, Optional, Tuple, Dict, Any from typing import List, Dict, Any
import time import time
import re import re
import threading 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): def clear_features(self):
@@ -878,13 +860,18 @@ class VideoEditor:
self.previous_frame_for_flow = None self.previous_frame_for_flow = None
# Template matching tracking # 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.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_start = None
self.template_selection_rect = 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 # Project view mode
self.project_view_mode = False self.project_view_mode = False
@@ -932,9 +919,15 @@ class VideoEditor:
'tracking_enabled': self.tracking_enabled, 'tracking_enabled': self.tracking_enabled,
'tracking_points': {str(k): v for k, v in self.tracking_points.items()}, 'tracking_points': {str(k): v for k, v in self.tracking_points.items()},
'feature_tracker': self.feature_tracker.get_state_dict(), 'feature_tracker': self.feature_tracker.get_state_dict(),
'template_matching_enabled': self.template_matching_enabled, 'template_matching_full_frame': self.template_matching_full_frame,
'template_region': self.template_region, 'templates': {str(k): {
'multi_scale_template_matching': self.multi_scale_template_matching '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: with open(state_file, 'w') as f:
@@ -1023,14 +1016,25 @@ class VideoEditor:
print(f"Loaded feature tracker state") print(f"Loaded feature tracker state")
# Load template matching state # Load template matching state
if 'template_matching_enabled' in state: if 'template_matching_full_frame' in state:
self.template_matching_enabled = state['template_matching_enabled'] self.template_matching_full_frame = state['template_matching_full_frame']
if 'template_region' in state and state['template_region'] is not None:
self.template_region = state['template_region'] # Load multiple templates state
# Recreate template from region when needed if 'templates' in state:
self.tracking_template = None self.templates = {}
if 'multi_scale_template_matching' in state: for template_id_str, template_data in state['templates'].items():
self.multi_scale_template_matching = state['multi_scale_template_matching'] # Will be recreated on first use 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 # Validate cut markers against current video length
if self.cut_start_frame is not None and self.cut_start_frame >= self.total_frames: if self.cut_start_frame is not None and self.cut_start_frame >= self.total_frames:
@@ -1375,6 +1379,10 @@ class VideoEditor:
print(f"DEBUG: Frame {self.current_frame} has {feature_count} features") print(f"DEBUG: Frame {self.current_frame} has {feature_count} features")
else: else:
print(f"DEBUG: Frame {self.current_frame} has NO features") 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 # Auto-extract features if feature tracking is enabled and auto-tracking is on
print(f"DEBUG: seek_to_frame {frame_number}: is_image_mode={self.is_image_mode}, tracking_enabled={self.feature_tracker.tracking_enabled}, auto_tracking={self.feature_tracker.auto_tracking}, display_frame={self.current_display_frame is not None}") print(f"DEBUG: seek_to_frame {frame_number}: is_image_mode={self.is_image_mode}, tracking_enabled={self.feature_tracker.tracking_enabled}, auto_tracking={self.feature_tracker.auto_tracking}, display_frame={self.current_display_frame is not None}")
@@ -1647,37 +1655,44 @@ class VideoEditor:
# Calculate offset from template matching if enabled # Calculate offset from template matching if enabled
template_offset = None 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: if self.current_display_frame is not None:
# Use only the cropped region for much faster template matching if self.template_matching_full_frame:
if self.crop_rect: # Full frame mode - use the entire original frame
crop_x, crop_y, crop_w, crop_h = self.crop_rect result = self.track_template(self.current_display_frame)
# Extract only the cropped region from raw frame if result:
cropped_frame = self.current_display_frame[crop_y:crop_y+crop_h, crop_x:crop_x+crop_w] center_x, center_y, confidence = result
if cropped_frame is not None and cropped_frame.size > 0: print(f"DEBUG: Template match found at ({center_x}, {center_y}) with confidence {confidence:.2f}")
# Track template in cropped frame (much faster!) template_offset = (center_x, center_y)
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: else:
# No crop - use full frame # Cropped mode - use only the cropped region for faster template matching
raw_frame = self.current_display_frame.copy() if self.crop_rect:
if raw_frame is not None: crop_x, crop_y, crop_w, crop_h = self.crop_rect
result = self.track_template(raw_frame) # Extract only the cropped region from raw frame
if result: cropped_frame = self.current_display_frame[crop_y:crop_y+crop_h, crop_x:crop_x+crop_w]
center_x, center_y, confidence = result if cropped_frame is not None and cropped_frame.size > 0:
print(f"DEBUG: Template match found at ({center_x}, {center_y}) with confidence {confidence:.2f}") # Apply motion tracking offset to the cropped frame
offset_frame = self._apply_motion_tracking_offset(cropped_frame, base_pos)
# Template matching returns coordinates in raw frame space if offset_frame is not None:
template_offset = (center_x, center_y) # 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}")
# 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 # Calculate offset from feature tracking if enabled
feature_offset = None feature_offset = None
@@ -1765,35 +1780,91 @@ class VideoEditor:
return (x1 + t * (x2 - x1), y1 + t * (y2 - y1)) return (x1 + t * (x2 - x1), y1 + t * (y2 - y1))
return None 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): def _get_template_matching_position(self, frame_number):
"""Get template matching position and confidence for a frame""" """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 return None
if self.current_display_frame is not None: if self.current_display_frame is not None:
# Use only the cropped region for much faster template matching # Get base position for motion tracking offset
if self.crop_rect: base_pos = self._get_manual_tracking_position(frame_number)
crop_x, crop_y, crop_w, crop_h = self.crop_rect
# Extract only the cropped region from raw frame if self.template_matching_full_frame:
cropped_frame = self.current_display_frame[crop_y:crop_y+crop_h, crop_x:crop_x+crop_w] # Full frame mode - use the entire original frame
if cropped_frame is not None and cropped_frame.size > 0: result = self.track_template(self.current_display_frame)
# Track template in cropped frame (much faster!) if result:
result = self.track_template(cropped_frame) center_x, center_y, confidence = result
if result: return (center_x, center_y, confidence)
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: else:
# No crop - use full frame # Cropped mode - use only the cropped region for faster template matching
raw_frame = self.current_display_frame.copy() if self.crop_rect:
if raw_frame is not None: crop_x, crop_y, crop_w, crop_h = self.crop_rect
result = self.track_template(raw_frame) # Extract only the cropped region from raw frame
if result: cropped_frame = self.current_display_frame[crop_y:crop_y+crop_h, crop_x:crop_x+crop_w]
center_x, center_y, confidence = result if cropped_frame is not None and cropped_frame.size > 0:
return (center_x, center_y, confidence) # 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 return None
@@ -1866,8 +1937,6 @@ class VideoEditor:
def _map_screen_to_rotated(self, sx, sy): def _map_screen_to_rotated(self, sx, sy):
"""Map a point on canvas screen coords back to ROTATED frame coords (pre-crop).""" """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 # Use unified display params
params = self._get_display_params() params = self._get_display_params()
# Back to processed (zoomed+cropped) space # Back to processed (zoomed+cropped) space
@@ -2208,73 +2277,35 @@ class VideoEditor:
return (interp_x, interp_y) return (interp_x, interp_y)
def set_tracking_template(self, frame, region):
"""Set a template region for tracking (much better than optical flow)"""
try:
x, y, w, h = region
self.tracking_template = frame[y:y+h, x:x+w].copy()
self.template_region = region
print(f"DEBUG: Set tracking template with region {region}")
return True
except Exception as e:
print(f"Error setting tracking template: {e}")
return False
def track_template(self, frame): def track_template(self, frame):
"""Track the template in the current frame""" """Track the template in the current frame"""
if self.tracking_template is None: if self.current_template_id is None or self.current_template_id not in self.templates:
# Try to recreate template from saved region return None
if self.template_region is not None:
self._recreate_template_from_region(frame) template_data = self.templates[self.current_template_id]
if self.tracking_template is None: tracking_template = template_data['template']
return None
if tracking_template is None:
return None
try: try:
# Apply image preprocessing for better template matching # 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) # Single-scale template matching (faster)
if self.multi_scale_template_matching: result = cv2.matchTemplate(gray_frame, gray_template, cv2.TM_CCOEFF_NORMED)
scales = [0.8, 0.9, 1.0, 1.1, 1.2] # Different scales to try _, 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_match = None
best_confidence = 0.0 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 # Adaptive thresholding based on recent match history
if len(self.template_match_history) > 0: if len(self.template_match_history) > 0:
@@ -2298,36 +2329,6 @@ class VideoEditor:
print(f"Error in template tracking: {e}") print(f"Error in template tracking: {e}")
return None 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): def _improve_template_matching(self, frame, template):
"""Apply image preprocessing to improve template matching""" """Apply image preprocessing to improve template matching"""
@@ -2383,15 +2384,166 @@ class VideoEditor:
# Extract template from raw frame # Extract template from raw frame
template = self.current_display_frame[raw_y:raw_y+raw_h, raw_x:raw_x+raw_w] template = self.current_display_frame[raw_y:raw_y+raw_h, raw_x:raw_x+raw_w]
if template.size > 0: if template.size > 0:
self.tracking_template = template.copy() # Add template to collection
self.template_region = (raw_x, raw_y, raw_w, raw_h) template_id = self.add_template(template, (raw_x, raw_y, raw_w, raw_h))
self.show_feedback_message(f"Template set from region ({raw_w}x{raw_h})") self.show_feedback_message(f"Template {template_id} set from region ({raw_w}x{raw_h})")
print(f"DEBUG: Template set with size {template.shape}") print(f"DEBUG: Template {template_id} set with size {template.shape}")
else: else:
self.show_feedback_message("Template region too small") self.show_feedback_message("Template region too small")
else: else:
self.show_feedback_message("Template region outside frame bounds") 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): def apply_rotation(self, frame):
"""Apply rotation to frame""" """Apply rotation to frame"""
@@ -2847,6 +2999,41 @@ class VideoEditor:
1, 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): def display_current_frame(self):
"""Display the current frame with all overlays""" """Display the current frame with all overlays"""
if self.current_display_frame is None: if self.current_display_frame is None:
@@ -2961,11 +3148,10 @@ class VideoEditor:
feature_text = f" | Features: {feature_count} pts" feature_text = f" | Features: {feature_count} pts"
if self.optical_flow_enabled: if self.optical_flow_enabled:
feature_text += " (OPTICAL FLOW)" feature_text += " (OPTICAL FLOW)"
template_text = ( template_text = ""
f" | Template: {self.template_matching_enabled}" if self.template_matching_enabled else "" if self.templates:
) mode = "Full Frame" if self.template_matching_full_frame else "Cropped"
if self.template_matching_enabled and self.multi_scale_template_matching: template_text = f" | Template: {mode}"
template_text += " (MULTI-SCALE)"
autorepeat_text = ( autorepeat_text = (
f" | Loop: ON" if self.looping_between_markers else "" f" | Loop: ON" if self.looping_between_markers else ""
) )
@@ -3079,8 +3265,8 @@ class VideoEditor:
# Draw template matching point (blue circle with confidence) # Draw template matching point (blue circle with confidence)
if (not self.is_image_mode and if (not self.is_image_mode and
self.template_matching_enabled and self.templates and
self.tracking_template is not None): self.current_template_id is not None):
# Get template matching position for current frame # Get template matching position for current frame
template_pos = self._get_template_matching_position(self.current_frame) template_pos = self._get_template_matching_position(self.current_frame)
if template_pos: if template_pos:
@@ -3094,6 +3280,7 @@ class VideoEditor:
conf_text = f"{confidence:.2f}" conf_text = f"{confidence:.2f}"
cv2.putText(canvas, conf_text, (sx + 10, sy - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1) 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 # Draw selection rectangles for feature extraction/deletion
if self.selective_feature_extraction_rect: if self.selective_feature_extraction_rect:
x, y, w, h = 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) # Handle right-click for tracking points (no modifiers)
if event == cv2.EVENT_RBUTTONDOWN and not (flags & (cv2.EVENT_FLAG_CTRLKEY | cv2.EVENT_FLAG_SHIFTKEY)): if event == cv2.EVENT_RBUTTONDOWN and not (flags & (cv2.EVENT_FLAG_CTRLKEY | cv2.EVENT_FLAG_SHIFTKEY)):
if not self.is_image_mode: if not self.is_image_mode:
# 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) # Store tracking points in ROTATED frame coordinates (pre-crop)
rx, ry = self._map_screen_to_rotated(x, y) rx, ry = self._map_screen_to_rotated(x, y)
threshold = self.TRACKING_POINT_THRESHOLD threshold = self.TRACKING_POINT_THRESHOLD
@@ -3686,13 +3892,10 @@ class VideoEditor:
def _render_video_worker(self, output_path: str): def _render_video_worker(self, output_path: str):
"""Worker method that runs in the render thread""" """Worker method that runs in the render thread"""
render_cap = None
try: try:
if not output_path.endswith(".mp4"): if not output_path.endswith(".mp4"):
output_path += ".mp4" output_path += ".mp4"
start_time = time.time()
# Send progress update to main thread # Send progress update to main thread
self.render_progress_queue.put(("init", "Initializing render...", 0.0, 0.0)) 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.show_feedback_message(f"Optical flow {'ON' if self.optical_flow_enabled else 'OFF'}")
self.save_state() self.save_state()
elif key == ord("m"): elif key == ord("m"):
# Toggle template matching tracking # Clear all templates
self.template_matching_enabled = not self.template_matching_enabled if self.templates:
print(f"DEBUG: Template matching toggled to {self.template_matching_enabled}") self.templates.clear()
self.show_feedback_message(f"Template matching {'ON' if self.template_matching_enabled else 'OFF'}") 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() self.save_state()
elif key == ord("M"): # Shift+M - Toggle multi-scale template matching elif key == ord("M"): # Shift+M - Toggle multi-scale template matching
self.multi_scale_template_matching = not self.multi_scale_template_matching self.template_matching_full_frame = not self.template_matching_full_frame
print(f"DEBUG: Multi-scale template matching toggled to {self.multi_scale_template_matching}") print(f"DEBUG: Template matching full frame toggled to {self.template_matching_full_frame}")
self.show_feedback_message(f"Multi-scale template matching {'ON' if self.multi_scale_template_matching else 'OFF'}") self.show_feedback_message(f"Template matching: {'Full Frame' if self.template_matching_full_frame else 'Cropped'}")
self.save_state() 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"): elif key == ord("t"):
# Marker looping only for videos # Marker looping only for videos
if not self.is_image_mode: if not self.is_image_mode: