Python开发基础手语识别(基础框架版)
一、前期准备
想要实现这些,首先就是要模拟出来一个大致的框架,方便后续开展,下面的就是随便写的一个框架,大家凑合看看就行,基本上是这个意思:
from tkinter import *w = Tk()
w.title("手语识别(简易)")
w.geometry("805x640")l1 = Label(text='此窗口实时显示\n摄像头拍摄画面', font=("微软雅黑", 20),width=25,height=15,relief='groove', borderwidth=2)
l1.place(x=0, y=0)l2 = Label(text='此窗口实时显示\n手部骨骼绘画', font=("微软雅黑", 20),width=25,height=15,relief='groove', borderwidth=2)
l2.place(x=400, y=0)l3 = Label(text='此窗口实时显示手语识别结果', font=("微软雅黑", 20),width=50,height=3,relief='groove', borderwidth=2)
l3.place(x=0, y=530)w.mainloop()
运行效果大概也就这样:
解决了框架的问题之后,就要开始进一步的实现框架里面的内容了。
二、程序实现
1.相关库
目前大多数的写法基本上都是是用open-cv和PIL库来实现,但是PIL库容易暴雷,很抽象,实际开发中不建议使用PIL库进行开发,这里就更推荐使用Pillow库,因为因为原始PIL开发停滞,Pillow 是其友好分支,功能兼容且持续维护,安装Pillow 即可替代PIL使用。
简单说明一下open-cv和Pillow的相关用法
open-cv核心语法
1. 图像读取与显示import cv2# 读取图像(返回 BGR 格式的 NumPy 数组)
img = cv2.imread("image.jpg") # 路径支持中文,需用 UTF-8 编码# 显示图像(需配合 cv2.waitKey() 使用)
cv2.imshow("Image Window", img)
cv2.waitKey(0) # 0 表示无限等待,按任意键关闭窗口
cv2.destroyAllWindows() # 销毁所有窗口2. 图像基本操作# 转换为灰度图
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# 缩放图像(插值方法可选:cv2.INTER_AREA 适合缩小,cv2.INTER_LINEAR 适合放大)
resized_img = cv2.resize(img, (640, 480), interpolation=cv2.INTER_LINEAR)# 旋转图像(绕中心旋转 45 度,缩放因子 1.0)
(h, w) = img.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, 45, 1.0)
rotated_img = cv2.warpAffine(img, M, (w, h))3. 视频处理(摄像头实时流)# 打开摄像头(参数 0 表示默认摄像头,1 表示外接摄像头)
cap = cv2.VideoCapture(0)while True:ret, frame = cap.read() # ret 为布尔值,表示是否读取成功if not ret:break# 在视频帧上绘制矩形cv2.rectangle(frame, (100, 100), (300, 300), (0, 255, 0), 2)# 显示视频帧cv2.imshow("Video Stream", frame)# 按 'q' 键退出循环if cv2.waitKey(1) & 0xFF == ord('q'):breakcap.release() # 释放摄像头资源
cv2.destroyAllWindows()4. 绘图与标注# 在图像上绘制文字
font = cv2.FONT_HERSHEY_SIMPLEX # 字体类型
cv2.putText(img, # 目标图像"Hand Detected",# 文本内容(50, 50), # 文本位置坐标font, # 字体1.0, # 字体大小(0, 255, 0), # 颜色(BGR 格式)2, # 线条粗细cv2.LINE_AA # 抗锯齿
)# 绘制圆形
cv2.circle(img, (200, 200), 50, (255, 0, 0), -1) # -1 表示填充圆形
Pillow核心语法
1. 图像读取与保存from PIL import Image# 读取图像(返回 Image 对象)
img = Image.open("image.png")# 保存图像(自动根据扩展名判断格式,支持格式转换)
img.save("output.jpg") # 从 PNG 转为 JPEG
img.save("output.png", quality=95) # 保存为 PNG,设置质量(对支持的格式有效)2. 图像尺寸与模式操作# 获取图像尺寸(宽度, 高度)
width, height = img.size# 转换图像模式(如灰度图、RGB 图)
gray_img = img.convert("L") # "L" 表示灰度模式
rgb_img = img.convert("RGB") # 确保为 RGB 模式(某些操作需要)3. 图像编辑操作# 缩放图像(使用高质量抗锯齿)
resized_img = img.resize((200, 200), Image.Resampling.LANCZOS)# 裁剪图像(左上角坐标 (x1,y1),右下角坐标 (x2,y2))
cropped_img = img.crop((50, 50, 250, 250))# 水平翻转图像
flipped_img = img.transpose(Image.FLIP_LEFT_RIGHT)4. 像素级操作与绘图# 获取像素值(坐标 (x,y),返回 RGB 元组)
pixel_color = img.getpixel((100, 100))# 修改像素值(将 (200,200) 坐标设为红色)
img.putpixel((200, 200), (255, 0, 0)) # RGB 格式# 使用 ImageDraw 绘制图形
from PIL import ImageDrawdraw = ImageDraw.Draw(img)
draw.rectangle([(10, 10), (100, 100)], outline=(0, 255, 0), width=2) # 绘制矩形
draw.ellipse([(150, 150), (250, 250)], fill=(255, 0, 0)) # 绘制填充椭圆5. 批量图像处理import os
from PIL import Imageinput_folder = "images/"
output_folder = "processed/"# 创建输出文件夹(若不存在)
os.makedirs(output_folder, exist_ok=True)for filename in os.listdir(input_folder):if filename.endswith((".jpg", ".png")):file_path = os.path.join(input_folder, filename)with Image.open(file_path) as img:# 统一缩放为 500x500 像素resized = img.resize((500, 500), Image.Resampling.BILINEAR)# 转换为灰度图gray = resized.convert("L")# 保存到输出文件夹gray.save(os.path.join(output_folder, filename))
他们俩的关键语法对比
功能 | OpenCV(Python) | Pillow(PIL) |
---|---|---|
读取图像 | cv2.imread("path") | Image.open("path") |
显示图像 | cv2.imshow("window", img); cv2.waitKey(0) | 需要结合 Tkinter/Qt 等 GUI 库显示 |
图像格式转换 | cv2.cvtColor(img, cv2.COLOR_BGR2RGB) | img.convert("RGB") |
缩放图像 | cv2.resize(img, (w,h), interpolation=...) | img.resize((w,h), Image.Resampling.LANCZOS) |
绘制文字 | cv2.putText(img, text, (x,y), font, ...) | ImageDraw.Draw(img).text((x,y), text, fill=...) |
获取图像尺寸 | h, w = img.shape[:2] | width, height = img.size |
ps:上述不是很全面,仅作参考
好啦,回到正题,该逐步实现调用过程啦!
2.摄像头调用的具体代码实现
下面的我自己的代码,先发出来给大伙瞅瞅,稍后详细解释代码
import cv2
import tkinter as tk
from PIL import Image, ImageTkdef update_camera():ret, frame = cap.read()if ret:# 水平镜像翻转画面(参数1表示水平翻转)frame_flipped = cv2.flip(frame, 1)# 摄像头画面显示在l1(添加镜像)frame_rgb = cv2.cvtColor(frame_flipped, cv2.COLOR_BGR2RGB)frame_resized = cv2.resize(frame_rgb, (400, 400))img = Image.fromarray(frame_resized)imgtk = ImageTk.PhotoImage(image=img)l1.imgtk = imgtkl1.configure(image=imgtk)# 手部骨骼绘制显示在l2(预留位置)# 识别结果显示在l3(预留位置)w.after(10, update_camera)# 初始化摄像头
cap = cv2.VideoCapture(0)# 创建窗口
w = tk.Tk()
w.title("手语识别(简易)")
w.geometry("805x640")# Label用于摄像头画面(镜像显示)
l1 = tk.Label(text='摄像头加载中...', font=("微软雅黑", 20), width=25, height=15, relief='groove', borderwidth=2)
l1.place(x=0, y=0)# Label用于手部骨骼绘制(预留)
l2 = tk.Label(text='此窗口实时显示\n手部骨骼绘画', font=("微软雅黑", 20), width=25, height=15, relief='groove', borderwidth=2)
l2.place(x=400, y=0)# Label用于识别结果(预留)
l3 = tk.Label(text='此窗口实时显示手语识别结果', font=("微软雅黑", 20), width=50, height=3, relief='groove', borderwidth=2)
l3.place(x=0, y=530)# 启动摄像头更新
update_camera()w.mainloop()
cap.release()
好啦,现在来一步一步的理解上面的代码:
1. 导入依赖库
import cv2 # 计算机视觉库,用于摄像头控制和图像处理
import tkinter as tk # GUI 库,用于创建窗口和界面元素
from PIL import Image, ImageTk # 图像处理库,用于图像格式转换以适配 Tkinter
欸?为什么导入tkinter要用tk,直接*不更好嘛?我一开始也是这样想的,但是这里就有一个很致命的错误,因此我在导入这个地方卡了很久很久……
为啥捏? 在 Python 中,from tkinter import *
和 import tkinter as tk
是两种不同的导入方式,前者的*代表了全部导入,这就代表Python 会将 tkinter
模块中的 所有公有名称(如 Tk
、Label
、Button
等)直接导入到当前命名空间。这意味着:
- 无需通过模块名前缀(如
tk.
)即可直接使用这些名称。 - 如果当前命名空间中已有同名对象(如自定义的
Button
函数),会发生 名称冲突,导致程序报错或逻辑混乱。
如果我直接使用的话,就会一直报错,恰好大伙们还不知道这个小知识点的话,就很难发现自己错在啥地方!!!!如果真不小心了咋办?就会出现下面的问题:
- 覆盖内置函数或变量:
例如,若代码中定义了Tk = "my_string"
,则from tkinter import *
会尝试将tkinter.Tk
(窗口类)导入为Tk
,导致Tk
被重新赋值为字符串,引发错误。 - 难以追踪来源:
当代码中出现Button
时,无法直接判断它是tkinter.Button
还是其他模块 / 自定义的Button
,增加调试难度。 - 破坏代码可读性:
对于大型项目,未加前缀的名称会让读者难以快速识别其所属模块,尤其是在多个模块被import *
的情况下。
所以这是一个很抽象的错误,也是一个很小的知识点,一般来说,我们在系统性学习python的时候,是直接学的第二种方法,第一种老师也会讲,但是不会细讲,因为考试也不考,我们平时也接触不到这些比较难的库,所以这个方面的小知识点就很容易被忽略。
至于为啥第二种好,老师也不会说,同样考试也不考,我就来简要的说一下,过两天我整理一下,跟这篇一起发出来:
1. 避免命名污染,确保名称唯一性
-
隔离命名空间:
将 Tkinter 的所有名称(如Tk
、Label
)封装在tk
模块内,避免与当前代码中的自定义变量、函数或其他库(如custom_widgets
)的同名对象冲突。
示例:若代码中已有Button
函数,tk.Button
仍指向 Tkinter 的按钮类,不会被覆盖。 -
明确归属:
所有 Tkinter 对象均以tk.
为前缀(如tk.Entry
),清晰标识其来源,避免混淆。
2. 提升代码可读性和可维护性
-
快速定位来源:
看到tk.Canvas
即可明确其为 Tkinter 的画布类,无需查阅导入语句或猜测名称来源。
对比:from tkinter import *
中Canvas
的归属不明确,可能来自其他模块。 -
协作友好:
在团队项目中,前缀可帮助其他开发者快速识别框架组件,降低理解成本。
3. 减少内存占用与启动开销
- 按需加载:
仅导入tkinter
模块本身,而非其所有成员。对于大型模块,可减少初始加载时的内存占用和启动时间。
原理:import *
会一次性导入模块内所有公有对象,而import as
仅创建模块引用。
4. 兼容大型项目与复杂场景
-
多库共存:
当同时使用 Tkinter 和其他 GUI 库(如 PyQt、wxPython)时,前缀可避免跨库名称冲突。import tkinter as tk # Tkinter 组件前缀为 tk. from PyQt5 import QtCore # PyQt 组件前缀为 QtCore.
-
模块化开发:
便于将 Tkinter 相关代码封装在独立模块中,通过tk.
前缀明确接口边界,提升代码组织性。
5. 符合 Python 最佳实践(PEP 8 规范)
- 官方推荐:
PEP 8 明确建议避免使用from module import *
,除非是交互式环境或极小型脚本。- 理由:命名空间污染可能导致隐性错误,且违反 “明确优于隐含” 的 Python 哲学。
2. 核心函数:摄像头画面更新
def update_camera():ret, frame = cap.read() # 读取摄像头一帧画面if ret: # ret 为 True 表示读取成功# 水平镜像翻转画面(参数1表示水平翻转,0为垂直翻转,-1为水平+垂直翻转)frame_flipped = cv2.flip(frame, 1)# ---------------------- 显示原始镜像画面到 l1 ----------------------# OpenCV 默认颜色格式为 BGR,需转为 RGB 以正确显示frame_rgb = cv2.cvtColor(frame_flipped, cv2.COLOR_BGR2RGB)# 缩放画面至 400x400 像素(适配窗口大小)frame_resized = cv2.resize(frame_rgb, (400, 400))# 将 OpenCV 的 NumPy 数组转为 PIL 图像对象img = Image.fromarray(frame_resized)# 将 PIL 图像转为 Tkinter 可用的 PhotoImage 对象imgtk = ImageTk.PhotoImage(image=img)# 将图像绑定到 l1 标签,并更新显示l1.imgtk = imgtk # 保留引用避免被垃圾回收l1.configure(image=imgtk)# 递归调用自身,每 10ms 更新一次画面(实现实时效果)w.after(10, update_camera)
关键细节:
- 镜像翻转:
cv2.flip(frame, 1)
使画面左右对称,符合人类视觉习惯。 - 颜色转换:OpenCV 的
cv2.cvtColor
将 BGR 转为 RGB,否则画面颜色会错乱。 - 图像格式转换链:
这是在 Tkinter 中显示 OpenCV 画面的标准流程。OpenCV数组(BGR) → cvtColor → RGB数组 → PIL.Image → ImageTk.PhotoImage → Tkinter显示
3. 初始化摄像头
cap = cv2.VideoCapture(0) # 0 表示打开默认摄像头(笔记本内置或外接摄像头)
cv2.VideoCapture(n)
中n
为摄像头设备编号,0
通常为默认摄像头,1
为外接摄像头。- 若摄像头无法打开,
cap.read()
会返回ret=False
,画面停止更新。
4. 启动程序主循环和资源释放
update_camera() # 调用函数启动摄像头画面更新
w.mainloop() # Tkinter 主循环,保持窗口显示
cap.release() # 释放摄像头资源,避免硬件占用
w.mainloop()
是 GUI 程序的入口,用于处理用户交互(如关闭窗口)。cap.release()
必须在主循环结束后调用,否则可能导致摄像头无法正常关闭。
3.手部骨骼实现
想要实现手部骨骼,就得来到另一个库了----MediaPipe库,MediaPipe 是 Google 开发的开源跨平台机器学习框架,专注于实时多媒体处理和计算机视觉任务,提供预训练模型和模块化工具,可快速开发手势识别、人脸识别等 AI 应用。
核心特点
- 多模态感知能力
- 支持手部追踪(21 个关键点)、人脸检测(468 个关键点)、人体姿态估计(33 个关键点)、物体检测与追踪等。
- 跨平台与多语言
- 支持 Python、C++、Java、JavaScript 等语言,覆盖桌面、移动(Android/iOS)、边缘设备(如树莓派)。
- 模块化与实时性
- 通过 “计算器图” 灵活组合组件,优化后可在移动端实现 30+ FPS 实时处理。
- 开箱即用与轻量级
- 提供预训练模型,无需复杂训练;支持 TensorFlow Lite,适合资源受限设备。
但是呢,也不是使用pip安装完就能直接使用的,虽然库内有一个轻型的模型库,但是我不知道为啥,我就一直报错,很烦,很抽象,弄了很久,用内部API的时候,虽然成功了,但是更抽象了,就是简单了将手部轮廓标出来了而以,还不只,连背景的轮廓都标出来了,很难看,建议大伙在用这个库的时候,老老实实去官网下载模型文件再导入使用,也不要去github找,上面是 MediaPipe 框架的 模型配置文件(定义模型结构、输入输出等),并非直接可用的 “预训练权重文件”。很好分辨,.pbtxt
文件就是,下载模型文件呢就去官方模型仓库,链接:【https://storage.googleapis.com/mediapipe-models/】,要想正常访建议使用chrome浏览器,并且使用快捷键【shift+ctrl+n】开启无痕浏览后再尝试访问,我也不知为什么,直接访问就返回【MissingSecurityHeader: Your request was missing a required header. Authorization
】,百度了一下才知道原来遇到的错误 MissingSecurityHeader: Your request was missing a required header. Authorization
表示请求中缺少必要的 Authorization
认证头,这通常出现在需要身份验证的接口调用、云服务访问或权限控制场景中。说白了就是尝试访问的 Google Cloud Storage 链接(如 storage.googleapis.com
)属于需要身份验证的谷歌云资源。当直接通过浏览器或工具下载文件时,谷歌可能要求提供 API 密钥、OAuth 令牌 等认证信息,否则拒绝请求。说人话就是没有授权,不给你访问网站。
为了大伙,我直接给下载链接放下面,有需要的自行下载即可
https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task
至于为啥是float16,而不是32,因为float16
:表示模型参数的数值精度(平衡模型大小和计算效率),也有 float32
版本(精度更高但体积稍大),一般场景选 float16
即可。咱这小破笔记本真心带不动32版本的。
好啦,下载完成之后,就要开始下一步了,因为我是用的pycharm写的,直接用的虚拟环境,存放位置是有一定要求的,以python为例:
Python 项目(纯代码调用)
your_project/
├── models/ # 专门放模型文件
│ └── hand_landmarker.task
└── main.py # 主代码
正常来说是有一个依赖文件的,例如requirements.txt文件,不是很有必要,所以可有可不的
欧克,解决完上述的所有问题之后,就可以开始 实现手部骨骼啦!
import cv2
import tkinter as tk
from PIL import Image, ImageTk
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
import os# 模型路径
MODEL_PATH = os.path.join("models", "hand_landmarker.task")
if not os.path.exists(MODEL_PATH):raise FileNotFoundError(f"模型文件未找到: {MODEL_PATH}")# 初始化手部检测器
base_options = python.BaseOptions(model_asset_path=MODEL_PATH)
options = vision.HandLandmarkerOptions(base_options=base_options,num_hands=2,min_hand_detection_confidence=0.3,min_hand_presence_confidence=0.3,min_tracking_confidence=0.3
)
detector = vision.HandLandmarker.create_from_options(options)def draw_hand_skeleton(frame):original_height, original_width = frame.shape[:2]mp_image = mp.Image(image_format=mp.ImageFormat.SRGB,data=cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))results = detector.detect(mp_image)if results.hand_landmarks: # 确保检测到手部for hand_landmarks_list in results.hand_landmarks: # 遍历每只手的关键点列表(外层列表)for landmark in hand_landmarks_list: # 遍历单只手的关键点(内层列表)x = int(landmark.x * original_width)y = int(landmark.y * original_height)cv2.circle(frame, (x, y), 5, (255, 0, 0), -1)# 绘制骨骼连线(根据关键点列表索引)for connection in mp.solutions.hands.HAND_CONNECTIONS:start_idx, end_idx = connectionstart_landmark = hand_landmarks_list[start_idx]end_landmark = hand_landmarks_list[end_idx]start_x = int(start_landmark.x * original_width)start_y = int(start_landmark.y * original_height)end_x = int(end_landmark.x * original_width)end_y = int(end_landmark.y * original_height)cv2.line(frame, (start_x, start_y), (end_x, end_y), (0, 255, 0), 2)return framedef update_camera():ret, frame = cap.read()if ret:frame_flipped = cv2.flip(frame, 1)frame_original = frame_flipped.copy()skeleton_frame = draw_hand_skeleton(frame_original)# 缩放并显示画面frame_resized = cv2.resize(frame_flipped, (400, 400))skeleton_resized = cv2.resize(skeleton_frame, (400, 400))l1_img = Image.fromarray(cv2.cvtColor(frame_resized, cv2.COLOR_BGR2RGB))l1.imgtk = ImageTk.PhotoImage(l1_img)l1.configure(image=l1.imgtk)l2_img = Image.fromarray(cv2.cvtColor(skeleton_resized, cv2.COLOR_BGR2RGB))l2.imgtk = ImageTk.PhotoImage(l2_img)l2.configure(image=l2.imgtk)w.after(10, update_camera)# 初始化摄像头
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)# 创建窗口
w = tk.Tk()
w.title("手语识别(简易)")
w.geometry("805x640")l1 = tk.Label(w, width=400, height=400, bg="black")
l1.place(x=0, y=0)l2 = tk.Label(w, width=400, height=400, bg="black")
l2.place(x=400, y=0)l3 = tk.Label(w,text='手部骨骼检测已就绪,请将手放入画面中...',font=("微软雅黑", 12),bg="#f0f0f0",width=60,height=2
)
l3.place(x=10, y=530)update_camera()
w.mainloop()# 释放资源
cap.release()
detector.close()
cv2.destroyAllWindows()
也是直接将把整个代码直接发出来嗷,下面再详细分析代码:
代码整体功能概述
这段代码实现了一个基于 MediaPipe 的手部骨骼实时检测与可视化应用。程序通过摄像头捕获视频流,使用 MediaPipe 的手部关键点检测模型识别手部位置和姿态,然后在图像上绘制关键点和连接线,最后通过 Tkinter 界面展示原始画面和处理后的骨骼画面。主要功能模块包括:模型初始化、图像骨骼绘制、摄像头画面更新和 GUI 界面展示。
详细模块分析
1. 依赖库导入与模型初始化
import cv2
import tkinter as tk
from PIL import Image, ImageTk
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
import os# 模型路径配置
MODEL_PATH = os.path.join("models", "hand_landmarker.task")
if not os.path.exists(MODEL_PATH):raise FileNotFoundError(f"模型文件未找到: {MODEL_PATH}")# 初始化手部检测器
base_options = python.BaseOptions(model_asset_path=MODEL_PATH)
options = vision.HandLandmarkerOptions(base_options=base_options,num_hands=2,min_hand_detection_confidence=0.3,min_hand_presence_confidence=0.3,min_tracking_confidence=0.3
)
detector = vision.HandLandmarker.create_from_options(options)
关键点:
- 依赖库:
cv2
:处理视频流和图像绘制tkinter
:创建 GUI 界面PIL
:图像格式转换mediapipe
:提供手部检测模型
- 模型配置:
num_hands=2
:最多检测两只手min_detection_confidence=0.3
:检测置信度阈值min_tracking_confidence=0.3
:跟踪置信度阈值
2. 手部骨骼绘制函数
def draw_hand_skeleton(frame):original_height, original_width = frame.shape[:2]mp_image = mp.Image(image_format=mp.ImageFormat.SRGB,data=cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))results = detector.detect(mp_image)if results.hand_landmarks:for hand_landmarks_list in results.hand_landmarks:# 绘制关键点(蓝色圆点)for landmark in hand_landmarks_list:x = int(landmark.x * original_width)y = int(landmark.y * original_height)cv2.circle(frame, (x, y), 5, (255, 0, 0), -1)# 绘制骨骼连接线(绿色线条)for connection in mp.solutions.hands.HAND_CONNECTIONS:start_idx, end_idx = connectionstart_landmark = hand_landmarks_list[start_idx]end_landmark = hand_landmarks_list[end_idx]start_x = int(start_landmark.x * original_width)start_y = int(start_landmark.y * original_height)end_x = int(end_landmark.x * original_width)end_y = int(end_landmark.y * original_height)cv2.line(frame, (start_x, start_y), (end_x, end_y), (0, 255, 0), 2)return frame
关键点:
- 图像预处理:
- 将 OpenCV 的 BGR 格式转换为 MediaPipe 需要的 RGB 格式
- 创建
mp.Image
对象用于模型输入
- 骨骼绘制逻辑:
- 关键点:每个手部 21 个关键点,用蓝色圆点标记
- 连接线:使用
mp.solutions.hands.HAND_CONNECTIONS
定义的连接关系,用绿色线条连接关键点 - 坐标转换:将归一化坐标(0-1 范围)转换为图像像素坐标
3. 摄像头画面更新函数
def update_camera():ret, frame = cap.read()if ret:frame_flipped = cv2.flip(frame, 1) # 水平翻转(镜像效果)frame_original = frame_flipped.copy()# 检测并绘制手部骨骼skeleton_frame = draw_hand_skeleton(frame_original)# 缩放并显示画面frame_resized = cv2.resize(frame_flipped, (400, 400))skeleton_resized = cv2.resize(skeleton_frame, (400, 400))# 转换为Tkinter可用格式l1_img = Image.fromarray(cv2.cvtColor(frame_resized, cv2.COLOR_BGR2RGB))l1.imgtk = ImageTk.PhotoImage(l1_img)l1.configure(image=l1.imgtk)l2_img = Image.fromarray(cv2.cvtColor(skeleton_resized, cv2.COLOR_BGR2RGB))l2.imgtk = ImageTk.PhotoImage(l2_img)l2.configure(image=l2.imgtk)# 每10ms调用一次自身,实现实时更新w.after(10, update_camera)
关键点:
- 镜像效果:
cv2.flip(frame, 1)
使画面更符合用户习惯 - 双窗口显示:
- 左侧窗口(l1):显示原始摄像头画面
- 右侧窗口(l2):显示绘制了骨骼的画面
- 图像格式转换:
OpenCV数组(BGR) → cvtColor → RGB数组 → PIL.Image → ImageTk.PhotoImage → Tkinter显示
- 定时更新:
w.after(10, update_camera)
实现约 100FPS 的更新频率
4. GUI 界面初始化
# 初始化摄像头
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)# 创建窗口
w = tk.Tk()
w.title("手语识别(简易)")
w.geometry("805x640")# 创建三个标签分别用于显示原始画面、骨骼画面和提示文本
l1 = tk.Label(w, width=400, height=400, bg="black")
l1.place(x=0, y=0)l2 = tk.Label(w, width=400, height=400, bg="black")
l2.place(x=400, y=0)l3 = tk.Label(w,text='手部骨骼检测已就绪,请将手放入画面中...',font=("微软雅黑", 12),bg="#f0f0f0",width=60,height=2
)
l3.place(x=10, y=530)# 启动更新循环并进入主事件循环
update_camera()
w.mainloop()# 释放资源
cap.release()
detector.close()
cv2.destroyAllWindows()
关键点:
- 窗口布局:
- 左右并列两个 400x400 的窗口
- 底部一个提示文本区域
- 资源管理:
- 使用
cap.release()
释放摄像头资源 - 使用
detector.close()
关闭模型 - 使用
cv2.destroyAllWindows()
关闭所有 OpenCV 窗口
- 使用
ok啦,写到这里只能算半成品,因为还有模型训练等等非常麻烦的事情,先写到这里吧~