实现将静态图片和视频合成为类似iPhone的Live Photo(动态照片)效果
可以使用Python结合OpenCV和图像处理库来完成
技术说明
- Live Photo原理:iPhone的Live Photo实际上是3秒的MOV视频+一张高分辨率JPEG
- 格式选择:
.mov
是最兼容的格式.heic
是苹果专用格式(需要额外库)
- 优化建议:
- 短视频长度建议2-3秒
- 帧率建议8-15fps
- 确保所有素材分辨率一致
使用静态图片+短视频合成Live Photo
首先需要环境的安装,我在这里建议你使用conda
pip install opencv-python numpy pillow imageio imageio-ffmpeg
# 如需HEIC格式支持
pip install pyheif pillow-heif
- 上代码
import cv2
import numpy as np
from PIL import Image
import imageio
import osdef create_live_photo(static_image_path, video_path, output_path, duration=3.0):"""创建类似iPhone Live Photo的效果参数:static_image_path: 主静态图片路径video_path: 短视频路径(3秒左右)output_path: 输出路径(.mov或.heic)duration: 视频持续时间(秒)"""# 读取静态图片static_img = cv2.imread(static_image_path)if static_img is None:raise ValueError("无法加载静态图片")# 读取视频cap = cv2.VideoCapture(video_path)fps = cap.get(cv2.CAP_PROP_FPS)total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))# 调整视频长度target_frames = int(duration * fps)frame_step = max(1, total_frames // target_frames)# 处理视频帧processed_frames = []frame_count = 0while True:ret, frame = cap.read()if not ret:breakif frame_count % frame_step == 0:# 调整帧大小与静态图匹配frame = cv2.resize(frame, (static_img.shape[1], static_img.shape[0]))processed_frames.append(frame)frame_count += 1if len(processed_frames) >= target_frames:breakcap.release()# 确保有足够的帧if len(processed_frames) < 5:raise ValueError("视频太短或帧率太低")# 创建输出 - 两种格式选择if output_path.lower().endswith('.mov'):# 输出为QuickTime MOV格式(类似Live Photo)fourcc = cv2.VideoWriter_fourcc(*'avc1')out = cv2.VideoWriter(output_path, fourcc, fps, (static_img.shape[1], static_img.shape[0]))for frame in processed_frames:out.write(frame)out.release()elif output_path.lower().endswith('.heic') or output_path.lower().endswith('.jpg'):# 需要pyheif库处理HEIC格式try:import pyheiffrom pillow_heif import register_heif_openerregister_heif_opener()# 创建动态序列with imageio.get_writer(output_path, mode='I', fps=fps) as writer:# 先写入静态图片作为封面writer.append_data(cv2.cvtColor(static_img, cv2.COLOR_BGR2RGB))# 写入动态帧for frame in processed_frames:writer.append_data(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))except ImportError:raise ImportError("需要安装pyheif和pillow-heif库来处理HEIC格式")else:raise ValueError("不支持的输出格式,请使用.mov或.heic")# 使用示例
create_live_photo(static_image_path="main_photo.jpg",video_path="short_clip.mp4",output_path="output_live.mov",duration=3.0
)
import cv2
import numpy as np
from PIL import Image
import os
import subprocess
from typing import List
import logging# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)def create_live_photo(static_image_path: str,video_path: str,output_prefix: str,duration: float = 3.0,fade_duration: float = 0.5,with_audio: bool = True
) -> None:"""创建专业级Live Photo效果"""try:# 确保输出目录存在output_dir = os.path.dirname(output_prefix)if output_dir and not os.path.exists(output_dir):os.makedirs(output_dir, exist_ok=True)# 读取静态图片static_img = cv2.imread(static_image_path)if static_img is None:raise ValueError(f"无法加载静态图片: {static_image_path}")# 处理视频帧frames, fps = process_video_frames(video_path, static_img.shape, duration,fade_duration)# 输出MOV格式mov_path = f"{output_prefix}.mov"create_mov_file(frames, fps, static_img.shape[:2], mov_path)# 处理音频(如果需要)if with_audio:final_mov_path = f"{output_prefix}_with_audio.mov"if not extract_and_merge_audio(video_path, mov_path, final_mov_path):logger.warning("音频合并失败,将使用无音频版本")if os.path.exists(final_mov_path):os.remove(final_mov_path)os.rename(mov_path, final_mov_path)logger.info(f"最终输出文件: {final_mov_path}")except Exception as e:logger.error(f"程序运行出错: {str(e)}", exc_info=True)raisedef extract_and_merge_audio(source_video: str,video_without_audio: str,output_path: str
) -> bool:"""使用ffmpeg合并音频,返回是否成功"""try:# 首先检查源视频是否有音频流check_cmd = ['ffprobe','-v', 'error','-select_streams', 'a','-show_entries', 'stream=codec_type','-of', 'csv=p=0',source_video]result = subprocess.run(check_cmd, capture_output=True, text=True)if result.returncode != 0 or 'audio' not in result.stdout:logger.warning(f"源视频 {source_video} 没有音频流")return False# 合并音频cmd = ['ffmpeg','-y','-i', video_without_audio,'-i', source_video,'-c:v', 'copy','-c:a', 'aac','-map', '0:v:0','-map', '1:a:0?', # 添加?表示可选'-shortest',output_path]subprocess.run(cmd, check=True)return Trueexcept subprocess.CalledProcessError as e:logger.error(f"FFmpeg错误: {e.stderr.decode('utf-8')}")return Falseexcept Exception as e:logger.error(f"音频合并失败: {str(e)}")return Falsedef create_heic_with_new_api(frames: List[np.ndarray],fps: float,cover_image: np.ndarray,output_path: str
) -> None:"""使用pillow_heif的新API生成HEIC"""try:# 确保目录存在os.makedirs(os.path.dirname(output_path), exist_ok=True)# 转换图像cover_pil = Image.fromarray(cv2.cvtColor(cover_image, cv2.COLOR_BGR2RGB))frame_pils = [Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) for frame in frames]# 使用HeifFile APIheif_file = pillow_heif.HeifFile()heif_file.add_from_pillow(cover_pil, quality=95)for frame in frame_pils:heif_file.add_from_pillow(frame, quality=85)# 保存文件with open(output_path, "wb") as f:heif_file.save(f, quality=90)if os.path.getsize(output_path) < 1024:raise ValueError("生成的HEIC文件过小")logger.info(f"成功生成HEIC文件: {output_path}")except Exception as e:if os.path.exists(output_path):os.remove(output_path)raise RuntimeError(f"HEIC生成失败: {str(e)}")def process_video_frames(video_path: str,target_size: tuple,duration: float,fade_duration: float
) -> tuple:"""处理视频帧并添加特效"""cap = cv2.VideoCapture(video_path)if not cap.isOpened():raise ValueError(f"无法打开视频文件: {video_path}")fps = cap.get(cv2.CAP_PROP_FPS)total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))# 计算需要提取的帧target_frames = int(duration * fps)frame_step = max(1, total_frames // target_frames)# 提取并处理帧processed_frames = []frame_count = 0while True:ret, frame = cap.read()if not ret:breakif frame_count % frame_step == 0:frame = cv2.resize(frame, (target_size[1], target_size[0]))processed_frames.append(frame)frame_count += 1if len(processed_frames) >= target_frames:breakcap.release()# 验证帧数if len(processed_frames) < 5:raise ValueError("视频太短或帧率太低,至少需要5帧")# 添加淡入淡出效果processed_frames = add_fade_effect(processed_frames, fade_duration, fps)return processed_frames, fpsdef add_fade_effect(frames: List[np.ndarray],fade_duration: float,fps: float
) -> List[np.ndarray]:"""添加专业级淡入淡出效果"""fade_frames = int(fade_duration * fps)# 淡入效果(开头)for i in range(min(fade_frames, len(frames))):alpha = i / fade_framesframes[i] = cv2.addWeighted(frames[i], alpha,frames[-1], 1-alpha,0)# 淡出效果(结尾)for i in range(1, min(fade_frames + 1, len(frames))):alpha = i / fade_framesframes[-i] = cv2.addWeighted(frames[-i], alpha,frames[0], 1-alpha,0)return framesdef create_mov_file(frames: List[np.ndarray],fps: float,frame_size: tuple,output_path: str
) -> None:"""创建高质量MOV文件"""# 确保目录存在os.makedirs(os.path.dirname(output_path), exist_ok=True)fourcc = cv2.VideoWriter_fourcc(*'avc1')out = cv2.VideoWriter(output_path, fourcc, fps, (frame_size[1], frame_size[0]))if not out.isOpened():raise ValueError(f"无法创建视频文件: {output_path}")for frame in frames:out.write(frame)out.release()logger.info(f"已生成MOV文件: {output_path}")def extract_and_merge_audio(source_video: str,video_without_audio: str,output_path: str
) -> None:"""使用ffmpeg合并音频"""try:# 确保目录存在os.makedirs(os.path.dirname(output_path), exist_ok=True)cmd = ['ffmpeg','-y','-i', video_without_audio,'-i', source_video,'-c:v', 'copy','-c:a', 'aac','-map', '0:v:0','-map', '1:a:0','-shortest',output_path]subprocess.run(cmd, check=True)logger.info(f"已合并音频到: {output_path}")except subprocess.CalledProcessError as e:raise RuntimeError(f"音频合并失败: {str(e)}")def test_heic_support():"""测试HEIC支持"""try:test_img = Image.new("RGB", (100, 100), "red")test_path = "heic_test.heic"# 使用新API测试heif_file = pillow_heif.HeifFile()heif_file.add_from_pillow(test_img)with open(test_path, "wb") as f:heif_file.save(f)if os.path.exists(test_path) and os.path.getsize(test_path) > 0:os.remove(test_path)logger.info("HEIC支持测试通过")return Trueraise RuntimeError("生成的测试文件无效")except Exception as e:raise RuntimeError(f"HEIC支持测试失败: {str(e)}")if __name__ == "__main__":try:logger.info("开始生成Live Photo...")create_live_photo(static_image_path="1.jpg",video_path="1.mp4",output_prefix="output/live_photo",duration=3.0,fade_duration=0.6,with_audio=True)logger.info("Live Photo生成完成!")except Exception as e:logger.error(f"程序终止: {str(e)}", exc_info=True)