尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python多通道手部穴位关键点识别:21点热力图与HRNet实战

Python多通道手部穴位关键点识别:21点热力图与HRNet实战 简介本资源面向计算机视觉与深度学习方向的开发者、中医药信息化研究者及高校学生提供一套基于YOLOv8实现多通道手部穴位关键点识别的完整项目代码与配套资料可用于智能按摩仪、健康管理APP等场景的技术验证与二次开发。压缩包共1036个文件约22.13MB包含160个Python脚本、492个Markdown说明文档、121张PNG与121张JPG图像样本、43个YAML及21个YML配置、12个CSV数据表另有ipynb实验笔记、sh运行脚本与Dockerfile部署文件覆盖数据预处理、模型构建、训练验证到部署的完整链路。目前已有376人学习下载。资源内含YOLOv8关键点检测的训练配置、标注数据组织方式与评估指标记录读者可据此复现手部穴位识别流程理解OpenCV图像增强与PyTorch训练细节并参考目录结构快速定位数据、模型与日志模块适合作为课程设计或科研项目的实践起点。1. 多通道手部穴位关键点识别从一张手掌图到 21 个坐标点手部穴位关键点识别说白了就是让程序在一张手掌图片里自动找出「合谷」「劳宫」「内关」这些穴位对应的像素坐标。传统做法靠人工标注一个中医师标一只手要几分钟批量做数据集时效率极低。而基于 Python 实现多通道手部穴位关键点识别核心思路是把「多通道」理解成两条线一条是输入侧的多通道RGB 三通道 深度图 红外图或者单图切出的多尺度特征通道另一条是输出侧的多通道每个关键点一个热力图通道21 个点就是 21 通道。这两条线决定了你后面选什么模型、怎么标数据、怎么部署。这篇文章面向的是想用 Python 把这件事跑起来的工程师——你可能做过 MediaPipe 手部检测但发现它只给 21 个关节不给穴位也可能手里有一批手掌照片想自己训一个能输出穴位坐标的模型。我会按「数据怎么造 → 模型怎么搭 → 多通道怎么接 → 坑在哪 → 怎么验证」的顺序讲代码全部用 PyTorch 和 OpenCV环境用 Python 3.10 CUDA 11.8你照着改路径就能跑。先记住一个反直觉结论穴位关键点识别最难的不是模型结构而是穴位定义本身在不同流派里就不统一所以你的标注规范比网络结构重要十倍。2. 数据准备穴位坐标怎么标、怎么增强、怎么转成热力图2.1 穴位定义与 21 点标注规范手部穴位没有像 COCO 那样的公开标准数据集你得自己定一套。我一般会参考 WHO 标准针灸经穴定位再结合 MediaPipe 的 21 个手部关节做映射。为什么用 21 点因为 MediaPipe Hands 已经能稳定输出 21 个关节坐标你可以把穴位点挂在这 21 个关节的相对位置上减少标注工作量。比如「合谷」大致在拇指食指掌骨之间对应 MediaPipe 的 INDEX_FINGER_MCP 和 THUMB_MCP 连线的中点偏桡侧。具体标注时我建议用 LabelMe 或 CVAT 标 21 个点每个点一个类别名。标注文件存成 JSON结构如下{ image: hand_001.jpg, width: 640, height: 480, points: [ {name: hegu, x: 312, y: 287}, {name: laogong, x: 298, y: 341}, {name: neiguan, x: 275, y: 402} ] }这里 x、y 是像素坐标原点在左上角。注意不同人手掌大小差异大直接回归绝对坐标会让模型学偏所以后面要转成相对坐标或热力图。2.2 从 JSON 到热力图多通道输出的关键一步关键点识别主流有两种输出直接回归坐标FC 层输出 42 维和热力图每个点一张高斯热力图。热力图对多通道更友好因为 21 个点就是 21 个通道空间信息保留得好小目标也稳。下面是把 JSON 转成 21 通道热力图的代码import json import numpy as np import cv2 def json_to_heatmap(json_path, img_size(256, 256), sigma3): with open(json_path, r) as f: data json.load(f) h, w img_size heatmaps np.zeros((21, h, w), dtypenp.float32) # 21 个穴位按固定顺序排列顺序必须和训练时一致 keypoint_order [ hegu, laogong, neiguan, waiguan, houxi, zhongzhu, jianjing, quchi, shousanli, yangchi, wenliu, pianli, lieque, jingqu, taiyuan, yuji, yuzhen, shaofu, shenmen, daling, zhongchong ] for idx, name in enumerate(keypoint_order): for p in data[points]: if p[name] name: # 坐标归一化到 0-1再映射到热力图尺寸 cx int(p[x] / data[width] * w) cy int(p[y] / data[height] * h) # 生成高斯热力图sigma 控制峰值扩散范围 heatmap np.zeros((h, w), dtypenp.float32) if 0 cx w and 0 cy h: heatmap[cy, cx] 1.0 heatmap cv2.GaussianBlur(heatmap, (0, 0), sigma) heatmap heatmap / (heatmap.max() 1e-6) heatmaps[idx] heatmap return heatmaps逻辑说明先按固定顺序遍历 21 个穴位名找到对应坐标后归一化再生成高斯热力图。sigma 取 3 是经验值太小会让模型对 1 像素误差都敏感太大则相邻穴位热力图会糊在一起。参数 img_size 建议和网络输入一致我一般用 256×256显存占用和精度平衡得比较好。2.3 数据增强别让模型只认一种手手部数据增强和通用目标检测不一样旋转和缩放要控制幅度。我常用这几招随机旋转 ±30 度超过 30 度穴位相对位置会失真随机缩放 0.81.2 倍模拟不同拍摄距离随机亮度对比度调整模拟不同光照水平翻转要慎用因为左右手穴位不对称翻转后必须重新映射穴位名增强代码用 Albumentations 最省事import albumentations as A transform A.Compose([ A.Rotate(limit30, p0.7), A.RandomScale(scale_limit0.2, p0.5), A.RandomBrightnessContrast(p0.5), ], keypoint_paramsA.KeypointParams(formatxy, remove_invisibleFalse))注意 keypoint_params 里的 format 要和你的坐标格式一致remove_invisible 设 False 是为了保留边界点后面用热力图时再统一处理越界。3. 模型搭建多通道输入与 21 通道热力图输出怎么接3.1 为什么选 HRNet 而不是 ResNet热力图回归任务里高分辨率特征保留得越好关键点定位越准。ResNet 经过多次下采样后小穴位点容易丢。HRNet 全程保持高分辨率分支并行连接不同尺度在 COCO 关键点上比 ResNet 高 35 个 AP。我一般用 HRNet-W18 作为 backbone参数量约 10M单张 256×256 输入在 RTX 3060 上推理约 15ms够用。多通道输入怎么接如果你只有 RGB那就是 3 通道输入。如果你有深度图或红外图可以在第一层卷积把输入通道改成 4 或 5。代码里改一个参数就行import torch import torch.nn as nn from hrnet import HRNet # 假设你用的是开源 HRNet 实现 class HandAcupointModel(nn.Module): def __init__(self, in_channels3, num_keypoints21): super().__init__() # HRNet 主干输入通道可配置 self.backbone HRNet(width18, in_channelsin_channels) # 输出头把最后一层特征图映射到 21 通道热力图 self.head nn.Conv2d(18 * 4, num_keypoints, kernel_size1) def forward(self, x): feat self.backbone(x) # 假设输出是 concat 后的多尺度特征 heatmaps self.head(feat) return heatmaps参数说明in_channels 根据你的输入改RGB 是 3加深度图是 4。num_keypoints 固定 21。head 用 1×1 卷积不改变空间尺寸输出和输入同分辨率方便后面算损失。3.2 损失函数MSE 还是 Wing Loss热力图回归常用 MSE但它对离群点敏感。Wing Loss 在小误差时梯度更大收敛更快。我一般先用 MSE 跑通再换 Wing Loss 微调。PyTorch 里 Wing Loss 要自己写class WingLoss(nn.Module): def __init__(self, omega10, epsilon2): super().__init__() self.omega omega self.epsilon epsilon def forward(self, pred, target): diff torch.abs(pred - target) loss torch.where( diff self.omega, self.omega * torch.log(1 diff / self.epsilon), diff - self.omega self.omega * torch.log(1 self.omega / self.epsilon) ) return loss.mean()omega 控制非线性区域范围epsilon 控制曲率。经验值 omega10、epsilon2 在热力图任务上比较稳。注意 pred 和 target 都要归一化到 01否则 omega 要跟着调。3.3 训练循环与多通道输出解码训练时输入 batch 图像输出 21 通道热力图和标注热力图算损失。解码时取每个通道最大值位置作为预测坐标def decode_heatmaps(heatmaps): # heatmaps: (B, 21, H, W) B, K, H, W heatmaps.shape coords torch.zeros((B, K, 2), deviceheatmaps.device) for b in range(B): for k in range(K): hm heatmaps[b, k] idx torch.argmax(hm) cy, cx idx // W, idx % W coords[b, k] torch.tensor([cx, cy], dtypetorch.float32) return coords这段代码效率不高实际部署时可以用 soft-argmax 替代 argmax得到亚像素精度def soft_argmax(heatmaps, beta100): B, K, H, W heatmaps.shape device heatmaps.device x torch.arange(W, devicedevice).float().view(1, 1, 1, W) y torch.arange(H, devicedevice).float().view(1, 1, H, 1) hm heatmaps.view(B, K, -1) hm torch.softmax(hm * beta, dim-1).view(B, K, H, W) cx (hm * x).sum(dim(2, 3)) cy (hm * y).sum(dim(2, 3)) return torch.stack([cx, cy], dim-1)beta 越大越接近 argmax越小越平滑。我一般训练时用 beta100推理时用 beta500 提高定位精度。4. 避坑与排查多通道穴位识别里最容易翻车的 5 个点4.1 穴位顺序错乱导致热力图通道对不上现象训练 loss 一直不降预测出来的点全挤在手掌中心。 原因标注 JSON 里穴位名和代码里的 keypoint_order 顺序不一致比如「合谷」在标注里排第 3代码里排第 1热力图通道就错位了。 解决把 keypoint_order 写成一个常量文件标注、训练、推理三处都 import 同一个列表别手抄。4.2 左右手混训导致模型左右不分现象验证集上左手样本误差是右手的两倍。 原因左手和右手穴位镜像对称但标注时没有区分左右模型学混了。 解决在数据加载时加一个 handedness 标签模型里加一个二分类头或者干脆左右手分开训两个模型。我一般选后者简单粗暴。4.3 热力图 sigma 设太小导致梯度消失现象训练初期 loss 下降极慢预测热力图全是零。 原因sigma1 时高斯峰值只覆盖 12 个像素大部分位置梯度接近零。 解决sigma 至少设 2我一般用 3。如果输入分辨率是 128×128sigma 可以降到 2。4.4 输入通道数改了但预训练权重没改现象加载 HRNet 预训练权重时报 shape mismatch。 原因预训练权重第一层是 3 通道你改成 4 通道后形状对不上。 解决加载时用 strictFalse或者手动把第一层权重复制三份再补一个随机通道。代码state_dict torch.load(hrnet_w18.pth) if in_channels ! 3: old_conv state_dict[backbone.conv1.weight] new_conv torch.zeros(in_channels, old_conv.shape[1], *old_conv.shape[2:]) new_conv[:3] old_conv new_conv[3:] old_conv.mean(dim0, keepdimTrue) state_dict[backbone.conv1.weight] new_conv model.load_state_dict(state_dict, strictFalse)4.5 推理时忘记归一化导致坐标全偏现象训练时 loss 很低推理时画出来的点全在图像外。 原因训练时输入做了 /255 归一化推理时直接喂原始像素。 解决把预处理写成一个函数训练和推理都调用同一个def preprocess(img): img cv2.resize(img, (256, 256)) img img.astype(np.float32) / 255.0 img (img - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] return img.transpose(2, 0, 1)均值方差用 ImageNet 的就行手部数据量不大时没必要重算。5. 验证与进阶用 PCK 和可视化把模型调到位5.1 PCK 指标怎么算才合理关键点任务常用 PCKPercentage of Correct Keypoints但阈值怎么定有讲究。手部穴位定位精度要求比人脸关键点低我一般用 0.05×手掌宽度作为阈值。手掌宽度取手腕到中指根的距离。代码def compute_pck(pred, gt, palm_width, threshold0.05): # pred, gt: (N, 21, 2) dist torch.norm(pred - gt, dim-1) # (N, 21) correct (dist threshold * palm_width).float() return correct.mean().item()palm_width 要按每张图单独算不能用一个固定值。如果 PCK 低于 0.8先查标注质量再查热力图 sigma。5.2 可视化排查把热力图叠回原图训练日志看不出问题时把预测热力图叠回原图最直观def visualize(img, heatmaps, alpha0.5): # img: (H, W, 3), heatmaps: (21, H, W) hm heatmaps.max(dim0)[0].cpu().numpy() hm (hm - hm.min()) / (hm.max() - hm.min() 1e-6) hm cv2.applyColorMap((hm * 255).astype(np.uint8), cv2.COLORMAP_JET) overlay cv2.addWeighted(img, 1 - alpha, hm, alpha, 0) return overlay如果热力图峰值分散在多个位置说明模型不确定可能是标注不一致或 sigma 太大。如果峰值偏移到手掌边缘检查归一化参数。5.3 一个具体技巧用关节热力图做辅助监督穴位点之间有关节约束比如「合谷」一定在拇指和食指之间。我一般会同时训一个 21 关节热力图分支用 MediaPipe 的关节坐标做辅助监督主分支训穴位。两个分支共享 backbone关节分支的 loss 权重设 0.3。这样穴位分支收敛更快PCK 能涨 35 个点。代码上就是 head 改成两个self.head_acupoint nn.Conv2d(18 * 4, 21, kernel_size1) self.head_joint nn.Conv2d(18 * 4, 21, kernel_size1)训练时 loss wing_loss(acupoint_pred, acupoint_gt) 0.3 * wing_loss(joint_pred, joint_gt)。关节标注可以用 MediaPipe 自动生成省人工。5.4 部署时的轻量化选择如果要在移动端跑HRNet-W18 还是偏重。我一般用 MobileNetV3 做 backbone热力图分辨率降到 128×128PCK 会掉 23 个点但推理能到 5ms 以内。导出 ONNX 时注意 soft-argmax 里的 arange 要改成常量否则某些推理引擎不支持动态 shape。torch.onnx.export(model, dummy_input, hand_acupoint.onnx, input_names[input], output_names[heatmaps], dynamic_axes{input: {0: batch}, heatmaps: {0: batch}}, opset_version11)opset 用 11 兼容性最好别用 13 以上很多移动端推理库还没跟上。最后说个血泪经验我最早做这个任务时花了两个月调模型结构PCK 卡在 0.75 上不去。后来把标注数据重新过了一遍发现 30% 的样本里「劳宫」和「少府」标反了。改完标注同一个模型直接到 0.89。所以如果你刚开始做先把标注规范定死找两个人交叉验证 100 张图比换任何 backbone 都管用。希望帮到你。本文还有配套的精品资源点击获取
返回列表