如何把「螞蟻呀嘿」換臉特效用在前端 WebRTC 影片通話中

因為疫情的原因,線上影片會議軟體異軍突起,成為了在家辦公的主要溝通渠道。而最近抖音上「螞蟻呀嘿」惡搞換臉的小影片也突然火了起來,那我就想了想能不能在影片會議的時候換張臉活躍下氣氛?在 Github 上一番搜尋之後發現還真有辦法,有一個開源的 Python 人工智慧換臉的庫,那正好趁著這個機會研究一下前端 WebRTC 實現影片通話功能,外加換臉操作。先看一下效果吧:

換臉效果

因為有涉及到一點點的後台,所以專案分成了兩部分,一個是用於存放前端程式碼的 frontend 專案和存放後端程式碼的 backend 專案:

1
2
3
專案根目錄
  |-- backend
  |-- frontend

另外這個影片需要電腦上有攝像頭,沒有的話可以想辦法把行動電話當作電腦的攝像頭。

注意:本教學中的程式碼僅供展示 AI 換臉技術的應用,不可以用於取得其他人隱私或其他任何非法目的。

撰寫頁面

因為是前端實現,首先肯定是撰寫頁面。這個頁面比較簡單,就是一個影片套件、顯示使用者 ID 的文字、呼叫對方影片的輸入框和按鈕,影片套件預設顯示自己的影片,當影片接通之後就會顯示對方的影片,而自己的影片會縮小到右上角。

HTML

在 frontend 目錄下新建 index.html、style.css 和 index.js 檔案。首先看一下 HTML 結構,index.html 中主要的程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- frontend/index.html -->
<head>
  <!-- 其他程式碼 -->
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <main>
    <div id="container">
      <div class="videos">
        <video id="myVideo" class="videoSize" autoplay></video>
        <video id="peerVideo" class="videoSize" autoplay></video>
      </div>
      <p id="idText"></p>
      <div class="call">
        <input type="text" id="peerIdInput" placeholder="請輸入對方 id" />
        <button id="joinBtn">影片通話</button>
      </div>
    </div>
  </main>
  <script src="index.js"></script>
</body>

每個標籤的作用是:

  • 用於設定頁面背景,把所有套件居中對齊。
  • 裡邊分別放了顯示自己影片 #myVideo 和對方影片 #peerVideo 套件,並設定為自動播放,以便於在載入攝像頭後立刻開始播放畫面。
  • 在頁面開啟時顯示自己的使用者 ID,相當於是電話號碼。

  • 里是輸入對方 ID 的文字框 #peerIdInput 和呼叫按鈕 #joinBtn
  • 最後在 中引入樣式檔案 style.css,在 結束前引入 index.js。我們將主要在 index.js 中寫程式。

CSS

css 的程式碼都比較簡單,基本就是設定一下樣式,這裡介紹一下重要的部分,剩餘的可以在原始碼中檢視。因為自己的影片要在影片接通時移動到右上角,那麼就需要把

容器設定為相對定位,把我的影片和對方的影片設定成一樣的寬高,然後先隱藏對方影片,當影片接通時,利用 JavaScript 加上接通後,我的影片的樣式,把我的影片設定為絕對定位,寬高調小,放到右上角:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* frontend/style.css */
videos {<!-- -->
  position: relative;
}

.videoSize {<!-- -->
  width: 500px;
  height: 600px;
  object-fit: cover; /* 讓影片按比例佔滿整個空間 */
}

.rightTop {<!-- -->
  /* 以下是通話中的樣式 */
  position: absolute;
  width: 150px;
  height: 180px;
  right: 0;
  top: 0;
}

#peerVideo {<!-- -->
  display: none;
}

其他套件基本只是設定了下 Grid 佈局、寬高、大小、陰影、背景、字型,沒有什麼特殊的,可以直接檢視原始碼。樣式中所用到的圖示在 frontend/icons 目錄下。

訪問攝像頭

接下來我們先熟悉一下訪問攝像頭的程式碼。在 JavaScript 中訪問使用者的攝像頭主要使用 navigator.mediaDevices.getUserMedia() 方法, 它接收一個物件作為引數,用於指定要取得的裝置,例如影片或音訊,然後回傳一個 Promise,在 Promise 完成之後它會傳遞給我們一個 Stream 流,我們把它放到 標籤的 srcObject 屬性中就可以了,是不是很簡單?程式碼如下:

1
2
3
4
5
6
7
8
// frontend/index.js
const myVideo = document.getElementById("myVideo");

navigator.mediaDevices
  .getUserMedia({<!-- --> video: true, audio: true })
  .then((stream) => {<!-- -->
    myVideo.srcObject = stream;
  });

在這段程式碼中:

  • 取得了 #myVideo 這個 套件。
  • 使用 navigator.mediaDevices.getUserMedia() 並給它傳遞了一個物件,物件的 video 和 audio 屬性都設定為了 true,表示要訪問攝像頭和音訊裝置。

這時,使用 VS Code 的 Live Server 外掛執行專案(沒有的話安裝一下,很簡單),在 index.html 檔案里右擊,選擇 Open with Live Server,開啟之後,瀏覽器可能會提示此網站需要訪問攝像頭和音訊裝置,點選允許,就能看到自己的影片了。

撰寫後台

要實現影片通話,需要使用 WebRTC 技術,這個技術牽扯的概念和 API 過於龐大和複雜,不過有開源的庫來幫我們簡化了 WebRTC 的操作,這裡使用一個叫做 Peer.js 的庫,它封裝了 WebRTC 雜亂的 API,提供了完整的、可配置的、易於使用的 API。
https://peerjs.com/
我們將會透過把 Peer 附加到 Express.js 伺服器上,來生成使用者 ID 並管理 WebRTC 連接。首先在 backend 目錄下執行:

1
2
3
npm init -y
# 或
yarn init -y

接著初始化一個 node.js 專案,使用 npm 或 yarn 安裝 peer 和 express 依賴,因為想在改動程式碼時自動重啟服務,我們也可以再安裝一個 nodemon 依賴:

1
2
3
yarn add peer express nodemon
# 或
npm install --save peer express nodemon

安裝完成之後新建一個 server.js 檔案,整個後台服務我們就只需要這一個檔案,都是一些簡單的初始化程式碼,它裡邊的內容是:

1
2
3
4
5
6
7
8
9
10
11
12
13
const express = require("express");
const {<!-- --> ExpressPeerServer } = require("peer");

const app = express();

const server = app.listen(3000);

const peerServer = ExpressPeerServer(server, {<!-- -->
  debug: true,
  path: "/",
});

app.use("/video", peerServer);

這些程式碼的含義是:

  • 匯入 express 庫,並從 peer 庫中匯入與 express 進行結合的 ExpressPeerServer。
  • 建立 express 例項 app ,並監聽 3000 連接埠。
  • 把 ExpressPeerServer 掛載到 express 中,設定 debug 開發模式為 true,這樣有更好的錯誤提示。路徑為根目錄。
  • ExpressPeerServer 掛載之後會回傳一個 express 的控制器。
  • 把回傳的控制器掛載到 /video 路徑下,這樣 /video 路徑就是主要的、peer.js 提供的 WebRTC 通訊路徑。

然後修改一下 package.json 檔案,新增一個 script 配置項,裡邊定義 start 命令值為 nodemon server.js

1
2
3
  "scripts": {<!-- -->
    "start": "nodemon server.js"
  },

這樣使用 nodemon 執行 server.js 檔案後,如果 server.js 中的內容發生變化,它會自動幫助我們重啟伺服器。
我們現在來執行一下 yarn start 或者 npm start ,看到命令列提示 [nodemon] starting node server.js 就算啟動成功了,我們訪問一下 http://localhost:3000/video ,看到下方輸出結果就說明 peer 也載入成功了:

1
2
3
4
5
{<!-- -->
  "name": "PeerJS Server",
  "description": "A server side element to broker connections between PeerJS clients.",
  "website": "https://peerjs.com/"
}

後端程式碼到這裡就撰寫完成了。下一步就是在前端頁面中呼叫 Peer 相關的 api,並建立影片通話。

生成使用者 ID

在前端中呼叫後端 Peer 服務可以使用 Peer.js 官方的前端庫,可以直接使用 cdn 形式:

1
<script src="//i2.wp.com/unpkg.com/[email protected]/dist/peerjs.min.js"></script>

也可以開啟上邊 src 中的網址,把檔案儲存到本地,或者在 Github 上下載:
https://github.com/peers/peerjs/blob/master/dist/peerjs.min.js
下載完成之後把它放到 frontend/peer 目錄下,然後在 index.html 中,在引入 index.js 的上方,引入 peerjs:

1
2
<script src="peer/peerjs.min.js"></script>
<script src="index.js"></script>

接下來開啟 index.js 檔案,建立與後台 peer 服務的連接:

1
2
3
4
5
const peer = new Peer({<!-- -->
  host: "localhost",
  port: "3000",
  path: "/video",
});

這裡直接使用 peer.js 前端庫匯出的建構函式 Peer(),它接收一個物件作為引數,這裡就分別把後台服務的 host、port 和 path 傳遞進去就可以了,它會回傳 peer 例項,後續有關影片通話的操作就主要使用它來實現。
當成功的連接到後台服務之後,我們首先給自己生成一個唯一的使用者 ID,就等同於是一個電話號碼,那麼這裡我們可以監聽 peer 的 open 事件,當連接開啟後,會把生成的使用者 ID 回傳到事件處理回呼函式中,然後我們取得 html 中的 #idText 這個 p 元素來顯示自己的 ID:

1
2
3
4
5
const idText = document.getElementById("idText");

peer.on("open", (id) => {<!-- -->
  idText.textContent = "我的 id 是:" + id;
});

這時在 Live Server 中開啟 index.html,就可以看到顯示出了 ID,類似於:

1
我的 id 是:2573c3ae-ba79-404a-b807-2128856ef3c9

多開啟幾個頁面,可以看到每個人的 ID 都不同。

呼叫影片通話

在有了使用者 ID 之後,就可以呼叫對方了。這裡的邏輯是,使用者在輸入框輸入對方 ID 之後,點選影片通話按鈕進行呼叫。那麼我們應該先取得影片通話按鈕元素,然後監聽它的點選事件,在裡邊發起呼叫:

1
2
3
4
5
6
7
8
9
10
11
12
13
const joinBtn = document.getElementById("joinBtn");
// 發起呼叫
joinBtn.addEventListener("click", () => {<!-- -->
  const peerId = peerIdInput.value;
  console.log("正在連接:" + peerId);

  navigator.mediaDevices
    .getUserMedia({<!-- --> video: true, audio: true })
    .then((stream) => {<!-- -->
      const call = peer.call(peerId, stream);
      call.on("stream", showVideo);
    });
});

在點選按鈕的時候,事件處理函式作了如下操作:

  • 取得使用者輸入的對方的 ID。
  • 取得當前使用者的影片流,然後呼叫 peer.call() 呼叫對方, peer.call() 需要對方的 ID 和自己的影片流作為引數,然後回傳與呼叫有關的例項,儲存到 call 中。
  • 這時當前使用者就開始等待對方應答了,為了簡單起見,這裡沒有做等待的樣式。
  • 下一步監聽 call 的 stream 事件,這個事件會在對方應答後觸發,它會回傳對方的影片流作為事件處理函式的引數,然後我們使用 showVideo() 函式處理對方的影片流。

showVideo() 函式的程式碼如下:

1
2
3
4
5
6
7
const peerVideo = document.getElementById("peerVideo");

function showVideo(stream) {<!-- -->
  myVideo.classList.add("rightTop");
  peerVideo.srcObject = stream;
  peerVideo.style.display = "block";
}

這個函式就是簡單的把自己的影片移動到右上角,透過之前定義的 .rightTop class 樣式,然後把對方的影片流放到 #peerVideo 影片套件中,之後把它顯示出來(之前設定的是 display: none 隱藏)。現在因為一直是等待對方接聽,所以需要有一個應答的處理。

應答影片通話

應答的處理是監聽 peer 的 call 事件,然後透過事件引數中與呼叫有關的例項來應答通話:

1
2
3
4
5
6
7
8
9
// 應答呼叫
peer.on("call", (call) => {<!-- -->
  navigator.mediaDevices
    .getUserMedia({<!-- --> video: true, audio: true })
    .then((stream) => {<!-- -->
      call.answer(stream);
      call.on("stream", showVideo);
    });
});

這一步的程式碼雖然在同一個檔案中,但是應該想像為收到影片通話呼叫的對方,程式碼作了下邊的操作:

  • 取得自己的影片流。
  • 呼叫 answer() 函式應答影片呼叫,並把自己的影片流傳回給呼叫者。這裡是當有呼叫時直接進行應答,為了簡單起見,沒有撰寫點選應答按鈕相關的樣式和事件。
  • 監聽 ** stream ** 事件,這一步和之前的一樣,應答後這個事件就會觸發,然後同樣使用 showVideo() 函式載入影片。

好了,現在開啟兩個頁面試試,在其中一個頁面輸入另一個頁面生成的 ID,然後點選影片通話,就可以看到雙方影片顯示出來了。如果用的是同一個攝像頭,那麼顯示的畫面都是一樣的。

實現換臉

實現換臉主要是用到了 Avatarify 這個 AI 換臉庫,它是使用 Python 撰寫的,利用了 First-Order Motion Model 演算法來實現換臉。我們這裡只需要按照步驟進行安裝就可以了,此外還需要安裝虛擬攝像頭軟體,用於把 Avatarify 換臉後的資料作為攝像頭影片,然後在前端頁面中訪問這個虛擬的攝像頭。
Avatarify 由於是對攝像頭的影片進行即時計算,所以對顯卡的要求非常高,且只支援啟用了 CUDA 的 Nvidia 顯卡來進行 GPU 加速,官方的統計資料是這樣的:

  • GeForce GTX 1080 Ti: 33 幀/秒
  • GeForce GTX 1070: 15 幀/秒
  • GeForce GTX 950: 9 幀/秒

如果配置達不到,可以使用騰訊雲或阿里雲的 GPU 例項,把顯卡有關的計算交給遠端伺服器去進行,本地只需要建立影片流即可。本文將主要使用這種方法。
如果配置達到了要求,也可以在本地安裝執行,不過我沒有試過,後邊簡單的貼一下官方安裝指導(Windows 下),你可以自己嘗試一下。
要注意的是,如果是在本地執行演算法,可以在 Windows 和 Linux 作業系統中進行,或者也可以使用 docker 進行建置,不過只支援 linux 作業系統下的 docker。

購買伺服器

這裡以騰訊雲 GPU 例項為例,介紹一下伺服器的配置,關於計費我們會使用按量計費,完成這篇教學所用的總體費用可能在 30 元左右,
用完之後一定要記得關機或者銷毀來停止計費。如果是關機,記得先把頻寬調整為 0,停止頻寬計費,然後關機時選擇關機不收費選項。
開啟下方連結(或複製 https://curl.qcloud.com/KiCrCXo9 ):

騰訊雲伺服器購買連結

然後在選擇機型中選擇如下配置:

  • 計費模式:按量計費。
  • 地域:中國香港。這裡選擇香港地區的原因是安裝 Avatarify 需要大量的境外源,例如 Github、Docker、Nividia、Python 等,且數量多,體積也大(有的 700 多 M),在親身測試過國內區的之後,發現行不通,而香港地區因為延遲相對較低,且訪問境外源速度快,所以這裡選擇了香港。
  • 例項:選擇 GPU 型,在機型串列中選擇 GPU 計算型 GN7,這個是最便宜的,7.16 元/小時。
  • 對映:選擇 Ubuntu 64 位 18.04,不要選過高的版本。顯卡驅動可能支援不完善。
  • 勾選上後台自動安裝 GPU 驅動,CUDA 版本選擇 10.0.130,cuDNN 版本選擇 7.4.2。
  • 頻寬實測需要 20M 起步,大概 1 元/小時,不過在安裝 Avatarify 的時候頻寬可以小一點,後邊真正開啟影片的時候再把它改大。
  • 注意這裡系統盤費用是 0.03 元/小時,這個在關機後仍然會收取

在下一步設定主機中:

  • 新建安全組。
  • 登入方式這裡使用密碼,可以選擇自行設定,也可以自動生成。

再下一步確認成功後完成購買,稍後就可以在主控臺例項串列中看到剛購買的例項了,新購買的機器可能需要等 5 分鐘才能配置完成。如果要修改頻寬,可以在串列中找到對應的例項,在最右側操作欄里選擇更多->資源調整->調整頻寬,進行調整。
因為 Avatarify 需要使用 5557 和 5558 連接埠,我們需要在安全組中放行這兩個連接埠。點選例項名稱超連結,進入例項詳情頁面,在上方索引標籤中選擇 安全組 ,在右側 規則預覽 中的 入站規則 索引標籤里,點選 編輯規則,在新頁面里的入站規則里,點選 **新增規則,**在對話盒裡填寫:

  • 型別:自定義。
  • 來源:all。
  • 協定連接埠:TCP:5557,5558
  • 策略:允許。

點選完成即可。到這裡例項配置就完成了,留意一下例項的公網 IP 地址,稍後使用 SSH 登入時需要用到它。

建置 Avatarify

我們使用 Docker 的方式來建置 Avatarify,先登入到我們的 GPU 例項中,可以直接在騰訊雲主控臺登入,也可以使用 ssh 命令或 putty 工具。這裡以 ssh 為例,輸入如下命令:

1
ssh ubuntu@你的例項ip

然後根據提示輸入密碼,第一次登入可能有一大段英文提示(是否信任裝置),直接輸入 yes 回車即可。

安裝 Docker

登入進去之後先安裝 docker,可以直接使用簡易的安裝腳本:

1
2
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

不過實測香港的例項好像並不能訪問這個腳本,我們還可以使用普通方式安裝,按照以下步驟複製貼上命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 第一步
sudo apt-get update

# 第二步
sudo apt-get install
    apt-transport-https
    ca-certificates
    curl
    gnupg

# 第三步
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# 第四步
echo
  "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

這些步驟都是在配置 Docker 的安裝倉庫源和金鑰,接下來執行下面兩條命令來安裝 Docker:

1
2
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

安裝完成之後, docker 命令需要管理員許可權才能執行,每次都需要輸入 sudo,如果要省略 sudo ,可以把當前使用者(騰訊雲預設為 ubuntu)新增到 docker 使用者組中:

1
sudo usermod -aG docker ubuntu

之後退出 ssh 並重新登入來讓配置生效,我們可以執行下面的命令檢查 docker 是否安裝成功:

1
docker run hello-world

有列印出 hello world 字樣和一大段英文就說明成功了。

安裝 Nvidia Docker tookit

要讓 docker 使用 GPU,需要安裝 Nvidia Docker tookit,這一步也很簡單,首先設定 Nvidia 的倉庫源:

1
2
3
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
   && curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
   && curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list

然後安裝 tookit 並重啟 Docker:

1
2
3
sudo apt-get update
sudo apt-get install -y nvidia-docker2
sudo systemctl restart docker

執行一個範例 container 檢查是否安裝成功:

1
sudo docker run --rm --gpus all nvidia/cuda:11.0-base nvidia-smi

如果列印出類似的如下訊息就算成功了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Fri Mar  5 08:47:39 2021
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 418.126.02   Driver Version: 418.126.02   CUDA Version: 11.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla T4            Off  | 00000000:00:08.0 Off |                    0 |
| N/A   26C    P8     9W /  70W |      0MiB / 15079MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

構建 Avatarify Docker 對映

選擇一個資料夾,使用 git 克隆 Avatarify 倉庫:

1
git clone https://github.com/alievk/avatarify.git

進入到克隆下來的 avatarify 倉庫中,使用 docker 構建對映:

1
2
cd avatarify
docker build -t avatarify .

構建需要的時間比較長,耐心等待完成之後,啟動服務:

1
bash run.sh --is-worker --docker

看到有 5557 和 5558 連接埠就說明啟動成功了。到現在,Avatarify 的伺服端配置完成了,之後就是使用客戶端呼叫服務,把計算部分交給我們的 GPU 例項。這時如果擔心計費,可以先把例項關機,在本地安裝好客戶端後再開機。
接下來的步驟以 MacOS 為例,Windows 版可以直接跳到後邊的 Windows 安裝方法 小節。

安裝 Avatarify 客戶端

在本地電腦上,首先安裝 Miniconda Python 3.8,可以從這裡下載:
https://docs.conda.io/en/latest/miniconda.html#macosx-installers
或者使用 Homebrew:

1
brew install --cask miniconda

克隆 avatarify 倉庫並執行安裝腳本:

1
2
3
git clone https://github.com/alievk/avatarify.git
cd avatarify
bash scripts/install_mac.sh

下載 CamTwist 虛擬攝像頭軟體(類似 OBS,但是 OBS 的虛擬攝像頭在 Mac 中訪問不到)並安裝:
http://camtwiststudio.com/download

啟動 Avatarify 客戶端

確保遠端 GPU 例項已啟動,且執行了 avatarify docker 對映監聽 5557 和 5558 連接埠,然後在本地克隆的 avatarify 倉庫中,執行 mac 客戶端:

1
./run_mac.sh --is-client --in-addr tcp://server_address:5557 --out-addr tcp://server_address:5558

記得把兩個 server_address 改成 GPU 例項的公網 IP。伺服端可能需要下載一些必要的檔案,所以連接會慢一些,稍等啟動成功後 Avatarify 會自動開啟攝像頭視窗。
我們可以根據提示調整好臉部位置,這裡我們最好把臉放到紅色矩形框中,光線要充足,不能太黑,可以使用 W/D 鍵縮放畫面,調整好之後按 X 鍵就可以呼出換臉之後的預覽影片視窗,它就是作為虛擬攝像頭的影片來源。
Avatarify 內建了 9 張範例的人臉照片,可以按 1-9 鍵進行切換,也可以自定義人臉照片,放到 Avatarify 中的 avatars 資料夾下。
Avatarify 成功顯示之後,開啟 CamTwist 虛擬攝像頭,在左側選擇 Desktop+,然後點選底部的 select,然後在右側的 settings 下,勾上 Confine to Application Window,然後勾上下方的 Select from existing windows ,在下拉框中選擇 python (avatarify) ,如果找不到可以退出 CamTwist 重新開啟,或者多點選下拉框幾次試試。現在虛擬攝像頭就準備好了。

Windows 安裝方法

在 Windows 下的安裝方法與 MacOS 類似,而且如果顯卡配置夠高,可以省略購買遠端 GPU 伺服器步驟。

  1. 首先安裝 Minicoda Python 3.8,可以從這裡下載:

https://docs.conda.io/en/latest/miniconda.html#windows-installers

  1. 安裝完成之後開啟 Anaconda 命令提示符,克隆 avatarify 倉庫,並執行 windows 作業系統下的安裝腳本:
1
2
3
git clone https://github.com/alievk/avatarify.git
cd avatarify
scriptsinstall_windows.bat
  1. 如果要在本地執行換臉演算法,那麼需要下載 network weights(228 MB):

https://openavatarify.s3.amazonaws.com/weights/vox-adv-cpk.pth.tar

https://yadi.sk/d/M0FWpz2ExBfgAA

https://drive.google.com/file/d/1coUCdyRXDbpWnEkA99NLNY60mb9dQ_n3/view?usp=sharing

  1. 下載完成後放到 avatarify 的根目錄下,無需解壓。
  2. 執行 avatarify 客戶端,如果是在本地執行,可以使用:
1
./scripts/run_windows.bat
  1. Windows 系統下也可以使用遠端 GPU 例項,使用下面的方式啟動客戶端:
1
./scripts/run_windows.bat --is-client --in-addr tcp://server_address:5557 --out-addr tcp://server_address:5558
  1. 兩個 server_address 需要取代成遠端 GPU 例項的公網 IP。
  2. 啟動成功之後和 Mac 的操作一樣,使用 W/D 縮放畫面,完成之後按 X 鍵調出 avatarify 預覽視窗。

Windows 下的虛擬攝像頭可以使用 OBS,最新版的 OBS 26.1 及以上已經內建了虛擬攝像頭外掛了,無需再單獨安裝。

  1. 下載並安裝 OBS:

https://obsproject.com/

  1. 安裝完成之後開啟 OBS,在左下角的 Source(源)面板中新增 Windows Capture,把影片輸入源設定為某個應用程式視窗,然後選擇 [python.exe]: avatarify 程式,點選 OK,可以調整以下大小。
  2. 再在 Tools(工具)選單中選擇 VirtualCam(虛擬攝像頭),選擇 AutoStart(自動開啟),設定 Buffered Frames(緩衝)為 0,然點選開始。(如果 Tools 選單沒有 VirtualCam,看看介面右下角的 start recording 附近有沒有 start virtual cam)。

現在虛擬攝像頭就準備好了。

實現換臉

接下來就是在我們的影片通話前端專案中,使用 CamTwist 或 OBS 提供的虛擬攝像頭了。我們需要明確知道它們的裝置 ID 才能指定具體使用哪個攝像頭。
首先使用 Live Server 開啟前端專案的 index.html 檔案,在谷歌開發者工具的 console 中,使用下面這段程式碼列印出影片裝置訊息:

1
2
3
4
5
6
7
navigator.mediaDevices.enumerateDevices().then(function (devices) {<!-- -->
  devices.forEach(function (device) {<!-- -->
    if (device.kind === "videoinput") {<!-- -->
      console.log(device);
    }
  });
});

這段程式碼就是訪問所有媒體裝置,然後取得其中的影片裝置並列印出來,從列印結果里找到 label 包含 CamTwist 或者 OBS(視作業系統而定)的那一項,紀錄 deviceId 屬性。
在 index.js 檔案中,儲存 deviceId 值到一個常數中:

1
2
const cameraId =
  "982947417cf490bae44ffb6a837bddcb813704ee491dd85d9149c45389f5521b";

在使用 navigator.mediaDevices.getUserMedia() 取得攝像頭時,除了可以把 video 設定為 true 之外,還可以把他的值設定為一個物件,用來指定更多的訊息,我們把它單獨定義出來,在裡邊使用 cameraId 指定訪問哪個攝像頭:

1
2
3
4
5
6
7
8
const mediaConstrains = {<!-- -->
  video: {<!-- -->
    deviceId: {<!-- -->
      exact: cameraId,
    },
  },
  audio: true,
};

exact 是精確的、只使用指定 ID 的攝像頭,不會使用其他備選的。
然後在取得自己攝像頭裝置的部分,取代成上邊這個物件。或者為了方便測試,也可以把所有的都取代掉:

1
navigator.mediaDevices.getUserMedia(mediaConstrains).then(/* ... */);

好了,現在再試試影片通話,是不是成功的換臉了呢?
這裡要注意的是,可能是因為 CamTwist 軟體和真正的攝像頭有衝突,頁面會不時的重新重新整理,這個暫時還沒有比較好的方案,如果你有精力的話可以研究一下。

附上本文的原始碼:
https://github.com/zxuqian/code-examples/tree/master/webrtc/video-call-change-face

總結

這篇教學所用到的技術都很簡單,只是配置比較複雜,本教學提供了換臉的基本步驟,剩下的可以根據需要繼續進行完善。範例中實現的的是一對一的影片通話,不過在這個基礎上也完全可以實現多方影片會議,只需要調整一下影片顯示的樣式,然後在有新人加進來時,建立相應的 標籤,並新增到現有的影片串列中即可。現在我們來回顧一下過程:

  • 撰寫前端 HTML/CSS 頁面結構和樣式,並嘗試載入攝像頭。
  • 撰寫簡單的後台應用,利用 epxress 和 peer 建立基於 WebRTC 的影片通話基礎。
  • 前端利用 peer.js 前端庫呼叫後台生成使用者 ID,並呼叫、應答影片通話。
  • 配置 Avatarify 換臉伺服器程式,利用雲 GPU 服務把顯卡計算相關部分轉移到 GPU 例項中。
  • 安裝 Avatarify 客戶端程式和虛擬攝像頭應用,把換臉後的資料用虛擬攝像頭展現出來。
  • 前端取得虛擬攝像頭 ID,在影片通話的時候使用此攝像頭。

如果有幫助請點贊並分享,有問題可以評論留言,感謝閱讀!