AI-人工智能-实现将静态图片和视频合成为类似iPhone的Live Photo(动态照片)效果
实现将静态图片和视频合成为类似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 os
def 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 = 0
while True:
ret, frame = cap.read()
if not ret:
break
if frame_count % frame_step == 0:
# 调整帧大小与静态图匹配
frame = cv2.resize(frame, (static_img.shape[1], static_img.shape[0]))
processed_frames.append(frame)
frame_count += 1
if len(processed_frames) >= target_frames:
break
cap.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 pyheif
from pillow_heif import register_heif_opener
register_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)
raise
def 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 True
except subprocess.CalledProcessError as e:
logger.error(f"FFmpeg错误: {e.stderr.decode('utf-8')}")
return False
except Exception as e:
logger.error(f"音频合并失败: {str(e)}")
return False
def 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 API
heif_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 = 0
while True:
ret, frame = cap.read()
if not ret:
break
if frame_count % frame_step == 0:
frame = cv2.resize(frame, (target_size[1], target_size[0]))
processed_frames.append(frame)
frame_count += 1
if len(processed_frames) >= target_frames:
break
cap.release()
# 验证帧数
if len(processed_frames) < 5:
raise ValueError("视频太短或帧率太低,至少需要5帧")
# 添加淡入淡出效果
processed_frames = add_fade_effect(processed_frames, fade_duration, fps)
return processed_frames, fps
def 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_frames
frames[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_frames
frames[-i] = cv2.addWeighted(
frames[-i], alpha,
frames[0], 1-alpha,
0
)
return frames
def 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 True
raise 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)