YOLO 主要用 Python 語言開發,我通常會將它視為一個外掛程式,用來當作我的 Unity 遊戲的一個輸入,例如:用 YOLO 分析玩家在鏡頭前的畫面,辨識出肢體關節的座標,同步到遊戲內的角色身上。
而今天在這邊我們沒有要搞得那麼複雜,先從最簡單的操作開始,帶各位從用 OpenCV 來確認 YOLO 的辨識結果、JSON 編碼,最後經由 UDP 將座標傳輸到 Unity 之中。
我們大致上會經歷以下步驟,你需要先建立好開發環境:
- 安裝 OpenCV
- 用 Webcam 當作 YOLO 串流輸入
- 取得鼻子的 XY 座標
- JSON 編碼與 UDP 傳輸
- 在 Unity 接收 UDP 與解碼
說明到這邊,現在開始正式主題:
安裝 OpenCV
我們待會要用 OpenCV 來將 Webcam 的影像和辨識後的座標顯示出來,方便測試結果。請開啟終端機,輸入以下指令來安裝 Python 的 OpenCV。
1pip install opencv-python
用 Webcam 當作 YOLO 串流輸入
接下來新增一個 Python 腳本,複製貼上以下程式碼與執行。這個腳本將會載入 YOLO 11 的人類骨架辨識模型,並將編號 0 的 Webcam 影像串流到模型中 (source=0
、stream=True
),以顯示卡運算 (device=0
)。
我們額外做的事情是用 cv2.imshow
將 Webcam 的影像用視窗來顯示,當作監看使用。
1from ultralytics import YOLO
2import cv2
3
4# 載入 YOLO 模型
5model = YOLO("yolo11n-pose.pt")
6
7while True:
8 # 執行模型推論
9 results = model(source=0, stream=True, device=0)
10
11 for r in results:
12 # 取得 Webcam 影像
13 frame = r.orig_img
14
15 # 顯示結果
16 cv2.imshow("webcam", frame)
17 cv2.waitKey(1)
取得鼻子的 XY 座標
接下來我們在 for r in results:
裡面輸入以下程式碼,將辨識到的關鍵點 (Key Points) 提取出來,並把它轉成整數。
1keypoint = r.keypoints[0].xy[0]
2x = int(keypoint[0][0])
3y = 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 的監看視窗中。
目前的程式碼內容如下:
1from ultralytics import YOLO
2import cv2
3
4# 載入 YOLO 模型
5model = YOLO("yolo11n-pose.pt")
6
7while True:
8 # 執行模型推論
9 results = model(source=0, stream=True, device=0)
10
11 for r in results:
12 # 取得 Webcam 影像
13 frame = r.orig_img
14
15 # 取得鼻子座標
16 keypoint = r.keypoints[0].xy[0]
17 x = int(keypoint[0][0])
18 y = int(keypoint[0][1])
19 text = "x: {}, y: {}".format(x, y)
20
21 # 顯示結果
22 cv2.putText(frame, text, (0, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 1, cv2.LINE_AA)
23 cv2.imshow("webcam", frame)
24 cv2.waitKey(1)
執行腳本,YOLO 會以左上角為原點,精準辨識我的鼻子座標並顯示在畫面左上方。
測試一下即時的辨識效果。
YOLO 的辨識算是告一段落,接下來的工作就是要將拿到的座標編碼成 JSON 格式,再透過 UDP 傳輸的方式將它輸入到 Unity 裡面。
JSON 編碼與 UDP 傳輸
選用 UDP 而不是 TCP 的原因在於「效率」,UDP 的發送端 (YOLO) 無須理會接收端 (Unity) 有沒有正確的拿到結果,也不會跳出傳送錯誤,效能也比 TCP 高,是個理想的方式來傳輸座標。
我們修改一下程式碼,首先需要引用 json 和 socket:
1import json
2import socket
再建立發送的目的地與 UDP 發送端,我們預計將資料傳輸到本機的 7000 端口:
1server_addr = ("127.0.0.1", 7000)
2client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
最後在 for r in results:
裡面加上以下程式碼,先將座標包成物件,再編碼成 JSON 格式,並使用 UDP 把它傳出去。
1output = {
2 "x": x,
3 "y": y
4}
5client.sendto(json.dumps(output).encode(), server_addr)
最後的程式碼內容如下:
1from ultralytics import YOLO
2import cv2
3import json
4import socket
5
6# UDP 發送設定
7server_addr = ("127.0.0.1", 7000)
8client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
9
10# 載入 YOLO 模型
11model = YOLO("yolo11n-pose.pt")
12
13while True:
14 # 執行模型推論
15 results = model(source=0, stream=True, device=0)
16
17 for r in results:
18 # 取得 Webcam 影像
19 frame = r.orig_img
20
21 # 取得鼻子座標
22 keypoint = r.keypoints[0].xy[0]
23 x = int(keypoint[0][0])
24 y = int(keypoint[0][1])
25 text = "x: {}, y: {}".format(x, y)
26
27 # JSON 編碼並發送
28 output = {
29 "x": x,
30 "y": y
31 }
32 client.sendto(json.dumps(output).encode(), server_addr)
33
34 # 顯示結果
35 cv2.putText(frame, text, (0, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 1, cv2.LINE_AA)
36 cv2.imshow("webcam", frame)
37
38 # 每 100 毫秒辨識一次
39 cv2.waitKey(100)
如果傳輸速度過快或辨識太吃資源,可以調整 cv2.waitKey
的數字來調整頻率,單位是毫秒。
YOLO 和 Python 的部分已經完畢,可以先讓它持續執行,我們要來處理 Unity 的 UDP 接收功能
在 Unity 接收 UDP 與解碼
開啟一個 Unity 專案,建立一個名為 UdpReceiver.cs 的 C# 腳本,並複製貼上以下程式碼:
1using System.Net;
2using System.Net.Sockets;
3using System.Text;
4using System.Threading.Tasks;
5using UnityEngine;
6
7public class UdpReceiver : MonoBehaviour
8{
9 // 接收的 JSON 資料格式
10 public class InputData
11 {
12 public int x;
13 public int y;
14 }
15
16 private UdpClient _client;
17 private IPEndPoint _endPoint;
18
19 private void Start()
20 {
21 // 以 UDP 監聽 7000 端口
22 _client = new UdpClient(7000);
23 _endPoint = new IPEndPoint(IPAddress.Any, 7000);
24
25 // 非同步接收資料
26 RunForever();
27 }
28
29 private async void RunForever()
30 {
31 while (true)
32 {
33 // 等待接收新資料
34 var raw = await Task.Run(() => _client.Receive(ref _endPoint));
35
36 // 解碼成 JSON 字串
37 var json = Encoding.UTF8.GetString(raw);
38
39 // 解碼成物件
40 var data = JsonUtility.FromJson<InputData>(json);
41
42 // 列印結果
43 Debug.Log($"x: {data.x}, y: {data.y}");
44 }
45 }
46
47 // 遊戲退出時關閉連線
48 private void OnApplicationQuit()
49 {
50 _client.Close();
51 }
52}
在上面的程式碼中,我們用 C# 內建的 UdpClient 來當作 UDP 的接收器,並使用非同步和 Task.Run
方法來防止在接收 UDP 的資料時讓 Unity 卡死。
當接收到資料時,我們會將它解碼成 JSON 格式,再透過 Unity 內建的 JsonUtility 將 JSON 解成物件,最後再把它列印到 Console 裡面。
最後同時執行 YOLO 和 Unity,現在 Unity 理應能接收到 YOLO 辨識出來的座標,並在 Console 裡面顯示出來。
測試結果,觀察一下 OpenCV 視窗上的座標和 Unity 顯示的座標是否相同:
今天教學到這邊,YOLO 是一個好用的工具,可以快速實現影像辨識功能。目前我們只有先用別人預先訓練的模型來做辨識,未來可能還可以拿自己準備的影像當作樣本來訓練模型,就能做更多的應用!
有遇到問題或有相關技術想要分享的歡迎在下方討論區留言,我會抽空回覆!