Add multiple templates management to VideoEditor
This commit introduces a system for managing multiple templates within the VideoEditor. Users can now add, remove, and navigate through templates, enhancing the flexibility of template tracking. The state management has been updated to save and load multiple templates, including their regions and frame ranges. Additionally, visual indicators for active templates have been implemented in the frame display, improving user feedback during editing sessions.
This commit is contained in:
231
croppa/main.py
231
croppa/main.py
@@ -885,6 +885,11 @@ class VideoEditor:
|
|||||||
self.multi_scale_template_matching = False # Disable multi-scale by default # (x, y, w, h) in rotated frame coordinates
|
self.multi_scale_template_matching = False # Disable multi-scale by default # (x, y, w, h) in rotated frame coordinates
|
||||||
self.template_selection_start = None
|
self.template_selection_start = None
|
||||||
self.template_selection_rect = None
|
self.template_selection_rect = None
|
||||||
|
|
||||||
|
# Multiple templates system
|
||||||
|
self.templates = {} # {template_id: {'template': image, 'region': (x,y,w,h), 'start_frame': int, 'end_frame': int}}
|
||||||
|
self.current_template_id = None
|
||||||
|
self.template_id_counter = 0
|
||||||
|
|
||||||
# Project view mode
|
# Project view mode
|
||||||
self.project_view_mode = False
|
self.project_view_mode = False
|
||||||
@@ -934,7 +939,15 @@ class VideoEditor:
|
|||||||
'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_enabled': self.template_matching_enabled,
|
||||||
'template_region': self.template_region,
|
'template_region': self.template_region,
|
||||||
'multi_scale_template_matching': self.multi_scale_template_matching
|
'multi_scale_template_matching': self.multi_scale_template_matching,
|
||||||
|
'templates': {str(k): {
|
||||||
|
'template': None, # Don't save template images (too large)
|
||||||
|
'region': v['region'],
|
||||||
|
'start_frame': v['start_frame'],
|
||||||
|
'end_frame': v['end_frame']
|
||||||
|
} for k, v in self.templates.items()},
|
||||||
|
'current_template_id': self.current_template_id,
|
||||||
|
'template_id_counter': self.template_id_counter
|
||||||
}
|
}
|
||||||
|
|
||||||
with open(state_file, 'w') as f:
|
with open(state_file, 'w') as f:
|
||||||
@@ -1031,6 +1044,23 @@ class VideoEditor:
|
|||||||
self.tracking_template = None
|
self.tracking_template = None
|
||||||
if 'multi_scale_template_matching' in state:
|
if 'multi_scale_template_matching' in state:
|
||||||
self.multi_scale_template_matching = state['multi_scale_template_matching'] # Will be recreated on first use
|
self.multi_scale_template_matching = state['multi_scale_template_matching'] # Will be recreated on first use
|
||||||
|
|
||||||
|
# Load multiple templates state
|
||||||
|
if 'templates' in state:
|
||||||
|
self.templates = {}
|
||||||
|
for template_id_str, template_data in state['templates'].items():
|
||||||
|
template_id = int(template_id_str)
|
||||||
|
self.templates[template_id] = {
|
||||||
|
'template': None, # Will be recreated when needed
|
||||||
|
'region': template_data['region'],
|
||||||
|
'start_frame': template_data['start_frame'],
|
||||||
|
'end_frame': template_data['end_frame']
|
||||||
|
}
|
||||||
|
print(f"Loaded {len(self.templates)} templates")
|
||||||
|
if 'current_template_id' in state:
|
||||||
|
self.current_template_id = state['current_template_id']
|
||||||
|
if 'template_id_counter' in state:
|
||||||
|
self.template_id_counter = state['template_id_counter']
|
||||||
|
|
||||||
# Validate cut markers against current video length
|
# Validate cut markers against current video length
|
||||||
if self.cut_start_frame is not None and self.cut_start_frame >= self.total_frames:
|
if self.cut_start_frame is not None and self.cut_start_frame >= self.total_frames:
|
||||||
@@ -2222,12 +2252,12 @@ class VideoEditor:
|
|||||||
|
|
||||||
def track_template(self, frame):
|
def track_template(self, frame):
|
||||||
"""Track the template in the current frame"""
|
"""Track the template in the current frame"""
|
||||||
|
# First, try to select the best template for current frame
|
||||||
|
if not self._select_best_template_for_frame(self.current_frame):
|
||||||
|
return None
|
||||||
|
|
||||||
if self.tracking_template is None:
|
if self.tracking_template is None:
|
||||||
# Try to recreate template from saved region
|
return None
|
||||||
if self.template_region is not None:
|
|
||||||
self._recreate_template_from_region(frame)
|
|
||||||
if self.tracking_template is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Apply image preprocessing for better template matching
|
# Apply image preprocessing for better template matching
|
||||||
@@ -2383,15 +2413,142 @@ 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 instead of setting single template
|
||||||
self.template_region = (raw_x, raw_y, raw_w, raw_h)
|
template_id = self.add_template(template, (raw_x, raw_y, raw_w, raw_h))
|
||||||
self.show_feedback_message(f"Template set from region ({raw_w}x{raw_h})")
|
self.show_feedback_message(f"Template {template_id} set from region ({raw_w}x{raw_h})")
|
||||||
print(f"DEBUG: Template set with size {template.shape}")
|
print(f"DEBUG: Template {template_id} set with size {template.shape}")
|
||||||
else:
|
else:
|
||||||
self.show_feedback_message("Template region too small")
|
self.show_feedback_message("Template region too small")
|
||||||
else:
|
else:
|
||||||
self.show_feedback_message("Template region outside frame bounds")
|
self.show_feedback_message("Template region outside frame bounds")
|
||||||
|
|
||||||
|
def add_template(self, template, region, start_frame=None, end_frame=None):
|
||||||
|
"""Add a new template to the collection"""
|
||||||
|
template_id = self.template_id_counter
|
||||||
|
self.template_id_counter += 1
|
||||||
|
|
||||||
|
if start_frame is None:
|
||||||
|
start_frame = self.current_frame
|
||||||
|
if end_frame is None:
|
||||||
|
end_frame = self.total_frames - 1
|
||||||
|
|
||||||
|
self.templates[template_id] = {
|
||||||
|
'template': template.copy(),
|
||||||
|
'region': region,
|
||||||
|
'start_frame': start_frame,
|
||||||
|
'end_frame': end_frame
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set as current template if it's the first one or if it covers current frame
|
||||||
|
if self.current_template_id is None or (start_frame <= self.current_frame <= end_frame):
|
||||||
|
self.current_template_id = template_id
|
||||||
|
self.tracking_template = template.copy()
|
||||||
|
self.template_region = region
|
||||||
|
|
||||||
|
self.show_feedback_message(f"Template {template_id} added (frames {start_frame}-{end_frame})")
|
||||||
|
return template_id
|
||||||
|
|
||||||
|
def remove_template(self, template_id):
|
||||||
|
"""Remove a template from the collection"""
|
||||||
|
if template_id in self.templates:
|
||||||
|
del self.templates[template_id]
|
||||||
|
|
||||||
|
# If we removed the current template, find a new one
|
||||||
|
if self.current_template_id == template_id:
|
||||||
|
self._select_best_template_for_frame(self.current_frame)
|
||||||
|
|
||||||
|
self.show_feedback_message(f"Template {template_id} removed")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_template_for_frame(self, frame_number):
|
||||||
|
"""Get the best template for a given frame number"""
|
||||||
|
best_template_id = None
|
||||||
|
best_priority = -1
|
||||||
|
|
||||||
|
for template_id, template_data in self.templates.items():
|
||||||
|
start_frame = template_data['start_frame']
|
||||||
|
end_frame = template_data['end_frame']
|
||||||
|
|
||||||
|
# Check if template covers this frame
|
||||||
|
if start_frame <= frame_number <= end_frame:
|
||||||
|
# Priority: templates that start earlier and end later are preferred
|
||||||
|
priority = (end_frame - start_frame) - abs(frame_number - (start_frame + end_frame) // 2)
|
||||||
|
if priority > best_priority:
|
||||||
|
best_priority = priority
|
||||||
|
best_template_id = template_id
|
||||||
|
|
||||||
|
return best_template_id
|
||||||
|
|
||||||
|
def _select_best_template_for_frame(self, frame_number):
|
||||||
|
"""Select the best template for the current frame"""
|
||||||
|
best_template_id = self.get_template_for_frame(frame_number)
|
||||||
|
|
||||||
|
if best_template_id is not None:
|
||||||
|
self.current_template_id = best_template_id
|
||||||
|
template_data = self.templates[best_template_id]
|
||||||
|
self.tracking_template = template_data['template'].copy()
|
||||||
|
self.template_region = template_data['region']
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.current_template_id = None
|
||||||
|
self.tracking_template = None
|
||||||
|
self.template_region = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
def navigate_to_next_template(self):
|
||||||
|
"""Navigate to next template (; key)"""
|
||||||
|
if not self.templates:
|
||||||
|
return
|
||||||
|
|
||||||
|
template_ids = sorted(self.templates.keys())
|
||||||
|
if self.current_template_id is None:
|
||||||
|
# Find first template that starts after current frame
|
||||||
|
for template_id in template_ids:
|
||||||
|
if self.templates[template_id]['start_frame'] > self.current_frame:
|
||||||
|
self.current_template_id = template_id
|
||||||
|
self._select_best_template_for_frame(self.current_frame)
|
||||||
|
self.show_feedback_message(f"Template {template_id} selected")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Find next template
|
||||||
|
current_idx = template_ids.index(self.current_template_id)
|
||||||
|
for i in range(current_idx + 1, len(template_ids)):
|
||||||
|
template_id = template_ids[i]
|
||||||
|
if self.templates[template_id]['start_frame'] > self.current_frame:
|
||||||
|
self.current_template_id = template_id
|
||||||
|
self._select_best_template_for_frame(self.current_frame)
|
||||||
|
self.show_feedback_message(f"Template {template_id} selected")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.show_feedback_message("No next template found")
|
||||||
|
|
||||||
|
def navigate_to_previous_template(self):
|
||||||
|
"""Navigate to previous template (: key)"""
|
||||||
|
if not self.templates:
|
||||||
|
return
|
||||||
|
|
||||||
|
template_ids = sorted(self.templates.keys())
|
||||||
|
if self.current_template_id is None:
|
||||||
|
# Find last template that starts before current frame
|
||||||
|
for template_id in reversed(template_ids):
|
||||||
|
if self.templates[template_id]['start_frame'] < self.current_frame:
|
||||||
|
self.current_template_id = template_id
|
||||||
|
self._select_best_template_for_frame(self.current_frame)
|
||||||
|
self.show_feedback_message(f"Template {template_id} selected")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Find previous template
|
||||||
|
current_idx = template_ids.index(self.current_template_id)
|
||||||
|
for i in range(current_idx - 1, -1, -1):
|
||||||
|
template_id = template_ids[i]
|
||||||
|
if self.templates[template_id]['start_frame'] < self.current_frame:
|
||||||
|
self.current_template_id = template_id
|
||||||
|
self._select_best_template_for_frame(self.current_frame)
|
||||||
|
self.show_feedback_message(f"Template {template_id} selected")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.show_feedback_message("No previous template found")
|
||||||
|
|
||||||
def apply_rotation(self, frame):
|
def apply_rotation(self, frame):
|
||||||
"""Apply rotation to frame"""
|
"""Apply rotation to frame"""
|
||||||
@@ -2847,6 +3004,41 @@ class VideoEditor:
|
|||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Draw template markers
|
||||||
|
for template_id, template_data in self.templates.items():
|
||||||
|
start_frame = template_data['start_frame']
|
||||||
|
end_frame = template_data['end_frame']
|
||||||
|
|
||||||
|
# Draw template range
|
||||||
|
start_progress = start_frame / max(1, self.total_frames - 1)
|
||||||
|
end_progress = end_frame / max(1, self.total_frames - 1)
|
||||||
|
start_x = bar_x_start + int(bar_width * start_progress)
|
||||||
|
end_x = bar_x_start + int(bar_width * end_progress)
|
||||||
|
|
||||||
|
# Template range color (green for active, blue for inactive)
|
||||||
|
is_active = (self.current_template_id == template_id)
|
||||||
|
template_color = (0, 255, 0) if is_active else (255, 0, 0) # Green if active, red if inactive
|
||||||
|
|
||||||
|
# Draw template range bar
|
||||||
|
cv2.rectangle(
|
||||||
|
frame,
|
||||||
|
(start_x, bar_y + 2),
|
||||||
|
(end_x, bar_y + self.TIMELINE_BAR_HEIGHT - 2),
|
||||||
|
template_color,
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Draw template ID number
|
||||||
|
cv2.putText(
|
||||||
|
frame,
|
||||||
|
str(template_id),
|
||||||
|
(start_x + 2, bar_y + 10),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.3,
|
||||||
|
(255, 255, 255),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
def display_current_frame(self):
|
def display_current_frame(self):
|
||||||
"""Display the current frame with all overlays"""
|
"""Display the current frame with all overlays"""
|
||||||
if self.current_display_frame is None:
|
if self.current_display_frame is None:
|
||||||
@@ -3309,6 +3501,21 @@ class VideoEditor:
|
|||||||
self.template_selection_start = None
|
self.template_selection_start = None
|
||||||
self.template_selection_rect = None
|
self.template_selection_rect = None
|
||||||
|
|
||||||
|
# Handle Ctrl+Right-click for template removal
|
||||||
|
if event == cv2.EVENT_RBUTTONDOWN and (flags & cv2.EVENT_FLAG_CTRLKEY) and not (flags & cv2.EVENT_FLAG_SHIFTKEY):
|
||||||
|
if not self.is_image_mode and self.templates:
|
||||||
|
# Check if click is near any template region
|
||||||
|
screen_x, screen_y = x, y
|
||||||
|
raw_x, raw_y = self._map_screen_to_rotated(screen_x, screen_y)
|
||||||
|
|
||||||
|
for template_id, template_data in self.templates.items():
|
||||||
|
tx, ty, tw, th = template_data['region']
|
||||||
|
# Check if click is within template region
|
||||||
|
if (tx <= raw_x <= tx + tw and ty <= raw_y <= ty + th):
|
||||||
|
self.remove_template(template_id)
|
||||||
|
self.save_state()
|
||||||
|
return
|
||||||
|
|
||||||
# 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:
|
||||||
@@ -4513,6 +4720,10 @@ class VideoEditor:
|
|||||||
print(f"DEBUG: Multi-scale template matching toggled to {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.show_feedback_message(f"Multi-scale template matching {'ON' if self.multi_scale_template_matching else 'OFF'}")
|
||||||
self.save_state()
|
self.save_state()
|
||||||
|
elif key == ord(";"): # Semicolon - Navigate to next template
|
||||||
|
self.navigate_to_next_template()
|
||||||
|
elif key == ord(":"): # Colon - Navigate to previous template
|
||||||
|
self.navigate_to_previous_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