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:
239
croppa/editor.py
239
croppa/editor.py
@@ -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
|
||||||
|
Reference in New Issue
Block a user