[YOLO 11] 透過 Webcam 即時辨識骨架並傳輸座標至 Unity

2025.01.16 / Ai
YOLO 可以透過 Webcam 來即時擷取分析我們的人體骨架,今天要來教大家如何擷取頭的中心座標 (鼻子),並經由 UDP 網路連線傳輸到 Unity 之中。

YOLO 主要用 Python 語言開發,我通常會將它視為一個外掛程式,用來當作我的 Unity 遊戲的一個輸入,例如:用 YOLO 分析玩家在鏡頭前的畫面,辨識出肢體關節的座標,同步到遊戲內的角色身上。

而今天在這邊我們沒有要搞得那麼複雜,先從最簡單的操作開始,帶各位從用 OpenCV 來確認 YOLO 的辨識結果、JSON 編碼,最後經由 UDP 將座標傳輸到 Unity 之中。

我們大致上會經歷以下步驟,你需要先建立好開發環境

  • 安裝 OpenCV
  • 用 Webcam 當作 YOLO 串流輸入
  • 取得鼻子的 XY 座標
  • JSON 編碼與 UDP 傳輸
  • 在 Unity 接收 UDP 與解碼

說明到這邊,現在開始正式主題:

安裝 OpenCV

我們待會要用 OpenCV 來將 Webcam 的影像和辨識後的座標顯示出來,方便測試結果。請開啟終端機,輸入以下指令來安裝 Python 的 OpenCV。

pip install opencv-python

用 Webcam 當作 YOLO 串流輸入

接下來新增一個 Python 腳本,複製貼上以下程式碼與執行。這個腳本將會載入 YOLO 11 的人類骨架辨識模型,並將編號 0 的 Webcam 影像串流到模型中 (source=0stream=True),以顯示卡運算 (device=0)。

我們額外做的事情是用 cv2.imshow 將 Webcam 的影像用視窗來顯示,當作監看使用。

from ultralytics import YOLO
import cv2

# 載入 YOLO 模型
model = YOLO("yolo11n-pose.pt")

while True:
    # 執行模型推論
    results = model(source=0, stream=True, device=0)

    for r in results:
        # 取得 Webcam 影像
        frame = r.orig_img
        
        # 顯示結果
        cv2.imshow("webcam", frame)
        cv2.waitKey(1)

取得鼻子的 XY 座標

接下來我們在 for r in results: 裡面輸入以下程式碼,將辨識到的關鍵點 (Key Points) 提取出來,並把它轉成整數。

keypoint = r.keypoints[0].xy[0]
x = int(keypoint[0][0])
y = int(keypoint[0][1])

在上面的第 2、3 行中的 keypoint[0][0]keypoint[0][1]是代表對應於索引 0 的身體部位座標,索引 0 就是代表鼻子。你可以自行修改成不同的部位,例如右肩的 X 座標就是 keypoint[6][0]

索引編號身體部位
0鼻子
1左眼
2右眼
3左耳
4右耳
5左肩
6右肩
7左手肘
8右手肘
9左手腕
10右手腕
11左髖骨
12右髖骨
13左膝蓋
14右膝蓋
15左腳踝
16右腳踝

為了方便觀察,我會用 OpenCV 的 cv2.putText 來將座標顯示到 Webcam 的監看視窗中。

目前的程式碼內容如下:

from ultralytics import YOLO
import cv2

# 載入 YOLO 模型
model = YOLO("yolo11n-pose.pt")

while True:
    # 執行模型推論
    results = model(source=0, stream=True, device=0)

    for r in results:
        # 取得 Webcam 影像
        frame = r.orig_img
        
        # 取得鼻子座標
        keypoint = r.keypoints[0].xy[0]
        x = int(keypoint[0][0])
        y = int(keypoint[0][1])
        text = "x: {}, y: {}".format(x, y)
        
        # 顯示結果
        cv2.putText(frame, text, (0, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 1, cv2.LINE_AA)
        cv2.imshow("webcam", frame)
        cv2.waitKey(1)

執行腳本,YOLO 會以左上角為原點,精準辨識我的鼻子座標並顯示在畫面左上方。

python_NVozeVWYFy.png

python_MgDOfc1zDq.png

測試一下即時的辨識效果。

YOLO 的辨識算是告一段落,接下來的工作就是要將拿到的座標編碼成 JSON 格式,再透過 UDP 傳輸的方式將它輸入到 Unity 裡面。

JSON 編碼與 UDP 傳輸

選用 UDP 而不是 TCP 的原因在於「效率」,UDP 的發送端 (YOLO) 無須理會接收端 (Unity) 有沒有正確的拿到結果,也不會跳出傳送錯誤,效能也比 TCP 高,是個理想的方式來傳輸座標。

我們修改一下程式碼,首先需要引用 json 和 socket:

import json
import socket

再建立發送的目的地與 UDP 發送端,我們預計將資料傳輸到本機的 7000 端口:

server_addr = ("127.0.0.1", 7000)
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

最後在 for r in results: 裡面加上以下程式碼,先將座標包成物件,再編碼成 JSON 格式,並使用 UDP 把它傳出去。

output = {
    "x": x,
    "y": y
}
client.sendto(json.dumps(output).encode(), server_addr)

最後的程式碼內容如下:

from ultralytics import YOLO
import cv2
import json
import socket

# UDP 發送設定
server_addr = ("127.0.0.1", 7000)
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 載入 YOLO 模型
model = YOLO("yolo11n-pose.pt")

while True:
        # 執行模型推論
    results = model(source=0, stream=True, device=0)

    for r in results:
        # 取得 Webcam 影像
        frame = r.orig_img

        # 取得鼻子座標
        keypoint = r.keypoints[0].xy[0]
        x = int(keypoint[0][0])
        y = int(keypoint[0][1])
        text = "x: {}, y: {}".format(x, y)
        
        # JSON 編碼並發送
        output = {
            "x": x,
            "y": y
        }
        client.sendto(json.dumps(output).encode(), server_addr)
        
        # 顯示結果
        cv2.putText(frame, text, (0, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 1, cv2.LINE_AA)
        cv2.imshow("webcam", frame)
        
        # 每 100 毫秒辨識一次
        cv2.waitKey(100)

如果傳輸速度過快或辨識太吃資源,可以調整 cv2.waitKey 的數字來調整頻率,單位是毫秒。

YOLO 和 Python 的部分已經完畢,可以先讓它持續執行,我們要來處理 Unity 的 UDP 接收功能

在 Unity 接收 UDP 與解碼

開啟一個 Unity 專案,建立一個名為 UdpReceiver.cs 的 C# 腳本,並複製貼上以下程式碼:

using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;

public class UdpReceiver : MonoBehaviour
{
    // 接收的 JSON 資料格式
    public class InputData
    {
        public int x;
        public int y;
    }
    
    private UdpClient _client;
    private IPEndPoint _endPoint;

    private void Start()
    {
        // 以 UDP 監聽 7000 端口
        _client = new UdpClient(7000);
        _endPoint = new IPEndPoint(IPAddress.Any, 7000);
        
        // 非同步接收資料
        RunForever();
    }

    private async void RunForever()
    {
        while (true)
        {
            // 等待接收新資料
            var raw = await Task.Run(() => _client.Receive(ref _endPoint));
            
            // 解碼成 JSON 字串
            var json = Encoding.UTF8.GetString(raw);
            
            // 解碼成物件
            var data = JsonUtility.FromJson<InputData>(json);
            
            // 列印結果
            Debug.Log($"x: {data.x}, y: {data.y}");
        }
    }

    // 遊戲退出時關閉連線
    private void OnApplicationQuit()
    {
        _client.Close();
    }
}

在上面的程式碼中,我們用 C# 內建的 UdpClient 來當作 UDP 的接收器,並使用非同步和 Task.Run 方法來防止在接收 UDP 的資料時讓 Unity 卡死。

當接收到資料時,我們會將它解碼成 JSON 格式,再透過 Unity 內建的 JsonUtility 將 JSON 解成物件,最後再把它列印到 Console 裡面。

最後同時執行 YOLO 和 Unity,現在 Unity 理應能接收到 YOLO 辨識出來的座標,並在 Console 裡面顯示出來。

Unity_VtefSWB31b.png

測試結果,觀察一下 OpenCV 視窗上的座標和 Unity 顯示的座標是否相同:

今天教學到這邊,YOLO 是一個好用的工具,可以快速實現影像辨識功能。目前我們只有先用別人預先訓練的模型來做辨識,未來可能還可以拿自己準備的影像當作樣本來訓練模型,就能做更多的應用!

有遇到問題或有相關技術想要分享的歡迎在下方討論區留言,我會抽空回覆!

相關文章

Ted Liou

雲科碩士在讀中,專注於 Unity C#、TouchDesigner 技術,常把技術筆記分享到部落格,偶爾還直接挪用文章來當教材的研究生。