Compare commits

..

2 Commits

Author SHA1 Message Date
4c0fc8ecbc Refactor video frame scaling logic in VideoEditor to simplify resizing process
This commit removes redundant checks for scaling dimensions and ensures that the video frame is resized correctly while maintaining the aspect ratio. The logic is streamlined to enhance clarity and efficiency in the resizing process, improving overall performance during video editing.
2025-09-26 15:10:19 +02:00
8f5eacaed5 Refactor video frame resizing logic in VideoEditor to maintain aspect ratio and improve window handling
This commit enhances the video frame resizing process by calculating actual window dimensions and ensuring that the video maintains its aspect ratio when displayed. It introduces a more robust method for determining available space and scaling the video frame accordingly, preventing upscaling beyond screen limits. Additionally, the canvas size is adjusted to match the actual window dimensions, improving the overall user experience during video editing.
2025-09-26 15:05:55 +02:00

View File

@@ -4,7 +4,7 @@ import cv2
import argparse
import numpy as np
from pathlib import Path
from typing import List, Dict, Any
from typing import List, Optional, Tuple, Dict, Any
import time
import re
import threading
@@ -200,6 +200,24 @@ 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):
@@ -860,17 +878,14 @@ 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
# (x, y, w, h) in rotated frame coordinates
self.multi_scale_template_matching = False # Disable multi-scale by default # (x, y, w, h) in rotated frame coordinates
self.template_selection_start = None
self.template_selection_rect = None
# Simple template system - list of (start_frame, region, template_image) tuples sorted by start_frame
self.templates = [] # [(start_frame, region, template_image), ...] sorted by start_frame
# 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
@@ -917,11 +932,9 @@ 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_full_frame': self.template_matching_full_frame,
'templates': [{
'start_frame': start_frame,
'region': region
} for start_frame, region, template_image in self.templates]
'template_matching_enabled': self.template_matching_enabled,
'template_region': self.template_region,
'multi_scale_template_matching': self.multi_scale_template_matching
}
with open(state_file, 'w') as f:
@@ -1010,23 +1023,14 @@ class VideoEditor:
print(f"Loaded feature tracker state")
# Load template matching state
if 'template_matching_full_frame' in state:
self.template_matching_full_frame = state['template_matching_full_frame']
# Load simple templates state
if 'templates' in state:
self.templates = []
for template_data in state['templates']:
start_frame = template_data['start_frame']
region = template_data['region']
# We'll recreate the template image when needed
self.templates.append((start_frame, region, None))
# Sort by start_frame
self.templates.sort(key=lambda x: x[0])
print(f"Loaded {len(self.templates)} templates")
# Recreate template images by seeking to capture frames
self._recreate_template_images()
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
# Validate cut markers against current video length
if self.cut_start_frame is not None and self.cut_start_frame >= self.total_frames:
@@ -1274,7 +1278,7 @@ class VideoEditor:
"""Calculate frame delay in milliseconds based on playback speed"""
# Round to 2 decimals to handle floating point precision issues
speed = round(self.playback_speed, 2)
# print(f"Playback speed: {speed}")
print(f"Playback speed: {speed}")
if speed >= 1.0:
# Speed >= 1: maximum FPS (no delay)
return 1
@@ -1372,10 +1376,6 @@ 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,27 +1647,16 @@ class VideoEditor:
# Calculate offset from template matching if enabled
template_offset = None
if self.templates:
if self.template_matching_enabled and self.tracking_template is not None:
if self.current_display_frame is not None:
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:
# Cropped mode - use only the cropped region for faster template matching
# 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:
# 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)
# 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}")
@@ -1676,14 +1665,18 @@ class VideoEditor:
# 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)
# 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
@@ -1719,7 +1712,7 @@ class VideoEditor:
# Add template matching position
if template_offset:
positions.append(template_offset)
# print(f"DEBUG: Template matching: ({template_offset[0]:.1f}, {template_offset[1]:.1f})")
print(f"DEBUG: Template matching: ({template_offset[0]:.1f}, {template_offset[1]:.1f})")
# Add feature tracking position
if feature_offset:
@@ -1730,7 +1723,7 @@ class VideoEditor:
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})")
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
@@ -1772,76 +1765,20 @@ 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.templates:
if not self.template_matching_enabled or self.tracking_template is None:
return None
if self.current_display_frame is not None:
# 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:
# Cropped mode - use only the cropped region for faster template matching
# 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:
# 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)
# 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
@@ -1850,10 +1787,10 @@ class VideoEditor:
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)
# 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)
@@ -1929,6 +1866,8 @@ 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
@@ -2269,26 +2208,60 @@ 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 not self.templates:
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
# Get the template for current frame
template_index = self.get_template_for_frame(self.current_frame)
if template_index is None:
return None
start_frame, region, template_image = self.templates[template_index]
# Use the stored template image from when it was captured
tracking_template = template_image
try:
# Apply image preprocessing for better template matching
gray_frame, gray_template = self._improve_template_matching(frame, tracking_template)
gray_frame, gray_template = self._improve_template_matching(frame, self.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
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)
@@ -2325,6 +2298,36 @@ 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"""
@@ -2380,138 +2383,15 @@ 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:
# 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}")
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 add_template(self, template, region, start_frame=None):
"""Add a new template to the collection"""
if start_frame is None:
start_frame = self.current_frame
# Add template to list with the actual template image
self.templates.append((start_frame, region, template.copy()))
# Sort by start_frame
self.templates.sort(key=lambda x: x[0])
self.show_feedback_message(f"Template added at frame {start_frame}")
return len(self.templates) - 1
def remove_template(self, template_id):
"""Remove a template from the collection"""
if not self.templates:
return False
# Use the existing function to find the template to remove
template_to_remove = self.get_template_for_frame(self.current_frame)
if template_to_remove is not None:
removed_template = self.templates.pop(template_to_remove)
self.show_feedback_message(f"Template removed (was at frame {removed_template[0]})")
return True
else:
self.show_feedback_message("No template to remove")
return False
def get_template_for_frame(self, frame_number):
"""Get the template for the current frame"""
if not self.templates:
return None
# Find template with start_frame > current_frame
for i, (start_frame, region, template_image) in enumerate(self.templates):
if start_frame > frame_number:
# Found template with start_frame > current_frame
# Use the previous one (if it exists)
if i > 0:
return i - 1
else:
return None
elif start_frame == frame_number:
# Found template with start_frame == current_frame
# Use THIS template
return i
# If no template found with start_frame > current_frame, use the last one
return len(self.templates) - 1 if self.templates else None
def _select_best_template_for_frame(self, frame_number):
"""Select the best template for the current frame"""
template_index = self.get_template_for_frame(frame_number)
return template_index is not None
def _recreate_template_images(self):
"""Recreate template images by seeking to their capture frames"""
if not self.templates:
return
current_frame_backup = self.current_frame
for i, (start_frame, region, template_image) in enumerate(self.templates):
if template_image is None: # Only recreate if missing
try:
# Seek to the capture frame
self.seek_to_frame(start_frame)
# Extract template from current frame
x, y, w, h = region
if (y + h <= self.current_display_frame.shape[0] and
x + w <= self.current_display_frame.shape[1]):
template_image = self.current_display_frame[y:y+h, x:x+w].copy()
# Update the template in the list
self.templates[i] = (start_frame, region, template_image)
print(f"DEBUG: Recreated template {i} from frame {start_frame}")
else:
print(f"DEBUG: Failed to recreate template {i} - region outside frame bounds")
except Exception as e:
print(f"DEBUG: Failed to recreate template {i}: {e}")
# Restore original frame
self.seek_to_frame(current_frame_backup)
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()
if not self.templates:
print("DEBUG: No template markers; prev jump ignored")
return
current = self.current_frame
candidates = [start_frame for start_frame, region, template_image in self.templates if start_frame < current]
if candidates:
target = candidates[-1]
print(f"DEBUG: Jump prev template from {current} -> {target}")
self.seek_to_frame(target)
else:
target = self.templates[0][0]
print(f"DEBUG: Jump prev template to first marker from {current} -> {target}")
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()
if not self.templates:
print("DEBUG: No template markers; next jump ignored")
return
current = self.current_frame
for start_frame, region, template_image in self.templates:
if start_frame > current:
print(f"DEBUG: Jump next template from {current} -> {start_frame}")
self.seek_to_frame(start_frame)
return
target = self.templates[-1][0]
print(f"DEBUG: Jump next template to last marker from {current} -> {target}")
self.seek_to_frame(target)
def apply_rotation(self, frame):
"""Apply rotation to frame"""
@@ -2967,37 +2847,6 @@ class VideoEditor:
1,
)
# Draw template markers
for start_frame, region, template_image in self.templates:
# Draw template start point
start_progress = start_frame / max(1, self.total_frames - 1)
start_x = bar_x_start + int(bar_width * start_progress)
# Template color (green for active, red for inactive)
template_index = self.get_template_for_frame(self.current_frame)
is_active = (template_index is not None and self.templates[template_index][0] == start_frame)
template_color = (0, 255, 0) if is_active else (255, 0, 0) # Green if active, red if inactive
# Draw template start marker
cv2.rectangle(
frame,
(start_x, bar_y + 2),
(start_x + 4, bar_y + self.TIMELINE_BAR_HEIGHT - 2),
template_color,
-1,
)
# Draw template number
cv2.putText(
frame,
str(start_frame),
(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:
@@ -3031,30 +2880,53 @@ class VideoEditor:
if display_frame is None:
return
# Resize to fit window while maintaining aspect ratio
height, width = display_frame.shape[:2]
available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
# Get actual window dimensions (important for fullscreen mode)
window_title = "Image Editor" if self.is_image_mode else "Video Editor"
try:
window_rect = cv2.getWindowImageRect(window_title)
if window_rect[2] > 0 and window_rect[3] > 0: # width and height > 0
actual_width = window_rect[2]
actual_height = window_rect[3]
else:
# Fallback to stored dimensions
actual_width = self.window_width
actual_height = self.window_height
except:
# Fallback to stored dimensions
actual_width = self.window_width
actual_height = self.window_height
# Scale video to fit screen bounds
scale = min(self.window_width / width, available_height / height)
if scale < 1.0:
# Scale down video to fit screen
new_width = int(width * scale)
new_height = int(height * scale)
display_frame = cv2.resize(display_frame, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
# Calculate available space for video (accounting for timeline)
available_height = actual_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
# Get original video dimensions to maintain aspect ratio
height, width = display_frame.shape[:2]
# Calculate scale to fit video in available space while maintaining aspect ratio
scale_x = actual_width / width
scale_y = available_height / height
scale = min(scale_x, scale_y) # Use the smaller scale to ensure video fits
# Calculate scaled dimensions
scaled_width = int(width * scale)
scaled_height = int(height * scale)
# Resize the frame to fit the window while maintaining aspect ratio
if scale != 1.0:
display_frame = cv2.resize(display_frame, (scaled_width, scaled_height), interpolation=cv2.INTER_LINEAR)
# Create canvas with timeline space
canvas = np.zeros((self.window_height, self.window_width, 3), dtype=np.uint8)
canvas = np.zeros((actual_height, actual_width, 3), dtype=np.uint8)
# Center the frame on canvas
frame_height, frame_width = display_frame.shape[:2]
available_height = actual_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
start_x = (actual_width - frame_width) // 2
# Ensure frame fits within canvas bounds
end_y = min(start_y + frame_height, available_height)
end_x = min(start_x + frame_width, self.window_width)
end_x = min(start_x + frame_width, actual_width)
actual_frame_height = end_y - start_y
actual_frame_width = end_x - start_x
@@ -3092,10 +2964,11 @@ class VideoEditor:
feature_text = f" | Features: {feature_count} pts"
if self.optical_flow_enabled:
feature_text += " (OPTICAL FLOW)"
template_text = ""
if self.templates:
mode = "Full Frame" if self.template_matching_full_frame else "Cropped"
template_text = f" | Template: {mode}"
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)"
autorepeat_text = (
f" | Loop: ON" if self.looping_between_markers else ""
)
@@ -3209,7 +3082,8 @@ class VideoEditor:
# Draw template matching point (blue circle with confidence)
if (not self.is_image_mode and
self.templates):
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:
@@ -3223,7 +3097,6 @@ 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
@@ -3442,23 +3315,6 @@ 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 i, (start_frame, region, template_image) in enumerate(self.templates):
tx, ty, tw, th = 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 <= 40:
self.remove_template(i) # Pass index instead of 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
@@ -3688,9 +3544,6 @@ class VideoEditor:
# Reset feature tracking
self.feature_tracker.clear_features()
# Reset templates
self.templates.clear()
# Reset cut markers
self.cut_start_frame = None
self.cut_end_frame = None
@@ -3836,10 +3689,13 @@ 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))
@@ -4511,14 +4367,6 @@ class VideoEditor:
self.cut_end_frame = self.current_frame
print(f"Set cut end at frame {self.current_frame}")
self.save_state() # Save state when cut end is set
elif key == ord("!"): # Shift+1 - Jump to cut start marker
if not self.is_image_mode and self.cut_start_frame is not None:
self.seek_to_frame(self.cut_start_frame)
print(f"Jumped to cut start marker at frame {self.cut_start_frame}")
elif key == ord("\""): # Shift+2 - Jump to cut end marker
if not self.is_image_mode and self.cut_end_frame is not None:
self.seek_to_frame(self.cut_end_frame)
print(f"Jumped to cut end marker at frame {self.cut_end_frame}")
elif key == ord("N"):
if len(self.video_files) > 1:
self.previous_video()
@@ -4658,24 +4506,16 @@ class VideoEditor:
self.show_feedback_message(f"Optical flow {'ON' if self.optical_flow_enabled else 'OFF'}")
self.save_state()
elif key == ord("m"):
# Clear all templates
if self.templates:
self.templates.clear()
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")
# 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("M"): # Shift+M - Toggle multi-scale template matching
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.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.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: