项目聚焦于音频 - 视频同步(检测音视频时间偏移、判定多人脸视频中的说话人),代码中音视频特征提取、偏移计算、多人脸追踪 / 裁剪等逻辑均与该仓库一致;
一、项目整体使用流程
该项目是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);- 示例翻译:
/path/to/output/pycrop/my_video/*.avi→ 裁剪后的人脸轨迹视频;/path/to/output/pywork/my_video/offsets.txt→ 音视频偏移值文件;/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)
-
下载预训练模型:
bashsh download_model.sh -
预处理(人脸检测+追踪+裁剪):
bashpython run_pipeline.py --videofile /path/to/video.mp4 --reference name_of_video --data_dir /path/to/output -
计算每个人脸视频的音视频偏移:
bashpython run_syncnet.py --videofile /path/to/video.mp4 --reference name_of_video --data_dir /path/to/output -
可视化结果(生成标注视频):
bashpython run_visualise.py --videofile /path/to/video.mp4 --reference name_of_video --data_dir /path/to/output
五、关键补充
- 所有脚本的
--reference参数必须保持一致,否则会找不到前序步骤的文件; tmp_dir/data_dir会自动创建,若重复运行同一reference,脚本会先删除旧文件再重新生成;- 仅支持25FPS视频(默认参数),若视频帧率不同,需调整
--frame_rate并重新测试; - 人脸检测依赖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_frames、offset_seconds、confidence 是衡量数字人/真人唇形与音频同步性的核心指标,结合实际场景解释如下:
| 参数名 | 中文释义 | 具体含义 & 实用解读 |
|---|---|---|
track_id |
人脸轨迹ID | 对应 pycrop 目录下的裁剪视频(如 track_id=0 → 000001.avi,track_id=1 → 000002.avi),代表视频中检测到的第N个人脸轨迹。 |
offset_frames |
音视频偏移(帧) | 核心指标:衡量音频和视频(唇形)的时间差,单位是帧 (默认视频帧率25帧/秒): ✅ 0:音视频完全同步(唇形和发音对得上); 🔺 正数(如 3):音频比视频快3帧(唇形动得晚,发音先出来); 🔻 负数(如 -2):视频比音频快2帧(唇形动得早,发音后出来)。 |
offset_seconds |
音视频偏移(秒) | offset_frames 转换为秒的直观值(偏移帧 ÷ 25),比如: offset_frames=3 → 0.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),说明这个偏移结果很可靠,且该人脸的唇形和音频同步性较好。
补充:
- 偏移修正方向 :
- 若
offset_frames=5(音频快):需把音频延迟5帧,或把视频提前5帧; - 若
offset_frames=-3(视频快):需把视频延迟3帧,或把音频提前3帧。
- 若
- 置信度阈值 :
- >8:同步性优秀;
- 5~8:同步性良好;
- 2~5:同步性一般(需检查);
- <2:同步性差(唇形和发音明显对不上)。
- 多个人脸场景 :
若视频中有多个人脸(多个track_id),置信度最高的那个track_id 通常是"正在说话的人"(SyncNet核心应用场景之一)。
总结:
offset_frames/offset_seconds:告诉你「音视频差多少时间不同步」;confidence:告诉你「这个不同步的结果准不准,以及同步性本身好不好」;track_id:告诉你「这个结果对应视频里的哪个人脸」。
