Jump to content
Main menu
Main menu
move to sidebar
hide
Navigation
Main page
Recent changes
Random page
freem
Search
Search
Appearance
Create account
Log in
Personal tools
Create account
Log in
Pages for logged out editors
learn more
Contributions
Talk
Editing
Openai/692fcfd8-4b44-800f-a184-fa098175a6d5
(section)
Add languages
Page
Discussion
English
Read
Edit
Edit source
View history
Tools
Tools
move to sidebar
hide
Actions
Read
Edit
Edit source
View history
General
What links here
Related changes
Special pages
Page information
Appearance
move to sidebar
hide
Warning:
You are not logged in. Your IP address will be publicly visible if you make any edits. If you
log in
or
create an account
, your edits will be attributed to your username, along with other benefits.
Anti-spam check. Do
not
fill this in!
== gpt-5-1-thinking == === User: 要 === 要 === Assistant: 好,我直接給你一個 「手勢 1~10」偵測版,用 CVZone + OpenCV。 === 好,我直接給你一個 「手勢 1~10」偵測版,用 CVZone + OpenCV。 邏輯說明: * 用 fingersUp() 拿到 [thumb, index, middle, ring, pinky] * 依照中國數字手勢的常見定義寫規則: - 1:食指 - 2:食指 + 中指 - 3:拇指 + 食指 + 中指 - 4:除拇指出外四指全伸 - 5:五指全伸 - 6:拇指 + 小指(shaka) - 7:拇指 + 食指 + 中指 - 8:拇指 + 食指(手槍狀) - 9:只用食指,額外用手指角度做粗略判斷(和 1 區分可能不完美) - 10:兩隻手都比「1」,視為交叉 10(不強制真的交叉,簡化處理) : ==== <syntaxhighlight lang="python">from cvzone.HandTrackingModule import HandDetector ==== import cv2 import math === 影像來源:0 為內建攝影機 === cap = cv2.VideoCapture(0) === 建立手部偵測器,允許最多 2 隻手(為了手勢10) === detector = HandDetector(detectionCon=0.7, maxHands=2) def classify_single_hand_number(hand, fingers): """ 根據單手的 fingersUp 結果判斷數字 1~9 hand: 單手資訊 (包含 lmList, bbox, type 等) fingers: [thumb, index, middle, ring, pinky] (0/1) 回傳 (number, label) 或 (None, "None") """ pattern = tuple(fingers) # 轉成 tuple 方便比對 # 1~8 直接用固定模式比對 mapping = { (0, 1, 0, 0, 0): (1, "1"), (0, 1, 1, 0, 0): (2, "2"), (1, 1, 1, 0, 0): (3, "3"), (0, 1, 1, 1, 1): (4, "4"), (1, 1, 1, 1, 1): (5, "5"), (1, 0, 0, 0, 1): (6, "6"), (1, 1, 1, 0, 0): (7, "7"), (1, 1, 0, 0, 0): (8, "8"), } if pattern in mapping: return mapping[pattern] # ---- 嘗試把「9」和「1」做粗略區別 ---- # 只伸出食指,有可能是 1 或 9 if pattern == (0, 1, 0, 0, 0): lm_list = hand["lmList"] # 21 個關鍵點 # 食指:MCP = 5, PIP = 6, DIP = 7, TIP = 8 mcp = lm_list[5] # [x, y, z] pip = lm_list[6] dip = lm_list[7] tip = lm_list[8] # 計算 MCP→TIP 的直線長度 dist_mcp_tip = math.dist(mcp[:2], tip[:2]) # 計算 MCP→PIP + PIP→DIP + DIP→TIP 的「折線」長度 dist_mcp_pip = math.dist(mcp[:2], pip[:2]) dist_pip_dip = math.dist(pip[:2], dip[:2]) dist_dip_tip = math.dist(dip[:2], tip[:2]) poly_len = dist_mcp_pip + dist_pip_dip + dist_dip_tip # 如果手指「很彎」,折線長度會明顯大於直線長度 # 這邊用一個很粗略的門檻:poly_len / dist_mcp_tip > 1.25 視為 9 if dist_mcp_tip > 0 and poly_len / dist_mcp_tip > 1.25: return (9, "9 (hook)") else: return (1, "1") # 其餘模式不判斷數字 return (None, "None") def classify_number(hands): """ 根據所有偵測到的手,回傳 (number, label, bbox_for_draw) number: 1~10 或 None label: 顯示在畫面上的文字 bbox_for_draw: 選一個 bbox 用來標文字 (x, y, w, h) """ if not hands: return None, "No Hand", None # 先嘗試判斷「10」:兩手都比 1(只伸食指) if len(hands) >= 2: nums = [] for h in hands[:2]: fingers = detector.fingersUp(h) n, _ = classify_single_hand_number(h, fingers) nums.append(n) if nums[0] == 1 and nums[1] == 1: # 視為手勢 10 # bbox 就抓第一隻手來畫 bbox10 = hands[0]["bbox"] return 10, "10", bbox10 # 若不是 10,就只看第一隻手來判斷 1~9 main_hand = hands[0] bbox = main_hand["bbox"] fingers = detector.fingersUp(main_hand) number, label = classify_single_hand_number(main_hand, fingers) if number is None: return None, "Unknown", bbox else: return number, label, bbox while cap.isOpened(): success, img = cap.read() if not success: break # 找手 hands, img = detector.findHands(img) number, label, bbox = classify_number(hands) # 在畫面上顯示結果 if bbox is not None: x, y, w, h = bbox text = f"Gesture: {label}" cv2.putText(img, text, (x, y - 20), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2) # 也在終端機印出方便 debug if number is not None: print("Detected number:", number, "|", label) cv2.imshow("Hand Number 1-10", img) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() </syntaxhighlight> ===== 1. 需要先安裝: ``<code>bash pip install opencv-python cvzone mediapipe </code>`` ===== # 執行程式後: - 對著鏡頭比出 1~8,應該會蠻穩定辨識。 - 兩手同時比「1」→ 會顯示為 10。 - 彎曲的「鉤狀食指」有機會被判為 9(但因為角度、距離關係,不一定每次都成功)。 如果你願意,我也可以幫你: * 把每個手勢對應的「圖片 + 數字」做成教學視窗(用 Tkinter 或 Pygame) * 或加上「語音播報」(比出幾,就念出幾,中文或台語都可以 😄) 你想接下來接哪一塊,我就幫你往那邊延伸。 === User: 都要 === 都要 === Assistant: 好,我一次給你「手勢教學視窗+中/台語語音」的完整版本。 === 好,我一次給你「手勢教學視窗+中/台語語音」的完整版本。 設計重點: * 用 CVZone 做手部偵測與手勢 1~10 辨識 * 用 OpenCV 畫面右側做教學面板:顯示 - 手勢圖片(1~10) - 阿拉伯數字、中文數字、台語 POJ * 用 pygame.mixer 播放語音: - audio/zh_1.mp3(國語) - audio/tai_1.mp3(台語) - 依此類推到 10 : ==== 請在專案資料夾中準備: ==== <syntaxhighlight lang="text">your_project/ │ hand_number_teach.py ← 程式 │ ├─gestures/ ← 手勢教學圖片 │ 1.png │ 2.png │ ... │ 10.png │ └─audio/ ← 語音檔(可自己錄或用 TTS 產生) zh_1.mp3 zh_2.mp3 ... zh_10.mp3 tai_1.mp3 ... tai_10.mp3 </syntaxhighlight> * gestures/1.png~10.png:可用你剛剛那組手勢圖裁切後放進去。 * audio/zh_X.mp3:國語「一、二、三…」 * audio/tai_X.mp3:台語「it、nn̄g、saⁿ…」 ==== <syntaxhighlight lang="python">from cvzone.HandTrackingModule import HandDetector ==== import cv2 import math import os import time import pygame === ========== 初始設定 ========== === cap = cv2.VideoCapture(0) # 0: 內建攝影機 detector = HandDetector(detectionCon=0.7, maxHands=2) === 初始化 pygame 音效 === pygame.mixer.init() === 台語 POJ 對照 === tai_map = { 1: "it", 2: "nn̄g", 3: "saⁿ", 4: "sì", 5: "gō͘", 6: "la̍k", 7: "chhit", 8: "peh", 9: "káu", 10: "chāp", } === 中文數字對照 === zh_map = { 1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "七", 8: "八", 9: "九", 10: "十", } === 讀取手勢教學圖片 === gesture_images = {} for n in range(1, 11): path = f"gestures/{n}.png" if os.path.exists(path): img = cv2.imread(path) gesture_images[n] = img else: print(f"[提示] 找不到手勢示意圖:{path}") === ========= 播放語音 ========= === def play_audio(number): """ 播放對應數字的國語 + 台語語音 audio/zh_N.mp3, audio/tai_N.mp3 會阻塞一下主程式(簡單好懂版) """ for lang in ["zh", "tai"]: fn = f"audio/{lang}_{number}.mp3" if os.path.exists(fn): try: pygame.mixer.music.load(fn) pygame.mixer.music.play() # 等待播放結束 while pygame.mixer.music.get_busy(): time.sleep(0.1) except Exception as e: print(f"[錯誤] 播放 {fn} 失敗:{e}") else: print(f"[提示] 找不到音檔:{fn}") === ========= 手勢判斷邏輯 ========= === def classify_single_hand_number(hand, fingers): """ 根據單手的 fingersUp 結果判斷數字 1~9 hand: 單手資訊 (包含 lmList, bbox, type 等) fingers: [thumb, index, middle, ring, pinky] (0/1) 回傳 (number, label) 或 (None, "None") """ pattern = tuple(fingers) # 固定手勢模式(不含「只伸食指」的情況,留給 1 / 9 特判) mapping = { (0, 1, 1, 0, 0): (2, "2"), # 食 + 中 (1, 1, 1, 0, 0): (3, "3"), # 拇 + 食 + 中 (0, 1, 1, 1, 1): (4, "4"), # 四指伸,拇指收 (1, 1, 1, 1, 1): (5, "5"), # 全部伸出 (1, 0, 0, 0, 1): (6, "6"), # 拇 + 小 (shaka) (1, 1, 0, 0, 1): (7, "7"), # 拇 + 食 + 小 (1, 1, 0, 0, 0): (8, "8"), # 拇 + 食 (手槍狀) } if pattern in mapping: return mapping[pattern] # ---- 特判:1 或 9(只伸食指)---- if pattern == (0, 1, 0, 0, 0): lm_list = hand["lmList"] # 21 個關鍵點 # 食指:MCP = 5, PIP = 6, DIP = 7, TIP = 8 mcp = lm_list[5] pip = lm_list[6] dip = lm_list[7] tip = lm_list[8] # 直線長度 MCP→TIP dist_mcp_tip = math.dist(mcp[:2], tip[:2]) # 折線長度 MCP→PIP→DIP→TIP dist_mcp_pip = math.dist(mcp[:2], pip[:2]) dist_pip_dip = math.dist(pip[:2], dip[:2]) dist_dip_tip = math.dist(dip[:2], tip[:2]) poly_len = dist_mcp_pip + dist_pip_dip + dist_dip_tip # 折線 / 直線 比例,用來粗略分辨「很彎」=9、「較直」=1 if dist_mcp_tip > 0 and poly_len / dist_mcp_tip > 1.25: return (9, "9") else: return (1, "1") # 其他模式先不判斷 return (None, "None") def classify_number(hands): """ 根據全部手部資料判斷數字 1~10 回傳 (number, label, bbox_for_draw) """ if not hands: return None, "No Hand", None # 先判斷是否為「10」:兩手都比 1 if len(hands) >= 2: nums = [] for h in hands[:2]: fingers = detector.fingersUp(h) n, _ = classify_single_hand_number(h, fingers) nums.append(n) if nums[0] == 1 and nums[1] == 1: bbox10 = hands[0]["bbox"] return 10, "10", bbox10 # 否則只看第一隻手 1~9 main_hand = hands[0] bbox = main_hand["bbox"] fingers = detector.fingersUp(main_hand) number, label = classify_single_hand_number(main_hand, fingers) if number is None: return None, "Unknown", bbox else: return number, label, bbox === ========= 主迴圈 ========= === last_number = None # 上一次辨識結果,用來避免重複播音 while cap.isOpened(): success, img = cap.read() if not success: break # 偵測手部 hands, img = detector.findHands(img) number, label, bbox = classify_number(hands) # ===== 教學面板內容 ===== h, w, _ = img.shape panel_width = 320 panel = 255 * np.ones((h, panel_width, 3), dtype='uint8') # 白底 if number is not None and number in tai_map: # 顯示手勢示意圖(若有) if number in gesture_images: g_img = gesture_images[number] # 調整示意圖高度約為畫面一半 gh, gw, _ = g_img.shape scale = min((h '' 0.6) / gh, (panel_width '' 0.9) / gw) new_size = (int(gw '' scale), int(gh '' scale)) g_resized = cv2.resize(g_img, new_size) # 貼到 panel 中央 x_offset = (panel_width - new_size[0]) // 2 y_offset = 20 panel[y_offset:y_offset + new_size[1], x_offset:x_offset + new_size[0]] = g_resized # 文字資訊 text_y = int(h * 0.75) cv2.putText(panel, f"{number}", (30, text_y), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 0), 2) cv2.putText(panel, f"中文:{zh_map[number]}", (30, text_y + 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 2) cv2.putText(panel, f"台語:{tai_map[number]}", (30, text_y + 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 2) else: cv2.putText(panel, "請比出 1~10 的手勢", (20, h // 2), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (128, 128, 128), 2) # 在原影像畫出偵測結果 if bbox is not None and number is not None: x, y, w0, h0 = bbox cv2.rectangle(img, (x, y), (x + w0, y + h0), (0, 255, 0), 2) cv2.putText(img, f"{label}", (x, y - 10), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2) # 將攝影機畫面與教學面板合併 import numpy as np # 放在這裡避免一開始沒用被 IDE 抱怨 panel = cv2.resize(panel, (panel_width, h)) combined = cv2.hconcat([img, panel]) cv2.imshow("Hand Number Teaching (1-10)", combined) # 若數字有變化,播放對應語音 if number is not None and number != last_number: last_number = number print("辨識到數字:", number) if number in tai_map: play_audio(number) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() </syntaxhighlight> ==== 1. 先測試無語音版 - 可以先把 play_audio(number) 裡面的程式碼註解掉,只看畫面是否會正確顯示手勢與數字。 ==== # 逐步補上音檔 - 先只錄或產生 zh_1.mp3、tai_1.mp3,測試「1」的播報 - 確定沒問題,再補到 10 # 教學活動玩法 - 學生輪流到鏡頭前比 1~10,螢幕會出現: - 手勢圖 - 阿拉伯數字+中文+台語 - 同時念出國語+台語 - 搭配你之前設計的「數字手勢小卡」、「黑板風講義」一起用很剛好。 如果你願意把你現有的手勢圖片(那 10 張)規格跟檔名告訴我,我也可以幫你調整尺寸建議,讓它貼上去更剛好、不變形。 === User: 都要 === 都要 === Assistant: 好,我幫你都生好了 ✅ === 好,我幫你都生好了 ✅ ==== 我已經把你上傳的 10 張手勢照片整理成: ==== * gestures/1.png * gestures/2.png * … * gestures/10.png 並打包成一個 ZIP: 👉 下載 1~10 手勢圖片包 gestures_1_10.zip<ref>{{cite web|title=下載 1~10 手勢圖片包 gestures_1_10.zip|url=https://chatgpt.com/share/%E4%B8%8B%E8%BC%89%201%EF%BD%9E10%20%E6%89%8B%E5%8B%A2%E5%9C%96%E7%89%87%E5%8C%85%20gestures_1_10.zip|publisher=下載 1~10 手勢圖片包 gestures_1_10.zip|access-date=2025-12-05}}</ref> : ==== 我也幫你產生了一支可以自己在電腦上跑、產生 MP3 語音的腳本: ==== 👉 下載 generate_audio_gtts.py<ref>{{cite web|title=下載 generate_audio_gtts.py|url=https://chatgpt.com/share/%E4%B8%8B%E8%BC%89%20generate_audio_gtts.py|publisher=下載 generate_audio_gtts.py|access-date=2025-12-05}}</ref> 裡面會自動產生: * audio/zh_1.mp3 ~ zh_10.mp3(國語一~十) * audio/tai_1.mp3 ~ tai_10.mp3(用國語 TTS 念出「台語 it / nn̄g / saⁿ…」字樣,之後你可以用自己錄音或更好的台語 TTS 覆蓋) 使用方式(在你自己的電腦上): # 安裝 gTTS: ``<code>bash pip install gTTS <syntaxhighlight>2. 把 generate_audio_gtts.py 放在你專案資料夾中(跟 1~10.py 同一層即可) # 執行: </syntaxhighlight>bash python generate_audio_gtts.py <syntaxhighlight>4. 會自動建立 audio 資料夾,裡面就會有 zh_''.mp3 和 tai_''.mp3 ==== 你的 1~10.py 裡面原本就有這段邏輯: ==== * 讀取 gestures/{number}.png * 播放 audio/zh_{number}.mp3 和 audio/tai_{number}.mp3 你現在只要: # 確認結構長這樣: </syntaxhighlight>text 專案資料夾/ │ 1~10.py │ generate_audio_gtts.py ├─gestures/ │ 1.png │ ... │ 10.png └─audio/ zh_1.mp3 ... zh_10.mp3 tai_1.mp3 ... tai_10.mp3 </code>`` # 然後執行你的 1~10.py 就可以看到: - 左邊是攝影機畫面 + 偵測框、手勢數字 - 右邊是教學面板(顯示你剛剛的手勢圖片 1~10) - 每次換一個數字,會自動念國語 + 台語(暫以 gTTS 方式) 如果你待會跑程式又遇到錯誤(路徑、編碼、TTS、pygame 播放等),你可以直接把錯誤訊息貼給我,我幫你一條一條修。 === User: 給完整程式碼 === 給完整程式碼 === Assistant: 下面給你 兩支完整程式: === 下面給你 兩支完整程式: # hand_gesture_1_10.py → 主程式:CVZone 手勢 1~10 + 教學面板 + 播放語音 # generate_audio_gtts.py → 一次產出 audio/zh_''.mp3、audio/tai_''.mp3 的小工具 你只要把這兩支丟到同一個資料夾,再配合我之前給你的 gestures_1_10.zip 解壓,就可以直接玩。 ==== : ==== <syntaxhighlight lang="python"># hand_gesture_1_10.py === 手勢 1~10 辨識+教學面板+國語/台語語音 === from cvzone.HandTrackingModule import HandDetector import cv2 import math import os import time import pygame import numpy as np === ========== 初始設定 ========== === === 0: 內建攝影機,如有外接可改成 1、2... === cap = cv2.VideoCapture(0) # Windows 上若遇到錯誤,可改成 cv2.VideoCapture(0, cv2.CAP_DSHOW) === 手部偵測器(最多 2 隻手,為了偵測 10) === detector = HandDetector(detectionCon=0.7, maxHands=2) === 初始化 pygame 音效系統 === pygame.mixer.init() === 台語 POJ 對照 === tai_map = { 1: "it", 2: "nn̄g", 3: "saⁿ", 4: "sì", 5: "gō͘", 6: "la̍k", 7: "chhit", 8: "peh", 9: "káu", 10: "chap", } === 中文數字對照 === zh_map = { 1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "七", 8: "八", 9: "九", 10: "十", } === ========= 讀取手勢示意圖 ========= === gesture_images = {} for n in range(1, 11): path = os.path.join("gestures", f"{n}.png") if os.path.exists(path): img_g = cv2.imread(path) if img_g is not None: gesture_images[n] = img_g else: print(f"[提示] 找不到手勢示意圖:{path}") === ========= 播放語音 ========= === def play_audio(number): """ 播放對應數字的國語 + 台語語音 audio/zh_N.mp3, audio/tai_N.mp3 """ for lang in ["zh", "tai"]: fn = os.path.join("audio", f"{lang}_{number}.mp3") if os.path.exists(fn): try: pygame.mixer.music.load(fn) pygame.mixer.music.play() # 等待播放完畢再繼續(簡單好理解版) while pygame.mixer.music.get_busy(): time.sleep(0.05) except Exception as e: print(f"[錯誤] 播放 {fn} 失敗:{e}") else: print(f"[提示] 找不到音檔:{fn}") === ========= 手勢判斷(單手 1~9) ========= === def classify_single_hand_number(hand, fingers): """ 根據單手的 fingersUp 結果判斷數字 1~9 hand: 單手資訊 (包含 lmList, bbox, type 等) fingers: [thumb, index, middle, ring, pinky] (0/1) 回傳 (number, label) 或 (None, "None") """ pattern = tuple(fingers) # 轉成 tuple 方便比對 # 固定手勢模式(不含「只伸食指」,那留給 1/9 特判) mapping = { (0, 1, 1, 0, 0): (2, "2"), # 食 + 中 (1, 1, 1, 0, 0): (3, "3"), # 拇 + 食 + 中(有時會跟 7 打架,可依實際調整) (0, 1, 1, 1, 1): (4, "4"), # 四指伸、拇指收 (1, 1, 1, 1, 1): (5, "5"), # 五指全伸 (1, 0, 0, 0, 1): (6, "6"), # 拇 + 小(shaka) (1, 1, 0, 0, 1): (7, "7"), # 拇 + 食 + 小 (1, 1, 0, 0, 0): (8, "8"), # 拇 + 食(手槍) } if pattern in mapping: return mapping[pattern] # ---- 特判:1 或 9(只伸食指)---- if pattern == (0, 1, 0, 0, 0): lm_list = hand["lmList"] # 21 個關鍵點 # 食指:MCP = 5, PIP = 6, DIP = 7, TIP = 8 mcp = lm_list[5] pip = lm_list[6] dip = lm_list[7] tip = lm_list[8] # 直線長度 MCP→TIP dist_mcp_tip = math.dist(mcp[:2], tip[:2]) # 折線長度 MCP→PIP→DIP→TIP dist_mcp_pip = math.dist(mcp[:2], pip[:2]) dist_pip_dip = math.dist(pip[:2], dip[:2]) dist_dip_tip = math.dist(dip[:2], tip[:2]) poly_len = dist_mcp_pip + dist_pip_dip + dist_dip_tip # 折線 / 直線 比例,用來粗略分辨「很彎」=9、「較直」=1 if dist_mcp_tip > 0 and poly_len / dist_mcp_tip > 1.25: return (9, "9") else: return (1, "1") # 其他模式先不判斷 return (None, "None") === ========= 判斷整體數字(包含 10) ========= === def classify_number(hands): """ 根據全部手部資料判斷數字 1~10 回傳 (number, label, bbox_for_draw) """ if not hands: return None, "No Hand", None # 先判斷是否為「10」:兩手都比 1 if len(hands) >= 2: nums = [] for h in hands[:2]: fingers = detector.fingersUp(h) n, _ = classify_single_hand_number(h, fingers) nums.append(n) if nums[0] == 1 and nums[1] == 1: bbox10 = hands[0]["bbox"] return 10, "10", bbox10 # 否則只看第一隻手 1~9 main_hand = hands[0] bbox = main_hand["bbox"] fingers = detector.fingersUp(main_hand) number, label = classify_single_hand_number(main_hand, fingers) if number is None: return None, "Unknown", bbox else: return number, label, bbox === ========= 主迴圈 ========= === last_number = None # 上一次辨識結果:避免重複放音 while cap.isOpened(): success, img = cap.read() if not success: break # 偵測手部 hands, img = detector.findHands(img) # 判斷數字 number, label, bbox = classify_number(hands) # 取得原始影像大小 h, w, _ = img.shape # 建立右側教學面板(白底) panel_width = 320 panel = 255 * np.ones((h, panel_width, 3), dtype="uint8") # ===== 在教學面板顯示內容 ===== if number is not None and number in tai_map: # 顯示手勢示意圖(若有) if number in gesture_images: g_img = gesture_images[number] gh, gw, _ = g_img.shape # 讓示意圖高度約為畫面 60%,寬度約 panel 的 90% scale = min((h '' 0.6) / gh, (panel_width '' 0.9) / gw) new_w = int(gw * scale) new_h = int(gh * scale) g_resized = cv2.resize(g_img, (new_w, new_h)) # 貼到 panel 置中(上方) x_offset = (panel_width - new_w) // 2 y_offset = 20 panel[y_offset:y_offset + new_h, x_offset:x_offset + new_w] = g_resized # 文字資訊(下半部) text_y = int(h * 0.75) cv2.putText(panel, f"{number}", (30, text_y), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 0), 2) cv2.putText(panel, f"中文:{zh_map[number]}", (30, text_y + 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 2) cv2.putText(panel, f"台語:{tai_map[number]}", (30, text_y + 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 2) else: cv2.putText(panel, "請比出 1~10 的手勢", (20, h // 2), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (128, 128, 128), 2) # 在原影像畫出偵測框與標籤 if bbox is not None and number is not None: x, y, bw, bh = bbox cv2.rectangle(img, (x, y), (x + bw, y + bh), (0, 255, 0), 2) cv2.putText(img, f"{label}", (x, y - 10), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2) # 將攝影機畫面與教學面板合併(左右排列) panel = cv2.resize(panel, (panel_width, h)) combined = cv2.hconcat([img, panel]) cv2.imshow("Hand Number Teaching (1-10)", combined) # 若辨識數字有變化 → 播放語音 if number is not None and number in tai_map and number != last_number: last_number = number print("辨識到數字:", number) play_audio(number) # 按下 q 離開 if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() </syntaxhighlight> ==== : ==== <syntaxhighlight lang="python"># generate_audio_gtts.py """ 使用說明: # 先安裝 gTTS: pip install gTTS # 確保此檔案與 hand_gesture_1_10.py 在同一層。 # 執行: python generate_audio_gtts.py # 會在 audio/ 底下產生: * zh_1.mp3 ~ zh_10.mp3 (國語) * tai_1.mp3 ~ tai_10.mp3(暫以國語念 POJ 文字,可自行換成更好的台語 TTS 或錄音覆蓋) """ from gtts import gTTS import os numbers_zh = { 1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "七", 8: "八", 9: "九", 10: "十", } numbers_tai = { 1: "it", 2: "nn̄g", 3: "saⁿ", 4: "sì", 5: "gō͘", 6: "la̍k", 7: "chhit", 8: "peh", 9: "káu", 10: "chap", } out_dir = "audio" os.makedirs(out_dir, exist_ok=True) === 產生國語音檔 === for n, word in numbers_zh.items(): text = f"{word}" # 只念數字本身 tts = gTTS(text=text, lang="zh-TW") fn = os.path.join(out_dir, f"zh_{n}.mp3") print("產生:", fn) tts.save(fn) === 產生「台語」音檔(實際仍用 zh-TW TTS 念 POJ 文字,可日後錄音覆蓋) === for n, word in numbers_tai.items(): text = f"台語 {word}" tts = gTTS(text=text, lang="zh-TW") fn = os.path.join(out_dir, f"tai_{n}.mp3") print("產生:", fn) tts.save(fn) print("全部產生完成!") </syntaxhighlight> ==== 最後確認你的專案看起來像這樣: ==== <syntaxhighlight lang="text">你的資料夾/ │ hand_gesture_1_10.py │ generate_audio_gtts.py │ ├─gestures/ │ 1.png │ 2.png │ ... │ 10.png │ └─audio/ ← 執行 generate_audio_gtts.py 後自動產生 zh_1.mp3 ... zh_10.mp3 tai_1.mp3 ... tai_10.mp3 </syntaxhighlight> 如果你跑 hand_gesture_1_10.py 又遇到錯誤(模組、路徑、decode、攝影機),把錯誤訊息貼給我,我直接幫你對著那一行修掉。 === User: 給 Python 3.7 完整版 === 給 Python 3.7 完整版 === Assistant: 好,我幫你整理成 完全相容 Python 3.7 的版本, === 好,我幫你整理成 完全相容 Python 3.7 的版本, 沒有 math.dist(),可以直接貼到 1~10.py 來跑。 ==== : ==== 請整份複製貼上: <syntaxhighlight lang="python"># hand_gesture_1_10.py === Python 3.7 版:手勢 1~10 辨識+教學面板+國語/台語語音 === from cvzone.HandTrackingModule import HandDetector import cv2 import math # 雖然不用 math.dist,但保留 math 以備後續若要加東西 import os import time import pygame import numpy as np === ========= 小工具:2D 距離(取代 math.dist) ========= === def euclidean(a, b): """ 計算 2D 歐幾里得距離 a, b: [x, y, (z)] 形式的 list 或 tuple,這裡只用前兩個值 """ return ((a[0] - b[0]) ''' 2 + (a[1] - b[1]) ''' 2) ** 0.5 === ========== 初始設定 ========== === === 攝影機:0 通常是內建鏡頭;如有外接可換成 1 === cap = cv2.VideoCapture(0) # 若遇到裝置問題,可試 cv2.VideoCapture(0, cv2.CAP_DSHOW) === 手部偵測器(最多 2 隻手,為了辨識 10) === detector = HandDetector(detectionCon=0.7, maxHands=2) === 初始化 pygame 音效系統 === pygame.mixer.init() === 台語 POJ 對照 === tai_map = { 1: "it", 2: "nn̄g", 3: "saⁿ", 4: "sì", 5: "gō͘", 6: "la̍k", 7: "chhit", 8: "peh", 9: "káu", 10: "chap", } === 中文數字對照 === zh_map = { 1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "七", 8: "八", 9: "九", 10: "十", } === ========= 讀取手勢示意圖 ========= === gesture_images = {} for n in range(1, 11): path = os.path.join("gestures", f"{n}.png") if os.path.exists(path): img_g = cv2.imread(path) if img_g is not None: gesture_images[n] = img_g else: print("[提示] 找不到手勢示意圖:{}".format(path)) === ========= 播放語音 ========= === def play_audio(number): """ 播放對應數字的國語 + 台語語音 需要 audio/zh_N.mp3, audio/tai_N.mp3 已存在 """ for lang in ["zh", "tai"]: fn = os.path.join("audio", "{}_{}.mp3".format(lang, number)) if os.path.exists(fn): try: pygame.mixer.music.load(fn) pygame.mixer.music.play() # 簡單阻塞等待播放完畢 while pygame.mixer.music.get_busy(): time.sleep(0.05) except Exception as e: print("[錯誤] 播放 {} 失敗:{}".format(fn, e)) else: print("[提示] 找不到音檔:{}".format(fn)) === ========= 手勢判斷(單手 1~9) ========= === def classify_single_hand_number(hand, fingers): """ 根據單手的 fingersUp 結果判斷數字 1~9 hand: 單手資訊 (包含 lmList, bbox, type 等) fingers: [thumb, index, middle, ring, pinky] (0/1) 回傳 (number, label) 或 (None, "None") """ pattern = tuple(fingers) # 固定手勢模式(不含「只伸食指」,那留給 1/9 特判) mapping = { (0, 1, 1, 0, 0): (2, "2"), # 食 + 中 (1, 1, 1, 0, 0): (3, "3"), # 拇 + 食 + 中 (0, 1, 1, 1, 1): (4, "4"), # 四指伸、拇指收 (1, 1, 1, 1, 1): (5, "5"), # 全部伸出 (1, 0, 0, 0, 1): (6, "6"), # 拇 + 小(shaka) (1, 1, 0, 0, 1): (7, "7"), # 拇 + 食 + 小 (1, 1, 0, 0, 0): (8, "8"), # 拇 + 食(手槍) } if pattern in mapping: return mapping[pattern] # --- 特判:1 或 9(只伸食指)--- if pattern == (0, 1, 0, 0, 0): lm_list = hand["lmList"] # 食指:MCP = 5, PIP = 6, DIP = 7, TIP = 8 mcp = lm_list[5] pip = lm_list[6] dip = lm_list[7] tip = lm_list[8] # 直線距離 MCP→TIP dist_mcp_tip = euclidean(mcp, tip) # 折線距離 MCP→PIP→DIP→TIP dist_mcp_pip = euclidean(mcp, pip) dist_pip_dip = euclidean(pip, dip) dist_dip_tip = euclidean(dip, tip) poly_len = dist_mcp_pip + dist_pip_dip + dist_dip_tip # 粗略規則:如果「折線 / 直線」比例大於 1.25,視為彎曲很多 = 9 if dist_mcp_tip > 0 and poly_len / dist_mcp_tip > 1.25: return (9, "9") else: return (1, "1") return (None, "None") === ========= 判斷整體數字(包含 10) ========= === def classify_number(hands): """ 根據全部手部資料判斷數字 1~10 回傳 (number, label, bbox_for_draw) """ if not hands: return None, "No Hand", None # 先判斷「10」:兩手都比 1 if len(hands) >= 2: nums = [] for h in hands[:2]: fingers = detector.fingersUp(h) n, _ = classify_single_hand_number(h, fingers) nums.append(n) if nums[0] == 1 and nums[1] == 1: bbox10 = hands[0]["bbox"] return 10, "10", bbox10 # 否則只看第一隻手的 1~9 main_hand = hands[0] bbox = main_hand["bbox"] fingers = detector.fingersUp(main_hand) number, label = classify_single_hand_number(main_hand, fingers) if number is None: return None, "Unknown", bbox else: return number, label, bbox === ========= 主迴圈 ========= === last_number = None # 上一次辨識結果,避免每幀都重播聲音 while cap.isOpened(): success, img = cap.read() if not success: break # 偵測手部 hands, img = detector.findHands(img) # 判斷數字 number, label, bbox = classify_number(hands) # 原始畫面大小 h, w, _ = img.shape # 建立右側教學面板(白底) panel_width = 320 panel = 255 * np.ones((h, panel_width, 3), dtype="uint8") # ===== 在教學面板顯示內容 ===== if number is not None and number in tai_map: # 顯示手勢示意圖(若存在) if number in gesture_images: g_img = gesture_images[number] gh, gw, _ = g_img.shape # 讓示意圖高度約畫面 60%,寬度約 panel 90% scale = min((h '' 0.6) / float(gh), (panel_width '' 0.9) / float(gw)) new_w = int(gw * scale) new_h = int(gh * scale) g_resized = cv2.resize(g_img, (new_w, new_h)) # 貼到 panel 置中(上方) x_offset = (panel_width - new_w) // 2 y_offset = 20 panel[y_offset:y_offset + new_h, x_offset:x_offset + new_w] = g_resized # 文字資訊(下半部) text_y = int(h * 0.75) cv2.putText(panel, str(number), (30, text_y), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 0), 2) cv2.putText(panel, "中文:{}".format(zh_map[number]), (30, text_y + 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 2) cv2.putText(panel, "台語:{}".format(tai_map[number]), (30, text_y + 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 2) else: cv2.putText(panel, "請比出 1~10 的手勢", (20, h // 2), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (128, 128, 128), 2) # 在原影像畫偵測框與標籤 if bbox is not None and number is not None: x, y, bw, bh = bbox cv2.rectangle(img, (x, y), (x + bw, y + bh), (0, 255, 0), 2) cv2.putText(img, label, (x, y - 10), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2) # 將攝影機畫面與教學面板合併(左右排) panel = cv2.resize(panel, (panel_width, h)) combined = cv2.hconcat([img, panel]) cv2.imshow("Hand Number Teaching (1-10) - Python 3.7", combined) # 若辨識數字有變化 → 播放語音 if number is not None and number in tai_map and number != last_number: last_number = number print("辨識到數字:", number) play_audio(number) # 按 q 離開 if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() </syntaxhighlight> ==== 如果你還沒產生 audio 資料夾的 mp3,可以用這支: ==== <syntaxhighlight lang="python"># generate_audio_gtts.py """ 使用說明: # 先安裝 gTTS: pip install gTTS # 把此檔案放在和 hand_gesture_1_10.py 同一層。 # 執行: python generate_audio_gtts.py # 會在 audio/ 底下產生: * zh_1.mp3 ~ zh_10.mp3 (國語) * tai_1.mp3 ~ tai_10.mp3(暫用 zh-TW TTS 念 POJ 文字,之後可自行錄音覆蓋) """ from gtts import gTTS import os numbers_zh = { 1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "七", 8: "八", 9: "九", 10: "十", } numbers_tai = { 1: "it", 2: "nn̄g", 3: "saⁿ", 4: "sì", 5: "gō͘", 6: "la̍k", 7: "chhit", 8: "peh", 9: "káu", 10: "chap", } out_dir = "audio" if not os.path.exists(out_dir): os.makedirs(out_dir) === 國語 === for n, word in numbers_zh.items(): text = word tts = gTTS(text=text, lang="zh-TW") fn = os.path.join(out_dir, "zh_{}.mp3".format(n)) print("產生:", fn) tts.save(fn) === 「台語」(實際仍用 zh-TW 念 POJ 文字,可之後自行錄音覆蓋) === for n, word in numbers_tai.items(): text = "台語 {}".format(word) tts = gTTS(text=text, lang="zh-TW") fn = os.path.join(out_dir, "tai_{}.mp3".format(n)) print("產生:", fn) tts.save(fn) print("全部產生完成!") </syntaxhighlight> ==== 你的資料夾要長這樣: ==== <syntaxhighlight lang="text">1-10 專案資料夾/ │ hand_gesture_1_10.py (或你原來的 1~10.py) │ generate_audio_gtts.py │ ├─gestures/ │ 1.png │ ... │ 10.png │ └─audio/ ← 跑 generate_audio_gtts.py 自動產生 zh_1.mp3 ~ zh_10.mp3 tai_1.mp3 ~ tai_10.mp3 </syntaxhighlight> 如果再跑的時候還有新的錯誤訊息,直接貼上來,我就對著那一行幫你修到能上課用 👍 === User: 現在 fingersUp 只能算數量。請幫我修改程式,增加台灣手勢的判斷:當『只有拇指和小指伸直』時,顯示數字 6 === 現在 fingersUp 只能算數量。請幫我修改程式,增加台灣手勢的判斷:當『只有拇指和小指伸直』時,顯示數字 6 === Assistant: OK,我直接在你一開始貼的那個「石頭剪刀布範例」上改, === OK,我直接在你一開始貼的那個「石頭剪刀布範例」上改, 讓 只有拇指+小指伸直([1,0,0,0,1])時顯示數字 6。 關鍵概念: * fingers = detector.fingersUp(hand) 會回傳 [thumb, index, middle, ring, pinky] * 台灣的 6 手勢 = fingers == [1, 0, 0, 0, 1] * 因為這種手勢伸出 2 根手指,所以 要在判斷「2 根手指=剪刀」之前就先特判 6,不然會被當成總數 2 的情況吃掉。 ==== <syntaxhighlight lang="python">from cvzone.HandTrackingModule import HandDetector ==== import cv2 cap = cv2.VideoCapture(0) detector = HandDetector(detectionCon=0.5, maxHands=1) while cap.isOpened(): success, img = cap.read() if not success: break hands, img = detector.findHands(img) msg = "None" if hands: hand = hands[0] bbox = hand["bbox"] fingers = detector.fingersUp(hand) # [thumb, index, middle, ring, pinky] totalFingers = fingers.count(1) print(fingers, " total:", totalFingers) # ★★★ 先判斷台灣 6:只有拇指和小指伸直 ★★★ if fingers == [1, 0, 0, 0, 1]: msg = "6" # 下面是原本的石頭剪刀布邏輯(可保留或改掉) elif totalFingers == 5: msg = "Paper" elif totalFingers == 0: msg = "Rock" elif totalFingers == 2: # 兩根手指,且是食指+中指 才當「剪刀」 if fingers[1] == 1 and fingers[2] == 1: msg = "Scissors" cv2.putText(img, msg, (bbox[0] + 200, bbox[1] - 30), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2) cv2.imshow("Image", img) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() </syntaxhighlight> ===== - if fingers == [1, 0, 0, 0, 1]: msg = "6" 這一段一定要放在「剪刀(totalFingers == 2)」之前。 ===== * 這樣一來: - 只伸拇指+小指 → 顯示 6 - 伸食指+中指 → 顯示 Scissors - 其他部分維持原本的 Rock / Paper 邏輯。 如果你接下來想: * 把 1~5 的手勢也改成「台灣版數字手勢判斷」 * 或乾脆全部改成「1~10 台灣數字手勢」的程式(不玩剪刀石頭布) 跟我說一聲,我可以幫你把完整 1~10 判斷表寫成一個乾淨的 if fingers == ... 模組給你用。 === User: 全部改成「1~10 台灣數字手勢」的程式 === 全部改成「1~10 台灣數字手勢」的程式 === Assistant: 好,我幫你把程式「全部改成 1~10 台灣數字手勢」, === 好,我幫你把程式「全部改成 1~10 台灣數字手勢」, 而且配合你現在的需求: * ✅ 用 cvzone.HandDetector + fingersUp() * ✅ 沒有聲音、沒有 pygame * ✅ Python 3.7 可用(沒有 math.dist) * ✅ 支援 台灣常見手勢 1~8 + 6 特別版(拇指+小指) * ✅ 9:用手指「彎曲程度」粗略區分(同樣是食指那種手勢時) * ✅ 10:用「兩手都比 5」當作 10(等於 5+5),方便教學 : ==== 用 fingersUp(hand) 取得 [拇指, 食指, 中指, 無名指, 小指]: ==== * 1 → [0,1,0,0,0](食指) * 2 → [0,1,1,0,0](食+中) * 3 → [1,1,1,0,0](拇+食+中) * 4 → [0,1,1,1,1](四指伸出,拇指收) * 5 → [1,1,1,1,1](五指全伸) * 6 → [1,0,0,0,1](拇+小,shaka 手勢) * 7 → [1,1,0,0,1](拇+食+小,這裡選一種台灣常見變形) * 8 → [1,1,0,0,0](拇+食,像手槍) * 9 → 手指 pattern 跟 1 一樣 [0,1,0,0,0],但明顯彎曲時視為 9 * 10 → 兩手同時都是 5(兩隻手全張開) ==== 請將整段貼到一個檔案,例如 taiwan_hand_number_1_10.py: ==== <syntaxhighlight lang="python">from cvzone.HandTrackingModule import HandDetector import cv2 import os import numpy as np === ========= 小工具:2D 距離(取代 math.dist,支援 Python 3.7) ========= === def euclidean(a, b): """ 計算 2D 歐幾里得距離 a, b: [x, y, (z)],這裡只用前兩個值 """ return ((a[0] - b[0])'''2 + (a[1] - b[1])'''2)**0.5 === ========= 初始設定 ========= === cap = cv2.VideoCapture(0) # 如有需要可改成 cv2.VideoCapture(0, cv2.CAP_DSHOW) detector = HandDetector(detectionCon=0.7, maxHands=2) === 中文數字對照(只是讓畫面顯示好看一點) === zh_map = { 1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "七", 8: "八", 9: "九", 10: "十", } === =========(可選)讀取手勢示意圖:gestures/1.png ~ 10.png ========= === gesture_images = {} for n in range(1, 11): path = os.path.join("gestures", f"{n}.png") if os.path.exists(path): img = cv2.imread(path) if img is not None: gesture_images[n] = img else: # 沒有圖也沒關係,只是少了右側示意圖 pass === ========= 單手台灣數字手勢 1~9 判斷 ========= === def classify_single_hand_number(hand): """ 根據單手手勢判斷 1~9(台灣數字手勢) 使用 fingersUp + 一點點關節角度判斷(區分 1 / 9) 回傳 (number, label, fingersPattern) """ fingers = detector.fingersUp(hand) # [thumb, index, middle, ring, pinky] pattern = tuple(fingers) # 固定 pattern 的 2~8(不含 1,因為 1 和 9 pattern 一樣,要特判) mapping = { (0, 1, 1, 0, 0): 2, # 食 + 中 (1, 1, 1, 0, 0): 3, # 拇 + 食 + 中 (0, 1, 1, 1, 1): 4, # 四指伸出,拇指收 (1, 1, 1, 1, 1): 5, # 五指全伸 (1, 0, 0, 0, 1): 6, # 拇 + 小(台灣 6) (1, 1, 0, 0, 1): 7, # 拇 + 食 + 小(台灣 7 的一種) (1, 1, 0, 0, 0): 8, # 拇 + 食(手槍,台灣 8) } # 先吃 mapping 裡的(2~8) if pattern in mapping: n = mapping[pattern] return n, str(n), fingers # ---- 只伸食指 → 可能是 1 或 9 ---- if pattern == (0, 1, 0, 0, 0): lm = hand["lmList"] # 食指:MCP = 5, PIP = 6, DIP = 7, TIP = 8 mcp = lm[5] pip = lm[6] dip = lm[7] tip = lm[8] # 直線距離 MCP→TIP dist_mcp_tip = euclidean(mcp, tip) # 折線距離 MCP→PIP→DIP→TIP dist_mcp_pip = euclidean(mcp, pip) dist_pip_dip = euclidean(pip, dip) dist_dip_tip = euclidean(dip, tip) poly_len = dist_mcp_pip + dist_pip_dip + dist_dip_tip # 比例 > 1.25 → 手指彎得比較明顯 → 當作 9 if dist_mcp_tip > 0 and poly_len / dist_mcp_tip > 1.25: return 9, "9", fingers else: return 1, "1", fingers # 其他 pattern 不判斷 return None, "Unknown", fingers === ========= 判斷整體數字(考慮 10) ========= === def classify_number(hands): """ hands: HandDetector 回傳的 hands 陣列 回傳 (number, label, bbox_for_draw) """ if not hands: return None, "No Hand", None # 若有兩手 → 有機會判斷「10」 if len(hands) >= 2: results = [] for h in hands[:2]: n, _, _ = classify_single_hand_number(h) results.append(n) # 這裡定義:兩隻手都比「5」 → 視為 10 if results[0] == 5 and results[1] == 5: bbox = hands[0]["bbox"] return 10, "10", bbox # 只有一手,或兩手但不符合 10 → 以第一隻手為主 main_hand = hands[0] bbox = main_hand["bbox"] number, label, _ = classify_single_hand_number(main_hand) return number, label, bbox === ========= 主迴圈 ========= === while True: success, img = cap.read() if not success: break hands, img = detector.findHands(img) number, label, bbox = classify_number(hands) h, w, _ = img.shape # 右側示意面板(可省略,不想要的話可以整塊刪掉) panel_width = 300 panel = 255 * np.ones((h, panel_width, 3), dtype="uint8") if number in zh_map: # 若有圖就貼圖 if number in gesture_images: g = gesture_images[number] gh, gw, _ = g.shape scale = min((h '' 0.6) / gh, (panel_width '' 0.9) / gw) nw = int(gw * scale) nh = int(gh * scale) g2 = cv2.resize(g, (nw, nh)) xo = (panel_width - nw) // 2 yo = 20 panel[yo:yo + nh, xo:xo + nw] = g2 # 底下寫上「阿拉伯 + 中文」 ty = int(h * 0.8) cv2.putText(panel, f"{number}", (20, ty), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 0), 2) cv2.putText(panel, f"中文:{zh_map[number]}", (20, ty + 35), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 0), 2) else: cv2.putText(panel, "請比出 1~10 的手勢", (20, h // 2), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (100, 100, 100), 2) # 在原影像上畫框&數字 if bbox and number: x, y, bw, bh = bbox cv2.rectangle(img, (x, y), (x + bw, y + bh), (0, 255, 0), 2) if number in zh_map: text = f"{number} ({zh_map[number]})" else: text = label cv2.putText(img, text, (x, y - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2) # 合併畫面:左邊攝影機,右邊示意 panel = cv2.resize(panel, (panel_width, h)) combined = cv2.hconcat([img, panel]) cv2.imshow("Taiwan Hand Number (1-10)", combined) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() </syntaxhighlight> ==== 1. 最少需求: - 安裝: ``<code>bash pip install opencv-python cvzone mediapipe </code>`<code> - 直接跑程式就可以了。 ==== # 如果想要右側有示意圖: - 在程式同一層資料夾下,放: </code>`<code>text gestures/ 1.png 2.png ... 10.png </code>`` - 不放也沒關係,只是右邊是純文字白板。 # 要看 debug 狀態: - 你可以在 classify_single_hand_number() 裡面加 print(fingers) 之類,幫學生看 pattern。 如果你願意下一步,我可以幫你: * 把 每個數字對應的 fingersUp pattern 做成一張表(給學生抄或 PPT 用) * 或把這個程式縮成「最簡版」(只有影像 + 左上角顯示數字)給學生改作業用。 === User: 給我辨識21個landmark的CVZone程式(不上是只能計算5隻手指) === 給我辨識21個landmark的CVZone程式(不上是只能計算5隻手指) === Assistant: 沒問題,我幫你做一支 專門看 21 個 landmark 的 CVZone 範例程式, === 沒問題,我幫你做一支 專門看 21 個 landmark 的 CVZone 範例程式, 不再只用 fingersUp(),而是直接讀 lmList 來顯示 21 個關節點。 ==== 這支程式會: ==== * 用 cvzone.HandTrackingModule.HandDetector 偵測手 * 取得 hand["lmList"](長度 21 的 list,每個是 [x, y, z]) * 在畫面上: - 把 21 個點畫出來 - 每個點旁邊標上編號 0~20 * 在終端機印出其中幾個重點點位(例如手腕 0、拇指尖 4、食指尖 8…) 非常適合拿來教學生「手部關節座標」跟後續自己算角度、距離。 ==== <syntaxhighlight lang="python">from cvzone.HandTrackingModule import HandDetector ==== import cv2 === 開啟攝影機(如有需要可改成 cv2.VideoCapture(0, cv2.CAP_DSHOW)) === cap = cv2.VideoCapture(0) === 建立手部偵測器 === detector = HandDetector(detectionCon=0.7, maxHands=1) while True: success, img = cap.read() if not success: break # 偵測手部,hands 會是一個 list,裡面每隻手是一個 dict hands, img = detector.findHands(img) # img 會自動畫出骨架 if hands: # 只拿第一隻手 hand = hands[0] # lmList:21 個 landmark 的座標,每個元素是 [x, y, z] lmList = hand["lmList"] # 長度 21 bbox = hand["bbox"] # [x, y, w, h] center = hand["center"] # 手掌中心點 [cx, cy] handType = hand["type"] # "Left" or "Right" # 在畫面左上角顯示手的類型 cv2.putText(img, handType, (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2) # 把 21 個點畫出來並標上 index for i, lm in enumerate(lmList): x, y, z = lm # 畫一個小圓點 cv2.circle(img, (x, y), 5, (0, 0, 255), cv2.FILLED) # 在旁邊寫上編號 cv2.putText(img, str(i), (x + 5, y - 5), cv2.FONT_HERSHEY_PLAIN, 1, (255, 255, 0), 1) # 也可以在終端機印出幾個重點 landmark wrist = lmList[0] # 手腕 thumb_tip = lmList[4] # 拇指尖 index_tip = lmList[8] # 食指尖 middle_tip = lmList[12] # 中指尖 print("手型:", handType) print("手腕(0):", wrist) print("拇指尖(4):", thumb_tip) print("食指尖(8):", index_tip) print("中指尖(12):", middle_tip) print("-" * 30) cv2.imshow("Hand 21 Landmarks (CVZone)", img) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() </syntaxhighlight> ==== 有了 lmList 之後,你就可以自己玩很多東西,例如: ==== * 算兩個 landmark 之間的距離(例如拇指尖到食指尖) * 算三個點的夾角,當作「彎曲程度」條件 * 自己定義「某幾個點在某種相對位置 → 自訂手勢」 如果你接下來想要: * 示範:怎麼用 21 個點算出「手指彎不彎」 * 或 寫一個 function:給你 lmList,回傳食指彎曲角度 跟我說一聲,我可以再幫你往下寫一層「運算思維示範版」。 === User: 寫一層「運算思維示範版」 === 寫一層「運算思維示範版」 === Assistant: 可以,來做一個「運算思維示範版」👍 === 可以,來做一個「運算思維示範版」👍 重點是:不只是畫 21 個點,而是 利用 landmark 做「計算 → 判斷 → 給回饋」。 我幫你加兩個示範任務: # 任務 A:判斷食指是「伸直」還是「彎曲」 - 用 index finger 的三個關節(MCP=5, PIP=6, DIP=7) - 算出在 PIP(6)這個點的「夾角」 - 角度大 ≈ 伸直,小 ≈ 彎曲 # 任務 B:判斷「拇指尖 + 食指尖」是否靠很近(Pinch) - 用 Thumb tip (4)、Index tip (8) - 算兩點距離,小於門檻就當作「捏起來」 這兩個都非常適合教「運算思維」四步驟: 👉 1. 取得資料(21 個點) 👉 2. 選關鍵點 👉 3. 做計算(距離 / 角度) 👉 4. 設條件,給出文字回饋 ==== 下面這支程式會: ==== * 顯示 21 個 landmark + 編號 * 在畫面左上角顯示: - 食指角度(度數)+「伸直/彎曲/介於中間」 - 拇指尖~食指尖距離+是否「Pinch」 <syntaxhighlight lang="python">from cvzone.HandTrackingModule import HandDetector import cv2 import math === ===== 小工具:距離與角度 ===== === def dist2d(a, b): """計算 2D 距離(不使用 math.dist,支援 Python 3.7)""" return ((a[0] - b[0]) ''' 2 + (a[1] - b[1]) ''' 2) ** 0.5 def angle_between(p1, p2, p3): """ 計算夾角 ∠p1-p2-p3(以 p2 為頂點) 回傳角度(degree) """ # 向量 v1 = p1 - p2, v2 = p3 - p2 v1 = (p1[0] - p2[0], p1[1] - p2[1]) v2 = (p3[0] - p2[0], p3[1] - p2[1]) # 內積 v1·v2 dot = v1[0] '' v2[0] + v1[1] '' v2[1] # 長度 |v1|, |v2| len1 = math.sqrt(v1[0] ''' 2 + v1[1] ''' 2) len2 = math.sqrt(v2[0] ''' 2 + v2[1] ''' 2) if len1 == 0 or len2 == 0: return 0.0 cosang = dot / (len1 * len2) # 避免浮點數誤差造成超出 [-1, 1] cosang = max(-1.0, min(1.0, cosang)) ang = math.degrees(math.acos(cosang)) return ang === ===== CVZone 初始化 ===== === cap = cv2.VideoCapture(0) # 如有需要可改成 cv2.VideoCapture(0, cv2.CAP_DSHOW) detector = HandDetector(detectionCon=0.7, maxHands=1) while True: success, img = cap.read() if not success: break hands, img = detector.findHands(img) # img 會自動畫骨架 info_lines = [] # 要顯示在畫面左上的文字行 if hands: hand = hands[0] lmList = hand["lmList"] # 21 個 [x,y,z] handType = hand["type"] # "Left" or "Right" # --- 把 21 點畫出來 + 編號(可當教學用)--- for i, lm in enumerate(lmList): x, y, z = lm cv2.circle(img, (x, y), 5, (0, 0, 255), cv2.FILLED) cv2.putText(img, str(i), (x + 5, y - 5), cv2.FONT_HERSHEY_PLAIN, 1, (255, 255, 0), 1) # ===== 任務 A:判斷食指彎曲程度 ===== # 食指關節點:MCP=5, PIP=6, DIP=7 p_mcp = lmList[5] p_pip = lmList[6] p_dip = lmList[7] ang_index = angle_between(p_mcp, p_pip, p_dip) # 簡單規則: # 角度 > 150 度 → 幾乎筆直 # 角度 < 120 度 → 明顯彎曲 # 介於中間 → 半彎 if ang_index > 150: index_state = "伸直" elif ang_index < 120: index_state = "彎曲" else: index_state = "介於直/彎之間" info_lines.append("食指角度: {:.1f} 度 ({})".format(ang_index, index_state)) # ===== 任務 B:判斷拇指尖 + 食指尖距離(Pinch or not) ===== thumb_tip = lmList[4] # 拇指尖 index_tip = lmList[8] # 食指尖 d_thumb_index = dist2d(thumb_tip, index_tip) # 簡單門檻:距離 < 40 pixel 視為 Pinch # (實際可視鏡頭解析度調整) if d_thumb_index < 40: pinch_state = "Pinch (捏住)" else: pinch_state = "分開" info_lines.append("拇指尖-食指尖距離: {:.1f} ({})".format(d_thumb_index, pinch_state)) # 也可印在 console 給老師 debug # print(info_lines[0]) # print(info_lines[1]) # ===== 把資訊畫到畫面左上角 ===== y0 = 30 for line in info_lines: cv2.putText(img, line, (10, y0), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) y0 += 25 cv2.imshow("Hand 21 Landmarks - 運算思維示範版", img) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() </syntaxhighlight> ==== 可以直接拿這支程式跟學生拆解: ==== ===== <syntaxhighlight lang="python">lmList = hand["lmList"] ===== </syntaxhighlight> * 這裡得到的是: [(x0,y0,z0), (x1,y1,z1), ..., (x20,y20,z20)] * 運算思維重點:先知道系統給你的原始資料長什麼樣子 ===== - 食指角度 → 只用 5, 6, 7 三個點 ===== * Pinch → 只用 4, 8 兩個點 => 從 21 點裡挑出「對解題有用的那幾點」 ===== - 距離: dist = sqrt((x1-x2)^2 + (y1-y2)^2) ===== * 角度: 利用內積公式: cosθ = (v1·v2) / (|v1||v2|) → θ = arccos(...) 可以讓學生自己寫在學習單上,或用白板演算。 ===== - 如果 角度 > 150 → 顯示「伸直」 ===== * 如果 距離 < 40 → 顯示「Pinch」 這邊很適合讓學生自己改數字,試看看效果: * 把 150 改成 140,看「伸直」變多還是變少 * 把 40 改成 60,看「Pinch」變容易還是變困難 如果你願意,我也可以幫你: * 把這段「運算思維示範」改寫成 學習單題目(引導學生填空) * 或產出一份 PPT 架構(每一頁拆一個步驟:資料 → 抽象 → 演算法 → 條件判斷) 你想往「教案 / 學習單 / PPT」哪一邊走,我可以直接幫你接下一步。 === User: 都要 === 都要 === Assistant: 好,我一次把四樣都給你,直接可以拿去上課/改作業用 👇 === 好,我一次把四樣都給你,直接可以拿去上課/改作業用 👇 ==== ### ==== : ===== 1. 左側大圖:一隻手的示意圖 - 建議用白底線條風格的手掌圖(掌心朝鏡頭)。 - 在每個關節位置畫出圓點 + 編號:0~20。 - 點的顏色: - 手腕 0:深藍 - 拇指 1–4:橘色 - 食指 5–8:綠色 - 中指 9–12:紫色 - 無名指 13–16:紅色 - 小指 17–20:藍色 ===== # 右側表格:Landmark 對照表(可以做成 2 欄或 3 欄) | | | | | --- | --- | --- | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | # 下方說明區(運算思維連結) - 小標題:「電腦怎麼看你的手?」 - 文字建議: - 電腦不會直接「看懂手勢」,它只看見 21 個點的座標 (x, y)。 - 透過「距離」、「角度」、「相對位置」,我們可以自己設計規則, 讓電腦判斷:手指是彎的?直的?兩指靠很近? - 這就是從「資料 → 計算 → 規則 → 判斷」的 運算思維。 ==== 你可以直接貼到 Word / Google Docs,調整行距即可: ==== ===== #### ===== # 問題一: (1)請先執行老師提供的程式,觀察畫面中手上的 編號 0~20。 (2)請在下圖或空白處,畫出一隻手,並標出你觀察到的 3 個重要點(寫出編號與位置說明)。 - 重要點 1:編號 **''',位置說明:''''''______________________ - 重要點 2:編號 '''''',位置說明:''''''______________________ - 重要點 3:編號 '''''',位置說明:'''**______________________ # 問題二: 當你伸出食指時,哪幾個點看起來特別重要?請圈選: ☐ 5 ☐ 6 ☐ 7 ☐ 8 ☐ 其它:__________ 為什麼?______________________________________________________ ====== 1. 問題三: 如果我要判斷「食指是伸直還是彎曲」, 你覺得只需要哪 3 個點 就夠用?請寫出編號: - 點 A(起點):______ - 點 B(中間關節):______ - 點 C(末端):______ ====== # 問題四: 請用自己的話說明:「為什麼只要用這 3 個點,就可以判斷食指有沒有彎曲?」 ====== 1. 問題五(距離): 老師示範用公式 (x1−x2)2+(y1−y2)2(x1 - x2)^2 + (y1 - y2)^2(x1−x2)2+(y1−y2)2 的平方根當作「兩點距離」。 請試著寫出 「拇指尖 (4) 到 食指尖 (8) 的距離」 的計算式(只需寫概念,不用真的算值): 距離 d = _________________________________________________ ====== # 問題六(角度): 老師示範「夾角越大 → 越直;夾角越小 → 越彎」。 請用文字或圖畫說明:「在點 B 處(例如食指第 1 關節),如何用三個點 A、B、C 來決定角度?」 ====== 1. 問題七: 如果我們用「角度 > 150 度當作伸直」, 「角度 < 120 度當作彎曲」, 請完成下表: | | | | | --- | --- | --- | | | | | | | | | | | | | ====== # 問題八(設計自己的規則): 請你自己設計一個「捏住東西(Pinch)」的判斷條件,例如: - 兩點:拇指尖 (4)、食指尖 (8) - 當距離 d < ______ 像素時,顯示文字「Pinch!」 請在下面寫出你的規則說明: ====== 1. 問題九: 你覺得「電腦看手勢」比較像: ☐ 自己看一張照片、立刻懂意思 ☐ 看很多數字,再用數學和規則算出來 為什麼?______________________________________________________ ====== # 問題十: 你覺得這樣的「用資料 + 規則讓電腦做判斷」的想法, 可以應用在哪些地方?至少寫出兩個例子。 - 例子 1:__________________________________ - 例子 2:__________________________________ ==== 這一版是「刻意留空」,讓學生填入距離 / 角度 / 判斷條件。 ==== 你可以把 TODO 故意留著,當回家作業或課堂練習。 <syntaxhighlight lang="python">from cvzone.HandTrackingModule import HandDetector import cv2 import math === ===== TODO:請學生完成 ===== === def dist2d(a, b): """ TODO1: 計算 2D 距離 提示:用 (x1 - x2)^2 + (y1 - y2)^2 再開根號 a, b: (x, y) """ # 在這裡完成程式碼 # return ... return 0 # 先放假值,避免整支當掉 def angle_between(p1, p2, p3): """ TODO2: 計算三點 ∠p1-p2-p3 的角度 提示:使用內積公式:cosθ = (v1·v2)/(|v1||v2|),再用 math.acos 反三角 """ # 在這裡完成程式碼 # 回傳角度(單位:度) return 0.0 # 先放假值 cap = cv2.VideoCapture(0) detector = HandDetector(detectionCon=0.7, maxHands=1) while True: success, img = cap.read() if not success: break hands, img = detector.findHands(img) info_lines = [] if hands: hand = hands[0] lmList = hand["lmList"] # 手腕到食指尖的距離示範 wrist = lmList[0] index_tip = lmList[8] d_wrist_index = dist2d((wrist[0], wrist[1]), (index_tip[0], index_tip[1])) # TODO3: 設計一個距離門檻,當距離小於某值時顯示「靠很近」 # 例如:if d_wrist_index < 100: state = "靠很近" else: state = "很遠" state_distance = "請同學自己設計規則" info_lines.append("手腕-食指尖距離: {:.1f} ({})" .format(d_wrist_index, state_distance)) # 食指彎曲角度示範(用 5,6,7) p5 = (lmList[5][0], lmList[5][1]) p6 = (lmList[6][0], lmList[6][1]) p7 = (lmList[7][0], lmList[7][1]) ang_index = angle_between(p5, p6, p7) # TODO4: 設計「伸直 / 彎曲 / 中間」的門檻 # 例如:>150 伸直、<120 彎曲、其它中間 state_angle = "請同學自己設計規則" info_lines.append("食指角度: {:.1f} 度 ({})" .format(ang_index, state_angle)) # 顯示畫面上的文字 y0 = 30 for line in info_lines: cv2.putText(img, line, (10, y0), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) y0 += 25 cv2.imshow("運算思維作業版:請同學完成 TODO", img) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() </syntaxhighlight> : ==== 設計概念: ==== * 螢幕左上角顯示:「請比出:X」(X 為 1~5 或 1~8,也可以改 1~10) * 使用前面「台灣數字手勢」中的 fingersUp 規則 * 學生比出正確手勢 → 顯示「✔ 正確!」,得分 +1,換下一題 * 10 秒內連對幾題可以顯示「Combo!」之類(你可再加) 下面先做一個 1~5 簡化版(讓學生較容易成功),你之後可以擴充到 1~8 或 1~10: <syntaxhighlight lang="python">import cv2 import random import time from cvzone.HandTrackingModule import HandDetector cap = cv2.VideoCapture(0) detector = HandDetector(detectionCon=0.7, maxHands=1) === 題目目前只用 1~5(方便學生) === valid_numbers = [1, 2, 3, 4, 5] current_target = random.choice(valid_numbers) last_change_time = time.time() score = 0 feedback = "" # 顯示「正確 / 再試一次」 def classify_taiwan_number(fingers): """ 台灣手勢簡化版 1~5: 這裡先用 fingersUp 的 pattern: 1: [0,1,0,0,0] 2: [0,1,1,0,0] 3: [1,1,1,0,0] 4: [0,1,1,1,1] 5: [1,1,1,1,1] 其它回傳 None """ pattern = tuple(fingers) mapping = { (0,1,0,0,0): 1, (0,1,1,0,0): 2, (1,1,1,0,0): 3, (0,1,1,1,1): 4, (1,1,1,1,1): 5, } return mapping.get(pattern, None) while True: success, img = cap.read() if not success: break hands, img = detector.findHands(img) detected_number = None if hands: hand = hands[0] fingers = detector.fingersUp(hand) detected_number = classify_taiwan_number(fingers) # 在畫面顯示目前辨識到的數字(debug 用) if detected_number is not None: cv2.putText(img, f"你現在的手勢:{detected_number}", (10, 100), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2) else: cv2.putText(img, "無法辨識為 1~5 手勢", (10, 100), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) # 判斷是否答對 if detected_number == current_target: feedback = "✔ 正確!" score += 1 # 換下一題 current_target = random.choice(valid_numbers) last_change_time = time.time() else: # 只有在有偵測到數字時才顯示錯誤 if detected_number is not None: feedback = "再試一次!" # 顯示題目 cv2.putText(img, f"請比出:{current_target}", (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) # 顯示得分 cv2.putText(img, f"Score: {score}", (10, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2) # 顯示回饋 if feedback: cv2.putText(img, feedback, (10, 140), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2) cv2.imshow("數字手勢訓練遊戲 (1~5)", img) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() </syntaxhighlight> : 如果你願意,我也可以幫你把: * 完整版 1~10 台灣手勢判斷函式 抽成一個獨立 .py(讓學生 import 使用) * 或幫你把上述學習單排成「正式 A4 版(含題號、配分建議)」的文字草稿,再貼進 Word 排版。
Summary:
Please note that all contributions to freem are considered to be released under the Creative Commons Attribution-ShareAlike 4.0 (see
Freem:Copyrights
for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource.
Do not submit copyrighted work without permission!
Cancel
Editing help
(opens in new window)