Compare commits
26 Commits
c3bf49f301
...
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 |
525
croppa/main.py
525
croppa/main.py
@@ -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,14 +860,17 @@ 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
|
||||||
|
|
||||||
|
# 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
|
# Project view mode
|
||||||
self.project_view_mode = False
|
self.project_view_mode = False
|
||||||
self.project_view = None
|
self.project_view = None
|
||||||
@@ -932,9 +917,11 @@ 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': [{
|
||||||
'multi_scale_template_matching': self.multi_scale_template_matching
|
'start_frame': start_frame,
|
||||||
|
'region': region
|
||||||
|
} for start_frame, region, template_image in self.templates]
|
||||||
}
|
}
|
||||||
|
|
||||||
with open(state_file, 'w') as f:
|
with open(state_file, 'w') as f:
|
||||||
@@ -1023,14 +1010,23 @@ 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 simple 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_data in state['templates']:
|
||||||
self.multi_scale_template_matching = state['multi_scale_template_matching'] # Will be recreated on first use
|
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
|
# 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:
|
||||||
@@ -1278,7 +1274,7 @@ class VideoEditor:
|
|||||||
"""Calculate frame delay in milliseconds based on playback speed"""
|
"""Calculate frame delay in milliseconds based on playback speed"""
|
||||||
# Round to 2 decimals to handle floating point precision issues
|
# Round to 2 decimals to handle floating point precision issues
|
||||||
speed = round(self.playback_speed, 2)
|
speed = round(self.playback_speed, 2)
|
||||||
print(f"Playback speed: {speed}")
|
# print(f"Playback speed: {speed}")
|
||||||
if speed >= 1.0:
|
if speed >= 1.0:
|
||||||
# Speed >= 1: maximum FPS (no delay)
|
# Speed >= 1: maximum FPS (no delay)
|
||||||
return 1
|
return 1
|
||||||
@@ -1376,6 +1372,10 @@ class VideoEditor:
|
|||||||
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,16 +1647,27 @@ 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:
|
||||||
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:
|
||||||
|
# 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:
|
if self.crop_rect:
|
||||||
crop_x, crop_y, crop_w, crop_h = self.crop_rect
|
crop_x, crop_y, crop_w, crop_h = self.crop_rect
|
||||||
# Extract only the cropped region from raw frame
|
# 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]
|
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:
|
if cropped_frame is not None and cropped_frame.size > 0:
|
||||||
# Track template in cropped frame (much faster!)
|
# Apply motion tracking offset to the cropped frame
|
||||||
result = self.track_template(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:
|
if result:
|
||||||
center_x, center_y, confidence = result
|
center_x, center_y, confidence = result
|
||||||
print(f"DEBUG: Template match found at ({center_x}, {center_y}) with confidence {confidence:.2f}")
|
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
|
# Add crop offset back
|
||||||
raw_x = center_x + crop_x
|
raw_x = center_x + crop_x
|
||||||
raw_y = center_y + crop_y
|
raw_y = center_y + crop_y
|
||||||
|
|
||||||
template_offset = (raw_x, raw_y)
|
template_offset = (raw_x, raw_y)
|
||||||
else:
|
else:
|
||||||
# No crop - use full frame
|
# No crop - use full frame with offset
|
||||||
raw_frame = self.current_display_frame.copy()
|
offset_frame = self._apply_motion_tracking_offset(self.current_display_frame, base_pos)
|
||||||
if raw_frame is not None:
|
if offset_frame is not None:
|
||||||
result = self.track_template(raw_frame)
|
result = self.track_template(offset_frame)
|
||||||
if result:
|
if result:
|
||||||
center_x, center_y, confidence = 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)
|
template_offset = (center_x, center_y)
|
||||||
|
|
||||||
# Calculate offset from feature tracking if enabled
|
# Calculate offset from feature tracking if enabled
|
||||||
@@ -1712,7 +1719,7 @@ class VideoEditor:
|
|||||||
# Add template matching position
|
# Add template matching position
|
||||||
if template_offset:
|
if template_offset:
|
||||||
positions.append(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
|
# Add feature tracking position
|
||||||
if feature_offset:
|
if feature_offset:
|
||||||
@@ -1723,7 +1730,7 @@ class VideoEditor:
|
|||||||
if positions:
|
if positions:
|
||||||
avg_x = sum(pos[0] for pos in positions) / len(positions)
|
avg_x = sum(pos[0] for pos in positions) / len(positions)
|
||||||
avg_y = sum(pos[1] 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)
|
return (avg_x, avg_y)
|
||||||
|
|
||||||
# Fall back to individual tracking methods if no base position
|
# 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 (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:
|
||||||
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
|
||||||
|
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:
|
if self.crop_rect:
|
||||||
crop_x, crop_y, crop_w, crop_h = self.crop_rect
|
crop_x, crop_y, crop_w, crop_h = self.crop_rect
|
||||||
# Extract only the cropped region from raw frame
|
# 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]
|
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:
|
if cropped_frame is not None and cropped_frame.size > 0:
|
||||||
# Track template in cropped frame (much faster!)
|
# Apply motion tracking offset to the cropped frame
|
||||||
result = self.track_template(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:
|
if result:
|
||||||
center_x, center_y, confidence = result
|
center_x, center_y, confidence = result
|
||||||
# Map from cropped frame coordinates to raw frame coordinates
|
# Map from cropped frame coordinates to raw frame coordinates
|
||||||
@@ -1787,10 +1850,10 @@ class VideoEditor:
|
|||||||
raw_y = center_y + crop_y
|
raw_y = center_y + crop_y
|
||||||
return (raw_x, raw_y, confidence)
|
return (raw_x, raw_y, confidence)
|
||||||
else:
|
else:
|
||||||
# No crop - use full frame
|
# No crop - use full frame with offset
|
||||||
raw_frame = self.current_display_frame.copy()
|
offset_frame = self._apply_motion_tracking_offset(self.current_display_frame, base_pos)
|
||||||
if raw_frame is not None:
|
if offset_frame is not None:
|
||||||
result = self.track_template(raw_frame)
|
result = self.track_template(offset_frame)
|
||||||
if result:
|
if result:
|
||||||
center_x, center_y, confidence = result
|
center_x, center_y, confidence = result
|
||||||
return (center_x, center_y, confidence)
|
return (center_x, center_y, confidence)
|
||||||
@@ -1866,8 +1929,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,60 +2269,26 @@ 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 not self.templates:
|
||||||
# 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
|
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:
|
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)
|
|
||||||
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)
|
# Single-scale template matching (faster)
|
||||||
result = cv2.matchTemplate(gray_frame, gray_template, cv2.TM_CCOEFF_NORMED)
|
result = cv2.matchTemplate(gray_frame, gray_template, cv2.TM_CCOEFF_NORMED)
|
||||||
_, max_val, _, max_loc = cv2.minMaxLoc(result)
|
_, max_val, _, max_loc = cv2.minMaxLoc(result)
|
||||||
@@ -2298,36 +2325,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 +2380,138 @@ 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):
|
||||||
|
"""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):
|
def apply_rotation(self, frame):
|
||||||
"""Apply rotation to frame"""
|
"""Apply rotation to frame"""
|
||||||
@@ -2847,6 +2967,37 @@ class VideoEditor:
|
|||||||
1,
|
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):
|
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:
|
||||||
@@ -2884,39 +3035,19 @@ class VideoEditor:
|
|||||||
height, width = display_frame.shape[:2]
|
height, width = display_frame.shape[:2]
|
||||||
available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
|
available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
|
||||||
|
|
||||||
# Don't downscale - keep original video quality
|
# Scale video to fit screen bounds
|
||||||
# If video is larger than window, we'll handle it by resizing the window
|
|
||||||
scale = min(self.window_width / width, available_height / height)
|
scale = min(self.window_width / width, available_height / height)
|
||||||
if scale < 1.0:
|
if scale < 1.0:
|
||||||
# Resize window to fit video instead of downscaling video
|
# Scale down video to fit screen
|
||||||
new_window_width = int(width * 1.1) # Add 10% padding
|
new_width = int(width * scale)
|
||||||
new_window_height = int(height * 1.1) + (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
|
new_height = int(height * scale)
|
||||||
|
display_frame = cv2.resize(display_frame, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
|
||||||
# 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)
|
|
||||||
|
|
||||||
# Create canvas with timeline space
|
# Create canvas with timeline space
|
||||||
canvas = np.zeros((self.window_height, self.window_width, 3), dtype=np.uint8)
|
canvas = np.zeros((self.window_height, self.window_width, 3), dtype=np.uint8)
|
||||||
|
|
||||||
# Center the frame on canvas
|
# Center the frame on canvas
|
||||||
frame_height, frame_width = display_frame.shape[:2]
|
frame_height, frame_width = display_frame.shape[:2]
|
||||||
available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
|
|
||||||
|
|
||||||
# Simple fullscreen fix: scale down if video is too large for screen
|
|
||||||
if self.is_fullscreen and (frame_height > available_height or frame_width > self.window_width):
|
|
||||||
scale_x = self.window_width / frame_width
|
|
||||||
scale_y = available_height / frame_height
|
|
||||||
scale = min(scale_x, scale_y)
|
|
||||||
if scale < 1.0:
|
|
||||||
new_width = int(frame_width * scale)
|
|
||||||
new_height = int(frame_height * scale)
|
|
||||||
display_frame = cv2.resize(display_frame, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
|
|
||||||
frame_height, frame_width = display_frame.shape[:2]
|
|
||||||
|
|
||||||
start_y = (available_height - frame_height) // 2
|
start_y = (available_height - frame_height) // 2
|
||||||
start_x = (self.window_width - frame_width) // 2
|
start_x = (self.window_width - frame_width) // 2
|
||||||
@@ -2961,11 +3092,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 +3209,7 @@ 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):
|
||||||
self.tracking_template 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 +3223,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 +3442,23 @@ 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 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)
|
# 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
|
||||||
@@ -3541,6 +3688,9 @@ class VideoEditor:
|
|||||||
# Reset feature tracking
|
# Reset feature tracking
|
||||||
self.feature_tracker.clear_features()
|
self.feature_tracker.clear_features()
|
||||||
|
|
||||||
|
# Reset templates
|
||||||
|
self.templates.clear()
|
||||||
|
|
||||||
# Reset cut markers
|
# Reset cut markers
|
||||||
self.cut_start_frame = None
|
self.cut_start_frame = None
|
||||||
self.cut_end_frame = None
|
self.cut_end_frame = None
|
||||||
@@ -3686,13 +3836,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))
|
||||||
|
|
||||||
@@ -4364,6 +4511,14 @@ class VideoEditor:
|
|||||||
self.cut_end_frame = self.current_frame
|
self.cut_end_frame = self.current_frame
|
||||||
print(f"Set cut end at frame {self.current_frame}")
|
print(f"Set cut end at frame {self.current_frame}")
|
||||||
self.save_state() # Save state when cut end is set
|
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"):
|
elif key == ord("N"):
|
||||||
if len(self.video_files) > 1:
|
if len(self.video_files) > 1:
|
||||||
self.previous_video()
|
self.previous_video()
|
||||||
@@ -4503,16 +4658,24 @@ 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'}")
|
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:
|
||||||
|
Reference in New Issue
Block a user