Add "go to next interesting frame" button
This commit is contained in:
243
croppa/main.py
243
croppa/main.py
@@ -871,6 +871,15 @@ class VideoEditor:
|
|||||||
# Template matching modes
|
# Template matching modes
|
||||||
self.template_matching_full_frame = False # Toggle for full frame vs cropped template matching
|
self.template_matching_full_frame = False # Toggle for full frame vs cropped template matching
|
||||||
|
|
||||||
|
# Frame difference for interesting point detection
|
||||||
|
self.frame_difference_threshold = 10.0 # Percentage threshold for frame difference (10% default)
|
||||||
|
self.frame_difference_gap = 10 # Number of frames between comparisons (default 10)
|
||||||
|
|
||||||
|
# Search state for interesting point detection
|
||||||
|
self.searching_interesting_point = False
|
||||||
|
self.search_progress_text = ""
|
||||||
|
self.search_progress_percent = 0.0
|
||||||
|
|
||||||
# Project view mode
|
# Project view mode
|
||||||
self.project_view_mode = False
|
self.project_view_mode = False
|
||||||
self.project_view = None
|
self.project_view = None
|
||||||
@@ -918,6 +927,8 @@ class VideoEditor:
|
|||||||
'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_full_frame': self.template_matching_full_frame,
|
'template_matching_full_frame': self.template_matching_full_frame,
|
||||||
|
'frame_difference_threshold': self.frame_difference_threshold,
|
||||||
|
'frame_difference_gap': self.frame_difference_gap,
|
||||||
'templates': [{
|
'templates': [{
|
||||||
'start_frame': start_frame,
|
'start_frame': start_frame,
|
||||||
'region': region
|
'region': region
|
||||||
@@ -1013,6 +1024,16 @@ class VideoEditor:
|
|||||||
if 'template_matching_full_frame' in state:
|
if 'template_matching_full_frame' in state:
|
||||||
self.template_matching_full_frame = state['template_matching_full_frame']
|
self.template_matching_full_frame = state['template_matching_full_frame']
|
||||||
|
|
||||||
|
# Load frame difference threshold
|
||||||
|
if 'frame_difference_threshold' in state:
|
||||||
|
self.frame_difference_threshold = state['frame_difference_threshold']
|
||||||
|
print(f"Loaded frame difference threshold: {self.frame_difference_threshold:.1f}%")
|
||||||
|
|
||||||
|
# Load frame difference gap
|
||||||
|
if 'frame_difference_gap' in state:
|
||||||
|
self.frame_difference_gap = state['frame_difference_gap']
|
||||||
|
print(f"Loaded frame difference gap: {self.frame_difference_gap} frames")
|
||||||
|
|
||||||
# Load simple templates state
|
# Load simple templates state
|
||||||
if 'templates' in state:
|
if 'templates' in state:
|
||||||
self.templates = []
|
self.templates = []
|
||||||
@@ -1465,6 +1486,116 @@ class VideoEditor:
|
|||||||
print(f"DEBUG: Jump next tracking to last marker from {current} -> {target}; tracking_frames={tracking_frames}")
|
print(f"DEBUG: Jump next tracking to last marker from {current} -> {target}; tracking_frames={tracking_frames}")
|
||||||
self.seek_to_frame(target)
|
self.seek_to_frame(target)
|
||||||
|
|
||||||
|
def go_to_next_interesting_point(self):
|
||||||
|
"""Go to the next frame where the difference from the previous frame exceeds the threshold"""
|
||||||
|
if self.is_image_mode:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stop_auto_repeat_seek()
|
||||||
|
|
||||||
|
if self.current_frame >= self.total_frames - 1:
|
||||||
|
print("Already at last frame")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Store current frame for comparison
|
||||||
|
current_frame_backup = self.current_frame
|
||||||
|
current_display_frame = self.current_display_frame.copy() if self.current_display_frame is not None else None
|
||||||
|
|
||||||
|
print(f"Searching for next interesting point from frame {current_frame_backup + 1} with threshold {self.frame_difference_threshold:.1f}% (gap: {self.frame_difference_gap} frames)")
|
||||||
|
|
||||||
|
# Start searching from the next frame
|
||||||
|
target_frame = current_frame_backup + 1
|
||||||
|
search_cancelled = False
|
||||||
|
frames_checked = 0
|
||||||
|
total_frames_to_check = self.total_frames - target_frame
|
||||||
|
|
||||||
|
# Enable search mode for OSD display
|
||||||
|
self.searching_interesting_point = True
|
||||||
|
|
||||||
|
# Fast search using N-frame gap comparisons
|
||||||
|
try:
|
||||||
|
# Performance optimization: sample frames for faster processing
|
||||||
|
sample_size = (320, 240) # Small sample size for fast difference calculation
|
||||||
|
update_interval = 10 # Update OSD every 10 comparisons
|
||||||
|
|
||||||
|
# Read the first frame to start comparisons
|
||||||
|
self.cap.cap.set(cv2.CAP_PROP_POS_FRAMES, current_frame_backup)
|
||||||
|
ret, base_frame = self.cap.cap.read()
|
||||||
|
if not ret:
|
||||||
|
search_cancelled = True
|
||||||
|
raise Exception("Could not read base frame")
|
||||||
|
|
||||||
|
base_frame_num = current_frame_backup
|
||||||
|
|
||||||
|
while target_frame < self.total_frames:
|
||||||
|
# Check for cancellation key (less frequent checks for speed)
|
||||||
|
if target_frame % 10 == 0:
|
||||||
|
key = cv2.waitKey(1) & 0xFF
|
||||||
|
if key == ord(";"):
|
||||||
|
search_cancelled = True
|
||||||
|
print("Search cancelled")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Read comparison frame that's N frames ahead
|
||||||
|
comparison_frame_num = min(target_frame + self.frame_difference_gap, self.total_frames - 1)
|
||||||
|
self.cap.cap.set(cv2.CAP_PROP_POS_FRAMES, comparison_frame_num)
|
||||||
|
ret, comparison_frame = self.cap.cap.read()
|
||||||
|
if not ret:
|
||||||
|
break
|
||||||
|
|
||||||
|
frames_checked += 1
|
||||||
|
|
||||||
|
# Fast difference calculation using downsampled frames
|
||||||
|
base_small = cv2.resize(base_frame, sample_size)
|
||||||
|
comparison_small = cv2.resize(comparison_frame, sample_size)
|
||||||
|
|
||||||
|
# Calculate frame difference between frames N apart
|
||||||
|
diff_percentage = self.calculate_frame_difference(base_small, comparison_small)
|
||||||
|
|
||||||
|
# Update OSD less frequently for speed
|
||||||
|
if frames_checked % update_interval == 0 or diff_percentage >= self.frame_difference_threshold:
|
||||||
|
progress_percent = (frames_checked / max(1, (self.total_frames - current_frame_backup) // self.frame_difference_gap)) * 100
|
||||||
|
self.search_progress_percent = progress_percent
|
||||||
|
self.search_progress_text = f"Gap search: {base_frame_num}↔{comparison_frame_num} ({diff_percentage:.1f}% change, gap: {self.frame_difference_gap}) - Press ; to cancel"
|
||||||
|
|
||||||
|
# Force display update to show search progress
|
||||||
|
self.display_needs_update = True
|
||||||
|
self.display_current_frame()
|
||||||
|
|
||||||
|
# Check if difference exceeds threshold
|
||||||
|
if diff_percentage >= self.frame_difference_threshold:
|
||||||
|
# Re-calculate with full resolution for accuracy
|
||||||
|
full_diff = self.calculate_frame_difference(base_frame, comparison_frame)
|
||||||
|
print(f"Found interesting point between frames {base_frame_num} and {comparison_frame_num} ({full_diff:.1f}% change)")
|
||||||
|
self.show_feedback_message(f"Interesting: {full_diff:.1f}% change over {self.frame_difference_gap} frames")
|
||||||
|
|
||||||
|
# Go to the later frame in the comparison
|
||||||
|
self.current_frame = comparison_frame_num
|
||||||
|
self.current_display_frame = comparison_frame
|
||||||
|
break
|
||||||
|
|
||||||
|
# Move base frame forward for next comparison
|
||||||
|
target_frame += self.frame_difference_gap
|
||||||
|
base_frame_num = target_frame
|
||||||
|
base_frame = comparison_frame.copy() if comparison_frame is not None else None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during search: {e}")
|
||||||
|
search_cancelled = True
|
||||||
|
|
||||||
|
# Disable search mode
|
||||||
|
self.searching_interesting_point = False
|
||||||
|
self.search_progress_text = ""
|
||||||
|
|
||||||
|
# If no interesting point found or search was cancelled, go back to original frame
|
||||||
|
if search_cancelled:
|
||||||
|
self.seek_to_frame(current_frame_backup)
|
||||||
|
self.show_feedback_message("Search cancelled")
|
||||||
|
elif target_frame >= self.total_frames:
|
||||||
|
print(f"No interesting point found within threshold in remaining frames")
|
||||||
|
self.seek_to_frame(current_frame_backup)
|
||||||
|
self.show_feedback_message("No interesting point found")
|
||||||
|
|
||||||
def _get_previous_tracking_point(self):
|
def _get_previous_tracking_point(self):
|
||||||
"""Get the tracking point from the previous frame that has tracking points."""
|
"""Get the tracking point from the previous frame that has tracking points."""
|
||||||
if self.is_image_mode or not self.tracking_points:
|
if self.is_image_mode or not self.tracking_points:
|
||||||
@@ -1613,6 +1744,48 @@ class VideoEditor:
|
|||||||
|
|
||||||
return processed_frame
|
return processed_frame
|
||||||
|
|
||||||
|
def calculate_frame_difference(self, frame1, frame2) -> float:
|
||||||
|
"""Calculate percentage difference between two frames"""
|
||||||
|
if frame1 is None or frame2 is None:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ensure frames are the same size
|
||||||
|
if frame1.shape != frame2.shape:
|
||||||
|
# Resize frame2 to match frame1
|
||||||
|
frame2 = cv2.resize(frame2, (frame1.shape[1], frame1.shape[0]))
|
||||||
|
|
||||||
|
# Convert to grayscale for difference calculation
|
||||||
|
if len(frame1.shape) == 3:
|
||||||
|
gray1 = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)
|
||||||
|
else:
|
||||||
|
gray1 = frame1
|
||||||
|
|
||||||
|
if len(frame2.shape) == 3:
|
||||||
|
gray2 = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY)
|
||||||
|
else:
|
||||||
|
gray2 = frame2
|
||||||
|
|
||||||
|
# Calculate absolute difference
|
||||||
|
diff = cv2.absdiff(gray1, gray2)
|
||||||
|
|
||||||
|
# Calculate percentage of pixels that changed significantly
|
||||||
|
# Use threshold to ignore minor noise
|
||||||
|
_, thresh_diff = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY)
|
||||||
|
|
||||||
|
# Count changed pixels
|
||||||
|
changed_pixels = cv2.countNonZero(thresh_diff)
|
||||||
|
total_pixels = gray1.size
|
||||||
|
|
||||||
|
# Calculate percentage
|
||||||
|
difference_percentage = (changed_pixels / total_pixels) * 100.0
|
||||||
|
|
||||||
|
return difference_percentage
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error calculating frame difference: {e}")
|
||||||
|
return 0.0
|
||||||
|
|
||||||
# --- Motion tracking helpers ---
|
# --- Motion tracking helpers ---
|
||||||
def _get_effective_crop_rect_for_frame(self, frame_number):
|
def _get_effective_crop_rect_for_frame(self, frame_number):
|
||||||
"""Return EFFECTIVE crop_rect in ROTATED frame coords for this frame (applies tracking follow)."""
|
"""Return EFFECTIVE crop_rect in ROTATED frame coords for this frame (applies tracking follow)."""
|
||||||
@@ -2998,6 +3171,18 @@ class VideoEditor:
|
|||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Draw frame difference threshold info
|
||||||
|
threshold_text = f"Interesting: {self.frame_difference_threshold:.0f}% (gap: {self.frame_difference_gap})"
|
||||||
|
cv2.putText(
|
||||||
|
frame,
|
||||||
|
threshold_text,
|
||||||
|
(bar_x_start, bar_y - 15),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.4,
|
||||||
|
(200, 200, 200),
|
||||||
|
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:
|
||||||
@@ -3324,6 +3509,45 @@ class VideoEditor:
|
|||||||
# Draw feedback message (if visible)
|
# Draw feedback message (if visible)
|
||||||
self.draw_feedback_message(canvas)
|
self.draw_feedback_message(canvas)
|
||||||
|
|
||||||
|
# Draw search progress (if searching for interesting point)
|
||||||
|
if self.searching_interesting_point and self.search_progress_text:
|
||||||
|
# Draw search progress overlay
|
||||||
|
height, width = canvas.shape[:2]
|
||||||
|
|
||||||
|
# Background for search progress
|
||||||
|
text_size = cv2.getTextSize(self.search_progress_text, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)[0]
|
||||||
|
padding = 10
|
||||||
|
bg_x = (width - text_size[0]) // 2 - padding
|
||||||
|
bg_y = height // 2 - 50
|
||||||
|
bg_w = text_size[0] + 2 * padding
|
||||||
|
bg_h = 30
|
||||||
|
|
||||||
|
# Semi-transparent background
|
||||||
|
overlay = canvas.copy()
|
||||||
|
cv2.rectangle(overlay, (bg_x, bg_y), (bg_x + bg_w, bg_y + bg_h), (0, 0, 0), -1)
|
||||||
|
cv2.addWeighted(overlay, 0.7, canvas, 0.3, 0, canvas)
|
||||||
|
|
||||||
|
# Border
|
||||||
|
cv2.rectangle(canvas, (bg_x, bg_y), (bg_x + bg_w, bg_y + bg_h), (255, 255, 0), 2)
|
||||||
|
|
||||||
|
# Text
|
||||||
|
cv2.putText(canvas, self.search_progress_text, (bg_x + padding, bg_y + 20),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
|
||||||
|
|
||||||
|
# Progress bar
|
||||||
|
bar_width = 200
|
||||||
|
bar_height = 6
|
||||||
|
bar_x = (width - bar_width) // 2
|
||||||
|
bar_y = bg_y + bg_h + 5
|
||||||
|
|
||||||
|
# Background
|
||||||
|
cv2.rectangle(canvas, (bar_x, bar_y), (bar_x + bar_width, bar_y + bar_height), (100, 100, 100), -1)
|
||||||
|
|
||||||
|
# Progress fill
|
||||||
|
fill_width = int(bar_width * (self.search_progress_percent / 100.0))
|
||||||
|
if fill_width > 0:
|
||||||
|
cv2.rectangle(canvas, (bar_x, bar_y), (bar_x + fill_width, bar_y + bar_height), (0, 255, 0), -1)
|
||||||
|
|
||||||
window_title = "Image Editor" if self.is_image_mode else "Video Editor"
|
window_title = "Image Editor" if self.is_image_mode else "Video Editor"
|
||||||
cv2.imshow(window_title, canvas)
|
cv2.imshow(window_title, canvas)
|
||||||
|
|
||||||
@@ -4519,6 +4743,25 @@ class VideoEditor:
|
|||||||
if not self.is_image_mode and self.cut_end_frame is not None:
|
if not self.is_image_mode and self.cut_end_frame is not None:
|
||||||
self.seek_to_frame(self.cut_end_frame)
|
self.seek_to_frame(self.cut_end_frame)
|
||||||
print(f"Jumped to cut end marker at frame {self.cut_end_frame}")
|
print(f"Jumped to cut end marker at frame {self.cut_end_frame}")
|
||||||
|
elif key == ord(";"): # ; - Go to next interesting point
|
||||||
|
if not self.is_image_mode:
|
||||||
|
self.go_to_next_interesting_point()
|
||||||
|
elif key == ord("0"): # 0 - Decrease frame difference threshold
|
||||||
|
self.frame_difference_threshold = max(1.0, self.frame_difference_threshold - 1.0)
|
||||||
|
print(f"Frame difference threshold: {self.frame_difference_threshold:.1f}%")
|
||||||
|
self.show_feedback_message(f"Threshold: {self.frame_difference_threshold:.1f}%")
|
||||||
|
elif key == ord("9"): # 9 - Increase frame difference threshold
|
||||||
|
self.frame_difference_threshold = min(100.0, self.frame_difference_threshold + 1.0)
|
||||||
|
print(f"Frame difference threshold: {self.frame_difference_threshold:.1f}%")
|
||||||
|
self.show_feedback_message(f"Threshold: {self.frame_difference_threshold:.1f}%")
|
||||||
|
elif key == ord("7"): # 7 - Decrease frame difference gap
|
||||||
|
self.frame_difference_gap = max(1, self.frame_difference_gap - 1)
|
||||||
|
print(f"Frame difference gap: {self.frame_difference_gap} frames")
|
||||||
|
self.show_feedback_message(f"Gap: {self.frame_difference_gap} frames")
|
||||||
|
elif key == ord("8"): # 8 - Increase frame difference gap
|
||||||
|
self.frame_difference_gap = min(100, self.frame_difference_gap + 1)
|
||||||
|
print(f"Frame difference gap: {self.frame_difference_gap} frames")
|
||||||
|
self.show_feedback_message(f"Gap: {self.frame_difference_gap} frames")
|
||||||
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()
|
||||||
|
Reference in New Issue
Block a user