从人脸检测到音频偏移:基于SyncNet的音视频偏移计算与人脸轨迹追踪技术解析

项目聚焦于音频 - 视频同步(检测音视频时间偏移、判定多人脸视频中的说话人),代码中音视频特征提取、偏移计算、多人脸追踪 / 裁剪等逻辑均与该仓库一致;

一、项目整体使用流程

该项目是SyncNet(音视频同步网络),核心用于检测音视频时间偏移、判定视频中说话人,整体使用分为「快速演示」和「完整流水线」两种方式,先确保依赖安装完成:

bash 复制代码
# 1. 安装Python依赖
pip install -r requirements.txt
# 2. 安装ffmpeg(系统级依赖,用于音视频处理)
# Ubuntu: sudo apt install ffmpeg
# Mac: brew install ffmpeg
# Windows: 下载ffmpeg并配置环境变量
# 3. 下载预训练模型(SyncNet权重+人脸检测权重)
sh download_model.sh

二、核心脚本功能 & 输入输出详解

1. demo_syncnet.py:快速演示脚本(单视频音视频偏移检测)

(1)输入(命令行参数)
参数名 默认值 说明
--initial_model data/syncnet_v2.model SyncNet预训练模型路径
--batch_size 20 推理时的批次大小(影响速度,无需修改)
--vshift 15 最大偏移搜索范围(单位:帧,25FPS对应0.6秒)
--videofile data/example.avi 输入视频文件路径(需包含音频流)
--tmp_dir data/work/pytmp 临时目录(存放视频帧、音频wav等中间文件)
--reference demo 视频标识(用于命名临时文件,无实际业务意义)

运行示例:

bash 复制代码
python demo_syncnet.py --videofile data/example.avi --tmp_dir /path/to/temp
(2)输出(控制台打印,无文件输出)

示例输出:

复制代码
Model data/syncnet_v2.model loaded.
Compute time 0.520 sec.
Framewise conf: 
[ 9.876 10.021  9.980 ...]
AV offset:      3 
Min dist:       5.353
Confidence:     10.021
(3)输出参数意义
参数名 意义
Compute time 特征提取+偏移计算总耗时(秒)
Framewise conf 逐帧的音视频同步置信度(值越高,该帧音视频同步性越好)
AV offset 音视频偏移量(单位:帧,25FPS对应1帧=0.04秒): - 正数:视频比音频慢(音频超前N帧) - 负数:视频比音频快(视频超前N帧) - 0:音视频完全同步
Min dist 音视频特征的最小距离(值越小,音视频同步性越好,核心判定指标)
Confidence 同步置信度(= 距离中位数 - 最小距离,值越高,偏移检测结果越可信)

2. run_pipeline.py:完整预处理流水线(人脸检测+追踪+视频裁剪)

(1)核心用处

对输入视频做全流程预处理:拆帧/拆音频 → 场景检测 → 人脸检测(S3FD)→ 人脸追踪 → 裁剪出单人脸视频片段(仅保留符合时长的人脸轨迹)。

(2)输入(命令行参数)
参数名 默认值 说明
--data_dir data/work 输出根目录(所有中间/最终文件都存在这里)
--videofile 输入视频文件路径(必填,如/path/to/video.mp4)
--reference 视频唯一标识(必填,如"my_video",用于创建子目录分类文件)
--facedet_scale 0.25 人脸检测缩放系数(越小越快,0.25兼顾速度和精度)
--crop_scale 0.40 人脸裁剪框缩放系数(控制裁剪范围,0.4对应围绕人脸扩展40%)
--min_track 100 最小人脸轨迹时长(帧,25FPS下100帧=4秒,过滤短轨迹)
--frame_rate 25 视频帧率(需和输入视频一致,默认25)
--num_failed_det 25 允许人脸检测失败的最大帧数(超过则停止追踪该人脸)
--min_face_size 100 最小人脸尺寸(像素,过滤过小的人脸框)

运行示例:

bash 复制代码
python run_pipeline.py --videofile /path/to/video.mp4 --reference my_video --data_dir /path/to/output
(3)输出(文件,均在--data_dir下)
文件路径 内容说明
DATADIR/pyavi/DATA_DIR/pyavi/DATADIR/pyavi/REFERENCE/ 存放拆分后的视频(video.avi)、音频(audio.wav)
DATADIR/pyframes/DATA_DIR/pyframes/DATADIR/pyframes/REFERENCE/ 存放视频拆帧后的所有jpg图片(%06d.jpg命名)
DATADIR/pywork/DATA_DIR/pywork/DATADIR/pywork/REFERENCE/ 存放中间结果: - faces.pckl:逐帧人脸检测结果(bbox/置信度) - scene.pckl:场景检测结果(视频分镜) - tracks.pckl:人脸追踪结果
DATADIR/pycrop/DATA_DIR/pycrop/DATADIR/pycrop/REFERENCE/*.avi 裁剪后的单人脸视频片段(每个文件对应一个人脸轨迹,命名为000001.avi等) 「cropped face tracks」即「裁剪后的人脸轨迹视频」

3. run_syncnet.py:音视频偏移计算(针对裁剪后的人脸视频)

(1)核心用处

run_pipeline.py裁剪出的每个人脸视频片段,单独计算音视频偏移量,输出偏移结果文件。

(2)输入(命令行参数)
参数名 默认值 说明
--initial_model data/syncnet_v2.model SyncNet模型路径(同demo)
--batch_size 20 推理批次大小
--vshift 15 最大偏移搜索范围(帧)
--data_dir data/work 根目录(需和run_pipeline的--data_dir一致)
--videofile 原始视频路径(仅用于校验,实际用裁剪后的视频)
--reference 视频标识(需和run_pipeline的--reference一致,必填)

运行示例:

bash 复制代码
python run_syncnet.py --videofile /path/to/video.mp4 --reference my_video --data_dir /path/to/output
(3)输出(文件)
文件路径 内容说明
DATADIR/pywork/DATA_DIR/pywork/DATADIR/pywork/REFERENCE/activesd.pckl 每个人脸视频片段的音视频距离矩阵(pickle格式)
DATADIR/pywork/DATA_DIR/pywork/DATADIR/pywork/REFERENCE/offsets.txt 「audio-video offset values」即「音视频偏移值」:每行对应一个人脸视频的AV offset/Min dist/Confidence(同demo输出)

4. run_visualise.py:可视化结果(生成带标注的视频)

(1)核心用处

run_syncnet.py的偏移/置信度结果,可视化到原始视频中:给每个人脸框标注「轨迹ID+同步置信度」,最终生成带音频的标注视频。

(2)输入(命令行参数)
参数名 默认值 说明
--data_dir data/work 根目录(需和前两步一致)
--videofile 原始视频路径(必填)
--reference 视频标识(需和前两步一致,必填)
--frame_rate 25 视频帧率(和输入视频一致)

运行示例:

bash 复制代码
python run_visualise.py --videofile /path/to/video.mp4 --reference my_video --data_dir /path/to/output
(3)输出(文件)
文件路径 内容说明
DATADIR/pyavi/DATA_DIR/pyavi/DATADIR/pyavi/REFERENCE/video_only.avi 无音频的标注视频(仅人脸框+置信度文字)
DATADIR/pyavi/DATA_DIR/pyavi/DATADIR/pyavi/REFERENCE/video_out.avi 「output video」即「最终可视化视频」:合并音频后的标注视频,可直接播放查看每个人脸的同步置信度

输出路径示例解释

复制代码
$DATA_DIR/pycrop/$REFERENCE/*.avi - cropped face tracks
$DATA_DIR/pywork/$REFERENCE/offsets.txt - audio-video offset values
$DATA_DIR/pyavi/$REFERENCE/video_out.avi - output video (as shown below)
  • $DATA_DIR:你运行脚本时指定的--data_dir参数值(如/path/to/output);
  • $REFERENCE:你指定的--reference参数值(如my_video);
  • 示例翻译:
    1. /path/to/output/pycrop/my_video/*.avi → 裁剪后的人脸轨迹视频;
    2. /path/to/output/pywork/my_video/offsets.txt → 音视频偏移值文件;
    3. /path/to/output/pyavi/my_video/video_out.avi → 带人脸标注+音频的最终可视化视频。

快速演示(Demo)

运行单视频音视频偏移检测:

bash 复制代码
python demo_syncnet.py --videofile data/example.avi --tmp_dir /path/to/temp/directory

预期输出:

复制代码
AV offset:      3 
Min dist:       5.353
Confidence:     10.021

完整流水线(Full Pipeline)

  1. 下载预训练模型:

    bash 复制代码
    sh download_model.sh
  2. 预处理(人脸检测+追踪+裁剪):

    bash 复制代码
    python run_pipeline.py --videofile /path/to/video.mp4 --reference name_of_video --data_dir /path/to/output
  3. 计算每个人脸视频的音视频偏移:

    bash 复制代码
    python run_syncnet.py --videofile /path/to/video.mp4 --reference name_of_video --data_dir /path/to/output
  4. 可视化结果(生成标注视频):

    bash 复制代码
    python run_visualise.py --videofile /path/to/video.mp4 --reference name_of_video --data_dir /path/to/output

五、关键补充

  1. 所有脚本的--reference参数必须保持一致,否则会找不到前序步骤的文件;
  2. tmp_dir/data_dir会自动创建,若重复运行同一reference,脚本会先删除旧文件再重新生成;
  3. 仅支持25FPS视频(默认参数),若视频帧率不同,需调整--frame_rate并重新测试;
  4. 人脸检测依赖CUDA(默认device='cuda'),无GPU需修改detectors/S3FD的device为cpu(速度会大幅下降)。

自动化处理扩展

run_automation.py(完整流水线自动化调用)

py 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import subprocess
import os
import time
import sys
from pathlib import Path
import glob

# ==================== 基础配置 ====================
# 日志目录
LOG_DIR = Path("./logs")
# 要执行的脚本列表
SCRIPTS = [
    "run_pipeline.py",
    "run_syncnet.py",
    "run_visualise.py"
]
# 支持的视频格式(可自行扩展)
SUPPORTED_VIDEO_EXT = ['.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv']

# ==================== 工具函数 ====================
def get_timestamp():
    """生成时间戳(YYYYMMDD_HHMMSS)"""
    return time.strftime("%Y%m%d_%H%M%S", time.localtime())

def init_log_dir():
    """初始化日志目录"""
    LOG_DIR.mkdir(exist_ok=True, parents=True)

def get_video_files(input_dir, recursive=True):
    """递归/非递归获取目录下所有支持的视频文件"""
    video_files = []
    input_path = Path(input_dir).resolve()
    
    if not input_path.exists():
        print(f"❌ 输入目录不存在: {input_dir}")
        return video_files
    
    # 遍历所有支持的视频格式
    for ext in SUPPORTED_VIDEO_EXT:
        glob_pattern = f"**/*{ext}" if recursive else f"*{ext}"
        files = glob.glob(str(input_path / glob_pattern), recursive=recursive)
        video_files.extend(files)
    
    # 去重并排序
    video_files = sorted(list(set(video_files)))
    return video_files

def run_command(cmd, log_file):
    """执行命令并记录日志"""
    # 记录命令执行信息
    log_content = f"\n{'='*50}\n执行命令: {' '.join(cmd)}\n开始时间: {time.ctime()}\n{'='*50}\n"
    log_file.write(log_content)
    log_file.flush()

    # 执行命令并捕获输出
    process = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        encoding="utf-8"
    )

    # 实时输出日志
    while True:
        line = process.stdout.readline()
        if not line and process.poll() is not None:
            break
        if line:
            log_file.write(line)
            log_file.flush()
            # 同时输出到控制台
            sys.stdout.write(line)
            sys.stdout.flush()

    # 记录执行结果
    return_code = process.returncode
    result = "成功" if return_code == 0 else "失败"
    end_log = f"\n{'='*50}\n命令执行{result},返回码: {return_code}\n结束时间: {time.ctime()}\n{'='*50}\n"
    log_file.write(end_log)
    log_file.flush()

    return return_code

# ==================== 参数解析 ====================
def parse_args():
    parser = argparse.ArgumentParser(description="SyncNet 批量全管线自动化脚本",
                                     formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    
    # ---------------- 批量处理核心参数 ----------------
    parser.add_argument("--input_dir", type=str, required=True,
                        help="视频文件目录(批量处理必填,会递归查找所有视频)")
    parser.add_argument("--no-recursive", action="store_true",
                        help="是否禁用递归查找(仅处理input_dir下一级目录)")
    
    # ---------------- 共用核心参数 ----------------
    parser.add_argument("--data_dir", type=str, default="data/work",
                        help="输出数据根目录(每个视频会在该目录下按文件名创建子目录)")
    
    # ---------------- run_pipeline.py 特有参数 ----------------
    parser.add_argument("--facedet_scale", type=float, default=0.25,
                        help="[pipeline] 人脸检测缩放因子")
    parser.add_argument("--crop_scale", type=float, default=0.40,
                        help="[pipeline] 裁剪框缩放因子")
    parser.add_argument("--min_track", type=int, default=100,
                        help="[pipeline] 最小人脸跟踪时长")
    parser.add_argument("--num_failed_det", type=int, default=25,
                        help="[pipeline] 允许的最大检测失败次数")
    parser.add_argument("--min_face_size", type=int, default=100,
                        help="[pipeline] 最小人脸尺寸(像素)")
    
    # ---------------- run_syncnet.py 特有参数 ----------------
    parser.add_argument("--initial_model", type=str, default="data/syncnet_v2.model",
                        help="[syncnet] 初始模型路径")
    parser.add_argument("--batch_size", type=int, default=20,
                        help="[syncnet] 批处理大小")
    parser.add_argument("--vshift", type=int, default=15,
                        help="[syncnet] 视频偏移量")
    
    # ---------------- run_visualise.py 特有参数 ----------------
    parser.add_argument("--frame_rate", type=int, default=25,
                        help="[visualise/pipeline] 帧率")
    
    # ---------------- 脚本控制参数 ----------------
    parser.add_argument("--skip-failed", action="store_true",
                        help="某个脚本执行失败时,是否跳过该视频的后续脚本")
    parser.add_argument("--skip-video-failed", action="store_true",
                        help="某个视频处理失败时,是否跳过下一个视频")

    return parser.parse_args()

# ==================== 主执行逻辑 ====================
def main():
    # 1. 解析参数
    args = parse_args()
    
    # 2. 初始化日志目录
    init_log_dir()
    
    # 3. 生成批量日志文件名(带时间戳)
    timestamp = get_timestamp()
    batch_log_file_path = LOG_DIR / f"syncnet_batch_automation_{timestamp}.log"
    
    # 4. 获取所有视频文件
    video_files = get_video_files(args.input_dir, not args.no_recursive)
    if not video_files:
        print(f"❌ 在目录 {args.input_dir} 下未找到支持的视频文件(支持格式:{SUPPORTED_VIDEO_EXT})")
        sys.exit(1)
    print(f"✅ 共找到 {len(video_files)} 个视频文件,开始批量处理...")
    
    # 5. 打开批量日志文件
    with open(batch_log_file_path, "a", encoding="utf-8") as batch_log_file:
        # 写入批量执行头部信息
        batch_log_file.write(f"===== SyncNet 批量自动化管线执行日志 =====\n")
        batch_log_file.write(f"执行时间: {time.ctime()}\n")
        batch_log_file.write(f"输入目录: {args.input_dir}\n")
        batch_log_file.write(f"递归查找: {not args.no_recursive}\n")
        batch_log_file.write(f"输出根目录: {args.data_dir}\n")
        batch_log_file.write(f"视频文件数量: {len(video_files)}\n")
        batch_log_file.write(f"日志文件: {batch_log_file_path}\n")
        batch_log_file.write(f"==========================================\n\n")
        batch_log_file.flush()

        # 6. 遍历处理每个视频
        total_success = 0
        total_failed = 0
        failed_videos = []
        
        for idx, videofile in enumerate(video_files, 1):
            # 生成reference(视频文件名,不含路径和后缀)
            video_path = Path(videofile)
            reference = video_path.stem  # 核心:用文件名作为reference
            # 替换特殊字符(避免目录创建失败)
            reference = reference.replace('/', '_').replace('\\', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_').replace('<', '_').replace('>', '_').replace('|', '_')
            
            batch_log_file.write(f"\n\n{'='*60}\n开始处理第 {idx}/{len(video_files)} 个视频:\n文件路径: {videofile}\nReference: {reference}\n{'='*60}\n")
            batch_log_file.flush()
            print(f"\n\n📌 开始处理第 {idx}/{len(video_files)} 个视频: {videofile} (reference: {reference})")

            # 标记当前视频是否处理成功
            video_success = True

            # 构造每个脚本的执行命令
            # 6.1 run_pipeline.py 命令
            pipeline_cmd = [
                sys.executable, "run_pipeline.py",
                "--videofile", videofile,
                "--reference", reference,
                "--data_dir", args.data_dir,
                "--facedet_scale", str(args.facedet_scale),
                "--crop_scale", str(args.crop_scale),
                "--min_track", str(args.min_track),
                "--frame_rate", str(args.frame_rate),
                "--num_failed_det", str(args.num_failed_det),
                "--min_face_size", str(args.min_face_size)
            ]

            # 6.2 run_syncnet.py 命令
            syncnet_cmd = [
                sys.executable, "run_syncnet.py",
                "--videofile", videofile,
                "--reference", reference,
                "--data_dir", args.data_dir,
                "--initial_model", args.initial_model,
                "--batch_size", str(args.batch_size),
                "--vshift", str(args.vshift)
            ]

            # 6.3 run_visualise.py 命令
            visualise_cmd = [
                sys.executable, "run_visualise.py",
                "--videofile", videofile,
                "--reference", reference,
                "--data_dir", args.data_dir,
                "--frame_rate", str(args.frame_rate)
            ]

            # 按顺序执行脚本
            scripts_cmds = [
                ("run_pipeline.py", pipeline_cmd),
                ("run_syncnet.py", syncnet_cmd),
                ("run_visualise.py", visualise_cmd)
            ]

            for script_name, cmd in scripts_cmds:
                batch_log_file.write(f"\n\n========== 开始执行 {script_name} ==========\n")
                batch_log_file.flush()
                
                # 执行命令
                return_code = run_command(cmd, batch_log_file)
                
                # 检查执行结果
                if return_code != 0:
                    video_success = False
                    batch_log_file.write(f"\n❌ {script_name} 执行失败 (视频: {videofile})\n")
                    batch_log_file.flush()
                    print(f"\n❌ {script_name} 执行失败 (视频: {videofile})")
                    
                    # 若开启skip-failed,跳过该视频后续脚本
                    if args.skip_failed:
                        batch_log_file.write(f"\n⚠️  已开启--skip-failed,跳过该视频后续脚本\n")
                        batch_log_file.flush()
                        print(f"\n⚠️  已开启--skip-failed,跳过该视频后续脚本")
                        break

            # 统计结果
            if video_success:
                total_success += 1
                batch_log_file.write(f"\n✅ 视频 {videofile} 处理完成\n")
                print(f"\n✅ 视频 {videofile} 处理完成")
            else:
                total_failed += 1
                failed_videos.append(videofile)
                batch_log_file.write(f"\n❌ 视频 {videofile} 处理失败\n")
                print(f"\n❌ 视频 {videofile} 处理失败")
                
                # 若开启skip-video-failed,跳过下一个视频
                if args.skip_video_failed:
                    batch_log_file.write(f"\n⚠️  已开启--skip-video-failed,终止批量处理\n")
                    batch_log_file.flush()
                    print(f"\n⚠️  已开启--skip-video-failed,终止批量处理")
                    break

        # 7. 批量处理完成,写入汇总信息
        batch_log_file.write(f"\n\n===== 批量处理汇总 =====\n")
        batch_log_file.write(f"总视频数: {len(video_files)}\n")
        batch_log_file.write(f"成功数: {total_success}\n")
        batch_log_file.write(f"失败数: {total_failed}\n")
        if failed_videos:
            batch_log_file.write(f"失败视频列表: {failed_videos}\n")
        batch_log_file.write(f"完成时间: {time.ctime()}\n")
        batch_log_file.write(f"批量日志文件: {batch_log_file_path}\n")
        batch_log_file.write(f"========================\n")
        batch_log_file.flush()

        # 控制台输出汇总
        print(f"\n\n===== 批量处理汇总 =====")
        print(f"总视频数: {len(video_files)}")
        print(f"成功数: {total_success}")
        print(f"失败数: {total_failed}")
        if failed_videos:
            print(f"失败视频列表: {failed_videos}")
        print(f"批量日志文件: {batch_log_file_path}")
        print(f"========================")

if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print(f"\n❌ 批量脚本执行出错: {str(e)}")
        # 错误信息写入日志
        timestamp = get_timestamp()
        init_log_dir()
        error_log_path = LOG_DIR / f"syncnet_batch_automation_error_{timestamp}.log"
        with open(error_log_path, "a", encoding="utf-8") as f:
            f.write(f"执行出错时间: {time.ctime()}\n")
            f.write(f"错误信息: {str(e)}\n")
        sys.exit(1)
核心逻辑说明
  • --reference 自动使用视频文件名(不含后缀),无需手动指定;
  • 输出目录(--data_dir)可固定(或自定义),脚本会在该目录下按reference(即视频文件名)创建pycrop/pywork/pyavi子目录,完全兼容原管线的目录结构。
    • 视频文件:/data/videos/xxx.mp4 → reference:xxx
    • 输出目录:/path/to/output/pycrop/xxx//path/to/output/pywork/xxx//path/to/output/pyavi/xxx/
  • 批量递归查找:通过--input_dir指定视频目录,自动递归查找所有支持格式的视频(mp4/avi/mov/mkv/flv/wmv),可通过--no-recursive禁用递归;
  • 完整日志:
    • 批量日志:记录所有视频的处理汇总;
    • 实时日志:每个脚本的输出实时写入日志 + 打印到控制台;
    • 错误日志:脚本异常时自动生成错误日志。
使用方法
py 复制代码
python run_syncnet_batch.py \
  --input_dir /path/to/videos \
  --data_dir /path/to/output

查看所有参数:

bash 复制代码
python run_syncnet_batch.py -h

activesd.pckl读取与offsets.txt(README 中提到的)缺失问题

pckl文件读取与offsets.txt文件生成
  • activesd.pckl 是 run_syncnet.py 的直接输出,包含更完整的计算过程数据;
  • offsets.txt 是基于 activesd.pckl 解析出的最终结果,需要手动生成(原代码未自动生成,仅 README 提及)。

完整流程(生成 offsets.txt):

  • 先运行 run_pipeline.py → 生成裁剪视频(pycrop 目录);
  • 再运行 run_syncnet.py → 生成 activesd.pckl;
  • 运行上述脚本 → 从 activesd.pckl 解析出 offsets.txt。
py 复制代码
import pickle
import numpy as np

# ==================== 配置路径 ====================
# 替换为你的实际路径
data_dir = "data/work"    # 对应 --data_dir 参数
reference = "name_of_video"  # 对应 --reference 参数

# ==================== 读取 activesd.pckl ====================
pckl_path = os.path.join(data_dir, "pywork", reference, "activesd.pckl")
with open(pckl_path, 'rb') as f:
    dists_list = pickle.load(f)  # 每个元素对应一个裁剪视频的距离矩阵

# ==================== 解析偏移值(核心逻辑) ====================
# 偏移计算逻辑与 SyncNetInstance.evaluate 一致
vshift = 15  # 对应 run_syncnet.py 中的 --vshift 参数(默认15)
offsets = []
confidences = []

for dists in dists_list:
    # 步骤1:计算每帧的平均距离
    mdist = np.mean(dists, axis=1)
    # 步骤2:找到最小距离的索引
    minidx = np.argmin(mdist)
    # 步骤3:计算音视频偏移(单位:帧,25帧/秒)
    offset = vshift - minidx
    # 步骤4:计算置信度(中位数距离 - 最小距离)
    conf = np.median(mdist) - mdist[minidx]
    
    offsets.append(offset)
    confidences.append(conf)

# ==================== 生成 offsets.txt(与 README 对齐) ====================
txt_path = os.path.join(data_dir, "pywork", reference, "offsets.txt")
with open(txt_path, 'w') as f:
    f.write("track_id\toffset_frames\toffset_seconds\tconfidence\n")
    for idx, (offset, conf) in enumerate(zip(offsets, confidences)):
        offset_sec = offset / 25.0  # 转换为秒(视频帧率25)
        f.write(f"{idx}\t{offset}\t{offset_sec:.4f}\t{conf:.4f}\n")

print(f"偏移值已保存到 {txt_path}")
print("=== 偏移值汇总 ===")
for idx, (offset, conf) in enumerate(zip(offsets, confidences)):
    print(f"裁剪视频 {idx}:偏移 {offset} 帧({offset/25:.4f} 秒),置信度 {conf:.4f}")
或者直接修改run_syncnet.py

好处是在生成 ·activesd.pckl· 后自动解析并生成 ·offsets.txt·,无需单独运行脚本。

py 复制代码
#!/usr/bin/python
#-*- coding: utf-8 -*-

import time, pdb, argparse, subprocess, pickle, os, gzip, glob
import numpy as np  # 新增:导入numpy

from SyncNetInstance import *

# ==================== PARSE ARGUMENT ====================

parser = argparse.ArgumentParser(description = "SyncNet");
parser.add_argument('--initial_model', type=str, default="data/syncnet_v2.model", help='');
parser.add_argument('--batch_size', type=int, default='20', help='');
parser.add_argument('--vshift', type=int, default='15', help='');
parser.add_argument('--data_dir', type=str, default='data/work', help='');
parser.add_argument('--videofile', type=str, default='', help='');
parser.add_argument('--reference', type=str, default='', help='');
opt = parser.parse_args();

setattr(opt,'avi_dir',os.path.join(opt.data_dir,'pyavi'))
setattr(opt,'tmp_dir',os.path.join(opt.data_dir,'pytmp'))
setattr(opt,'work_dir',os.path.join(opt.data_dir,'pywork'))
setattr(opt,'crop_dir',os.path.join(opt.data_dir,'pycrop'))


# ==================== LOAD MODEL AND FILE LIST ====================

s = SyncNetInstance();

s.loadParameters(opt.initial_model);
print("Model %s loaded."%opt.initial_model);

flist = glob.glob(os.path.join(opt.crop_dir,opt.reference,'0*.avi'))
flist.sort()

# ==================== GET OFFSETS ====================

dists = []
offsets_list = []  # 新增:存储每个裁剪视频的偏移值
confidences_list = []  # 新增:存储每个裁剪视频的置信度

for idx, fname in enumerate(flist):
    print(f"\nProcessing crop video {idx}: {fname}")
    offset, conf, dist = s.evaluate(opt,videofile=fname)
    dists.append(dist)
    offsets_list.append(offset)  # 保存偏移值
    confidences_list.append(conf)  # 保存置信度

# ==================== SAVE ACTIVESD.PCKL ====================

with open(os.path.join(opt.work_dir,opt.reference,'activesd.pckl'), 'wb') as fil:
    pickle.dump(dists, fil)
print(f"\nSaved raw distance matrix to: {os.path.join(opt.work_dir,opt.reference,'activesd.pckl')}")

# ==================== 新增:解析并生成 offsets.txt ====================
def generate_offsets_txt(opt, offsets, confidences):
    """从activesd.pckl的原始数据/直接结果生成offsets.txt"""
    txt_path = os.path.join(opt.work_dir, opt.reference, 'offsets.txt')
    frame_rate = 25  # 固定帧率(与run_pipeline.py一致)
    
    with open(txt_path, 'w', encoding='utf-8') as f:
        # 写入表头
        f.write("track_id\toffset_frames\toffset_seconds\tconfidence\n")
        # 写入每个裁剪视频的结果
        for track_id, (offset, conf) in enumerate(zip(offsets, confidences)):
            offset_sec = offset / frame_rate  # 转换为秒
            f.write(f"{track_id}\t{offset}\t{offset_sec:.4f}\t{conf:.4f}\n")
    
    print(f"Saved offset results to: {txt_path}")
    print("\n=== Final Offset Summary ===")
    for track_id, (offset, conf) in enumerate(zip(offsets, confidences)):
        print(f"Track {track_id}: Offset = {offset} frames ({offset/25:.4f} sec), Confidence = {conf:.4f}")

# 调用生成函数
generate_offsets_txt(opt, offsets_list, confidences_list)
offsets.txt 中核心参数含义

offsets.txt 里的 offset_framesoffset_secondsconfidence 是衡量数字人/真人唇形与音频同步性的核心指标,结合实际场景解释如下:

参数名 中文释义 具体含义 & 实用解读
track_id 人脸轨迹ID 对应 pycrop 目录下的裁剪视频(如 track_id=0000001.avitrack_id=1000002.avi),代表视频中检测到的第N个人脸轨迹。
offset_frames 音视频偏移(帧) 核心指标:衡量音频和视频(唇形)的时间差,单位是 (默认视频帧率25帧/秒): ✅ 0:音视频完全同步(唇形和发音对得上); 🔺 正数(如 3):音频比视频快3帧(唇形动得晚,发音先出来); 🔻 负数(如 -2):视频比音频快2帧(唇形动得早,发音后出来)。
offset_seconds 音视频偏移(秒) offset_frames 转换为秒的直观值(偏移帧 ÷ 25),比如: offset_frames=30.12秒(音频超前0.12秒); offset_frames=-2-0.08秒(视频超前0.08秒)。
confidence 同步置信度 衡量偏移结果的可靠性 + 音视频同步的好坏程度 : ✅ 值越大越好(通常>5就代表同步性不错); ❌ 值接近0/负数:同步性极差(唇形和发音完全对不上); 📌 计算逻辑:中位数距离 - 最小距离,值越大说明"最优同步帧"和"其他帧"的差异越明显,结果越可信。

示例:

比如 offsets.txt 里的一行:

复制代码
1	-2	-0.0800	8.5670

解读:

  • 第1个人脸轨迹(track_id=1);
  • 视频比音频快2帧(offset_frames=-2),也就是唇形动了0.08秒后,对应的发音才出来;
  • 置信度8.5670(>5),说明这个偏移结果很可靠,且该人脸的唇形和音频同步性较好。

补充:

  1. 偏移修正方向
    • offset_frames=5(音频快):需把音频延迟5帧,或把视频提前5帧;
    • offset_frames=-3(视频快):需把视频延迟3帧,或把音频提前3帧。
  2. 置信度阈值
    • >8:同步性优秀;
    • 5~8:同步性良好;
    • 2~5:同步性一般(需检查);
    • <2:同步性差(唇形和发音明显对不上)。
  3. 多个人脸场景
    若视频中有多个人脸(多个 track_id),置信度最高的那个track_id 通常是"正在说话的人"(SyncNet核心应用场景之一)。

总结:

  • offset_frames/offset_seconds:告诉你「音视频差多少时间不同步」;
  • confidence:告诉你「这个不同步的结果准不准,以及同步性本身好不好」;
  • track_id:告诉你「这个结果对应视频里的哪个人脸」。

相关推荐
飞Link2 小时前
数据合成中的通用模型蒸馏、领域模型蒸馏和模型自我提升
算法·数据挖掘
linmoo19862 小时前
Langchain4j 系列之二十一 - Language Models
人工智能·语言模型·自然语言处理·langchain·指令微调·langchain4j·languagemodel
ai_top_trends2 小时前
2026 年 AI 生成 PPT 工具推荐清单:测评后给出的答案
人工智能·python·powerpoint
程序新视界2 小时前
“提供溢出的情绪价值”是AI产品极具可能性的方向
人工智能·后端·产品
努力犯错2 小时前
GLM-Image:首个开源工业级自回归图像生成模型完全指南
机器学习·数据挖掘·回归·开源
xixixi777772 小时前
AGI-Next前沿峰会——对于唐杰教授提到的AI下一步方向的“两条思路一次取舍”的思考(思路分析+通俗易懂解释)
人工智能·ai·大模型·agi·通用人工智能·asi
技术大咖--上好嘉2 小时前
聚焦老龄化AI赋能 京能天云数据-智慧康养服务 APP重构老年健康管理新范式
人工智能
地球资源数据云2 小时前
1960年-2024年中国农村居民消费价格指数数据集
大数据·数据库·人工智能·算法·数据集
一点一木2 小时前
2025 年终复盘:当 AI 工具链从生产力进化为我的生活秩序
人工智能