简介:这份合辑面向计算机视觉与人工智能方向的初学者及有一定 Python 基础的开发者,围绕手势识别、虚拟键盘、姿态检测等典型应用,提供经过调试、可直接运行的实践案例,帮助读者理解从摄像头采集、关键点检测到交互控制的完整流程。压缩包共收录 35 个文件,约 16.91MB,以 Python 脚本为主体,配有 JPG、PNG 示意图与运行效果图,并包含 Arduino 固件与电路相关文件,便于软硬件联调时对照使用。已有 436 人学习,说明其在入门实践与课程设计场景中具有一定参考价值。内容覆盖手部追踪、面部网格、手指计数、音量手势控制、虚拟鼠标与虚拟键盘等方向,模块划分较清晰,既适合按主题拆解学习,也方便在现有代码上做二次开发与功能扩展,能够为视觉交互项目提供可复用的实现思路与调试参照。
1. 先跑手势识别而不是虚拟键盘:cvzone 部分合辑的启动顺序
拿到一份 cvzone 部分合辑,目录里手势识别、虚拟键盘、姿态检测混在一起,最先跑哪个入口,往往决定后面要不要返工。我的建议反着来:先跑手势识别,不要先碰虚拟键盘。手势识别把摄像头采集、21 个手部关键点、像素坐标映射三件事一次打通,虚拟键盘只是在这套坐标上做矩形碰撞;姿态检测换一套关键点索引,但采集链路完全复用。对刚接触计算机视觉的 Python 开发者,这条顺序能少走很多弯路;对熟手,合辑的价值在于几个入口共用一套检测器和摄像头循环,改参数时知道边界在哪。下面按环境、手势、键盘、姿态、排查、验证的顺序拆,每一步都落到能直接跑的代码和参数上。
2. 环境与摄像头链路:把 cvzone 合辑跑到稳定 30 FPS 的最小闭环
2.1 依赖安装与最小自检:先确认 cv2、cvzone、mediapipe 能同时导入
合辑里的入口脚本通常只做两件事:打开摄像头,调用 cvzone 封装好的检测器。依赖没装齐时,报错往往出现在import那一行,而不是业务代码里,所以先做一个最小自检,比直接跑 demo 更省时间。我一般会新建虚拟环境,不往系统 Python 里堆包,后面换机器复现也干净。
python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate python -m pip install --upgrade pip pip install opencv-python cvzone mediapipe这段命令的逻辑是:先建隔离环境,再升级 pip,最后装三个核心包。opencv-python负责摄像头读取和绘图,cvzone提供 HandTrackingModule、PoseModule 这类封装,mediapipe是底层关键点检测依赖。参数上不用急着锁版本,先把导入跑通;如果机器上已经有旧版 OpenCV,建议在虚拟环境里重装,避免cvzone调用到不兼容的接口。
python -c "import cv2, cvzone, mediapipe; print(cv2.__version__); print(cvzone.__file__)"这条自检命令会打印 OpenCV 版本和 cvzone 的安装路径。能同时导入,说明依赖链路基本通了;如果cvzone报找不到模块,先看当前终端是不是还在虚拟环境里。血泪经验是:IDE 里选错解释器,终端装完能跑,点运行按钮又报错,先排查这个比改代码快。
提示:装完先跑一次自检,不要直接开摄像头;导入失败和摄像头失败是两类问题,混在一起排查很容易绕远。
2.2 合辑目录怎么读:入口文件、模块文件与资源文件的分工
合辑里的文件命名不一定统一,但职责通常能按类型分开。先找入口脚本,再看它 import 了谁,最后确认素材路径。下面这张表是我读这类合辑时的顺序,不涉及具体文件数量,只按类型判断。
| 目录/文件类型 | 典型命名 | 作用 | 首次运行注意 |
|---|---|---|---|
| 入口脚本 | main.py、demo_*.py | 启动某个功能 | 先看cv2.VideoCapture(0)的编号 |
| 模块封装 | HandTrackingModule.py、PoseModule.py | 封装检测器与绘制 | 确认和入口脚本在同一级或包路径正确 |
| 素材目录 | images/、audio/ | 键盘背景、音效、贴图 | 路径别带中文和空格 |
| 配置/参数 | config.py、args | 分辨率、阈值、摄像头编号 | 先保持默认,跑通再改 |
读目录时先回答三个问题:入口脚本用的是哪个摄像头编号,检测器是在循环外创建还是循环内创建,键盘坐标是不是写死在 640×480。前两个问题决定性能,第三个问题决定你改分辨率后键盘会不会错位。常见做法是先把入口脚本里的VideoCapture(0)和imshow跑通,再去看模块文件里的阈值。
2.3 摄像头采集链路:分辨率、镜像与 FPS 的三个参数
摄像头链路是后面所有模块的地基。分辨率太高,手势和姿态检测会掉帧;太低,指尖坐标又不够细。我一般先用 640×480 跑通,再根据机器性能往上调。镜像只影响显示和坐标方向,不影响检测本身,但虚拟键盘的左右手判定会受它影响,所以要从一开始就固定下来。
import cv2 cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30) while True: ok, frame = cap.read() if not ok: break frame = cv2.flip(frame, 1) # 镜像,符合用户照镜子的直觉 cv2.putText(frame, f"{int(cap.get(cv2.CAP_PROP_FPS))} FPS", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow("cvzone demo", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()这段代码的逻辑是:打开默认摄像头,设置采集宽高和帧率,逐帧读取并镜像显示,按 q 退出。参数上,VideoCapture(0)是默认摄像头,外接摄像头可以试 1 或 2;CAP_PROP_FRAME_WIDTH/HEIGHT是请求值,不是所有摄像头都接受,跑起来后可以用cap.get打印实际值;flip(frame, 1)做左右镜像,手势和键盘都建议统一用镜像后的画面。
注意:
cap.set设置的分辨率可能被摄像头驱动改成最近的支持值。键盘坐标如果按 640×480 写死,先打印实际宽高,确认没有被改成 1280×720。
2.4 HandTrackingModule 的坐标体系:归一化坐标到像素坐标
cvzone 的 HandTrackingModule 把检测和绘制包在一起,入口脚本通常只拿hands[0]["lmList"]。这个列表里是 21 个手部关键点,每个点是[x, y, z],x、y 已经是像素坐标,不是 0 到 1 的归一化值。食指指尖是 8 号点,拇指指尖是 4 号点,做虚拟键盘和手势判定都绕不开这几个索引。
import cv2 from cvzone.HandTrackingModule import HandDetector cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) detector = HandDetector(detectionCon=0.8, maxHands=1) while True: ok, frame = cap.read() if not ok: break frame = cv2.flip(frame, 1) hands, frame = detector.findHands(frame, draw=True) if hands: lmList = hands[0]["lmList"] x, y = lmList[8][0], lmList[8][1] # 食指指尖 cv2.circle(frame, (x, y), 12, (0, 255, 0), cv2.FILLED) cv2.putText(frame, f"{x},{y}", (x + 15, y - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow("hand", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()这段代码的逻辑是:创建 HandDetector,逐帧调用findHands,从返回的 hands 列表里取第一只手的 21 个关键点,把 8 号点画出来并显示坐标。参数上,detectionCon=0.8是检测置信度阈值,调高更保守,手抖或光照差时容易漏检;maxHands=1只检测一只手,减少误检和算力,做虚拟键盘时通常够用。findHands返回两个值:hands 列表和绘制后的 frame,后者可以直接 imshow。
2.5 手势判定代码与去抖:先看手指伸直,再谈组合手势
手势判定的第一步不是写复杂逻辑,而是把每根手指的伸直状态算出来。食指到小指可以用指尖 y 和近端指关节 y 比较,因为画面坐标 y 轴向下,指尖 y 更小就是伸直;大拇指是横向比较,左右手镜像后逻辑会反过来,最好结合hands[0]["type"]判断。
def fingers_up(lmList, hand_type="Right"): fingers = [] # 大拇指:横向比较,左右手镜像后方向相反 if hand_type == "Right": fingers.append(1 if lmList[4][0] < lmList[3][0] else 0) else: fingers.append(1 if lmList[4][0] > lmList[3][0] else 0) # 食指到小指:指尖 y 小于近端指关节 y 视为伸直 for tip, pip in [(8, 6), (12, 10), (16, 14), (20, 18)]: fingers.append(1 if lmList[tip][1] < lmList[pip][1] else 0) return fingers这段函数的逻辑是:先按左右手判断大拇指,再用四组指尖/指关节比较判断其余四指。参数上,tip和pip是 MediaPipe 的手部关键点索引,8/6 是食指,12/10 是中指,16/14 是无名指,20/18 是小指;实际代码里可以加 5 到 10 像素容差,避免指尖刚好和指关节齐平时来回跳。
单帧判定会抖,尤其是手指边缘和光照变化时。我一般加一个连续帧确认:同一组手指状态连续出现 3 帧才切换,30 FPS 下大约 100 毫秒,既抗抖又不拖沓。
stable_count = 0 last_state = None current_gesture = None # 在循环里拿到 fingers 后 state = tuple(fingers) if state == last_state: stable_count += 1 else: stable_count = 0 last_state = state if stable_count >= 3: current_gesture = state这段逻辑是:状态不变就累加计数,状态一变就清零重来,连续 3 帧一致才认。参数上,3是去抖帧数,调大更稳但手势切换变慢,调小更灵敏但容易误判;如果摄像头帧率低于 20,建议调到 4 或 5。
3. 虚拟键盘:从指尖坐标到可点击键位的四步映射
3.1 键盘布局:把 640×480 画面切成按键矩形
虚拟键盘的本质是“矩形碰撞 + 状态机”,布局先定死,后面调参才有参照。640×480 下我一般把键盘放在画面下半部,上半部留给手势,按键宽度 45 到 55 像素,太小手指点不中,太大遮挡画面。下面是一套可直接改的布局代码,三行字母键加一行特殊键,坐标都基于 640×480。
keys = [list("QWERTYUIOP"), list("ASDFGHJKL;"), list("ZXCVBNM,./")] key_w, key_h, gap = 50, 50, 5 start_x, start_y = 47, 290 button_list = [] for r, row in enumerate(keys): for c, key in enumerate(row): x = start_x + c * (key_w + gap) y = start_y + r * (key_h + gap) button_list.append((key, x, y, key_w, key_h)) # 特殊键单独一行,宽度按功能拉开 special = [ ("SPACE", 47, 455, 200, 50), ("BACK", 252, 455, 120, 50), ("CLEAR", 377, 455, 120, 50), ] button_list.extend(special)这段代码的逻辑是:按行生成字母键矩形,再追加空格、退格、清空三个特殊键。button_list里每个元素是(标签, x, y, w, h),后面绘制和碰撞都用这个结构。参数上,key_w/key_h控制按键大小,gap控制间距,start_x/start_y控制整体位置;改分辨率时这三个值要同步改,否则键盘会偏出画面。
3.2 绘制与碰撞检测:指尖落在哪个矩形里
绘制和碰撞要共用同一套矩形,不要一边画一套、判断另一套。绘制时给 hover 的按键换颜色,用户能看到指尖当前落在哪;碰撞判断用食指指尖坐标,边界建议用开区间,快速移动时再加历史点插值,减少漏判。
def draw_keyboard(frame, button_list, hover_key=None): for key, x, y, w, h in button_list: color = (0, 200, 0) if key == hover_key else (50, 50, 50) cv2.rectangle(frame, (x, y), (x + w, y + h), color, cv2.FILLED) cv2.rectangle(frame, (x, y), (x + w, y + h), (200, 200, 200), 2) scale = 0.8 if len(key) == 1 else 0.6 cv2.putText(frame, key, (x + 8, y + h - 15), cv2.FONT_HERSHEY_SIMPLEX, scale, (255, 255, 255), 2) def hit_key(px, py, button_list): for key, x, y, w, h in button_list: if x < px < x + w and y < py < y + h: return key, (x, y, w, h) return None, Nonedraw_keyboard的逻辑是遍历按键矩形,hover 时用绿色填充,其余用深灰,再画边框和标签;hit_key的逻辑是判断点是否落在矩形内,命中就返回键名和矩形。参数上,hover_key由上一帧的指尖位置决定,px/py是食指指尖像素坐标。注意len(key) == 1用来区分字母键和 SPACE/BACK 这类长标签,字体大小要分开调。
3.3 点击判定:食指与中指距离阈值加状态机
虚拟键盘最容易翻车的地方是“点一次输入一串”。原因是捏合动作会保持多帧,如果每帧都触发,就会连发。正确做法是边沿触发:距离小于阈值时只认第一次,必须等距离回到阈值以上才允许下一次;再加一个冷却时间,防止手指抖动反复触发。
import math last_key = None pressed = False cooldown_until = 0 CLICK_DIST = 38 COOLDOWN = 0.25 while True: ok, frame = cap.read() if not ok: break frame = cv2.flip(frame, 1) hands, frame = detector.findHands(frame, draw=True) now = cv2.getTickCount() / cv2.getTickFrequency() hover = None if hands: lmList = hands[0]["lmList"] px, py = lmList[8][0], lmList[8][1] key, rect = hit_key(px, py, button_list) if key: hover = key mx, my = lmList[12][0], lmList[12][1] dist = math.hypot(px - mx, py - my) if dist < CLICK_DIST and not pressed and now > cooldown_until: pressed = True cooldown_until = now + COOLDOWN print("click:", key) # 这里接文本输出 elif dist >= CLICK_DIST: pressed = False draw_keyboard(frame, button_list, hover) cv2.imshow("keyboard", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break这段代码的逻辑是:先算 hover 键,再算食指和中指指尖距离,小于CLICK_DIST且当前不是按下状态、冷却已过,才触发一次点击;距离回到阈值以上时复位pressed。参数上,CLICK_DIST=38是捏合阈值,手指粗或摄像头远时可以调到 45,调太大容易误触;COOLDOWN=0.25是冷却秒数,调大输入变慢,调小容易连发;now用 tick 计数换秒,比time.time()更轻。
3.4 文本输出:退格、空格、清空与显示截断
点击事件要落到一个字符串上,特殊键单独处理。退格用切片,清空直接置空,空格追加一个字符;显示时只取最后 30 个字符,避免文本溢出画面。大小写可以先不做,合辑里如果没有 SHIFT 键,硬加反而增加误触。
text = "" def apply_key(text, key): if key == "BACK": return text[:-1] if key == "CLEAR": return "" if key == "SPACE": return text + " " return text + key # 在点击分支里 text = apply_key(text, key) cv2.putText(frame, text[-30:], (50, 80), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), 3)这段逻辑是:按特殊键分支处理,普通键直接追加;显示时用text[-30:]截断。参数上,30是显示长度,按画面宽度调;字体大小1.0和线宽3保证在摄像头画面里看得清。BACK对空字符串切片不会报错,返回空字符串,适合快速连按。
3.5 防误触参数怎么调:阈值、冷却与按键尺寸
虚拟键盘的体验主要由三个参数决定:捏合距离、冷却时间、按键尺寸。调参时不要同时改三个,一次只动一个,用同一段手势录屏对比。下面这张表是我常用的起点,具体值按摄像头高度和手的大小微调。
| 参数 | 建议起点 | 调大后果 | 调小后果 |
|---|
本文还有配套的精品资源,点击获取