AmazingHand 網頁控制
用瀏覽器驅動 AmazingHand 靈巧手 —— 左手或右手皆可,並提供兩種操控方式:
- 手動控制 —— 8 個伺服馬達滑桿 + 19 個預設姿態 + 一鍵回到中位
- 攝影機遙控 —— MediaPipe 在瀏覽器本機執行,即時把你的手映射到手指上(影像永遠不會離開本機)
- 雙語介面 —— 在右上角切換中文與英文;選擇會由瀏覽器記住
伺服馬達映射、姿態庫與換算公式全部對齊 AmazingHandControl(官方 GUI)。
快速開始
開啟終端機,進入到 Amazinghand-web 目錄下:
pip install -r requirements.txt
python server.py --no-connect # 推薦:先啟動伺服器,在網頁中選擇序列埠(不會有任何動作)
# python server.py # 自動偵測序列埠並連線
# python server.py --port COM9 # 指定序列埠
# python server.py --hand left # 只啟用左手
# python server.py --dry-run # 完全不開序列埠,只驗證介面接著在瀏覽器開啟 http://localhost:8000。
在網頁中選擇序列埠
頁面頂端有一排序列埠工具列:
Port [COM3 ★ USB-Enhanced-SERIAL CH343 ▾] [Refresh] [Connect] [Disconnect] Connected to COM3- 下拉選單列出本機上的每一個序列埠。常見的 USB 轉序列埠晶片(CH34x / CP210x / FTDI …)會標示 ★ 並排在最前面
- Refresh 會重新掃描(插上板子後請使用)
- Connect 會開啟所選的序列埠;連線後手會立刻回到中位
- Disconnect 會先鬆開力矩再斷開,伺服馬達才不會僵在原位
- 要切換序列埠,直接選新的再點 Connect 即可 —— 舊的埠會自動幫你關閉
--no-connect正是為這個流程而設計:伺服器啟動時完全不碰任何序列埠, 所以它不會抓錯埠而把手扯得猛然一動。想讓它在啟動時就連線,就拿掉這個旗標。
序列埠日誌會寫入 server.log(若你透過 .bat 啟動,視窗會一閃而過 —— 請改讀這個檔案)。
接線檢查
列出偵測到的伺服馬達 ID,並逐手與 config.json 核對。
python tools/scan_servos.py --port COM9把 COM9 換成你實際使用的埠。
伺服馬達映射
來源:AmazingHandControl/hand_logic.py 中的 DEFAULT_HAND_SERVOS 與 docs/zh-hans/CONFIG_FORMAT.md。
| 手指 | 右手 ID | 左手 ID |
|---|---|---|
| 無名指 | 5, 6 | 11, 12 |
| 中指 | 3, 4 | 13, 14 |
| 食指 | 1, 2 | 15, 16 |
| 拇指 | 7, 8 | 17, 18 |
一根手指的兩個伺服馬達分工不同 ⚠️
servo1(奇數 ID)= 屈伸軸 張開 ↔ 握緊
servo2(偶數 ID)= 側擺軸 手指併攏 ↔ 張開介面上顯示的正是這兩個軸。「手指張不開/合不攏」就代表側擺軸沒有在動 —— 剪刀手那樣的 V 字,來自食指側擺 +17 與中指側擺 −30(一正一負)。
兩者在匯流排上會合成:
servo1 = 屈伸 + 側擺
servo2 = 屈伸 − 側擺範圍(由官方 19 個姿態加上中位推導而來):
| 軸 | 範圍 | 說明 |
|---|---|---|
| 屈伸 | −50 ~ +90 | 張開(負)↔ 握緊(正) |
| 側擺 | −45 ~ +45 | 併攏(負)↔ 張開(正) |
兩條 ID 規則(弄錯會燒毀伺服馬達或使動作反向):
- 奇數 ID 是手指的第一個伺服馬達,偶數 ID 是第二個。 兩隻手都成立 (左手 11/12、13/14… 同樣是奇偶交替)。第二個伺服馬達是反向安裝的,換算時要取負號。
- 左手不是 9–16,而是 11–18,且以無名指優先編號。 依序接成 1–8 一定是錯的。

換算
與 AmazingHandControl 中的 apply_pose() 完全相同(已逐個姿態驗證):
1) mirror_pose 為真時,先交換每根手指兩個伺服馬達的值
2) servo_deg = (ID 奇數 ? +1 : -1) × (姿態值 − angle_offset_deg)| 手 | angle_offset_deg | mirror_pose |
|---|---|---|
| 右手 | 0 | 是 ⚠️ |
| 左手 | -35.16 | 是 |
-35.16° 是左手伺服馬達臂相對右手錯開半格的實體偏移量,取自官方設定。
關於
mirror_pose—— 本版本與官方不同在官方參考件中只有左手是
true。本版本的右手模組同樣將伺服馬達對反裝, 因此右手也設為true。這個交換只會翻轉側擺,絕不會動到屈伸:
base = (a+b)/2交換後不變,而side = (a−b)/2會變號。所以如果哪隻手的側擺出現鏡像反轉,這就是第一個該檢查的開關。
姿態陣列固定為 8 個值,順序為 [ring, middle, index, thumb],每根手指兩個值 —— 與官方 data/hand_config.yaml 逐位元組相同,方便你直接比對。
實測範例
| 姿態 | 右手 | 左手 |
|---|---|---|
open [-35]*8 | ID5 −35 / ID6 +35 | ID11 +0.16 / ID12 −0.16 |
close [75]*8 | ID5 +75 / ID6 −75 | ID11 +110.16 / ID12 −110.16 |
middle [-17.578]*8 | ID5 −17.58 / ID6 +17.58 | ID11 +17.58 / ID12 −17.58 |
請注意左手的
close需要 ±110.16°,遠超過右手。這就是config.json把限位設為 ±120° 的原因(SCS0009 的滿量程約 ±150°)。限位設成 ±90 會裁掉左手的握拳。
預設姿態(19)
| 姿態 | 姿態 | ||
|---|---|---|---|
open | 張開 | wave1 | 揮手 |
close / stone | 握拳 | wave_r / wave_l | 向右/向左揮 |
ok | OK | ring_close | 僅無名指 |
greeting | 打招呼 | middle_close | 僅中指 |
victory / two / scissors | 剪刀手/二 | pointer_close | 僅食指 |
paper | 布 | thumb_close | 僅拇指 |
one / three | 一/三 | hifive | 擊掌 |
要修改某個姿態,請編輯 config.json 的 poses 區段;鍵名與官方 YAML 中的名稱一致。
左右手可以使用不同的值
在一個姿態內,positions 是兩隻手共用的。如果某隻手因機械組裝差異需要不同的一組值, 在該筆項目中新增 right 或 left 鍵,它就會覆寫 positions:
"close": {
"label": "握拳",
"positions": [75, 75, 75, 75, 75, 75, 18, 18], // 兩隻手共用
"right": [ ... 8 個值 ... ] // 可選:只給右手用
}兩種模式
| 模式 | 行為 |
|---|---|
| 右手 | 只下發到右手的 8 個伺服馬達 |
| 左手 | 只下發到左手的 8 個伺服馬達(自動套用鏡像 + −35.16° 偏移) |
一次控制一隻手。兩隻手位於同一條匯流排上;用頂端的 Target 切換。 切換不會干擾另一隻手 —— 它會維持在目前的姿態。
攝影機遙控
MediaPipe 手部特徵點在瀏覽器本機執行(資源位於 web/mediapipe/,可離線運作)。 這 21 個特徵點會被轉換成 4 根手指的姿態值,並串流下發給所選的手。
一、先擷取三個姿勢
啟動攝影機後,擺好手部姿勢,再點該姿勢的擷取按鈕(約 1 秒取平均)。 順序不拘;再點同一個按鈕即可重新擷取。狀態要三個都擷取完成才會變綠 —— 在那之前不會下發任何值,所以手會停在中位。
| 按鈕 | 姿勢 | 提供 |
|---|---|---|
| ① 張開 | 手指伸直並張開,拇指向外 | 屈伸的「伸直」端 + 側擺的「張開」端 |
| ② 併攏 | 手指伸直並併攏,拇指貼靠食指 | 側擺的「併攏」端 |
| ③ 握拳 | 握拳,拇指收向掌心 | 屈伸的「握緊」端 |
二、每個軸都是一條通過兩個量測點的直線
屈伸與側擺都使用兩個量測到的端點來定義一條直線:
t = (量測值 − 端點A) / (端點B − 端點A) // 鉗制在 0~1
目標 = A的目標值 + (B的目標值 − A的目標值) × t好處是:每根手指量測值的正負號會被自動吸收 —— 因此不需要勾選 任何「讓這根手指反向」的核取方塊。(食指與無名指分居中線兩側,而拇指的關節鏈 也不同,所以它們的方向確實相反 —— 兩個量測端點就能把它吸收掉。)
端點目標值位於 config.json 的 camera 區段:
| 軸 | 端點 | 無名指 | 中指 | 食指 |
|---|---|---|---|---|
| 屈伸 | 伸直 → 握拳 | −50 → 90 | −50 → 90 | −50 → 90 |
| 側擺 | 併攏 → 張開 | −45 → +45 | 0 → 0 | +45 → −45 |
中指側擺的兩端都是 0(它不會左右擺動)。
三、拇指只做屈伸 ⚠️
拇指的 CMC 是鞍狀(對掌)關節,它的屈伸與外展緊密耦合 —— 當拇指貼靠食指時,它已經處於側擺位置,因此在該處量到的「屈伸零點」是被污染的。 若硬把它拆成兩個軸,兩者都會算錯。
所以拇指只做屈伸;它的側擺永遠是 0(它的兩個伺服馬達會拿到相同的值)。它需要的兩個 參考姿勢是 ①(向外指)與 ③(指向掌心)—— 不需要額外擷取。
四、側擺會隨手指蜷曲而遞減
最終的側擺會依下式縮放:
side = side_measured × (1 − curl)splayDeg 是 MCP→PIP 方向在手掌橫軸上的投影,而手指屈伸 正是繞著這根軸旋轉 —— 超過某個蜷曲程度後,這個投影會翻轉正負號,量到的 「張開」會突然跳到另一端。
沒有這個遞減,那個突跳會經由 servo2 = flexion − splay 傳遞,並把偶數 ID 的伺服馬達 拖入反向。(奇數 ID 拿到的是 flexion + splay,所以它會朝同方向移動,只是更快 —— 你看不出來。) 症狀是「握拳握到一半,12 與 16 號伺服馬達突然反向」—— 12 是左手無名指的 側擺伺服馬達、16 是左手食指的,正是側擺目標值非零的那兩個。
有了這個遞減,無論量測值多離譜,在全蜷曲時側擺都會正好是 0。 而且握拳本來就該是手指併攏的。
五、骨架顏色
| 手指 | 顏色 |
|---|---|
| 拇指 | 橘色 |
| 食指 | 紅色 |
| 中指 | 藍色 |
| 無名指 | 黃色 |
| 小指 | 暗灰色 |
| 掌骨 | 中性灰 |
小指不用於手勢映射,但它的根部 MCP(第 17 號特徵點)定義了右手的手掌座標系, 所以不能直接捨棄。
調校速查表
| 你想改什麼 | 在哪裡改 |
|---|---|
| 各軸的目標值 | config.json 的 camera 區段 |
| 左手與右手的側擺方向相反 | 在 camera.side_sign 中把該隻手設為 -1(1 = 不變) |
| 平滑度 | tracking.js 中的 SMOOTH(預設 0.35;數值越低越穩但越遲鈍) |
| 蜷曲多快會被判定 | 重新擷取三個姿勢 |
side_sign 會套用到 Target 下所選的那隻手。修改後重新載入頁面即可 —— 不需要重啟伺服器:
"side_sign": { "right": 1, "left": -1 } // 左手把整條側擺軸反向屈伸不受 side_sign 影響。
專案結構
Amazinghand-web/
├── config.json 伺服馬達映射 / 姿態庫 / 序列 / 限位(由官方 YAML 產生)
├── server.py 後端:序列埠 + 靜態檔案服務 + WebSocket
├── requirements.txt
├── README.md 英文版
├── tutorials/ de · en · es · fr · it · ja · ko · pt-br · pt-pt · zh-hans · zh-hant
├── tools/
│ └── scan_servos.py 掃描匯流排上的伺服馬達並核對 ID
└── web/
├── index.html · app.js · tracking.js · style.css
├── i18n.js 中英文文案(詞典 + 右上角切換)
└── mediapipe/ MediaPipe 資源(本機打包、可離線使用,約 42 MB)前端/後端通訊協定:
// 前端 → 後端 target: "left" | "right"
{ "cmd": "ports" } // 重新掃描序列埠
{ "cmd": "connect", "port": "COM3" }
{ "cmd": "disconnect" }
{ "cmd": "target", "target": "left" }
{ "cmd": "set", "target": "left", "angles": { "pointer": [0, 0], ... } }
{ "cmd": "pose", "target": "left", "name": "close" }
{ "cmd": "middle", "target": "left" }
{ "cmd": "torque", "target": "left", "on": true }
{ "cmd": "stop" }
// 後端 → 前端
{ "type": "state", "hands": {...}, "poses": {...}, "positions": {...},
"axes": {...}, "camera": {...}, "target": "left",
"limits": {...}, "port": { "available": [...], "current": "COM3", "connected": true } }
{ "type": "ports" | "connected" | "disconnected", "ports": [...], ... }
{ "type": "error", "msg": "連接 COM_FAKE 失敗:系統找不到指定的檔案。" }安全
| 項目 | |
|---|---|
| 姿態限位 | ±120°(config.json 中的 limits);所有 19 個姿態都經檢查、維持在範圍內 |
| 變更去重 | 小於 0.5° 的變更不會下發,避免匯流排被雜訊塞滿 |
| 寫入節流 | 兩次寫入之間最少間隔 20 ms |
| 急停 | 介面上的紅色按鈕會一次鬆開所有伺服馬達 |
| 退出保護 | 按下 Ctrl+C 時會自動鬆開力矩 |
| 單手除錯 | --hand left/right |
疑難排解
動作方向相反 → 檢查接線是否遵循「奇數 ID 優先」;左手還要確認 angle_offset_deg = -35.16。
左手的握拳太小/伺服馬達發出異音 → 限位可能還是 ±90;檢查 config.json 是否為 ±120。
某根手指不動/「expected N motors, found M」 → 執行 tools/scan_servos.py;左手的 ID 最容易接錯。
中位偏移 → 中位值 -17.578125 是由官方原始值 451/571 換算而來。 要依你自己的組裝進行校正,請編輯 config.json 中的 middle.positions。
攝影機追蹤會抖動 → 光線均勻、手掌朝向攝影機。tracking.js 中的 SMOOTH (預設 0.35)—— 數值越高越平滑但越遲鈍。
攝影機手勢錯誤/某根手指不跟隨 → 見上方「攝影機遙控」一節。先檢查三件事:
- 三個姿勢都擷取了嗎? —— 狀態必須變綠;在那之前不會下發任何值,手會停在中位
- 方向相反代表那個姿勢被反向擷取了 —— 只要重新擷取那一個按鈕即可,不需修改程式碼
- 拇指不應該側擺 —— 它只做屈伸,見上方第三節
握拳握到一半,某個伺服馬達突然反向 → 那個伺服馬達應該是偶數 ID(側擺軸,例如左手 12 / 16)。 正常情況下側擺會隨蜷曲遞減到 0,絕不會跳動;如果你看到這個現象,代表 tracking.js 中的 side *= (1 - curl) 那行沒有生效,或是 curl 沒有被計算出來。

