Enhance VideoEditor with tracking-adjusted cropping and improved transformation logic

This commit introduces a new method, _get_tracking_adjusted_crop, to calculate crop rectangles that center on tracked points when motion tracking is enabled. The transform_video_to_screen and transform_screen_to_video methods are refactored to utilize this new method, streamlining the handling of crop, rotation, and zoom transformations. These changes improve the accuracy and efficiency of video rendering and editing processes, enhancing the overall user experience.
This commit is contained in:
2025-09-16 17:49:50 +02:00
parent cacaa5f2ac
commit e97ce026da

View File

@@ -1764,6 +1764,28 @@ class VideoEditor:
self.feedback_message = message self.feedback_message = message
self.feedback_message_time = time.time() self.feedback_message_time = time.time()
def _get_tracking_adjusted_crop(self) -> Tuple[int, int, int, int]:
"""Return crop rect adjusted to center on tracked point when enabled, clamped to frame."""
if self.crop_rect:
base_x, base_y, w, h = map(int, self.crop_rect)
else:
base_x, base_y, w, h = 0, 0, int(self.frame_width), int(self.frame_height)
x, y = base_x, base_y
if self.motion_tracker.tracking_enabled:
pos = self.motion_tracker.get_interpolated_position(self.current_frame)
if pos:
tx, ty = pos
x = int(tx - w // 2)
y = int(ty - h // 2)
# clamp
x = max(0, min(x, int(self.frame_width) - 1))
y = max(0, min(y, int(self.frame_height) - 1))
w = min(int(w), int(self.frame_width) - x)
h = min(int(h), int(self.frame_height) - y)
return (x, y, w, h)
def transform_video_to_screen( def transform_video_to_screen(
self, video_x: int, video_y: int self, video_x: int, video_y: int
) -> Optional[Tuple[int, int]]: ) -> Optional[Tuple[int, int]]:
@@ -1771,93 +1793,47 @@ class VideoEditor:
if self.current_display_frame is None: if self.current_display_frame is None:
return None return None
# Get the original frame dimensions angle = int(self.rotation_angle) % 360
original_height, original_width = self.current_display_frame.shape[:2] zoom = float(self.zoom_factor)
available_height = self.window_height - ( crop_x, crop_y, crop_w, crop_h = self._get_tracking_adjusted_crop()
0 if self.is_image_mode else self.TIMELINE_HEIGHT
)
# Step 1: Apply crop (subtract crop offset, including motion tracking offset) # translate to crop
display_x = video_x display_x = float(video_x - crop_x)
display_y = video_y display_y = float(video_y - crop_y)
if self.crop_rect:
crop_x, crop_y, crop_w, crop_h = self.crop_rect
# Apply motion tracking offset if enabled # rotate within crop space
if self.motion_tracker.tracking_enabled: if angle == 90:
current_pos = self.motion_tracker.get_interpolated_position(
self.current_frame
)
if current_pos:
# Move crop center to tracked point (same logic as in apply_crop_zoom_and_rotation)
tracked_x, tracked_y = current_pos
new_x = int(tracked_x - crop_w // 2)
new_y = int(tracked_y - crop_h // 2)
crop_x, crop_y = new_x, new_y
display_x -= crop_x
display_y -= crop_y
# Step 2: Apply rotation
if self.rotation_angle != 0:
if self.crop_rect:
crop_w, crop_h = int(self.crop_rect[2]), int(self.crop_rect[3])
else:
crop_w, crop_h = original_width, original_height
if self.rotation_angle == 90:
# 90° clockwise rotation: (x,y) -> (y, crop_w-x)
new_x = display_y new_x = display_y
new_y = crop_w - display_x new_y = float(crop_w) - display_x
elif self.rotation_angle == 180: elif angle == 180:
# 180° rotation: (x,y) -> (crop_w-x, crop_h-y) new_x = float(crop_w) - display_x
new_x = crop_w - display_x new_y = float(crop_h) - display_y
new_y = crop_h - display_y elif angle == 270:
elif self.rotation_angle == 270: new_x = float(crop_h) - display_y
# 270° clockwise rotation: (x,y) -> (crop_h-y, x)
new_x = crop_h - display_y
new_y = display_x new_y = display_x
else: else:
new_x, new_y = display_x, display_y new_x, new_y = display_x, display_y
display_x, display_y = new_x, new_y display_x, display_y = new_x, new_y
# Step 3: Apply zoom # zoom and pan
if self.zoom_factor != 1.0: display_x *= zoom
display_x *= self.zoom_factor display_y *= zoom
display_y *= self.zoom_factor display_x += float(self.display_offset[0])
display_y += float(self.display_offset[1])
# Step 4: Apply display offset (panning when zoomed) # fit to window
display_x += self.display_offset[0] available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
display_y += self.display_offset[1] rot_w = float(crop_h) if angle in (90, 270) else float(crop_w)
rot_h = float(crop_w) if angle in (90, 270) else float(crop_h)
rot_w *= zoom
rot_h *= zoom
scale = min(self.window_width / max(1.0, rot_w), available_height / max(1.0, rot_h))
start_x = (self.window_width - rot_w * scale) / 2.0
start_y = (available_height - rot_h * scale) / 2.0
# Step 5: Scale to fit window
if self.crop_rect:
crop_w, crop_h = int(self.crop_rect[2]), int(self.crop_rect[3])
else:
crop_w, crop_h = original_width, original_height
# Apply zoom factor to dimensions
if self.zoom_factor != 1.0:
crop_w = int(crop_w * self.zoom_factor)
crop_h = int(crop_h * self.zoom_factor)
# Calculate scale to fit window
scale_x = self.window_width / crop_w
scale_y = available_height / crop_h
scale = min(scale_x, scale_y)
# Center the scaled content
scaled_w = crop_w * scale
scaled_h = crop_h * scale
start_x = (self.window_width - scaled_w) // 2
start_y = (available_height - scaled_h) // 2
# Apply final scaling and centering
screen_x = start_x + display_x * scale screen_x = start_x + display_x * scale
screen_y = start_y + display_y * scale screen_y = start_y + display_y * scale
return (int(round(screen_x)), int(round(screen_y)))
return (int(screen_x), int(screen_y))
def transform_screen_to_video( def transform_screen_to_video(
self, screen_x: int, screen_y: int self, screen_x: int, screen_y: int
@@ -1866,93 +1842,44 @@ class VideoEditor:
if self.current_display_frame is None: if self.current_display_frame is None:
return None return None
# Get the original frame dimensions angle = int(self.rotation_angle) % 360
original_height, original_width = self.current_display_frame.shape[:2] zoom = float(self.zoom_factor)
available_height = self.window_height - ( crop_x, crop_y, crop_w, crop_h = self._get_tracking_adjusted_crop()
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)
) rot_w = float(crop_h) if angle in (90, 270) else float(crop_w)
rot_h = float(crop_w) if angle in (90, 270) else float(crop_h)
rot_w *= zoom
rot_h *= zoom
scale = min(self.window_width / max(1.0, rot_w), available_height / max(1.0, rot_h))
start_x = (self.window_width - rot_w * scale) / 2.0
start_y = (available_height - rot_h * scale) / 2.0
# Step 1: Reverse scaling and centering # reverse fit
if self.crop_rect: display_x = (float(screen_x) - start_x) / scale
crop_w, crop_h = int(self.crop_rect[2]), int(self.crop_rect[3]) display_y = (float(screen_y) - start_y) / scale
else: # reverse pan
crop_w, crop_h = original_width, original_height display_x -= float(self.display_offset[0])
display_y -= float(self.display_offset[1])
# Apply zoom factor to dimensions # reverse zoom
if self.zoom_factor != 1.0: if zoom != 1.0:
crop_w = int(crop_w * self.zoom_factor) display_x /= zoom
crop_h = int(crop_h * self.zoom_factor) display_y /= zoom
# reverse rotation
# Calculate scale to fit window if angle == 90:
scale_x = self.window_width / crop_w new_x = float(crop_w) - display_y
scale_y = available_height / crop_h
scale = min(scale_x, scale_y)
# Center the scaled content
scaled_w = crop_w * scale
scaled_h = crop_h * scale
start_x = (self.window_width - scaled_w) // 2
start_y = (available_height - scaled_h) // 2
# Reverse scaling and centering
display_x = (screen_x - start_x) / scale
display_y = (screen_y - start_y) / scale
# Step 2: Reverse display offset (panning when zoomed)
display_x -= self.display_offset[0]
display_y -= self.display_offset[1]
# Step 3: Reverse zoom
if self.zoom_factor != 1.0:
display_x /= self.zoom_factor
display_y /= self.zoom_factor
# Step 4: Reverse rotation
if self.rotation_angle != 0:
if self.crop_rect:
crop_w, crop_h = int(self.crop_rect[2]), int(self.crop_rect[3])
else:
crop_w, crop_h = original_width, original_height
if self.rotation_angle == 90:
# Reverse 90° clockwise rotation: (y, crop_w-x) -> (x,y)
new_x = crop_w - display_y
new_y = display_x new_y = display_x
elif self.rotation_angle == 180: elif angle == 180:
# Reverse 180° rotation: (crop_w-x, crop_h-y) -> (x,y) new_x = float(crop_w) - display_x
new_x = crop_w - display_x new_y = float(crop_h) - display_y
new_y = crop_h - display_y elif angle == 270:
elif self.rotation_angle == 270:
# Reverse 270° clockwise rotation: (crop_h-y, x) -> (x,y)
new_x = display_y new_x = display_y
new_y = crop_h - display_x new_y = float(crop_h) - display_x
else: else:
new_x, new_y = display_x, display_y new_x, new_y = display_x, display_y
display_x, display_y = new_x, new_y video_x = new_x + float(crop_x)
video_y = new_y + float(crop_y)
# Step 5: Reverse crop (add crop offset, including motion tracking offset) return (int(round(video_x)), int(round(video_y)))
video_x = display_x
video_y = display_y
if self.crop_rect:
crop_x, crop_y, crop_w, crop_h = self.crop_rect
# Apply motion tracking offset if enabled
if self.motion_tracker.tracking_enabled:
current_pos = self.motion_tracker.get_interpolated_position(
self.current_frame
)
if current_pos:
# Move crop center to tracked point (same logic as in apply_crop_zoom_and_rotation)
tracked_x, tracked_y = current_pos
new_x = int(tracked_x - crop_w // 2)
new_y = int(tracked_y - crop_h // 2)
crop_x, crop_y = new_x, new_y
video_x += crop_x
video_y += crop_y
return (int(video_x), int(video_y))
def transform_point_for_display( def transform_point_for_display(
self, video_x: int, video_y: int self, video_x: int, video_y: int