Code format
This commit is contained in:
361
croppa/main.py
361
croppa/main.py
@@ -34,7 +34,7 @@ class VideoEditor:
|
||||
ZOOM_INCREMENT = 0.1
|
||||
|
||||
# Supported video extensions
|
||||
VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v'}
|
||||
VIDEO_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv", ".wmv", ".flv", ".webm", ".m4v"}
|
||||
|
||||
def __init__(self, path: str):
|
||||
self.path = Path(path)
|
||||
@@ -98,13 +98,16 @@ class VideoEditor:
|
||||
"""Get all video files from a directory, sorted by name"""
|
||||
video_files = []
|
||||
for file_path in directory.iterdir():
|
||||
if file_path.is_file() and file_path.suffix.lower() in self.VIDEO_EXTENSIONS:
|
||||
if (
|
||||
file_path.is_file()
|
||||
and file_path.suffix.lower() in self.VIDEO_EXTENSIONS
|
||||
):
|
||||
video_files.append(file_path)
|
||||
return sorted(video_files)
|
||||
|
||||
def _load_video(self, video_path: Path):
|
||||
"""Load a video file and initialize video properties"""
|
||||
if hasattr(self, 'cap') and self.cap:
|
||||
if hasattr(self, "cap") and self.cap:
|
||||
self.cap.release()
|
||||
|
||||
self.video_path = video_path
|
||||
@@ -137,7 +140,9 @@ class VideoEditor:
|
||||
self.cut_end_frame = None
|
||||
self.display_offset = [0, 0]
|
||||
|
||||
print(f"Loaded video: {self.video_path.name} ({self.current_video_index + 1}/{len(self.video_files)})")
|
||||
print(
|
||||
f"Loaded video: {self.video_path.name} ({self.current_video_index + 1}/{len(self.video_files)})"
|
||||
)
|
||||
|
||||
def switch_to_video(self, index: int):
|
||||
"""Switch to a specific video by index"""
|
||||
@@ -172,18 +177,22 @@ class VideoEditor:
|
||||
|
||||
def seek_video(self, frames_delta: int):
|
||||
"""Seek video by specified number of frames"""
|
||||
target_frame = max(0, min(self.current_frame + frames_delta, self.total_frames - 1))
|
||||
target_frame = max(
|
||||
0, min(self.current_frame + frames_delta, self.total_frames - 1)
|
||||
)
|
||||
self.current_frame = target_frame
|
||||
self.load_current_frame()
|
||||
|
||||
def seek_video_with_modifier(self, direction: int, shift_pressed: bool, ctrl_pressed: bool):
|
||||
def seek_video_with_modifier(
|
||||
self, direction: int, shift_pressed: bool, ctrl_pressed: bool
|
||||
):
|
||||
"""Seek video with different frame counts based on modifiers"""
|
||||
if ctrl_pressed:
|
||||
frames = direction * 60 # Ctrl: 60 frames
|
||||
elif shift_pressed:
|
||||
frames = direction * 10 # Shift: 10 frames
|
||||
else:
|
||||
frames = direction * 1 # Default: 1 frame
|
||||
frames = direction * 1 # Default: 1 frame
|
||||
|
||||
self.seek_video(frames)
|
||||
|
||||
@@ -223,7 +232,7 @@ class VideoEditor:
|
||||
w = min(w, processed_frame.shape[1] - x)
|
||||
h = min(h, processed_frame.shape[0] - y)
|
||||
if w > 0 and h > 0:
|
||||
processed_frame = processed_frame[y:y+h, x:x+w]
|
||||
processed_frame = processed_frame[y : y + h, x : x + w]
|
||||
|
||||
# Apply rotation
|
||||
if self.rotation_angle != 0:
|
||||
@@ -234,7 +243,9 @@ class VideoEditor:
|
||||
height, width = processed_frame.shape[:2]
|
||||
new_width = int(width * self.zoom_factor)
|
||||
new_height = int(height * self.zoom_factor)
|
||||
processed_frame = cv2.resize(processed_frame, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
|
||||
processed_frame = cv2.resize(
|
||||
processed_frame, (new_width, new_height), interpolation=cv2.INTER_LINEAR
|
||||
)
|
||||
|
||||
# Handle zoom center and display offset
|
||||
if new_width > self.window_width or new_height > self.window_height:
|
||||
@@ -272,7 +283,9 @@ class VideoEditor:
|
||||
brightness_value = self.brightness * 2.55
|
||||
|
||||
# Apply brightness and contrast: new_pixel = contrast * old_pixel + brightness
|
||||
adjusted = cv2.convertScaleAbs(frame, alpha=self.contrast, beta=brightness_value)
|
||||
adjusted = cv2.convertScaleAbs(
|
||||
frame, alpha=self.contrast, beta=brightness_value
|
||||
)
|
||||
return adjusted
|
||||
|
||||
def adjust_brightness(self, delta: int):
|
||||
@@ -300,41 +313,103 @@ class VideoEditor:
|
||||
self.timeline_rect = (bar_x_start, bar_y, bar_width, self.TIMELINE_BAR_HEIGHT)
|
||||
|
||||
# Draw timeline background
|
||||
cv2.rectangle(frame, (bar_x_start, bar_y), (bar_x_end, bar_y + self.TIMELINE_BAR_HEIGHT), self.TIMELINE_COLOR_BG, -1)
|
||||
cv2.rectangle(frame, (bar_x_start, bar_y), (bar_x_end, bar_y + self.TIMELINE_BAR_HEIGHT), self.TIMELINE_COLOR_BORDER, 1)
|
||||
cv2.rectangle(
|
||||
frame,
|
||||
(bar_x_start, bar_y),
|
||||
(bar_x_end, bar_y + self.TIMELINE_BAR_HEIGHT),
|
||||
self.TIMELINE_COLOR_BG,
|
||||
-1,
|
||||
)
|
||||
cv2.rectangle(
|
||||
frame,
|
||||
(bar_x_start, bar_y),
|
||||
(bar_x_end, bar_y + self.TIMELINE_BAR_HEIGHT),
|
||||
self.TIMELINE_COLOR_BORDER,
|
||||
1,
|
||||
)
|
||||
|
||||
# Draw progress
|
||||
if self.total_frames > 0:
|
||||
progress = self.current_frame / max(1, self.total_frames - 1)
|
||||
progress_width = int(bar_width * progress)
|
||||
if progress_width > 0:
|
||||
cv2.rectangle(frame, (bar_x_start, bar_y), (bar_x_start + progress_width, bar_y + self.TIMELINE_BAR_HEIGHT), self.TIMELINE_COLOR_PROGRESS, -1)
|
||||
cv2.rectangle(
|
||||
frame,
|
||||
(bar_x_start, bar_y),
|
||||
(bar_x_start + progress_width, bar_y + self.TIMELINE_BAR_HEIGHT),
|
||||
self.TIMELINE_COLOR_PROGRESS,
|
||||
-1,
|
||||
)
|
||||
|
||||
# Draw current position handle
|
||||
handle_x = bar_x_start + progress_width
|
||||
handle_y = bar_y + self.TIMELINE_BAR_HEIGHT // 2
|
||||
cv2.circle(frame, (handle_x, handle_y), self.TIMELINE_HANDLE_SIZE // 2, self.TIMELINE_COLOR_HANDLE, -1)
|
||||
cv2.circle(frame, (handle_x, handle_y), self.TIMELINE_HANDLE_SIZE // 2, self.TIMELINE_COLOR_BORDER, 2)
|
||||
cv2.circle(
|
||||
frame,
|
||||
(handle_x, handle_y),
|
||||
self.TIMELINE_HANDLE_SIZE // 2,
|
||||
self.TIMELINE_COLOR_HANDLE,
|
||||
-1,
|
||||
)
|
||||
cv2.circle(
|
||||
frame,
|
||||
(handle_x, handle_y),
|
||||
self.TIMELINE_HANDLE_SIZE // 2,
|
||||
self.TIMELINE_COLOR_BORDER,
|
||||
2,
|
||||
)
|
||||
|
||||
# Draw cut points
|
||||
if self.cut_start_frame is not None:
|
||||
cut_start_progress = self.cut_start_frame / max(1, self.total_frames - 1)
|
||||
cut_start_progress = self.cut_start_frame / max(
|
||||
1, self.total_frames - 1
|
||||
)
|
||||
cut_start_x = bar_x_start + int(bar_width * cut_start_progress)
|
||||
cv2.line(frame, (cut_start_x, bar_y), (cut_start_x, bar_y + self.TIMELINE_BAR_HEIGHT), self.TIMELINE_COLOR_CUT_POINT, 3)
|
||||
cv2.putText(frame, "1", (cut_start_x - 5, bar_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.4, self.TIMELINE_COLOR_CUT_POINT, 1)
|
||||
cv2.line(
|
||||
frame,
|
||||
(cut_start_x, bar_y),
|
||||
(cut_start_x, bar_y + self.TIMELINE_BAR_HEIGHT),
|
||||
self.TIMELINE_COLOR_CUT_POINT,
|
||||
3,
|
||||
)
|
||||
cv2.putText(
|
||||
frame,
|
||||
"1",
|
||||
(cut_start_x - 5, bar_y - 5),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.4,
|
||||
self.TIMELINE_COLOR_CUT_POINT,
|
||||
1,
|
||||
)
|
||||
|
||||
if self.cut_end_frame is not None:
|
||||
cut_end_progress = self.cut_end_frame / max(1, self.total_frames - 1)
|
||||
cut_end_x = bar_x_start + int(bar_width * cut_end_progress)
|
||||
cv2.line(frame, (cut_end_x, bar_y), (cut_end_x, bar_y + self.TIMELINE_BAR_HEIGHT), self.TIMELINE_COLOR_CUT_POINT, 3)
|
||||
cv2.putText(frame, "2", (cut_end_x - 5, bar_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.4, self.TIMELINE_COLOR_CUT_POINT, 1)
|
||||
cv2.line(
|
||||
frame,
|
||||
(cut_end_x, bar_y),
|
||||
(cut_end_x, bar_y + self.TIMELINE_BAR_HEIGHT),
|
||||
self.TIMELINE_COLOR_CUT_POINT,
|
||||
3,
|
||||
)
|
||||
cv2.putText(
|
||||
frame,
|
||||
"2",
|
||||
(cut_end_x - 5, bar_y - 5),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.4,
|
||||
self.TIMELINE_COLOR_CUT_POINT,
|
||||
1,
|
||||
)
|
||||
|
||||
def draw_crop_overlay(self, canvas, start_x, start_y, frame_width, frame_height):
|
||||
"""Draw crop overlay on canvas using screen coordinates"""
|
||||
# Draw preview rectangle (green) - already in screen coordinates
|
||||
if self.crop_preview_rect:
|
||||
x, y, w, h = self.crop_preview_rect
|
||||
cv2.rectangle(canvas, (int(x), int(y)), (int(x + w), int(y + h)), (0, 255, 0), 2)
|
||||
cv2.rectangle(
|
||||
canvas, (int(x), int(y)), (int(x + w), int(y + h)), (0, 255, 0), 2
|
||||
)
|
||||
|
||||
# Draw final crop rectangle (red) - convert from video to screen coordinates
|
||||
if self.crop_rect:
|
||||
@@ -345,7 +420,9 @@ class VideoEditor:
|
||||
original_height, original_width = self.current_display_frame.shape[:2]
|
||||
available_height = self.window_height - self.TIMELINE_HEIGHT
|
||||
|
||||
scale = min(self.window_width / original_width, available_height / original_height)
|
||||
scale = min(
|
||||
self.window_width / original_width, available_height / original_height
|
||||
)
|
||||
if scale < 1.0:
|
||||
new_width = int(original_width * scale)
|
||||
new_height = int(original_height * scale)
|
||||
@@ -359,8 +436,13 @@ class VideoEditor:
|
||||
screen_w = w * new_width / original_width
|
||||
screen_h = h * new_height / original_height
|
||||
|
||||
cv2.rectangle(canvas, (int(screen_x), int(screen_y)),
|
||||
(int(screen_x + screen_w), int(screen_y + screen_h)), (255, 0, 0), 2)
|
||||
cv2.rectangle(
|
||||
canvas,
|
||||
(int(screen_x), int(screen_y)),
|
||||
(int(screen_x + screen_w), int(screen_y + screen_h)),
|
||||
(255, 0, 0),
|
||||
2,
|
||||
)
|
||||
|
||||
def display_current_frame(self):
|
||||
"""Display the current frame with all overlays"""
|
||||
@@ -368,7 +450,9 @@ class VideoEditor:
|
||||
return
|
||||
|
||||
# Apply crop, zoom, and rotation transformations for preview
|
||||
display_frame = self.apply_crop_zoom_and_rotation(self.current_display_frame.copy())
|
||||
display_frame = self.apply_crop_zoom_and_rotation(
|
||||
self.current_display_frame.copy()
|
||||
)
|
||||
|
||||
if display_frame is None:
|
||||
return
|
||||
@@ -391,25 +475,59 @@ class VideoEditor:
|
||||
start_y = (available_height - frame_height) // 2
|
||||
start_x = (self.window_width - frame_width) // 2
|
||||
|
||||
canvas[start_y:start_y + frame_height, start_x:start_x + frame_width] = display_frame
|
||||
canvas[start_y : start_y + frame_height, start_x : start_x + frame_width] = (
|
||||
display_frame
|
||||
)
|
||||
|
||||
# Draw crop overlay
|
||||
if self.crop_rect or self.crop_preview_rect:
|
||||
self.draw_crop_overlay(canvas, start_x, start_y, frame_width, frame_height)
|
||||
|
||||
# Add info overlay
|
||||
rotation_text = f" | Rotation: {self.rotation_angle}°" if self.rotation_angle != 0 else ""
|
||||
brightness_text = f" | Brightness: {self.brightness}" if self.brightness != 0 else ""
|
||||
contrast_text = f" | Contrast: {self.contrast:.1f}" if self.contrast != 1.0 else ""
|
||||
rotation_text = (
|
||||
f" | Rotation: {self.rotation_angle}°" if self.rotation_angle != 0 else ""
|
||||
)
|
||||
brightness_text = (
|
||||
f" | Brightness: {self.brightness}" if self.brightness != 0 else ""
|
||||
)
|
||||
contrast_text = (
|
||||
f" | Contrast: {self.contrast:.1f}" if self.contrast != 1.0 else ""
|
||||
)
|
||||
info_text = f"Frame: {self.current_frame}/{self.total_frames} | Speed: {self.playback_speed:.1f}x | Zoom: {self.zoom_factor:.1f}x{rotation_text}{brightness_text}{contrast_text} | {'Playing' if self.is_playing else 'Paused'}"
|
||||
cv2.putText(canvas, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
||||
cv2.putText(canvas, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1)
|
||||
cv2.putText(
|
||||
canvas,
|
||||
info_text,
|
||||
(10, 30),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.7,
|
||||
(255, 255, 255),
|
||||
2,
|
||||
)
|
||||
cv2.putText(
|
||||
canvas, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1
|
||||
)
|
||||
|
||||
# Add video navigation info
|
||||
if len(self.video_files) > 1:
|
||||
video_text = f"Video: {self.current_video_index + 1}/{len(self.video_files)} - {self.video_path.name}"
|
||||
cv2.putText(canvas, video_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
|
||||
cv2.putText(canvas, video_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1)
|
||||
cv2.putText(
|
||||
canvas,
|
||||
video_text,
|
||||
(10, 60),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.6,
|
||||
(255, 255, 255),
|
||||
2,
|
||||
)
|
||||
cv2.putText(
|
||||
canvas,
|
||||
video_text,
|
||||
(10, 60),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.6,
|
||||
(0, 0, 0),
|
||||
1,
|
||||
)
|
||||
y_offset = 90
|
||||
else:
|
||||
y_offset = 60
|
||||
@@ -417,15 +535,49 @@ class VideoEditor:
|
||||
# Add crop info
|
||||
if self.crop_rect:
|
||||
crop_text = f"Crop: {int(self.crop_rect[0])},{int(self.crop_rect[1])} {int(self.crop_rect[2])}x{int(self.crop_rect[3])}"
|
||||
cv2.putText(canvas, crop_text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
|
||||
cv2.putText(canvas, crop_text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1)
|
||||
cv2.putText(
|
||||
canvas,
|
||||
crop_text,
|
||||
(10, y_offset),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.6,
|
||||
(255, 255, 255),
|
||||
2,
|
||||
)
|
||||
cv2.putText(
|
||||
canvas,
|
||||
crop_text,
|
||||
(10, y_offset),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.6,
|
||||
(0, 0, 0),
|
||||
1,
|
||||
)
|
||||
y_offset += 30
|
||||
|
||||
# Add cut info
|
||||
if self.cut_start_frame is not None or self.cut_end_frame is not None:
|
||||
cut_text = f"Cut: {self.cut_start_frame or '?'} - {self.cut_end_frame or '?'}"
|
||||
cv2.putText(canvas, cut_text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
|
||||
cv2.putText(canvas, cut_text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1)
|
||||
cut_text = (
|
||||
f"Cut: {self.cut_start_frame or '?'} - {self.cut_end_frame or '?'}"
|
||||
)
|
||||
cv2.putText(
|
||||
canvas,
|
||||
cut_text,
|
||||
(10, y_offset),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.6,
|
||||
(255, 255, 255),
|
||||
2,
|
||||
)
|
||||
cv2.putText(
|
||||
canvas,
|
||||
cut_text,
|
||||
(10, y_offset),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.6,
|
||||
(0, 0, 0),
|
||||
1,
|
||||
)
|
||||
|
||||
# Draw timeline
|
||||
self.draw_timeline(canvas)
|
||||
@@ -483,9 +635,13 @@ class VideoEditor:
|
||||
if flags & cv2.EVENT_FLAG_CTRLKEY:
|
||||
if event == cv2.EVENT_MOUSEWHEEL:
|
||||
if flags > 0: # Scroll up
|
||||
self.zoom_factor = min(self.MAX_ZOOM, self.zoom_factor + self.ZOOM_INCREMENT)
|
||||
self.zoom_factor = min(
|
||||
self.MAX_ZOOM, self.zoom_factor + self.ZOOM_INCREMENT
|
||||
)
|
||||
else: # Scroll down
|
||||
self.zoom_factor = max(self.MIN_ZOOM, self.zoom_factor - self.ZOOM_INCREMENT)
|
||||
self.zoom_factor = max(
|
||||
self.MIN_ZOOM, self.zoom_factor - self.ZOOM_INCREMENT
|
||||
)
|
||||
|
||||
def set_crop_from_screen_coords(self, screen_rect):
|
||||
"""Convert screen coordinates to video frame coordinates and set crop"""
|
||||
@@ -497,14 +653,18 @@ class VideoEditor:
|
||||
available_height = self.window_height - self.TIMELINE_HEIGHT
|
||||
|
||||
# Calculate how the original frame is displayed (after crop/zoom/rotation)
|
||||
display_frame = self.apply_crop_zoom_and_rotation(self.current_display_frame.copy())
|
||||
display_frame = self.apply_crop_zoom_and_rotation(
|
||||
self.current_display_frame.copy()
|
||||
)
|
||||
if display_frame is None:
|
||||
return
|
||||
|
||||
display_height, display_width = display_frame.shape[:2]
|
||||
|
||||
# Calculate scale for the display frame
|
||||
scale = min(self.window_width / display_width, available_height / display_height)
|
||||
scale = min(
|
||||
self.window_width / display_width, available_height / display_height
|
||||
)
|
||||
if scale < 1.0:
|
||||
final_display_width = int(display_width * scale)
|
||||
final_display_height = int(display_height * scale)
|
||||
@@ -558,7 +718,6 @@ class VideoEditor:
|
||||
original_w = min(original_w, original_width - original_x)
|
||||
original_h = min(original_h, original_height - original_y)
|
||||
|
||||
|
||||
if original_w > 10 and original_h > 10: # Minimum size check
|
||||
# Save current crop for undo
|
||||
if self.crop_rect:
|
||||
@@ -581,15 +740,19 @@ class VideoEditor:
|
||||
|
||||
def render_video(self, output_path: str):
|
||||
"""Optimized video rendering with multithreading and batch processing"""
|
||||
if not output_path.endswith('.mp4'):
|
||||
output_path += '.mp4'
|
||||
if not output_path.endswith(".mp4"):
|
||||
output_path += ".mp4"
|
||||
|
||||
print(f"Rendering video to {output_path}...")
|
||||
start_time = time.time()
|
||||
|
||||
# Determine frame range
|
||||
start_frame = self.cut_start_frame if self.cut_start_frame is not None else 0
|
||||
end_frame = self.cut_end_frame if self.cut_end_frame is not None else self.total_frames - 1
|
||||
end_frame = (
|
||||
self.cut_end_frame
|
||||
if self.cut_end_frame is not None
|
||||
else self.total_frames - 1
|
||||
)
|
||||
|
||||
if start_frame >= end_frame:
|
||||
print("Invalid cut range!")
|
||||
@@ -612,8 +775,10 @@ class VideoEditor:
|
||||
output_height = int(crop_height * self.zoom_factor)
|
||||
|
||||
# Use mp4v codec (most compatible with MP4)
|
||||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||||
out = cv2.VideoWriter(output_path, fourcc, self.fps, (output_width, output_height))
|
||||
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
||||
out = cv2.VideoWriter(
|
||||
output_path, fourcc, self.fps, (output_width, output_height)
|
||||
)
|
||||
|
||||
if not out.isOpened():
|
||||
print("Error: Could not open video writer!")
|
||||
@@ -632,7 +797,9 @@ class VideoEditor:
|
||||
break
|
||||
|
||||
# Process and write frame directly (minimize memory copies)
|
||||
processed_frame = self._process_frame_for_render(frame, output_width, output_height)
|
||||
processed_frame = self._process_frame_for_render(
|
||||
frame, output_width, output_height
|
||||
)
|
||||
|
||||
if processed_frame is not None:
|
||||
out.write(processed_frame)
|
||||
@@ -645,9 +812,14 @@ class VideoEditor:
|
||||
progress = frames_written / total_output_frames * 100
|
||||
elapsed = current_time - start_time
|
||||
fps_rate = frames_written / elapsed
|
||||
eta = (elapsed / frames_written) * (total_output_frames - frames_written)
|
||||
print(f"Progress: {progress:.1f}% | {frames_written}/{total_output_frames} | "
|
||||
f"FPS: {fps_rate:.1f} | ETA: {eta:.1f}s\r", end="")
|
||||
eta = (elapsed / frames_written) * (
|
||||
total_output_frames - frames_written
|
||||
)
|
||||
print(
|
||||
f"Progress: {progress:.1f}% | {frames_written}/{total_output_frames} | "
|
||||
f"FPS: {fps_rate:.1f} | ETA: {eta:.1f}s\r",
|
||||
end="",
|
||||
)
|
||||
last_progress_update = current_time
|
||||
|
||||
out.release()
|
||||
@@ -657,7 +829,9 @@ class VideoEditor:
|
||||
avg_fps = total_frames_written / total_time if total_time > 0 else 0
|
||||
|
||||
print(f"\nVideo rendered successfully to {output_path}")
|
||||
print(f"Rendered {total_frames_written} frames in {total_time:.2f}s (avg {avg_fps:.1f} FPS)")
|
||||
print(
|
||||
f"Rendered {total_frames_written} frames in {total_time:.2f}s (avg {avg_fps:.1f} FPS)"
|
||||
)
|
||||
return True
|
||||
|
||||
def _process_frame_for_render(self, frame, output_width: int, output_height: int):
|
||||
@@ -675,7 +849,7 @@ class VideoEditor:
|
||||
h = min(h, h_frame - y)
|
||||
|
||||
if w > 0 and h > 0:
|
||||
frame = frame[y:y+h, x:x+w]
|
||||
frame = frame[y : y + h, x : x + w]
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -693,14 +867,27 @@ class VideoEditor:
|
||||
intermediate_height = int(height * self.zoom_factor)
|
||||
|
||||
# If zoom results in different dimensions than output, resize directly to output
|
||||
if intermediate_width != output_width or intermediate_height != output_height:
|
||||
frame = cv2.resize(frame, (output_width, output_height), interpolation=cv2.INTER_LINEAR)
|
||||
if (
|
||||
intermediate_width != output_width
|
||||
or intermediate_height != output_height
|
||||
):
|
||||
frame = cv2.resize(
|
||||
frame,
|
||||
(output_width, output_height),
|
||||
interpolation=cv2.INTER_LINEAR,
|
||||
)
|
||||
else:
|
||||
frame = cv2.resize(frame, (intermediate_width, intermediate_height), interpolation=cv2.INTER_LINEAR)
|
||||
frame = cv2.resize(
|
||||
frame,
|
||||
(intermediate_width, intermediate_height),
|
||||
interpolation=cv2.INTER_LINEAR,
|
||||
)
|
||||
|
||||
# Final size check and resize if needed
|
||||
if frame.shape[1] != output_width or frame.shape[0] != output_height:
|
||||
frame = cv2.resize(frame, (output_width, output_height), interpolation=cv2.INTER_LINEAR)
|
||||
frame = cv2.resize(
|
||||
frame, (output_width, output_height), interpolation=cv2.INTER_LINEAR
|
||||
)
|
||||
|
||||
return frame
|
||||
|
||||
@@ -749,19 +936,21 @@ class VideoEditor:
|
||||
# Note: OpenCV doesn't provide direct access to modifier keys in waitKey
|
||||
# We'll handle this through special key combinations
|
||||
|
||||
if key == ord('q') or key == 27: # ESC
|
||||
if key == ord("q") or key == 27: # ESC
|
||||
break
|
||||
elif key == ord(' '):
|
||||
elif key == ord(" "):
|
||||
self.is_playing = not self.is_playing
|
||||
elif key == ord('a') or key == ord('A'):
|
||||
elif key == ord("a") or key == ord("A"):
|
||||
# Check if it's uppercase A (Shift+A)
|
||||
if key == ord('A'):
|
||||
self.seek_video_with_modifier(-1, True, False) # Shift+A: -10 frames
|
||||
if key == ord("A"):
|
||||
self.seek_video_with_modifier(
|
||||
-1, True, False
|
||||
) # Shift+A: -10 frames
|
||||
else:
|
||||
self.seek_video_with_modifier(-1, False, False) # A: -1 frame
|
||||
elif key == ord('d') or key == ord('D'):
|
||||
elif key == ord("d") or key == ord("D"):
|
||||
# Check if it's uppercase D (Shift+D)
|
||||
if key == ord('D'):
|
||||
if key == ord("D"):
|
||||
self.seek_video_with_modifier(1, True, False) # Shift+D: +10 frames
|
||||
else:
|
||||
self.seek_video_with_modifier(1, False, False) # D: +1 frame
|
||||
@@ -769,45 +958,49 @@ class VideoEditor:
|
||||
self.seek_video_with_modifier(-1, False, True) # Ctrl+A: -60 frames
|
||||
elif key == 4: # Ctrl+D
|
||||
self.seek_video_with_modifier(1, False, True) # Ctrl+D: +60 frames
|
||||
elif key == ord('-') or key == ord('_'):
|
||||
elif key == ord("-") or key == ord("_"):
|
||||
self.rotate_clockwise()
|
||||
print(f"Rotated to {self.rotation_angle}°")
|
||||
elif key == ord('w'):
|
||||
self.playback_speed = min(self.MAX_PLAYBACK_SPEED, self.playback_speed + self.SPEED_INCREMENT)
|
||||
elif key == ord('s'):
|
||||
self.playback_speed = max(self.MIN_PLAYBACK_SPEED, self.playback_speed - self.SPEED_INCREMENT)
|
||||
elif key == ord('e') or key == ord('E'):
|
||||
elif key == ord("w"):
|
||||
self.playback_speed = min(
|
||||
self.MAX_PLAYBACK_SPEED, self.playback_speed + self.SPEED_INCREMENT
|
||||
)
|
||||
elif key == ord("s"):
|
||||
self.playback_speed = max(
|
||||
self.MIN_PLAYBACK_SPEED, self.playback_speed - self.SPEED_INCREMENT
|
||||
)
|
||||
elif key == ord("e") or key == ord("E"):
|
||||
# Brightness adjustment: E (increase), Shift+E (decrease)
|
||||
if key == ord('E'):
|
||||
if key == ord("E"):
|
||||
self.adjust_brightness(-5)
|
||||
print(f"Brightness: {self.brightness}")
|
||||
else:
|
||||
self.adjust_brightness(5)
|
||||
print(f"Brightness: {self.brightness}")
|
||||
elif key == ord('r') or key == ord('R'):
|
||||
elif key == ord("r") or key == ord("R"):
|
||||
# Contrast adjustment: R (increase), Shift+R (decrease)
|
||||
if key == ord('R'):
|
||||
if key == ord("R"):
|
||||
self.adjust_contrast(-0.1)
|
||||
print(f"Contrast: {self.contrast:.1f}")
|
||||
else:
|
||||
self.adjust_contrast(0.1)
|
||||
print(f"Contrast: {self.contrast:.1f}")
|
||||
elif key == ord('u'):
|
||||
elif key == ord("u"):
|
||||
self.undo_crop()
|
||||
elif key == ord('c'):
|
||||
elif key == ord("c"):
|
||||
if self.crop_rect:
|
||||
self.crop_history.append(self.crop_rect)
|
||||
self.crop_rect = None
|
||||
elif key == ord('1'):
|
||||
elif key == ord("1"):
|
||||
self.cut_start_frame = self.current_frame
|
||||
print(f"Set cut start at frame {self.current_frame}")
|
||||
elif key == ord('2'):
|
||||
elif key == ord("2"):
|
||||
self.cut_end_frame = self.current_frame
|
||||
print(f"Set cut end at frame {self.current_frame}")
|
||||
elif key == ord('n'):
|
||||
elif key == ord("n"):
|
||||
if len(self.video_files) > 1:
|
||||
self.previous_video()
|
||||
elif key == ord('N'):
|
||||
elif key == ord("N"):
|
||||
if len(self.video_files) > 1:
|
||||
self.next_video()
|
||||
elif key == 13: # Enter
|
||||
@@ -823,8 +1016,12 @@ class VideoEditor:
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Fast Video Editor - Crop, Zoom, and Cut videos")
|
||||
parser.add_argument("video", help="Path to video file or directory containing videos")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Fast Video Editor - Crop, Zoom, and Cut videos"
|
||||
)
|
||||
parser.add_argument(
|
||||
"video", help="Path to video file or directory containing videos"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
Reference in New Issue
Block a user