一、前期准备

想要实现这些,首先就是要模拟出来一个大致的框架,方便后续开展,下面的就是随便写的一个框架,大家凑合看看就行,基本上是这个意思:

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 模块中的 所有公有名称(如 TkLabelButton 等)直接导入到当前命名空间。这意味着:

  • 无需通过模块名前缀(如 tk.)即可直接使用这些名称。
  • 如果当前命名空间中已有同名对象(如自定义的 Button 函数),会发生 名称冲突,导致程序报错或逻辑混乱。

 如果我直接使用的话,就会一直报错,恰好大伙们还不知道这个小知识点的话,就很难发现自己错在啥地方!!!!如果真不小心了咋办?就会出现下面的问题:

  • 覆盖内置函数或变量
    例如,若代码中定义了 Tk = "my_string",则 from tkinter import * 会尝试将 tkinter.Tk(窗口类)导入为 Tk,导致 Tk 被重新赋值为字符串,引发错误。
  • 难以追踪来源
    当代码中出现 Button 时,无法直接判断它是 tkinter.Button 还是其他模块 / 自定义的 Button,增加调试难度。
  • 破坏代码可读性
    对于大型项目,未加前缀的名称会让读者难以快速识别其所属模块,尤其是在多个模块被 import * 的情况下。

所以这是一个很抽象的错误,也是一个很小的知识点,一般来说,我们在系统性学习python的时候,是直接学的第二种方法,第一种老师也会讲,但是不会细讲,因为考试也不考,我们平时也接触不到这些比较难的库,所以这个方面的小知识点就很容易被忽略。 

至于为啥第二种好,老师也不会说,同样考试也不考,我就来简要的说一下,过两天我整理一下,跟这篇一起发出来:

1. 避免命名污染,确保名称唯一性

  • 隔离命名空间
    将 Tkinter 的所有名称(如 TkLabel)封装在 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,否则画面颜色会错乱。
  • 图像格式转换链
    OpenCV数组(BGR) → cvtColor → RGB数组 → PIL.Image → ImageTk.PhotoImage → Tkinter显示
    
    这是在 Tkinter 中显示 OpenCV 画面的标准流程。

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 应用。

核心特点

  1. 多模态感知能力
    • 支持手部追踪(21 个关键点)、人脸检测(468 个关键点)、人体姿态估计(33 个关键点)、物体检测与追踪等。
  2. 跨平台与多语言
    • 支持 Python、C++、Java、JavaScript 等语言,覆盖桌面、移动(Android/iOS)、边缘设备(如树莓派)。
  3. 模块化与实时性
    • 通过 “计算器图” 灵活组合组件,优化后可在移动端实现 30+ FPS 实时处理。
  4. 开箱即用与轻量级
    • 提供预训练模型,无需复杂训练;支持 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啦,写到这里只能算半成品,因为还有模型训练等等非常麻烦的事情,先写到这里吧~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/84185.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/84185.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/84185.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

React从基础入门到高级实战:React 实战项目 - 项目一:在线待办事项应用

React 实战项目:在线待办事项应用 欢迎来到本 React 开发教程专栏的第 26 篇!在之前的 25 篇文章中,我们从 React 的基础概念逐步深入到高级技巧,涵盖了组件、状态、路由和性能优化等核心知识。这一次,我们将通过一个…

1991-2024年上市公司个股换手率数据

1991-2024年上市公司个股换手率数据 1、时间:1991-2024年 2、来源:上海证券交易所和深圳证券交易所 3、指标:证券代码、交易年份、开始日期、截止日期、年换手率(流通股数)(%)、年换手率(总股数)(%)、日均换手率(流通股数)(%)、日均换手率…

RAID存储技术概述

1 数据存储架构 数据存储架构是对数据存储方式、存储设备及相关组件的组织和规划,涵盖存储系统的布局、数据存储策略等,它明确数据如何存储、管理与访问,为数据的安全、高效使用提供支撑。 1.1 存储系统 存储系统是计算机的重要组成部分之…

LRU 和 DiskLRU实现相册缓存器

我是写Linux后端的(golang、c、py),后端缓存算法通常是指的是内存里面的lru、或diskqueue,都是独立使用。 很少有用内存lru与disklru结合的场景需求。近段时间研究android开发,里面有一些设计思想值得后端学习。 写这…

可视化预警:如何让生产风险预警更高效?

你有没有遇到过这种情况? 明明设备已经开始发热报警,但操作人员还在继续运行; 或者某个参数已经接近危险值,却没人注意到; 甚至问题早就埋下了隐患,只是当时没发现…… 这些情况的背后,其实都…

【MPC-C++】qpOASES 源码编译与链接,编译器设置细节

qpOASES 源码编译与链接 克隆源码 git clone https://github.com/coin-or/qpOASES.gitcd qpOASES mkdir build cd build接下来是构建,有一些细节。 查看 CMakeLists.txt,发现如果不显示指定 CMAKE_BUILD_TYPE 构建版本,会自动编译 Release…

【11408学习记录】考研数学攻坚:行列式本质、性质与计算全突破

行列式 数学线性代数一、对象(元素):向量二、运算三、行列式3.1 第一种定义——行列式的本质定义3.2 行列式的性质性质1:行列互换,其值不变性质2:若行列式中某行(列)元素全为零&…

Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动

一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…

小木的算法日记-线段树

🌳 线段树 (Segment Tree):玩转区间作的终极利器 你好,未来的算法大师! 想象一下,你正在处理一个巨大的数据集,比如某个电商网站一整天的用户点击流。老板突然问你:“下…

Day24 元组和OS模块

1、元组(有序 不可变 可重复) 管道工程中pipeline类接收的是一个包含多个小元组的列表作为输入。可以这样理解这个结构: (1) 列表 []: 定义了步骤执行的先后顺序。Pipeline 会按照列表中的顺序依次处理数据。之所以用列…

Auto-Coder使用GPT-4o完成:在用TabPFN这个模型构建一个预测未来3天涨跌的分类任务

通过akshare库,获取股票数据,并生成TabPFN这个模型 可以识别、处理的格式,写一个完整的预处理示例,并构建一个预测未来 3 天股价涨跌的分类任务 用TabPFN这个模型构建一个预测未来 3 天股价涨跌的分类任务,进行预测并输…

Device Mapper 机制

Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…

crackme006

crackme006 名称值软件名称aLoNg3x.1.exe加壳方式无保护方式Serial编译语言Delphi调试环境Win10 64位使用工具x32dbg,ida pro,PEid,DarkDe4破解日期2025-06-05 脱壳 1. 先用PEid查壳 查到无壳 寻找Serial 查询到编程语言为Delphi 导出Delphi符号表信息到x32dbg&#xff0c…

Conda 创建新环境时报错 HTTP 502,如何解决?

Conda 创建新环境时报错 HTTP 502&#xff0c;如何解决&#xff1f; 最近在用 Conda 创建新环境时&#xff0c;突然遇到这样一个错误&#xff1a; CondaHTTPError: HTTP 502 BAD GATEWAY for url <https://mirrors.westlake.edu.cn/ANACONDA/cloud/conda-forge/linux-64/r…

2025最全TS手写题之partial/Omit/Pick/Exclude/Readonly/Required

随着 TS 在工作中使用的越来越广泛&#xff0c;面试的时候面试官也都会加上一两个 TS 的问题来了解候选人对于 TS 的熟悉程度&#xff0c;其中就有不少手写题目&#xff0c;比如笔者在字节的一次二面&#xff0c;面试官就问到了我如何实现一个 Pick&#xff0c;在小红书的一面&…

基于江科大stm32屏幕驱动,实现OLED多级菜单(动画效果),结构体链表实现(独创源码)

引言 在嵌入式系统中&#xff0c;用户界面的设计往往直接影响到用户体验。本文将以STM32微控制器和OLED显示屏为例&#xff0c;介绍如何实现一个多级菜单系统。该系统支持用户通过按键导航菜单&#xff0c;执行相应操作&#xff0c;并提供平滑的滚动动画效果。 本文设计了一个…

LLMs之StructuredOutput:大模型结构化输出的简介、常用方案、前沿框架之详细攻略

LLMs之StructuredOutput&#xff1a;大模型结构化输出的简介、常用方案、前沿框架之详细攻略 目录 大模型结构化输出的简介 1、特点与难点 大模型结构化输出的常用方案及对比 1、前沿框架&#xff1a;vLLM 与 XGrammar 大模型结构化输出的案例应用 大模型结构化输出的简介…

Linux中shell流程控制语句

一、if条件控制 1.1 语法解读 单路决策 - 单分支if语句样式&#xff1a;if [ 条件 ]then指令fi特点&#xff1a;单一条件&#xff0c;只有一个输出 双路决策 - 双分支if语句样式&#xff1a;if [ 条件 ]then指令1else指令2fi特点&#xff1a;单一条件&#xff0c;两个输出 …

Python学习(8) ----- Python的类与对象

Python 中的类&#xff08;Class&#xff09;与对象&#xff08;Object&#xff09;是面向对象编程&#xff08;OOP&#xff09;的核心。我们可以通过“类是模板&#xff0c;对象是实例”来理解它们的关系。 &#x1f9f1; 一句话理解&#xff1a; 类就像“图纸”&#xff0c;对…

数据结构-文件

文件是性质相同的记录的集合。 记录是文件中存取的基本单位&#xff0c;数据项是文件可使用的最小单位。 操作系统研究的文件是一维的无结构连续字符序列&#xff0c;数据库中研究的文件是带有结构的记录集合。 文件在外存上的4种基本组织方式&#xff1a;顺序、索引、散列、链…