Compare commits
27 Commits
4c0fc8ecbc
...
master
Author | SHA1 | Date | |
---|---|---|---|
bd1824a7ca | |||
4806c95095 | |||
16c841d14d | |||
bfb9ed54d9 | |||
3ac725c2aa | |||
b5a0811cbd | |||
1ac8cd04b3 | |||
203d036a92 | |||
fa2ac22f9f | |||
2013ccf627 | |||
e1d94f2b24 | |||
9df6d73db8 | |||
01340a0a81 | |||
44ed4220b9 | |||
151744d144 | |||
e823a11929 | |||
c1c01e86ca | |||
184aceeee3 | |||
db2aa57ce5 | |||
92c2e62166 | |||
86c31a49d9 | |||
f5b8656bc2 | |||
b9c60ffc25 | |||
b6c7863b77 | |||
612d024161 | |||
840440eb1a | |||
c3bf49f301 |
515
croppa/main.py
515
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,17 @@ 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
|
||||
|
||||
# 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
|
||||
@@ -932,9 +917,11 @@ 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': [{
|
||||
'start_frame': start_frame,
|
||||
'region': region
|
||||
} for start_frame, region, template_image in self.templates]
|
||||
}
|
||||
|
||||
with open(state_file, 'w') as f:
|
||||
@@ -1023,14 +1010,23 @@ 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 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()
|
||||
|
||||
# Validate cut markers against current video length
|
||||
if self.cut_start_frame is not None and self.cut_start_frame >= self.total_frames:
|
||||
@@ -1278,7 +1274,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
|
||||
@@ -1376,6 +1372,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,16 +1647,27 @@ 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:
|
||||
if self.current_display_frame is not None:
|
||||
# Use only the cropped region for much faster template matching
|
||||
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
|
||||
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)
|
||||
# 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}")
|
||||
@@ -1665,18 +1676,14 @@ 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
|
||||
raw_frame = self.current_display_frame.copy()
|
||||
if raw_frame is not None:
|
||||
result = self.track_template(raw_frame)
|
||||
# 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
|
||||
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
|
||||
@@ -1712,7 +1719,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:
|
||||
@@ -1723,7 +1730,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
|
||||
@@ -1765,20 +1772,76 @@ 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:
|
||||
return 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
|
||||
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
|
||||
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)
|
||||
# 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
|
||||
@@ -1787,10 +1850,10 @@ class VideoEditor:
|
||||
raw_y = center_y + crop_y
|
||||
return (raw_x, raw_y, confidence)
|
||||
else:
|
||||
# No crop - use full frame
|
||||
raw_frame = self.current_display_frame.copy()
|
||||
if raw_frame is not None:
|
||||
result = self.track_template(raw_frame)
|
||||
# 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)
|
||||
@@ -1866,8 +1929,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,60 +2269,26 @@ 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:
|
||||
if not self.templates:
|
||||
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, 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
|
||||
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)
|
||||
@@ -2298,36 +2325,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 +2380,138 @@ 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):
|
||||
"""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"""
|
||||
@@ -2847,6 +2967,37 @@ 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:
|
||||
@@ -2884,28 +3035,20 @@ class VideoEditor:
|
||||
height, width = display_frame.shape[:2]
|
||||
available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
|
||||
|
||||
# Don't downscale - keep original video quality
|
||||
# If video is larger than window, we'll handle it by resizing the window
|
||||
# Scale video to fit screen bounds
|
||||
scale = min(self.window_width / width, available_height / height)
|
||||
if scale < 1.0:
|
||||
# Resize window to fit video instead of downscaling video
|
||||
new_window_width = int(width * 1.1) # Add 10% padding
|
||||
new_window_height = int(height * 1.1) + (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
|
||||
|
||||
# Update window size
|
||||
self.window_width = new_window_width
|
||||
self.window_height = new_window_height
|
||||
|
||||
# Resize the OpenCV window
|
||||
window_title = "Image Editor" if self.is_image_mode else "Video Editor"
|
||||
cv2.resizeWindow(window_title, self.window_width, self.window_height)
|
||||
# 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)
|
||||
|
||||
# Create canvas with timeline space
|
||||
canvas = np.zeros((self.window_height, self.window_width, 3), dtype=np.uint8)
|
||||
|
||||
# Center the frame on canvas
|
||||
frame_height, frame_width = display_frame.shape[:2]
|
||||
available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
|
||||
|
||||
start_y = (available_height - frame_height) // 2
|
||||
start_x = (self.window_width - frame_width) // 2
|
||||
|
||||
@@ -2949,11 +3092,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 ""
|
||||
)
|
||||
@@ -3067,8 +3209,7 @@ 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):
|
||||
# Get template matching position for current frame
|
||||
template_pos = self._get_template_matching_position(self.current_frame)
|
||||
if template_pos:
|
||||
@@ -3082,6 +3223,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
|
||||
@@ -3300,6 +3442,23 @@ 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
|
||||
@@ -3529,6 +3688,9 @@ 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
|
||||
@@ -3674,13 +3836,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))
|
||||
|
||||
@@ -4352,6 +4511,14 @@ 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()
|
||||
@@ -4491,16 +4658,24 @@ 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()
|
||||
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