import os import glob import random import subproc...
Created on: May 9, 2025
Created on: May 9, 2025
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)
textif 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()
texttemp_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()
texttemp_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)
textbufsize = 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. но по факту видео выглядит как растянутое вертикальное изображение с эффектом боке и еще черными полосами сбоку. я хочу чтобы было полностью эффект боке и нормальное растяжение изображения, а не какое-то непонятное не по масштабу
пиши полностью функции.
Ниже — полностью переписанные функции process_horizontal_image_fill
и process_vertical_image_fill
(я слегка переименовал вторую для симметрии).
Главная идея:
pythonimport 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
. Новый алгоритм заставляет фон заполнять кадр, кадрирует лишнее и блюрит — так гарантированно нет пустых областей.
Подставьте эти две функции вместо старых — и вертикальные изображения станут выглядеть как надо: полный боке-фон + нормальное, без искажений, фото поверх.