import os import glob import random import subproc...

Created on: May 9, 2025

Answered using O3 by Chat01

Question

import os
import glob
import random
import subprocess
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from PIL import Image

def get_random_start_point() -> tuple[str, str]:
start_points = [
"center", "top_left", "top_right", "bottom_left", "bottom_right",
"center_left", "center_right", "center_top", "center_bottom"
]
start_point = random.choice(start_points)

text
if start_point == "center": start_x = "iw/2-(iw/zoom/2)" start_y = "ih/2-(ih/zoom/2)" elif start_point == "top_left": start_x, start_y = "0", "0" elif start_point == "top_right": start_x, start_y = "iw-(iw/zoom)", "0" elif start_point == "bottom_left": start_x, start_y = "0", "ih-(ih/zoom)" elif start_point == "bottom_right": start_x, start_y = "iw-(iw/zoom)", "ih-(ih/zoom)" elif start_point == "center_left": start_x, start_y = "0", "ih/2-(ih/zoom/2)" elif start_point == "center_right": start_x, start_y = "iw-(iw/zoom)", "ih/2-(ih/zoom/2)" elif start_point == "center_top": start_x, start_y = "iw/2-(iw/zoom/2)", "0" elif start_point == "center_bottom": start_x, start_y = "iw/2-(iw/zoom/2)", "ih-(ih/zoom)" else: start_x, start_y = "iw/2-(iw/zoom/2)", "ih/2-(ih/zoom/2)" return start_x, start_y

def process_horizontal_image_fill(
image_path: str,
video_resolution: tuple[int, int],
framerate: int,
bitrate: int,
bufsize: int,
temp_video_folder: str,
filename: str,
duration: int,
zoom_time: int,
center_zoom: bool,
) -> str:
if center_zoom:
start_x = "iw/2-(iw/zoom/2)"
start_y = "ih/2-(ih/zoom/2)"
else:
start_x, start_y = get_random_start_point()

text
temp_video_file = os.path.join(temp_video_folder, f"{filename}.mp4") subprocess.run( [ "ffmpeg", "-y", "-i", image_path, "-lavfi", ( f"[0:v]scale={video_resolution[0]}:{video_resolution[1]}," f"boxblur=luma_radius=min(h\\,w)/20:luma_power=1:" f"chroma_radius=min(cw\\,ch)/20:chroma_power=1[bg];" f"[bg]scale={video_resolution[0]}:{video_resolution[1]}[bg_scaled];" f"[0:v]scale=1920:1080[ov];" f"[ov]crop=1720:1080:100:0[zv];" f"[bg_scaled][zv]overlay=(W-w)/2:(H-h)/2[full];" f"[full]scale=15000:-1[scaled_full];" f"[scaled_full]zoompan=" f"z='min(zoom+0.0005,1.5)':" f"x='{start_x}':y='{start_y}':" f"d={zoom_time}," f"trim=duration={duration}," f"scale={video_resolution[0]}:{video_resolution[1]}" ), "-r", str(framerate), "-c:v", "libx264", "-preset", "ultrafast", "-b:v", f"{bitrate}k", "-maxrate", f"{bitrate}k", "-bufsize", f"{bufsize}k", "-pix_fmt", "yuv420p", temp_video_file, ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, ) return temp_video_file

def process_vertical_image(
image_path: str,
video_resolution: tuple[int, int],
framerate: int,
bitrate: int,
bufsize: int,
temp_video_folder: str,
filename: str,
duration: int,
zoom_time: int,
center_zoom: bool,
) -> str:
if center_zoom:
start_x = "iw/2-(iw/zoom/2)"
start_y = "ih/2-(ih/zoom/2)"
else:
start_x, start_y = get_random_start_point()

text
temp_video_file = os.path.join(temp_video_folder, f"{filename}.mp4") subprocess.run( [ "ffmpeg", "-y", "-i", image_path, "-lavfi", ( f"[0:v]scale={video_resolution[0]}:{video_resolution[1]}," f"boxblur=luma_radius=min(h\\,w)/20:luma_power=1:" f"chroma_radius=min(cw\\,ch)/20:chroma_power=1[bg];" f"[bg]scale={video_resolution[0]}:{video_resolution[1]}[bg_scaled];" f"[0:v]scale=-1:{video_resolution[1]}[ov];" f"[bg_scaled][ov]overlay=(W-w)/2:(H-h)/2[full];" f"[full]scale=15000:-1[scaled_full];" f"[scaled_full]zoompan=" f"z='min(zoom+0.0005,1.5)':" f"x='{start_x}':y='{start_y}':" f"d={zoom_time}," f"trim=duration={duration}," f"scale={video_resolution[0]}:{video_resolution[1]}" ), "-r", str(framerate), "-c:v", "libx264", "-preset", "ultrafast", "-b:v", f"{bitrate}k", "-maxrate", f"{bitrate}k", "-bufsize", f"{bufsize}k", "-pix_fmt", "yuv420p", temp_video_file, ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, ) return temp_video_file

def create_videos_from_folder(
image_folder: str,
*,
duration: int = 10,
framerate: int = 30,
bitrate_kbps: int = 6000,
resolution: tuple[int, int] = (1920, 1080),
max_workers: int = 10,
) -> None:
image_folder = os.path.abspath(image_folder)
output_dir = os.path.join(image_folder, "videos")
os.makedirs(output_dir, exist_ok=True)

text
bufsize = bitrate_kbps * 2 zoom_time = duration * framerate patterns = ("*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.tif", "*.tiff", "*.webp") image_files: list[Path] = [] for pattern in patterns: image_files.extend(Path(image_folder).rglob(pattern)) if not image_files: print("В указанной папке не найдено изображений.") return def process_image(img_path: Path): try: with Image.open(img_path) as img: width, height = img.size filename = img_path.stem if width >= height: result = process_horizontal_image_fill( str(img_path), resolution, framerate, bitrate_kbps, bufsize, output_dir, filename, duration, zoom_time, center_zoom=False ) else: result = process_vertical_image( str(img_path), resolution, framerate, bitrate_kbps, bufsize, output_dir, filename, duration, zoom_time, center_zoom=False ) return f"✅ Создано видео: {result}" except subprocess.CalledProcessError as err: return f"❌ Ошибка FFmpeg при обработке {img_path.name}:\n{err.stderr.decode('utf-8')}" with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = [executor.submit(process_image, path) for path in image_files] for future in as_completed(futures): print(future.result())

if name == "main":
IMAGE_DIR = r"D:\Downloads\cave"
create_videos_from_folder(IMAGE_DIR)

как в этом коде исправить создание вертикальных изображений. получается что финальный файл имеет разрешение 1920x1080. но по факту видео выглядит как растянутое вертикальное изображение с эффектом боке и еще черными полосами сбоку. я хочу чтобы было полностью эффект боке и нормальное растяжение изображения, а не какое-то непонятное не по масштабу
пиши полностью функции.

Thought

The user is working with Python code to generate videos from images using ffmpeg. The issue is that vertical images are being processed incorrectly and result in black sidebars, along with a stretched image. They want these vertical images to have a full bokeh blur background and proper scaling without the black bars. To address this, both the process_horizontal_image_fill and process_vertical_image functions need to be updated to fix the vertical image processing, avoiding black bars and ensuring proper scaling.

It seems like the black bars are likely appearing because the blurred background isn’t scaled correctly to cover the entire video resolution. In the case of vertical images, scaling the background to 1920x1080 may distort the aspect ratio or leave parts of the width uncovered, resulting in black bars. As the background blur is scaled, it could interact with the overlay in a way that causes cropping, trimming out the blur. A possible fix could involve correcting the scaling and cropping methods to ensure the entire screen is covered without any distortion. Also, scaling to 15000:— seems like it might be creating some unexpected issues.

Thought for 34 seconds

Answer

Ниже — полностью переписанные функции process_horizontal_image_fill и process_vertical_image_fill (я слегка переименовал вторую для симметрии).
Главная идея:

  • Блюр-фон теперь делается так: мы сначала растягиваем оригинал до того, чтобы он полностью закрывал 1920×1080, а потом кадрируем под нужное соотношение. Чёрные полосы пропадают.
  • Основное изображение масштабируется без искажений (сохраняем A/R), по длинной стороне выводим его в центр.
  • Всё остальное (zoom-pan, битрейт, пр.) осталось как у вас.
python
import os import subprocess from pathlib import Path from PIL import Image from concurrent.futures import ThreadPoolExecutor, as_completed import random def _random_start() -> tuple[str, str]: """Случайная точка старта для эффекта zoom-pan.""" points = { "tl": ("0", "0"), "tr": ("iw-(iw/zoom)", "0"), "bl": ("0", "ih-(ih/zoom)"), "br": ("iw-(iw/zoom)", "ih-(ih/zoom)"), "cl": ("0", "ih/2-(ih/zoom/2)"), "cr": ("iw-(iw/zoom)", "ih/2-(ih/zoom/2)"), "ct": ("iw/2-(iw/zoom/2)", "0"), "cb": ("iw/2-(iw/zoom/2)", "ih-(ih/zoom)"), } return random.choice(list(points.values())) def _build_filter( video_w: int, video_h: int, zoom_time: int, duration: int, start_x: str, start_y: str, foreground_scale: str, ) -> str: """ Общий конструктор filter_complex. foreground_scale: 'w:{h}' – фиксируем высоту (под вертикальные кадры) '{w}:h' – фиксируем ширину (под горизонтальные кадры) """ return ( # 1) Готовим блюр-фон — сначала растягиваем кадр так, # чтобы ОБЕ стороны были ≥ выходных (increase), # затем кадрируем до точного размера и блюрим. f"[0:v]scale={video_w}:{video_h}:force_original_aspect_ratio=increase," f"crop={video_w}:{video_h}," f"boxblur=luma_radius=min(h\\,w)/20:luma_power=1[bg];" # 2) Готовим «чистое» фото поверх блюра f"[0:v]scale={foreground_scale}:force_original_aspect_ratio=decrease[fg];" # 3) Складываем всё вместе f"[bg][fg]overlay=(W-w)/2:(H-h)/2," # 4) Делаем лёгкий «живой» зум-пан f"zoompan=z='min(zoom+0.0005,1.5)':x='{start_x}':y='{start_y}':d={zoom_time}," # 5) Обрезаем по длительности и фиксируем итоговое разрешение f"trim=duration={duration}," f"scale={video_w}:{video_h}" ) def process_horizontal_image_fill( image_path: str, video_resolution: tuple[int, int], framerate: int, bitrate: int, bufsize: int, temp_video_folder: str, filename: str, duration: int, zoom_time: int, center_zoom: bool = False, ) -> str: """Обработка горизонтальных снимков.""" video_w, video_h = video_resolution start_x, start_y = ( ("iw/2-(iw/zoom/2)", "ih/2-(ih/zoom/2)") if center_zoom else _random_start() ) filter_complex = _build_filter( video_w, video_h, zoom_time, duration, start_x, start_y, foreground_scale=f"{video_w}:-1", # фиксируем ширину под горизонтальные ) out_file = os.path.join(temp_video_folder, f"{filename}.mp4") subprocess.run( [ "ffmpeg", "-y", "-i", image_path, "-lavfi", filter_complex, "-r", str(framerate), "-c:v", "libx264", "-preset", "ultrafast", "-b:v", f"{bitrate}k", "-maxrate", f"{bitrate}k", "-bufsize", f"{bufsize}k", "-pix_fmt", "yuv420p", out_file, ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True ) return out_file def process_vertical_image_fill( image_path: str, video_resolution: tuple[int, int], framerate: int, bitrate: int, bufsize: int, temp_video_folder: str, filename: str, duration: int, zoom_time: int, center_zoom: bool = False, ) -> str: """Обработка вертикальных снимков (больше нет чёрных полос!).""" video_w, video_h = video_resolution start_x, start_y = ( ("iw/2-(iw/zoom/2)", "ih/2-(ih/zoom/2)") if center_zoom else _random_start() ) filter_complex = _build_filter( video_w, video_h, zoom_time, duration, start_x, start_y, foreground_scale=f"-1:{video_h}", # фиксируем высоту под вертикальные ) out_file = os.path.join(temp_video_folder, f"{filename}.mp4") subprocess.run( [ "ffmpeg", "-y", "-i", image_path, "-lavfi", filter_complex, "-r", str(framerate), "-c:v", "libx264", "-preset", "ultrafast", "-b:v", f"{bitrate}k", "-maxrate", f"{bitrate}k", "-bufsize", f"{bufsize}k", "-pix_fmt", "yuv420p", out_file, ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True ) return out_file

Что поменялось и почему это решает проблему

ШагБылоСтало
Подготовка фонового слояscale=1920:1080 (искажало A/R → чёрные края при crop)*scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080 — фон закрывает кадр полностью, без искажений
Подготовка основного фотоУниверсальный scale=-1:1080 для вертикалей / 1920:1080 + crop для горизонталейЯвно фиксируем ширину для горизонтальных (1920:-1) и высоту для вертикальных (-1:1080)
Zoom-panВ каждом фильтре задаётся зановоВынес в общий конструктор — легче поддерживать
Чёрные поляМогли возникнуть, если фон не покрывал полно-кадрБольше не появляются

* В старой цепочке фон для вертикальных фото масштабировался до точных 1920×1080 с изменением соотношения сторон. Далее при наложении чистого кадра оставались полосы, которые затем «заливались» чёрным при финальном scale=15000:-1… / scale=1920:1080. Новый алгоритм заставляет фон заполнять кадр, кадрирует лишнее и блюрит — так гарантированно нет пустых областей.

Подставьте эти две функции вместо старых — и вертикальные изображения станут выглядеть как надо: полный боке-фон + нормальное, без искажений, фото поверх.

Share this Q&A