简介:这份合辑面向已具备Python基础、希望上手计算机视觉与人工智能应用的学习者,围绕手势识别、虚拟键盘、姿态检测等典型场景组织调试通过的示例,可用于课程实验、毕业设计选题参考与个人项目练手。压缩包共35个文件,约16.91MB,包含16个py源码、11张jpg示意图、6个ino单片机程序及少量pyc与png文件,兼顾Python视觉算法、Arduino联动与运行效果展示。已有436人学习,说明其在入门到进阶练习中具备一定参考价值。读者可从中了解摄像头采集、手部关键点跟踪、隔空输入、音量手势控制、虚拟鼠标与面部网格等模块的实现思路,并借鉴模块拆分与调试排错方式,快速搭建可运行的计算机视觉小项目。
1. 从摄像头前的捏合动作说起:这套 Python 计算机视觉合辑能做什么
摄像头一开,食指和拇指一捏,屏幕上的方块被拖走;手掌一抬,虚拟键盘从桌面上弹出来;人往左偏,姿态检测给出肩线角度。这套 Python 计算机视觉交互合辑,把人工智能里最容易被讲玄的三个方向——手势识别、虚拟键盘、姿态检测——做成了能直接跑的实验入口。它适合已经会一点 Python、想用 OpenCV 和 MediaPipe 做课程设计或交互原型的人,也适合想拆一套完整摄像头项目骨架的开发者。每个入口只保留一条最小链路,跑通后再加规则,不用先啃大框架。
2. 技术栈与目录结构:手势、键盘、姿态三条链怎么拆
这套合辑不是一个大而全的框架,更像三个可以独立启动的小项目拼在一起:手势入口负责手部关键点和捏合状态,虚拟键盘入口负责把关键点映射到画面里的键盘区域,姿态检测入口负责人体关键点和规则判断。三条链共用摄像头采图、窗口渲染和基础数学工具。读代码时先别急着从第一行往下翻,先看每个入口的输入输出,再回到公共模块看它们怎么复用。
2.1 技术选型:OpenCV 采图,MediaPipe 出关键点,cvzone 做轻封装
技术栈很集中,基本围绕摄像头实时 inference 展开。OpenCV 负责读帧、镜像、绘制、透视变换;MediaPipe 负责手部和人体关键点;cvzone 在 MediaPipe 外面包了一层,省掉一部分绘制和坐标整理代码;NumPy 负责矩阵和坐标运算。这个组合的好处是依赖少、上手快,缺点是实时性和稳定性要自己兜底,不能把检测结果当成绝对真值。
| 层级 | 选型 | 在项目里的作用 | 我通常重点看什么 |
|---|---|---|---|
| 采集层 | OpenCVVideoCapture | 读摄像头、设置分辨率、镜像 | 摄像头索引、FPS、分辨率 |
| 检测层 | MediaPipe + cvzone | 输出手部 21 点、人体关键点 | 置信度、关键点稳定性 |
| 交互层 | 自定义规则 | 捏合、点击、姿态角度 | 阈值、防抖、状态机 |
| 渲染层 | OpenCVimshow/putText | 画框、画线、显示状态 | 窗口阻塞、绘制开销 |
常见做法是把检测器和业务规则分开:检测器只吐lmList、bbox、type,业务层只接收坐标做判断。这样换模型或换规则时不用改一大片代码。我一般会把阈值全部收进配置文件,不散落在while循环里,否则调参时会翻车。
2.2 目录组织:每个入口只负责一个交互场景
合辑里的文件命名可能不完全一样,但按功能拆,结构大致是下面这样。拿到资源后先找入口文件,再顺着 import 找到公共模块,不要一上来就改检测器。
cv_interaction_lab/ ├── app_hand.py # 手势识别入口 ├── app_keyboard.py # 虚拟键盘入口 ├── app_pose.py # 姿态检测入口 ├── config.py # 摄像头、阈值、分辨率配置 ├── core/ │ ├── camera.py # 摄像头初始化和读取 │ ├── geometry.py # 距离、角度、透视变换 │ ├── gesture_state.py # 捏合状态机 │ └── render.py # 绘制和日志 ├── assets/ │ └── keyboard.png # 虚拟键盘贴图 └── requirements.txt入口文件只做三件事:初始化摄像头、初始化检测器、进入渲染循环。公共模块不依赖具体入口,手势入口能用的距离计算和状态机,虚拟键盘入口也能复用。如果资源包里的入口名不同,按app_hand、app_keyboard、app_pose这三个角色去对应即可。判断一个合辑是否好改,就看它的入口文件有没有把业务逻辑全塞在while里;塞得越多,后面加防抖和日志越痛苦。
2.3 启动参数基线:分辨率、镜像、置信度和帧率
实时视觉项目里,参数不是越多越好,但几个关键参数必须一开始就固定。下面这份配置是我在 1280×720 摄像头下比较常用的基线,手势和键盘都能兼顾,姿态检测如果机器性能一般再往下降。
# config.py APP_CONFIG = { "camera_index": 0, "frame_width": 1280, "frame_height": 720, "flip": True, "hand": { "max_hands": 1, "detection_con": 0.7, "track_con": 0.7, }, "pinch": { "down_ratio": 0.28, "up_ratio": 0.38, "hold_frames": 3, }, "pose": { "model_complexity": 1, "detection_con": 0.6, "track_con": 0.6, }, }camera_index=0是默认摄像头,外接摄像头常见是 1 或 2,这块有点玄学,后面排障会讲。flip=True让画面像镜子一样,用户抬右手时画面里也在右边,交互更自然。detection_con和track_con不要一上来拉满,太高会频繁丢检测,太低会满屏误检。down_ratio和up_ratio是捏合的双阈值,hold_frames=3表示连续 3 帧满足条件才切换状态,30 FPS 下大约 100 毫秒,能压掉一部分抖动。分辨率写死 1280×720 的原因是虚拟键盘的透视映射需要稳定比例,换分辨率时映射点也要跟着换。
3. 手势识别链路:从 21 个关键点到捏合判定
手势识别是这套合辑里最适合先跑通的一条链。它不需要键盘贴图,也不依赖人体姿态,只要摄像头能出画面,手部检测器就能返回 21 个关键点。真正难的不是把点画出来,而是把“捏合”这种连续动作变成稳定的按下和释放事件。很多 Demo 看起来能跑,一上手就发现按键连击、手势漂移,问题基本都出在阈值和状态管理上。
3.1 手部检测器初始化与画面预处理
先跑一个最小闭环:读帧、镜像、检测、绘制、退出。这个循环跑通之后,再往里加业务逻辑,排查问题时不会同时面对摄像头、模型和规则三层不确定性。
import cv2 from cvzone.HandTrackingModule import HandDetector from config import APP_CONFIG cap = cv2.VideoCapture(APP_CONFIG["camera_index"]) cap.set(cv2.CAP_PROP_FRAME_WIDTH, APP_CONFIG["frame_width"]) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, APP_CONFIG["frame_height"]) detector = HandDetector( staticMode=False, maxHands=APP_CONFIG["hand"]["max_hands"], detectionCon=APP_CONFIG["hand"]["detection_con"], minTrackCon=APP_CONFIG["hand"]["track_con"], ) while True: ok, frame = cap.read() if not ok: break if APP_CONFIG["flip"]: frame = cv2.flip(frame, 1) hands, frame = detector.findHands(frame, draw=True, flipType=False) if hands: hand = hands[0] lm_list = hand["lmList"] # 21 个关键点,像素坐标 bbox = hand["bbox"] # 手部外接框 hand_type = hand["type"] # Left / Right cv2.imshow("hand", frame) if cv2.waitKey(1) & 0xFF == 27: break cap.release() cv2.destroyAllWindows()staticMode=False适合连续视频流,检测器会利用上一帧结果做跟踪;如果做单张图片检测,再改成True。maxHands=1能减少误检和计算量,虚拟键盘场景通常只认一只手。flipType=False是因为画面已经镜像过,再让检测器翻转左右手类型反而会乱。lmList里每个点是[x, y, z],前两位是像素坐标,做距离计算时只取前两位。这个循环里没有业务逻辑,后面所有手势规则都从lm_list往下接。
3.2 关键点编号与捏合距离:别拿欧氏距离直接当点击
手部 21 点里,拇指尖是 4,食指尖是 8,中指指尖是 12,腕部是 0,中指根部是 9。捏合最直观的判断是算 4 和 8 的欧氏距离,但直接用像素距离会受手离摄像头远近影响:手近时 30 像素算捏合,手远时 30 像素可能还没碰上。更稳的做法是用手掌尺寸做归一化。
import math def normalized_pinch_distance(lm_list): # 4: 拇指尖,8: 食指尖,0: 腕部,9: 中指根部 thumb = lm_list[4][:2] index = lm_list[8][:2] wrist = lm_list[0][:2] middle_mcp = lm_list[9][:2] pinch = math.hypot(index[0] - thumb[0], index[1] - thumb[1]) hand_scale = math.hypot( middle_mcp[0] - wrist[0], middle_mcp[1] - wrist[1] ) if hand_scale < 1e-6: return 999.0 return pinch / hand_scalepinch是拇指尖到食指尖的像素距离,hand_scale是腕部到中指根部的距离,用后者做分母,得到无量纲比值。手离摄像头近时两个距离一起变大,比值仍然稳定。hand_scale小于1e-6说明关键点异常,直接返回一个大值,避免除零。实际调参时先打印十几帧比值:完全张开通常在 1.0 以上,捏合时大约落到 0.2 到 0.4,具体值跟手型和摄像头角度有关。不要照搬别人的阈值,先看自己的数据分布。
3.3 手势状态机:按下、保持、释放三态防抖
只有距离阈值还不够。手指在临界点附近抖动时,ratio会在 0.28 上下跳,按键就会连击。解决办法是双阈值加连续帧计数:低于down_ratio连续若干帧才认为按下,高于up_ratio连续若干帧才认为释放,中间区域保持原状态。
class PinchState: def __init__(self, down_ratio=0.28, up_ratio=0.38, hold_frames=3): self.down_ratio = down_ratio self.up_ratio = up_ratio self.hold_frames = hold_frames self.pressed = False self._down_count = 0 self._up_count = 0 def update(self, ratio): if not self.pressed: if ratio < self.down_ratio: self._down_count += 1 if self._down_count >= self.hold_frames: self.pressed = True self._down_count = 0 return "down" else: self._down_count = 0 else: if ratio > self.up_ratio: self._up_count += 1 if self._up_count >= self.hold_frames: self.pressed = False self._up_count = 0 return "up" else: self._up_count = 0 return "move" if self.pressed else "idle"down_ratio和up_ratio之间留出滞回区,手指在 0.28 到 0.38 之间晃动时状态不变。hold_frames控制灵敏度,帧率低时不要设太大,否则点击延迟明显;帧率高时可以加到 3 到 5。update返回down、up、move、idle四种状态,业务层只在down时触发一次点击,在move时更新拖拽位置。这个状态机很小,但它是整套交互能不能用的分水岭。
4. 虚拟键盘与姿态检测:把关键点映射成可操作控件
手势识别解决“手在哪、有没有捏”,虚拟键盘解决“捏的位置对应哪个键”,姿态检测解决“人体关键点怎么变成规则”。这三件事的共同点是:原始关键点只是坐标,业务价值来自坐标到控件、坐标到状态的映射。映射写得好,画面看起来就稳;映射写得糙,用户会觉得项目“不太聪明”。
4.1 虚拟键盘的透视映射:四点定位比固定坐标稳
虚拟键盘常见做法是把一张键盘贴图贴到画面里的某个四边形区域。用户用手去点,程序把手指坐标反算回键盘图片坐标,再判断落在哪个键上。固定矩形区域最省事,但摄像头角度一变就错位;四点透视映射更稳,代价是要维护四个角点。
import cv2 import numpy as np def build_keyboard_transform(frame_bgr, keyboard_bgr, corner_points): # corner_points 顺序:左上、右上、右下、左下 h, w = keyboard_bgr.shape[:2] src = np.float32([[0, 0], [w, 0], [w, h], [0, h]]) dst = np.float32(corner_points) matrix = cv2.getPerspectiveTransform(src, dst) warped = cv2.warpPerspective( keyboard_bgr, matrix, (frame_bgr.shape[1], frame_bgr.shape[0]) ) mask = (warped.sum(axis=2) > 0).astype(np.uint8) * 255 out = frame_bgr.copy() out[mask > 0] = warped[mask > 0] return out, matrix def keyboard_hit(point_xy, matrix, keyboard_size): h, w = keyboard_size inv = np.linalg.inv(matrix) p = np.array([point_xy[0], point_xy[1], 1.0], dtype=np.float32) q = inv @ p if abs(q[2]) < 1e-6: return None x, y = q[0] / q[2], q[1] / q[2] if 0 <= x < w and 0 <= y < h: return int(x), int(y) return Nonecorner_points必须按左上、右上、右下、左下排序,顺序错了贴图会翻转或扭曲。build_keyboard_transform把键盘图片坐标映射到摄像头画面,matrix是正向矩阵;keyboard_hit用逆矩阵把手指坐标反算回键盘图片坐标,落在[0, w)和[0, h)内才算命中。mask用像素和做简易透明区判断,如果键盘贴图带 alpha 通道,改成 alpha 判断更干净。实际项目里我一般会先固定四个角点,调好贴图位置,再让用户做一次四点校准,避免换摄像头后全部重调。
4.2 姿态检测:用角度和相对位置写规则,不要写死像素
姿态检测比手势多一层身体尺度问题。肩宽、臂长、人离摄像头远近都会变,拿固定像素判断“手有没有抬起来”很容易失效。更稳的规则是角度加相对位置:肘关节角度、肩线倾斜、手腕相对肩部的高度。
import math from cvzone.PoseModule import PoseDetector def angle_3p(a, b, c): # 计算 b 点处的夹角,返回角度 ab = (a[0] - b[0], a[1] - b[1]) cb = (c[0] - b[0], c[1] - b[1]) dot = ab[0] * cb[0] + ab[1] * cb[1] n1 = math.hypot(*ab) n2 = math.hypot(*cb) if n1 * n2 < 1e-6: return 0.0 cos_value = max(-1.0, min(1.0, dot / (n1 * n2))) return math.degrees(math.acos(cos_value)) pose = PoseDetector( staticMode=False, modelComplexity=1, smooth=True, detectionCon=0.6, trackCon=0.6, ) # lmList: 11 左肩,13 左肘,15 左腕 elbow_angle = angle_3p(lmList[11][:2], lmList[13][:2], lmList[15][:2])angle_3p用向量点积算夹角,max(-1.0, min(1.0, ...))是防止浮点误差把acos弄出定义域。modelComplexity=1是速度和精度的折中,机器吃力时降到 0,检测点会粗一点但帧率更稳。规则层不要只写wrist_y < 300,可以写成“手腕 y 小于肩部 y 减去 0.2 倍肩宽”,这样人走近走远都能用。姿态规则最好控制在三到五条,规则一多就会互相打架,调参成本直线上升。
4.3 三个 Demo 复用摄像头与渲染循环
合辑里三个入口如果各写一套while,后面加日志、加 FPS 显示、改退出键都要改三遍。更省事的做法是把循环抽出来,入口只传一个处理函数。处理函数接收帧,返回处理后的帧和状态字符串。
def run_loop(cap, handler, window_name="app"): while True: ok, frame = cap.read() if not ok: break frame = cv2.flip(frame, 1) frame, state = handler(frame) cv2.putText( frame, f"state={state}", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2 ) cv2.imshow(window_name, frame) if cv2.waitKey(1) & 0xFF == 27: breakhandler是手势、键盘、姿态三个入口各自的处理函数,内部可以调检测器、状态机和绘制逻辑。run_loop只负责采图、镜像、显示和退出,这样摄像头参数、窗口名、日志位置都统一。实际改造时我会再把cap包一层CameraStream,把打开失败、读帧失败和释放资源收在一起,入口函数就更干净。三个 Demo 共用一套循环之后,新增一个“人脸计数器”也只是多写一个 handler。
5. 跑起来与避坑:依赖、摄像头索引、误触和性能的排查清单
这套合辑的启动门槛不高,但实时视觉项目的坑很集中:环境装好了摄像头打不开,摄像头打开了帧率掉,帧率上来了手势又抖。下面按“先装环境、再跑入口、最后查现象”的顺序写,遇到问题不要同时改模型和阈值,一次只动一个变量。
5.1 环境安装与最小启动命令
建议用虚拟环境,Python 版本选 3.9 到 3.11,太新的版本有时会碰到 MediaPipe 轮子不匹配。先装依赖,再跑手势入口,手势通了再跑键盘和姿态,不要一上来三个窗口一起开。
# 建议 Python 3.9 - 3.11 python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate python -m pip install --upgrade pip pip install opencv-python mediapipe cvzone numpy # 进入合辑目录后,先跑手势入口 python app_hand.py # 手势正常后再跑虚拟键盘和姿态检测 python app_keyboard.py python app_pose.pysource .venv/bin/activate是 Linux/macOS 的激活方式,Windows 用.venv\Scripts\activate。opencv-python是带 GUI 的版本,服务器环境不要装opencv-python-headless,否则imshow会报错。入口文件名以资源包里的实际文件为准,角色对应关系是手势、键盘、姿态。运行后按ESC退出,如果窗口卡死,先确认waitKey(1)有没有被阻塞代码挤掉。
5.2 常见问题排查:黑屏、卡顿、误触、坐标漂移、模块找不到
现象一:窗口弹出来但一直黑屏,终端没有报错。
原因通常是摄像头索引不对,或者摄像头被会议软件占用。解决方法是先打印cap.isOpened(),再依次试 0、1、2;关掉占用摄像头的程序,检查系统权限。摄像头索引这事有点玄学,同一台机器不同 USB 口都可能变,我一般会在配置里留一个camera_index,不写死在代码里。
现象二:检测框忽有忽无,捏合时按键连击。
原因是单阈值判断加关键点抖动,手在临界距离附近来回跳。解决方法是把像素距离改成手掌归一化距离,再加双阈值和hold_frames。如果还抖,先把分辨率降到 1280×720 或 960×540,确认不是高分辨率下的坐标噪声。阈值不要照抄,打印二十帧ratio看分布再定。
现象三:虚拟键盘点击位置偏移,画面边缘更明显。
原因是四个角点顺序错、画面镜像后坐标没同步,或者正向矩阵和逆矩阵用反了。解决方法是固定左上、右上、右下、左下顺序,先做flip再取角点,点击判断统一走keyboard_hit的逆变换。边缘偏移特别大时,检查键盘贴图尺寸和corner_points是否在同一个坐标系里。
现象四:帧率从 30 掉到 8,风扇狂转。
常见原因是每帧都做全图透视变换、绘制太多关键点、姿态模型复杂度太高。解决方法是缓存键盘变换矩阵,只在键盘区域变化时重算;绘制线宽和圆点降到最小;姿态入口把modelComplexity降到 0,或者把分辨率降到 640×480。实时交互项目里,帧率比画质重要,先稳住 20 FPS 再谈精度。
现象五:报ModuleNotFoundError: No module named 'cvzone'或 MediaPipe 安装失败。
先确认虚拟环境激活了没有,which python和pip -V是不是指向.venv。Python 版本太新、系统架构不匹配、pip 缓存损坏都会导致安装失败,换 3.9 到 3.11 的解释器重新建环境通常能解决。不要在一个全局环境里反复装不同版本,血泪经验是环境隔离越早做越省事。
5.3 运行时日志与降级策略:别让黑匣子拖慢排查
实时项目出问题时,最怕只看到画面卡住,不知道是摄像头没帧、检测没结果,还是渲染太慢。加一个轻量 FPS 日志,能把问题范围缩到某一层。
import time import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s" ) class FpsLogger: def __init__(self, interval=2.0): self.interval = interval self.last = time.time() self.frames = 0 def tick(self, hands_count=0): self.frames += 1 now = time.time() if now - self.last >= self.interval: fps = self.frames / (now - self.last) logging.info("fps=%.1f hands=%d", fps, hands_count) self.frames = 0 self.last = nowinterval=2.0表示每两秒打一条日志,太频繁会拖慢主循环。hands_count用来区分“没检测到手”和“检测到了但规则没触发”。如果日志显示 FPS 低于 15,先降分辨率,再关绘制,最后降模型复杂度;如果 FPS 正常但hands_count经常是 0,检查光照、摄像头角度和detectionCon。降级策略不要写得太复杂,按“分辨率 → 绘制 → 模型”的顺序退,用户至少还能看到画面。
6. 二次开发:把合辑拆成可复用的交互组件
这套合辑最适合的二次开发方向,不是继续往while循环里堆if,而是拆成三个角色:检测器只负责出关键点,状态机只负责把连续信号变成事件,动作映射只负责把事件落到具体业务。手势入口可以抽出HandDetector和PinchState,虚拟键盘入口可以抽出KeyboardMapper,姿态入口可以抽出PoseRuleEngine。拆完之后,新增一个“捏合控制音量”只需要写一个动作映射,不用复制摄像头循环。
class InteractionModule: def reset(self): """切换场景时清空状态,避免上一帧的按下状态带进来""" raise NotImplementedError def update(self, frame, landmarks): """输入当前帧和关键点,返回 frame 与事件""" raise NotImplementedError验证方法我一般用两步:先用录好的视频回放,把摄像头不确定性排除掉;再用假关键点做单元测试,专门测阈值边界和防抖计数。比如构造ratio=0.27连续三帧,断言状态机返回down;构造0.30和0.40来回跳,断言不连击。这样改阈值时心里有底,不会只靠肉眼盯窗口。从那以后我每次接摄像头交互项目,都先跑十分钟空画面看 FPS 和丢帧,再开始写业务规则。希望帮到你。
本文还有配套的精品资源,点击获取