当前位置: 首页 > news >正文

AI-人工智能-实现将静态图片和视频合成为类似iPhone的Live Photo(动态照片)效果

实现将静态图片和视频合成为类似iPhone的Live Photo(动态照片)效果

可以使用Python结合OpenCV和图像处理库来完成

技术说明

  1. Live Photo原理:iPhone的Live Photo实际上是3秒的MOV视频+一张高分辨率JPEG
  2. 格式选择
    • .mov是最兼容的格式
    • .heic是苹果专用格式(需要额外库)
  3. 优化建议
    • 短视频长度建议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)

相关文章:

  • iPhone mini,永远再见了
  • 量子力学:从经典物理危机到思维革命的新纪元
  • 6.Excel:通过 Power Query 处理从网上获取的数据
  • 西门子s7协议
  • 自然语言处理(15:RNNLM的学习和评价(整个第4章大总结))
  • Linux子系统 - USB描述符结构体
  • 深入探索Windows Hook技术:原理、内核级实现与高级应用
  • 压力测试未覆盖边界条件的后果有哪些
  • 常见的锁策略
  • angular获取roleFormGroup的control值
  • k8s scheduler几种扩展方式的关系及区别
  • RDMA的挑战与限制
  • 远程办公新体验:用触屏手机流畅操作电脑桌面
  • PhotoScissors快速抠图与背景填充
  • 快速入手-基于Django-rest-framework的ModelViewSet终极版(七)
  • redis常见面试题
  • Java全栈面试宝典:多线程与Spring核心机制深度解析
  • Unix/Linux 中 dup、dup2 和 dup3 系统调用解析
  • Windows10清理机器大全集
  • parallelStream线程问题及解决方案
  • 企业网站seo教程/宁波网站推广公司有哪些
  • 哪些网站可以做翻译兼职/百度投放广告怎么收费
  • 潍坊专业网站建设多少钱/网站一般怎么推广
  • 建设教育信息网站工作总结/杭州优化外包哪里好
  • artisteer 做的网站/查排名官网
  • 国内哪些网站是php做的/seo优化网