使用 p5.js and ml5.js 打造 Google Meet 的濾鏡

前言

最近偶然在 The Coding Train 上看到 ml5 和 p5 的教學影片,就看著教學用 ml5 + p5 玩出有趣的效果了!

廢話不多說直接上圖:

螢幕錄製 2021-10-02 下午5

玩出上面圖片的效果以後,發現居然還可以搭配 OBS 放到 Google Meet 上面玩,讓會議上其他人看看這副模樣,所以現在就稍微來介紹一下這兩個小玩具。

另外,這篇文的靈感來源、實作很大部份都是參考 The Coding Train youtube 頻道裡的影片,大力推薦,這是個可以讓人找到寫程式快樂之處的頻道!

實作流程

首先看今天實作的流程:

Screen Shot 2021-11-24 at 11.51.08 PM

流程很單純,我們會開啟鏡頭取得畫面,接著把畫面當作 input 丟給 ml5,讓 ml5 去 classify,接著 ml5 就會回傳它辨識後的 data,我們接著就可以用這些資料搭配 p5,在畫面上做一些有趣的特效!

p5.js 介紹

Processing 是一個廣受歡迎用來做 creative coding 的語言,後來有了各語言的版本,而 p5.js 就是 Processing 的 JavaScript 版本,標榜簡單易用,讓非工程師相關的職業如教育者、設計師、藝術家也能輕鬆上手打造自己想要的作品。

要了解 p5 如何使用不難,我們可以從 p5 官方文件的 Get Started 開始著手,首先我們可以到 p5 web editor 實際的操作,會先看到以下的 code:

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);
}

p5.js 的專案由 setup() 與 draw() 這兩個主要的函式所組成,setup() 會負責程式的初始化,只會執行一次;draw 則是以每秒 60 次的方式做執行,會不斷地持續更新畫面。從 createCanvas(400, 400) 這個 function 不難看出用意是創建出一個 canvas 的畫布,這個畫布就是我們待會要揮灑創意的地方啦~

接著我們把 code 改成:

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);
  ellipse(50,50,80,80);
}

你看到的畫面應該會長得像這樣,恭喜成功畫出一個圈圈!

first-sketch

試著把 ellipse(50, 50, 80, 80) 改成 ellipse(150, 100, 80, 80),會發現圈圈往下移動了:

Screen Shot 2021-11-25 at 12.07.09 AM

聰明的你應該發現第一跟第二個參數分別代表了 ellipse 的方位,第二跟第三則是代表了 ellipse 的 width 和 height,在 p5 的世界中,X 軸就和我們認知的方向一樣,X 軸給了 150 就會往右 150px;Y 軸則是和我們一般數學上認知的方向相反,給 Y 軸 100 就會往下 100px。

接下來我們來了解一些 p5 內建的全域變數以及圖層的概念,假設我們有今天這段 code:

function setup() {
  createCanvas(400, 400);
}

function draw() {
  ellipse(mouseX, mouseY, 80, 80);
}

p5 - example

會發現隨著滑鼠移動,圈圈跟著滑鼠的位置移動,而且圈圈竟然會一直疊加上去!有兩個值得注意的地方:

  1. 每次 draw 執行的時候,可以想成是疊一層新的畫布上去,因此如果我們在 draw 最前面加上了 background(255),那等同每次執行的時候都會疊上一層白色的背景,所以我們這時候移動滑鼠就只會有一個圈圈跟著滑鼠,但如果移掉 background 後,每次 draw 執行都不會疊上新的背景,也因此會看到圈圈不會消失,而是一直疊加上去。
  2. p5 提供了一些內建的全域變數以及 function 使用,以上面程式碼來說,mouseXmouseY 是鼠標的 X 和 Y 軸的位置,透過這個全域變數就能夠讓 ellipse 跟著你跑啦!所以如果我們提供目前鏡頭某個部位的所在方位,那就可以讓 p5 畫出來的圖案跟著鏡頭上的部位移動了。

p5 的介紹大概先這樣,這些基本的介紹就足夠待會要做的事情了。

想要更了解 p5.js 如何玩的話,有個很不錯的網站 非關語言:玩轉 p5.js,還有前言提到的 The Coding Train 也很頻繁的會釋出關於 p5.js 的 coding challenge;當然,還有吳哲宇的 Open Processing 可以看到何謂厲害的 creative coding 創作。

ml5.js 介紹

ml5.js 是一個基於 tensorflow.js 之上開發的 library,在官網的介紹中:

ml5.js is being developed to make machine learning more accessible to a wider audience.

可以知道 ml5 跟 p5 一樣是以好上手為前提來開發的一個 library,不像使用 tensorflow.js 需要機器學習的前置知識,使用 ml5 只要看個文件,傳入想要使用的 pre-trained model,很快就能夠快樂的在瀏覽器使用 ml5。

印象中 p5 有部分的開發者同時也是 ml5 的開發者,所以搜尋 ml5 相關應用的時候,很常會看到跟 p5 結合一起使用,而因為兩個 library 結合起來也真的蠻順手的,打造一些小玩具非常方便,所以能夠搜尋到不少有趣的作品。

PoseNet

此段落內容皆出自於 PoseNetml5.js Pose Estimation with PoseNet,如果喜歡看影片的人歡迎進去觀看,然後就可以跳過這一 part。

我們待會只會用到 html + js,然後為了開發方便起見,建議安裝 VSCode 的 Live Server extension,開發時就直接啟動 server 就好囉。

首先是環境的設置,簡單起見我們就直接開一個 html 檔,然後引入 ml5 和 p5 的 CDN,以及引入我們待會要寫的 pose-net.js 檔:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Getting Started with ml5.js</title>
    <!-- p5 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.2.0/p5.min.js"></script>
    <!-- ml5 -->
    <script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"></script>
  </head>

  <body>
    <script src="./sketches/pose-net.js"></script>
  </body>
</html>

接著就可以開始實作了,首先我們要讀取 PoseNet 這個 pre-train model:

// 全域變數
let video
let poseNet
let pose

function setup() {
  createCanvas(640, 520)
  // Create the video
  video = createCapture(VIDEO)
  video.hide()

  poseNet = ml5.poseNet(video, modelLoaded)
  poseNet.on('pose', getPoses)
}

function modelLoaded() {
  console.log('poseNet loaded')
}

function getPoses() {}

createCapture 是 p5 提供的 method,可以幫我們創造 <video> 的 element 並且會取得我們 webcam 的內容,然後 video element 預設會和 canvas 分開並被顯示,也就是說如果單純使用 createCapture 並且 render 出來的時候,你會發現會有個 video element 在 canvas 的外面,看起來就怪怪的,所以會搭配 hide() 來去隱藏這個 canvas 之外的 element。

這時候你可能會想,不對吧,那我怎麼看到自己目前的 webcam 畫面長怎樣,所以我們會在 draw 裡面顯示鏡頭的畫面:

function draw() {
  background(0)

  // Draw the video
  image(video, 0, 0)
}

這時候就可以正常的在瀏覽器看到鏡頭畫面了。

回到剛剛的程式碼,執行 ml5.poseNet(video, modelLoaded) 代表我們要把目前捕捉到的畫面丟給 PoseNet 進行辨識,等 model 完成之後就會觸發 modelLoaded 這個 callback。

poseNet.on('pose', getPoses) 則是我們的重點,概念類似於 addEventListener 那樣,PoseNet 會監聽你目前畫面上的姿勢,當它發現你姿勢有變化的時候,就會觸發 getPoses 這個 callback,getPoses 預計會接收一個參數,這參數就是 PoseNet 辨識完之後回傳的 data。

接著,我們就要來看看 getPoses 得到什麼資料,首先我們在 getPoses 印出它究竟收到什麼資料吧:

function getPoses(poses) {
  console.log(poses)
}

這時候你應該會發現,哇靠,怎麼 console 瘋狂跑出資料,這就代表 PoseNet 已經在進行辨識了!資料大概會長得像這樣:

Screen Shot 2021-12-03 at 2.38.37 PM

會是 array 的原因是因為 PoseNet 可以辨識畫面上的多個人,而目前只有我自己被辨識到,所以 array 裡就只會有一筆資料,而 pose 裡面的資料 key 就對應到各部位的名稱,value 則是對應到部位的位置,如 leftEar: { x: 443, y: 244, cofidence: 0.94 } 就是指左耳的位置,以及它的 confidence(不知道中文怎麼翻,信心程度之類的嗎?)

有了 data 以後,我們要做的事情就很簡單了,只要取得部位的資料,把它畫出來就好了,舉個例子來說,假設我今天想在鼻子上畫個紅色的圈圈,首先我們要賦予 pose 值,這個值就是剛剛 callback 所接收到的 poses

// 變數
let video
let poseNet
let pose

// 創造 video element、load PoseNet
function setup() {
  createCanvas(640, 520)
  video = createCapture(VIDEO)
  video.hide()

  poseNet = ml5.poseNet(video, modelLoaded)
  poseNet.on('pose', getPoses)
}

function getPoses(poses) {
- console.log(poses)
+ if (poses.length) {
+   pose = poses[0].pose
+ }
}

接著就可以用 pose 來畫出鼻子,概念很簡單,我們把 pose.nose 當中的方位傳給 ellipse 以後,圓圈就能夠跟著目前 PoseNet 辨識到的鼻子位置移動了!fill 則是用於填入 ellipse 色彩用的:

function draw() {
  background(0, 10)

  // Draw the video
  image(video, 0, 0)
	
  // 畫出鼻子
  if (pose) {
    fill(255, 0, 0)
    ellipse(pose.nose.x, pose.nose.y, 64)
  }
}

這時候打開 Live Server,應該就能看到紅鼻子出現在你臉上!

螢幕錄製 2021-10-02 下午5

是不是很簡單!我個人認為 p5 和 ml5 搭配起來很簡單的原因在於 ml5 的資料會回傳方位的資訊(x, y),而 p5 很多繪製於畫面上的 API 都需要傳入方位,所以兩者搭配起來的時候只要簡單的把 ml5 的資料傳入 p5 API 就好,完全不費力氣。

搭配 OBS 在 Google Meet 耍腦!

知道怎麼在瀏覽器配合 ml5 + p5 在畫面上畫出自己想要的東西以後,就可以來試試看怎麼在 Google Meet 輸出這個影像了!首先讓我們看看可愛的概念圖:

Screen Shot 2021-12-03 at 3.54.09 PM

我們剛剛做的事情都是在 Browser 上面執行的,但怎麼放到 Google Meet 上讓其他人看到呢?這時候就需要搭配 OBS 這個 Open Source 的應用程式,它可以幫我們擷取想要影像輸出的地方,再搭配 Virtual Cam 就能夠順利輸出瀏覽器的畫面,接著在 Google Meet 的設定區選擇射影像的攝影機為 OBS Virtual Cam,就可以達到我們要的目的了。

用說的似乎太抽象,直接來進行操作,首先打開 OBS,並且新增 Window Capture 的 Screen:

Screen Shot 2021-12-03 at 4.05.42 PM

新增之後,在 Window 的部分選擇你要輸出的瀏覽器分頁,總之他會跑出你目前 Chrome 分頁的標題:

Screen Shot 2021-12-03 at 4.10.21 PM

By the way,我實作時一直找不到自己想要選擇的瀏覽器分頁,如果是這種狀況,那可能是因為 OBS 需要跟瀏覽器在同一個視窗裡,也就是說你要輸出的那個分頁不能是全螢幕,必須兩個都是小螢幕,然後放在同一個視窗(比如都放在桌面)

我知道這很瞎,但我目前還找不到其他解法。

選擇完之後,應該就會看到 OBS 目前預覽的畫面跑出你瀏覽器的畫面了,如果你這時有開啟剛剛開發的那個分頁,應該會看到你攝影機的畫面惹:

Screen Shot 2021-12-03 at 4.15.41 PM

如果擷取的畫面不滿意,可以用 option + shift 調整寬高,或是去 js 檔那裡調整 canvas、video 的大小。然後有注意到 OBS 右下角有個 Start Virtual Camera 嗎?大力按下去就對了,這時候就會啟動我們的 Virtual Cam。

接著到 Google Meet,去設定那裡選擇 Virutal Cam:

Screen Shot 2021-12-03 at 4.00.39 PM

這時候應該就可以順利輸出影像到 Google Meet 了,嗚呼!

ezgif.com-gif-maker (2)

抱歉,沒朋友只能開一人會議,嗚嗚。

所以如果再處理一下 p5 畫出來的圖,就能夠繪製出像文章開頭的畫面囉!

螢幕錄製 2021-10-02 下午5

結語

這次亂玩的結果收穫比我想的還多,首先 p5、ml5 這些都是我先前未接觸過的東西,每次看到 Creative Coding、機器學習都想說應該跟我無關吧,那不是藝術家跟神人們在玩的東西嗎(?)但這次才發現世界上已經有人默默創造出簡單好用的 library 讓平民如我可以一探究竟了。

當初為了理解圖像辨識的原理,還去看了 CNN 模型的運作方式,如果有興趣的話可以看 How do Convolutional Neural Networks work?,講得非常淺顯易懂。所以本來還想說要不要在文章放入機器學習相關的內容,但機器學習不是我的專業,感覺會錯誤連篇漏洞百出,索性就移除了,改成只講一些沒營養但是好玩的事物。

雖然目前工作上用不到這些東西,但我認為跨出自身工作的領域,去探索其他人的世界在玩些什麼也是很棒,不用把自己侷限於工作的領域。前陣子時常會覺得哎呀好像暫時沒有什麼覺得有趣的東西想學習時,偶然看到 p5 + ml5 的應用時,頓時又燃起了喜歡用 Coding 探索、創造有趣事物的熱忱 😎

References