Add a render progress bar
This commit is contained in:
152
croppa/main.py
152
croppa/main.py
@@ -28,6 +28,16 @@ class VideoEditor:
|
|||||||
TIMELINE_COLOR_BORDER = (200, 200, 200)
|
TIMELINE_COLOR_BORDER = (200, 200, 200)
|
||||||
TIMELINE_COLOR_CUT_POINT = (255, 0, 0)
|
TIMELINE_COLOR_CUT_POINT = (255, 0, 0)
|
||||||
|
|
||||||
|
# Progress bar configuration
|
||||||
|
PROGRESS_BAR_HEIGHT = 30
|
||||||
|
PROGRESS_BAR_MARGIN_PERCENT = 5 # 5% margin on each side
|
||||||
|
PROGRESS_BAR_TOP_MARGIN = 20 # Fixed top margin
|
||||||
|
PROGRESS_BAR_FADE_DURATION = 3.0 # seconds to fade out after completion
|
||||||
|
PROGRESS_BAR_COLOR_BG = (50, 50, 50)
|
||||||
|
PROGRESS_BAR_COLOR_FILL = (0, 255, 0) # Green when complete
|
||||||
|
PROGRESS_BAR_COLOR_PROGRESS = (0, 120, 255) # Blue during progress
|
||||||
|
PROGRESS_BAR_COLOR_BORDER = (200, 200, 200)
|
||||||
|
|
||||||
# Zoom and crop settings
|
# Zoom and crop settings
|
||||||
MIN_ZOOM = 0.1
|
MIN_ZOOM = 0.1
|
||||||
MAX_ZOOM = 10.0
|
MAX_ZOOM = 10.0
|
||||||
@@ -94,6 +104,14 @@ class VideoEditor:
|
|||||||
# Display offset for panning when zoomed
|
# Display offset for panning when zoomed
|
||||||
self.display_offset = [0, 0]
|
self.display_offset = [0, 0]
|
||||||
|
|
||||||
|
# Progress bar state
|
||||||
|
self.progress_bar_visible = False
|
||||||
|
self.progress_bar_progress = 0.0 # 0.0 to 1.0
|
||||||
|
self.progress_bar_complete = False
|
||||||
|
self.progress_bar_complete_time = None
|
||||||
|
self.progress_bar_text = ""
|
||||||
|
self.progress_bar_fps = 0.0 # Current rendering FPS
|
||||||
|
|
||||||
def _get_video_files_from_directory(self, directory: Path) -> List[Path]:
|
def _get_video_files_from_directory(self, directory: Path) -> List[Path]:
|
||||||
"""Get all video files from a directory, sorted by name"""
|
"""Get all video files from a directory, sorted by name"""
|
||||||
video_files = []
|
video_files = []
|
||||||
@@ -296,6 +314,104 @@ class VideoEditor:
|
|||||||
"""Adjust contrast by delta (0.1 to 3.0)"""
|
"""Adjust contrast by delta (0.1 to 3.0)"""
|
||||||
self.contrast = max(0.1, min(3.0, self.contrast + delta))
|
self.contrast = max(0.1, min(3.0, self.contrast + delta))
|
||||||
|
|
||||||
|
def show_progress_bar(self, text: str = "Processing..."):
|
||||||
|
"""Show progress bar with given text"""
|
||||||
|
self.progress_bar_visible = True
|
||||||
|
self.progress_bar_progress = 0.0
|
||||||
|
self.progress_bar_complete = False
|
||||||
|
self.progress_bar_complete_time = None
|
||||||
|
self.progress_bar_text = text
|
||||||
|
|
||||||
|
def update_progress_bar(self, progress: float, text: str = None, fps: float = None):
|
||||||
|
"""Update progress bar progress (0.0 to 1.0) and optionally text and FPS"""
|
||||||
|
if self.progress_bar_visible:
|
||||||
|
self.progress_bar_progress = max(0.0, min(1.0, progress))
|
||||||
|
if text is not None:
|
||||||
|
self.progress_bar_text = text
|
||||||
|
if fps is not None:
|
||||||
|
self.progress_bar_fps = fps
|
||||||
|
|
||||||
|
# Mark as complete when reaching 100%
|
||||||
|
if self.progress_bar_progress >= 1.0 and not self.progress_bar_complete:
|
||||||
|
self.progress_bar_complete = True
|
||||||
|
self.progress_bar_complete_time = time.time()
|
||||||
|
|
||||||
|
def hide_progress_bar(self):
|
||||||
|
"""Hide progress bar"""
|
||||||
|
self.progress_bar_visible = False
|
||||||
|
self.progress_bar_complete = False
|
||||||
|
self.progress_bar_complete_time = None
|
||||||
|
self.progress_bar_fps = 0.0
|
||||||
|
|
||||||
|
def draw_progress_bar(self, frame):
|
||||||
|
"""Draw progress bar on frame if visible - positioned at top with full width"""
|
||||||
|
if not self.progress_bar_visible:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if we should fade out
|
||||||
|
if self.progress_bar_complete and self.progress_bar_complete_time:
|
||||||
|
elapsed = time.time() - self.progress_bar_complete_time
|
||||||
|
if elapsed > self.PROGRESS_BAR_FADE_DURATION:
|
||||||
|
self.hide_progress_bar()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calculate fade alpha (1.0 at start, 0.0 at end)
|
||||||
|
fade_alpha = max(0.0, 1.0 - (elapsed / self.PROGRESS_BAR_FADE_DURATION))
|
||||||
|
else:
|
||||||
|
fade_alpha = 1.0
|
||||||
|
|
||||||
|
height, width = frame.shape[:2]
|
||||||
|
|
||||||
|
# Calculate progress bar position (top of frame with 5% margins)
|
||||||
|
margin_width = int(width * self.PROGRESS_BAR_MARGIN_PERCENT / 100)
|
||||||
|
bar_width = width - (2 * margin_width)
|
||||||
|
bar_x = margin_width
|
||||||
|
bar_y = self.PROGRESS_BAR_TOP_MARGIN
|
||||||
|
|
||||||
|
# Apply fade alpha to colors
|
||||||
|
bg_color = tuple(int(c * fade_alpha) for c in self.PROGRESS_BAR_COLOR_BG)
|
||||||
|
border_color = tuple(int(c * fade_alpha) for c in self.PROGRESS_BAR_COLOR_BORDER)
|
||||||
|
|
||||||
|
if self.progress_bar_complete:
|
||||||
|
fill_color = tuple(int(c * fade_alpha) for c in self.PROGRESS_BAR_COLOR_FILL)
|
||||||
|
else:
|
||||||
|
fill_color = tuple(int(c * fade_alpha) for c in self.PROGRESS_BAR_COLOR_PROGRESS)
|
||||||
|
|
||||||
|
# Draw background
|
||||||
|
cv2.rectangle(frame, (bar_x, bar_y), (bar_x + bar_width, bar_y + self.PROGRESS_BAR_HEIGHT), bg_color, -1)
|
||||||
|
|
||||||
|
# Draw progress fill
|
||||||
|
fill_width = int(bar_width * self.progress_bar_progress)
|
||||||
|
if fill_width > 0:
|
||||||
|
cv2.rectangle(frame, (bar_x, bar_y), (bar_x + fill_width, bar_y + self.PROGRESS_BAR_HEIGHT), fill_color, -1)
|
||||||
|
|
||||||
|
# Draw border
|
||||||
|
cv2.rectangle(frame, (bar_x, bar_y), (bar_x + bar_width, bar_y + self.PROGRESS_BAR_HEIGHT), border_color, 2)
|
||||||
|
|
||||||
|
# Draw progress percentage on the left
|
||||||
|
percentage_text = f"{self.progress_bar_progress * 100:.1f}%"
|
||||||
|
text_color = tuple(int(255 * fade_alpha) for _ in range(3))
|
||||||
|
cv2.putText(frame, percentage_text, (bar_x + 12, bar_y + 22), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 4)
|
||||||
|
cv2.putText(frame, percentage_text, (bar_x + 10, bar_y + 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, text_color, 2)
|
||||||
|
|
||||||
|
# Draw FPS on the right if available
|
||||||
|
if self.progress_bar_fps > 0:
|
||||||
|
fps_text = f"{self.progress_bar_fps:.1f} FPS"
|
||||||
|
fps_text_size = cv2.getTextSize(fps_text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)[0]
|
||||||
|
fps_x = bar_x + bar_width - fps_text_size[0] - 10
|
||||||
|
cv2.putText(frame, fps_text, (fps_x + 2, bar_y + 22), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 4)
|
||||||
|
cv2.putText(frame, fps_text, (fps_x, bar_y + 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, text_color, 2)
|
||||||
|
|
||||||
|
# Draw main text in center
|
||||||
|
if self.progress_bar_text:
|
||||||
|
text_size = cv2.getTextSize(self.progress_bar_text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)[0]
|
||||||
|
text_x = bar_x + (bar_width - text_size[0]) // 2
|
||||||
|
text_y = bar_y + 20
|
||||||
|
|
||||||
|
# Draw text shadow for better visibility
|
||||||
|
cv2.putText(frame, self.progress_bar_text, (text_x + 2, text_y + 2), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 4)
|
||||||
|
cv2.putText(frame, self.progress_bar_text, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, text_color, 2)
|
||||||
|
|
||||||
def draw_timeline(self, frame):
|
def draw_timeline(self, frame):
|
||||||
"""Draw timeline at the bottom of the frame"""
|
"""Draw timeline at the bottom of the frame"""
|
||||||
height, width = frame.shape[:2]
|
height, width = frame.shape[:2]
|
||||||
@@ -582,6 +698,9 @@ class VideoEditor:
|
|||||||
# Draw timeline
|
# Draw timeline
|
||||||
self.draw_timeline(canvas)
|
self.draw_timeline(canvas)
|
||||||
|
|
||||||
|
# Draw progress bar (if visible)
|
||||||
|
self.draw_progress_bar(canvas)
|
||||||
|
|
||||||
cv2.imshow("Video Editor", canvas)
|
cv2.imshow("Video Editor", canvas)
|
||||||
|
|
||||||
def mouse_callback(self, event, x, y, flags, param):
|
def mouse_callback(self, event, x, y, flags, param):
|
||||||
@@ -746,6 +865,9 @@ class VideoEditor:
|
|||||||
print(f"Rendering video to {output_path}...")
|
print(f"Rendering video to {output_path}...")
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
|
# Show progress bar
|
||||||
|
self.show_progress_bar("Initializing render...")
|
||||||
|
|
||||||
# Determine frame range
|
# Determine frame range
|
||||||
start_frame = self.cut_start_frame if self.cut_start_frame is not None else 0
|
start_frame = self.cut_start_frame if self.cut_start_frame is not None else 0
|
||||||
end_frame = (
|
end_frame = (
|
||||||
@@ -756,8 +878,12 @@ class VideoEditor:
|
|||||||
|
|
||||||
if start_frame >= end_frame:
|
if start_frame >= end_frame:
|
||||||
print("Invalid cut range!")
|
print("Invalid cut range!")
|
||||||
|
self.update_progress_bar(1.0, "Error: Invalid cut range!")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Update progress for initialization
|
||||||
|
self.update_progress_bar(0.05, "Calculating output dimensions...")
|
||||||
|
|
||||||
# Calculate output dimensions (accounting for rotation)
|
# Calculate output dimensions (accounting for rotation)
|
||||||
if self.crop_rect:
|
if self.crop_rect:
|
||||||
crop_width = int(self.crop_rect[2])
|
crop_width = int(self.crop_rect[2])
|
||||||
@@ -774,6 +900,9 @@ class VideoEditor:
|
|||||||
output_width = int(crop_width * self.zoom_factor)
|
output_width = int(crop_width * self.zoom_factor)
|
||||||
output_height = int(crop_height * self.zoom_factor)
|
output_height = int(crop_height * self.zoom_factor)
|
||||||
|
|
||||||
|
# Update progress for video writer setup
|
||||||
|
self.update_progress_bar(0.1, "Setting up video writer...")
|
||||||
|
|
||||||
# Use mp4v codec (most compatible with MP4)
|
# Use mp4v codec (most compatible with MP4)
|
||||||
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
||||||
out = cv2.VideoWriter(
|
out = cv2.VideoWriter(
|
||||||
@@ -782,11 +911,13 @@ class VideoEditor:
|
|||||||
|
|
||||||
if not out.isOpened():
|
if not out.isOpened():
|
||||||
print("Error: Could not open video writer!")
|
print("Error: Could not open video writer!")
|
||||||
|
self.update_progress_bar(1.0, "Error: Could not open video writer!")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Simple sequential processing - the I/O is the bottleneck anyway
|
# Simple sequential processing - the I/O is the bottleneck anyway
|
||||||
total_output_frames = end_frame - start_frame + 1
|
total_output_frames = end_frame - start_frame + 1
|
||||||
last_progress_update = 0
|
last_progress_update = 0
|
||||||
|
last_display_update = 0
|
||||||
|
|
||||||
for frame_idx in range(start_frame, end_frame + 1):
|
for frame_idx in range(start_frame, end_frame + 1):
|
||||||
# Read frame
|
# Read frame
|
||||||
@@ -805,29 +936,44 @@ class VideoEditor:
|
|||||||
out.write(processed_frame)
|
out.write(processed_frame)
|
||||||
|
|
||||||
frames_written = frame_idx - start_frame + 1
|
frames_written = frame_idx - start_frame + 1
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Update progress bar (10% to 95% of progress reserved for frame processing)
|
||||||
|
progress = 0.1 + (0.85 * (frames_written / total_output_frames))
|
||||||
|
|
||||||
# Throttled progress update
|
# Throttled progress update
|
||||||
current_time = time.time()
|
|
||||||
if current_time - last_progress_update > 0.5:
|
if current_time - last_progress_update > 0.5:
|
||||||
progress = frames_written / total_output_frames * 100
|
|
||||||
elapsed = current_time - start_time
|
elapsed = current_time - start_time
|
||||||
fps_rate = frames_written / elapsed
|
fps_rate = frames_written / elapsed
|
||||||
eta = (elapsed / frames_written) * (
|
eta = (elapsed / frames_written) * (
|
||||||
total_output_frames - frames_written
|
total_output_frames - frames_written
|
||||||
)
|
)
|
||||||
|
|
||||||
|
progress_text = f"Rendering {frames_written}/{total_output_frames} frames (ETA: {eta:.1f}s)"
|
||||||
|
self.update_progress_bar(progress, progress_text, fps_rate)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"Progress: {progress:.1f}% | {frames_written}/{total_output_frames} | "
|
f"Progress: {progress*100:.1f}% | {frames_written}/{total_output_frames} | "
|
||||||
f"FPS: {fps_rate:.1f} | ETA: {eta:.1f}s\r",
|
f"FPS: {fps_rate:.1f} | ETA: {eta:.1f}s\r",
|
||||||
end="",
|
end="",
|
||||||
)
|
)
|
||||||
last_progress_update = current_time
|
last_progress_update = current_time
|
||||||
|
|
||||||
|
# Update display more frequently to show progress bar
|
||||||
|
if current_time - last_display_update > 0.1: # Update display every 100ms
|
||||||
|
self.display_current_frame()
|
||||||
|
cv2.waitKey(1) # Allow OpenCV to process events
|
||||||
|
last_display_update = current_time
|
||||||
|
|
||||||
out.release()
|
out.release()
|
||||||
|
|
||||||
total_time = time.time() - start_time
|
total_time = time.time() - start_time
|
||||||
total_frames_written = end_frame - start_frame + 1
|
total_frames_written = end_frame - start_frame + 1
|
||||||
avg_fps = total_frames_written / total_time if total_time > 0 else 0
|
avg_fps = total_frames_written / total_time if total_time > 0 else 0
|
||||||
|
|
||||||
|
# Complete the progress bar
|
||||||
|
self.update_progress_bar(1.0, f"Complete! Rendered {total_frames_written} frames in {total_time:.1f}s", avg_fps)
|
||||||
|
|
||||||
print(f"\nVideo rendered successfully to {output_path}")
|
print(f"\nVideo rendered successfully to {output_path}")
|
||||||
print(
|
print(
|
||||||
f"Rendered {total_frames_written} frames in {total_time:.2f}s (avg {avg_fps:.1f} FPS)"
|
f"Rendered {total_frames_written} frames in {total_time:.2f}s (avg {avg_fps:.1f} FPS)"
|
||||||
|
Reference in New Issue
Block a user