01

三秒变脸:Deep-Live-Cam 初体验

GitHub 93K Star 的开源项目,只需一张照片就能实时换脸。先来看它做了什么,再拆开看看怎么做的。

它到底是什么?

想象你有一个神奇的化妆镜——站在镜子前,你的脸瞬间变成了另一个人。Deep-Live-Cam 就是这个镜子背后的AI 模型:你给它一张照片,它就能把照片里的脸"贴"到你脸上,而且是在实时视频中完成。

📸

单图换脸

只需要一张照片,就能把照片里的人脸替换到视频或摄像头画面中。

🎥

实时处理

在摄像头直播中实时换脸,延迟极低,可以用 OBS 推流。

👥

多人换脸

支持同时处理画面中的多张脸,每个人换成不同的人。

🛡️

安全过滤

内置 NSFW 检测,自动拒绝处理不当内容。

三步上手

就像使用自拍滤镜一样简单——选脸、选摄像头、按 Live。

1
选择一张源脸照片

任何包含清晰人脸的照片都可以,AI 会提取这张脸的"身份信息"

2
选择目标(摄像头或视频)

可以选摄像头做实时换脸,也可以选视频文件做后期处理

3
点击 Live 或 Start

等待 10-30 秒加载模型,然后就是见证魔法的时刻

项目长什么样?

点击下面的组件来了解项目各部分的作用:

用户界面层

🚪
run.py(入口)
🖥️
ui.py(界面)

核心处理层

🧠
core.py(调度器)
🔍
face_analyser.py
🎭
face_swapper.py

基础设施层

📋
globals.py(配置)
📹
video_capture.py
点击任意组件查看详细说明

代码解读:程序入口

一切从这里开始。看看 run.py 做了哪些事:

代码

import os, sys
project_root = os.path.dirname(
    os.path.abspath(__file__))
os.environ["PATH"] = (
    project_root + os.pathsep
    + os.environ.get("PATH", ""))

from modules import core

if __name__ == '__main__':
    core.run()
            
白话翻译

导入操作系统相关的工具库

找到项目根目录的绝对路径

把项目路径加到系统 PATH 里——

这样程序就能找到自带的 ffmpeg 等工具

(空行分隔导入和业务代码)

导入核心模块——真正干活的代码都在 modules/ 里

(空行)

当这个文件被直接运行时(而非被导入),

启动核心流程

💡
设计智慧

入口文件 run.py 只有不到 40 行,真正的逻辑全部在 modules/ 子目录里。这种"入口极简、逻辑分离"的结构让项目维护起来非常清晰——就像公司前台只负责接待,具体业务由各部门处理。

文件地图

了解每个文件的职责,就像认识一个公司里的每个部门:

Deep-Live-Cam/ 项目根目录
run.py 程序入口,设置环境后启动核心
modules/ 所有核心逻辑都在这里
models/ AI 模型文件(ONNX 格式)
requirements.txt Python 依赖清单
modules/processors/frame/ 帧处理器——换脸、增强、遮罩都在这里

检验理解

run.py 文件的主要职责是什么?

如果你想修改程序的最大内存限制,应该去哪个文件?

02

AI 如何认出你的脸

face_analyser.py 是项目的"眼睛"——它用 InsightFace 库找到画面中的人脸,提取关键特征,为后续换脸做准备。

机场安检的类比

想象你在机场过安检。安检人员做三件事:先在人群中找到你(检测),再扫描你的护照(识别身份),如果需要还可能让你摆个姿势拍个照(特征点定位)。face_analyser 做的事一模一样,只不过它的"眼睛"是 AI 模型。

1

检测:人脸在哪?

2

识别:这是谁?

3

定位:五官在哪?

背后的 AI 引擎

项目使用 InsightFace 库,底层运行在 ONNX Runtime 上。

🏔️
Buffalo_L 模型

InsightFace 自带的人脸分析套件,包含检测、识别、特征点三个子模型

📐
640 x 640 检测分辨率

AI 在这个分辨率下搜索人脸,足够精确又不会太慢

🔐
线程安全锁

多个线程不会同时初始化模型,避免资源冲突

代码解读:初始化人脸分析器

代码

def get_face_analyser() -> Any:
    global FACE_ANALYSER
    if FACE_ANALYSER is None:
        with FACE_ANALYSER_LOCK:
            if FACE_ANALYSER is None:
                FACE_ANALYSER = insightface
                    .app.FaceAnalysis(
                    name='buffalo_l',
                    allowed_modules=[
                      'detection',
                      'recognition',
                      'landmark_2d_106'])
            
白话翻译

定义获取人脸分析器的函数

声明使用全局变量 FACE_ANALYSER

如果分析器还没初始化过...

先加锁——确保只有一个线程能初始化

双重检查:拿到锁后再确认一次

创建 InsightFace 人脸分析器

使用 buffalo_l 模型套件

只加载三个需要的模块:

检测(找到脸在哪)

识别(提取脸的身份特征)

106 个特征点定位(五官精确位置)

💡
双重检查锁定模式

这段代码使用了一个经典的并发编程技巧:先快速检查(无锁),再加锁检查(有锁)。这既保证了线程安全,又避免了不必要的锁开销。就像先从窗户看看洗手间有没有人,没人再推门确认。

数据对话:一帧画面的旅程

当一帧画面进入系统,各模块之间是怎么"对话"的?点击按钮观看:

快 vs 精:两种检测模式

实时模式和离线模式对速度的需求完全不同。项目提供了两种检测策略:

快速模式(detect_one_face_fast)

只做检测,跳过识别和特征点。约 10ms/帧。实时直播用这种——不需要知道"这是谁",只需要知道"脸在哪"。

🎯

精确模式(get_one_face)

检测 + 识别 + 特征点三合一。约 16ms/帧。离线处理视频时使用,需要完整的身份信息来做映射。

代码

def detect_one_face_fast(frame):
    fa = get_face_analyser()
    bboxes, kpss = fa.det_model
        .detect(frame, max_num=0)
    if bboxes.shape[0] == 0:
        return None
    idx = int(bboxes[:, 0].argmin())
    return Face(bbox=bboxes[idx, :4],
               kps=kpss[idx])
            
白话翻译

定义快速人脸检测函数

获取全局人脸分析器(已初始化好的)

只调用检测模型,不碰识别和特征点

扫描画面中所有可能的人脸

如果没检测到任何脸,返回空

选最左边的人脸(argmin)——通常是主目标

返回一个轻量级 Face 对象,只包含

边界框和关键点坐标——够换脸用了

检验理解

实时换脸模式为什么能用"快速检测"跳过识别和特征点?

如果两个线程同时调用 get_face_analyser() 会怎样?

03

偷天换日:换脸的魔法引擎

face_swapper.py 是整个项目最核心的模块——它使用 inswapper 模型完成"换脸手术",再精细地贴回原画面。

面具制作坊的类比

想象一个顶级的面具制作坊。工匠拿到一张目标脸的照片,然后用源脸的"身份信息"(嵌入向量),制作出一个完美的面具。最后,工匠小心翼翼地把面具贴到目标脸上,边缘要自然过渡,不能看出接缝。

1

提取源脸身份向量

2

生成新面孔

3

精细贴回原画面

4

后处理(锐化/混合)

inswapper:换脸的核心模型

项目使用的 inswapper 模型是整个换脸过程的核心。它接受两个输入:一张目标脸的图像和源脸的 512 维嵌入向量,输出一张融合后的新面孔。

inswapper_128.onnx FP32 模型,兼容所有 GPU,精度最高
inswapper_128_fp16.onnx FP16 半精度模型,显存占用减半,NVIDIA Tensor Core GPU 速度更快
128 x 128 模型输入尺寸——所有脸都先缩放到这个统一大小
512 维向量 源脸的身份特征压缩成 512 个数字

换脸数据流

点击"下一步"追踪一张脸从输入到输出的完整旅程:

📸
源脸提取
🎭
inswapper
🔧
仿射贴回
后处理
点击"下一步"开始

代码解读:换脸手术

代码

def swap_face(source_face, target_face,
             temp_frame) -> Frame:
    face_swapper = get_face_swapper()

    bgr_fake, M = face_swapper.get(
        temp_frame, target_face,
        source_face, paste_back=False)

    swapped_frame = _fast_paste_back(
        temp_frame, bgr_fake,
        _aimg_dummy, M)
    return swapped_frame
            
白话翻译

定义换脸函数,需要三样东西:源脸、目标脸、原始画面

获取已加载的换脸模型

(空行分隔获取模型和使用模型)

让 inswapper 生成"假脸"——融合了源脸身份的新面孔

paste_back=False 表示先不贴回,我们自己来做(更快)

M 是仿射变换矩阵,记录了脸的空间位置关系

用优化的贴回函数把新面孔精确放回原画面

传入原始画面、假脸、辅助信息和位置矩阵

返回处理好的画面

边缘柔化的秘密

直接把生成的脸"硬贴"上去会留下明显的接缝。_fast_paste_back 使用了一个巧妙的技术来解决这个问题:

💡
预计算羽化模板

项目在对齐空间里预先计算了一个边缘羽化模板——中间完全不透明,边缘逐渐透明。这个模板缓存后每帧直接使用,用仿射变换把它"拉伸"到目标脸上。成本从原来跟脸面积的四次方成正比,变成了固定的一次方。

代码

def _get_soft_alpha(size):
    if _paste_cache['alpha_size'] != size:
        k_erode = max(size // 10, 3)
        k_blur = max(size // 20, 3)
        mask = np.full((size, size),
                     255, dtype=np.uint8)
        mask = cv2.erode(mask, ...)
        mask = cv2.GaussianBlur(mask, ...)
        _paste_cache['soft_alpha'] = mask
    return _paste_cache['soft_alpha']
            
白话翻译

定义获取柔化透明度模板的函数

如果缓存尺寸不匹配,重新计算

腐蚀核大小:脸的 1/10(确保边缘有过渡区)

模糊核大小:脸的 1/20(控制羽化柔和程度)

创建全白(完全不透明)的初始模板

让边缘向内收缩一点(腐蚀)

再用高斯模糊柔化边缘(从白渐变到透明)

缓存计算结果,下次直接用

返回缓存的模板

匹配概念

把左侧的换脸概念拖到右侧对应的描述上:

嵌入向量
仿射变换
羽化模板

512 个数字组成的"身份指纹",用来告诉 AI 这是哪张脸

拖到这里

数学矩阵,把 128x128 的新面孔精确对齐回画面中的脸部位置

拖到这里

边缘从完全不透明渐变到透明的模板,让贴上去的脸看不出接缝

拖到这里

检验理解

为什么 swap_face 设置 paste_back=False?

预计算羽化模板为什么能大幅提升性能?

04

实时换脸:从视频到直播

实时换脸比离线处理难得多——每帧只有 16 毫秒的预算。看看项目如何用三线程架构和智能调度达成实时性能。

16 毫秒的挑战

60 帧/秒的视频意味着每帧只有 16.7 毫秒。在这 16 毫秒里,程序需要:读取摄像头画面、检测人脸、运行换脸模型、显示结果。如果任何一步超时,画面就会卡顿。

⚠️
实时 vs 离线的本质区别

离线处理视频时,一帧花 100 毫秒也无所谓,大不了整体多等一会儿。但实时模式是"硬性截止"——必须在下一帧到来之前完成,否则就丢帧。这就像限时考试和带回家的作业的区别。

三线程分工协作

解决方案是把工作拆分给三个"工人",让他们同时工作:

📹
线程 1:采集线程(Capture Thread)

只负责从摄像头读帧。用一个大小为 2 的队列,满时就丢掉旧帧。

🧠
线程 2:处理线程(Processing Thread)

从采集队列取帧,做人脸检测和换脸。检测不是每帧都做,而是每隔约 80 毫秒检测一次,中间帧复用上一次的检测结果。

🖥️
线程 3:显示线程(Display Thread)

主线程用 Tk 的 after() 方法定期取处理好的帧并显示。先缩放到窗口大小再做颜色转换,减少计算量。

帧数据在三个线程间的流转

点击"下一步"看一帧画面如何在三个线程之间流转:

📹
采集线程
🧠
处理线程
🖥️
显示线程
点击"下一步"开始

代码解读:智能检测间隔

处理线程中最关键的优化——不是每帧都检测人脸:

代码

det_interval = max(1,
    round(camera_fps * 0.08))

det_count += 1
if det_count % det_interval == 0:
    if modules.globals.many_faces:
        cached_many_faces = (
            detect_many_faces_fast(
                temp_frame))
    else:
        cached_target_face = (
            detect_one_face_fast(
                temp_frame))
            
白话翻译

计算检测间隔:至少每 1 帧检测一次

公式:帧率 x 0.08秒(约 80ms)。60fps 时每 5 帧检测,30fps 时每 3 帧

(空行)

每处理一帧,计数器加 1

只有到达检测间隔时才做人脸检测

如果是多人模式...

检测所有脸的位置

用快速检测(跳过识别和特征点)

如果只换一个人的脸...

只检测一张脸的位置

同样用快速检测

💡
为什么 80ms 就够了?

在 30fps 的视频中,相邻两帧之间只间隔 33ms。一个人的脸在 80ms 内(约 2-3 帧)几乎不会移动太多,所以用旧的检测结果来换脸,视觉上完全看不出差异。这个策略把检测开销降低了 3-5 倍。

VideoCapturer:不只是打开摄像头

video_capture.py 封装了摄像头的复杂操作。它不只是简单地"打开摄像头"——还会自动选择最优后端、测量真实帧率:

代码

def _measure_fps(self, warmup=10,
                 sample=30,
                 fallback=30.0):
    for _ in range(warmup):
        self.cap.read()
    t0 = time.perf_counter()
    for _ in range(sample):
        ret, _ = self.cap.read()
        if not ret: return fallback
    elapsed = time.perf_counter() - t0
    return sample / elapsed
            
白话翻译

定义实测帧率函数:先读 warmup 帧热身

再读 sample 帧计时

如果测不出来就用默认 30fps

先读 10 帧让摄像头稳定(热身)

记录精确的起始时间

连续读 30 帧

如果读帧失败,返回默认值

计算 30 帧用了多少时间

帧率 = 帧数 / 时间

💡
为什么不信任摄像头报的帧率?

代码注释写道:"CAP_PROP_FPS 在 DirectShow 上经常撒谎——明明支持 60fps,却报 30fps。"所以项目用实际测量代替信任设备参数。这就像面试时不看简历上的"精通XX",直接让候选人写代码。

找 Bug 挑战

下面是简化版的采集线程代码,有一行可能导致丢帧严重。找出它:

1 def _capture_thread(cap, queue, stop):
2 while not stop.is_set():
3 queue.put_nowait(frame) # 队列满?
4 ret, frame = cap.read()
5 if not ret: stop.set()

检验理解

三个线程之间如何传递帧数据?

60fps 摄像头下,人脸检测间隔是多少帧?

05

性能黑科技:GPU 与优化

从 CUDA Graph 到 CoreML 优化,从内存管道到硬件编码器——项目里藏着一堆让人惊叹的性能技巧。

五大执行提供者

不同硬件需要不同的"驱动"。项目支持五种执行提供者,自动选择最优方案:

🟢

CUDA(NVIDIA GPU)

最快选项。支持 CUDA Graph 加速和硬件视频编码(h264_nvenc)。Tensor Core 还能用 FP16 模型。

🍎

CoreML(Apple Silicon)

M1-M5 芯片的 Neural Engine + GPU。项目专门做了 ONNX 模型优化和 ANE 分区消除。

🔵

DirectML(Windows GPU)

支持 AMD 和 Intel GPU。使用线程锁避免并发问题,配合 AMF 硬件编码器。

🟣

OpenVINO(Intel CPU/GPU)

Intel 硬件优化方案,适合没有独立显卡的 Intel 设备。

CPU(通用后备)

任何设备都能跑,但速度最慢。自动设置线程数:CPU 核心数 - 2。

CUDA Graph:GPU 的"录像回放"

正常执行 AI 模型时,CPU 每次都要一个一个地向 GPU 发送指令(内核启动),每次发送都有固定开销。CUDA Graph 的思路是:录制一次,之后无限回放。

代码

def _init_cuda_graph_session(
        model_path, swapper):
    providers = [(
        'CUDAExecutionProvider',
        {'enable_cuda_graph': '1'})]
    sess = ort.InferenceSession(
        model_path, providers=providers)

    # Pre-allocate GPU buffers
    ort_input = ort.OrtValue
        .ortvalue_from_numpy(
            dummy_inp, 'cuda', 0)

    # First run records the graph
    sess.run_with_iobinding(io)
    _cuda_graph_session['recorded'] = True
            
白话翻译

定义初始化 CUDA Graph 会话的函数

接收模型路径和换脸器对象

配置 ONNX Runtime 使用 CUDA

并开启 CUDA Graph 功能

创建推理会话(加载模型到 GPU)

(空行)

预先在 GPU 上分配好内存缓冲区

创建 GPU 端的张量(输入数据的容器)

从 numpy 数组创建,放在 GPU 设备 0 上

(空行)

第一次运行时 CUDA 会'录制'整个执行图

标记为已录制,后续调用直接回放

💡
为什么要用适配器模式?

代码创建了一个 _CudaGraphSessionAdapter 类来包装原始的 ONNX 会话。这样 insightface 的 INSwapper 调用 session.run() 时,实际上走的是 CUDA Graph 回放路径。这种"代理模式"不需要修改 insightface 库的任何代码。

Apple Silicon 专属优化

项目为 Apple M 系列芯片做了大量针对性优化。核心思路:让模型尽可能运行在 Neural Engine (ANE) 上,避免 CPU-ANE 之间的数据往返。

🔄

ONNX 模型改写

把 Pad(reflect) 算子替换为 Slice+Concat,让 CoreML 能把整个模型放在一个 ANE 分区里运行。

🎯

检测模型走 GPU

人脸检测模型路由到 GPU shader cores,换脸模型走 ANE。两者并行运行,互不干扰。

检测速度 21ms → 4ms

消除 Shape-Gather 动态链条后,RetinaFace FPN 在 M3 Max 上的检测时间从 21ms 降到 4ms。

内存管道:零磁盘 I/O

处理视频时,传统方法是:抽帧存磁盘 → 逐帧处理 → 存回磁盘 → 合成视频。大量时间浪费在磁盘读写上。项目用 FFmpeg 管道实现了全内存处理:

1

FFmpeg 解码器管道读取视频帧到内存

2

在内存中逐帧换脸处理

3

直接写入 FFmpeg 编码器管道

代码

reader_cmd = [
    'ffmpeg', '-hide_banner',
    '-hwaccel', 'auto',
    '-i', target_path,
    '-f', 'rawvideo',
    '-pix_fmt', 'bgr24',
    '-v', 'error',
    '-']
            
白话翻译

构建 FFmpeg 解码命令

静默模式(不显示 banner 信息)

自动选择硬件加速解码

输入文件是目标视频

输出格式为原始视频流

像素格式 BGR24(与 OpenCV 一致)

只显示错误信息

输出到标准输出(管道)

硬件视频编码

视频输出也可以用 GPU 硬件编码,速度远超 CPU 软编码。项目自动检测并选择最佳编码器:

h264_nvenc NVIDIA GPU 硬件编码,替代 libx264 软编码
hevc_nvenc NVIDIA GPU H.265 硬件编码,更小体积
h264_amf AMD/Intel GPU 硬件编码(DirectML 模式)
libx264 通用 CPU 软编码后备方案
💡
优雅降级策略

代码先尝试硬件编码,失败后自动降级到软件编码,不会崩溃。encoders_to_try 列表里排了两个方案:硬件优先,软件兜底。这种"先试快的,不行用稳的"策略贯穿整个项目。

视频处理中的流水线检测

处理视频文件时,项目使用了"流水线检测"——在处理第 N 帧的同时,提前开始检测第 N+1 帧的人脸。来看这个协作对话:

最终检验

CUDA Graph 为什么能加速推理?

内存管道相比传统方法最大的性能提升来自哪里?

Apple Silicon 上的 ONNX 模型改写做了什么?