
1. 项目概览:用骨骼和步态做身份识别 🦴
本项目面向“基于 3D 人体建模的身份识别”场景,核心思想是:不直接依赖人脸、指纹等显性生物特征,而是利用人体行走时的骨骼点空间信息与运动信息,提取具有个体差异的步态特征,从而完成身份判断。
这件事听起来像电影里的“走两步就暴露身份”,但工程上并不玄学。每个人走路时,肩、肘、髋、膝、脚等关节的相对位置、摆动幅度、帧间变化都有一定习惯性。系统要做的,就是把这些“走路签名”变成可计算的特征向量,再与已录入人员的数据集进行匹配。
| 项目条目 | 内容 |
|---|---|
| 来源 | 第一届 3D 视觉创新应用竞赛 + 王泽鹏 |
| 相机型号 | Astra Pro |
| 支持设备 | Zora P1 开发板 |
| 识别依据 | 人体骨骼点、步态运动、关节空间结构 |
| 典型应用 | 门禁系统、禁区报警、无感通行、隐私友好型身份识别 |
| 源码链接 | https://github.com/3DCVdeveloper/3DReconstruction_identification |
2. 为什么不用“刷脸”,而是看步态?😎
传统身份识别常见方案包括人脸、指纹、虹膜、门禁卡等。它们各有优势,但也存在配合度、隐私、遮挡、伪造或佩戴设备等问题。步态识别的特点是“人在自然行走时就能完成识别”,不需要停下来摆姿势,也不需要把脸凑到摄像头前。
项目总结中提到:体型与步态信息相比面部、指纹等特征更难模仿;同时,骨骼点数据比 RGB 彩色图像更少暴露肖像隐私。采集端只需要上传骨骼信息到服务端,而不是传输完整彩色画面,这一点对门禁、走廊、办公区等场景尤其友好。
| 识别方式 | 主要依据 | 优势 | 局限 |
|---|---|---|---|
| 人脸识别 | 面部纹理 | 精度高、应用成熟 | 受遮挡、光照、隐私影响 |
| 指纹识别 | 指纹纹路 | 设备便宜、速度快 | 需要主动接触或配合 |
| 门禁卡 | 物理凭证 | 部署简单 | 可转借、可遗失 |
| 骨骼步态识别 | 骨骼结构 + 行走运动 | 无感、隐私友好、较难模仿 | 需要连续帧与稳定采集环境 |
3. 系统架构:边端采骨骼,服务端认身份 🧩

从代码结构看,系统由两大部分组成:
| 模块 | 代表文件 | 职责 |
|---|---|---|
| 采集终端 | 3DSkeletonIdentify/ |
相机采集、骨骼点提取、界面显示、数据上传 |
| 服务端 API | api/ |
特征提取、向量化、身份匹配、数据录入 |
| 第三方依赖 | AstraSDK/、openpose/、opencv/ |
深度相机、骨骼识别、图像显示与处理 |
| 项目文档 | README.md、项目总结.pdf |
环境配置、系统说明、实验总结 |
整体流程可以概括为:
| 阶段 | 输入 | 输出 |
|---|---|---|
| 1. 采集 | Astra Pro RGB-D 数据 | RGB 帧、深度帧、BodyFrame |
| 2. 骨骼提取 | Astra SDK 或 OpenPose | 18 个关键点序列 |
| 3. 序列缓存 | 连续行走帧 | 最多 60 帧骨骼数据 |
| 4. 特征计算 | 骨骼点序列 | 空间、角度、运动、轨迹特征 |
| 5. 向量匹配 | 特征直方图向量 | 身份标签 + 相似度分数 |
| 6. 应用响应 | 识别结果 | 开门、报警、显示身份等 |
4. 采集端:Astra SDK 与 OpenPose 的双路线设计 📷

项目采集端使用 C++/MFC + OpenCV + Astra SDK 实现,并提供了两条骨骼提取路线:
| 路线 | 文件 | 特点 |
|---|---|---|
| Astra SDK BodyStream | main_process.cpp、camera.cpp |
速度较快,可利用深度相机骨骼流,适合边缘端探索 |
| OpenPose | joint_by_openpose.cpp |
骨骼点更稳定,但计算量大,对 GPU 依赖明显 |
项目总结中也给出了取舍:Astra SDK 速度更快,但远距离或复杂姿态下可能出现噪声点;OpenPose 准确性更好,但由于只基于 RGB 图像并依赖大量计算,实时性压力更大。于是系统保留了两种方案,用同一套后端特征流程进行对比和识别。
采集端初始化时会根据用户选择决定使用哪种算法:
Processer::Processer(bool* displayJoints, int algrothrim) : display(WIDTH, HEIGHT)
{
if (0 == algrothrim)
{
this->jointByOpenpose = new JointByOpenpose();
camera_initialize("license", ProcesserWorkMode::RGB, WIDTH, HEIGHT);
}
if (1 == algrothrim)
{
this->jointByOpenpose = NULL;
camera_initialize("license", ProcesserWorkMode::RGB_BODY, WIDTH, HEIGHT);
}
this->socket_client = new TcpClient("127.0.0.1", 1996,
&this->messOut, &this->messIsNew);
}
这里的 algrothrim == 0 表示 OpenPose;algrothrim == 1 表示 Astra SDK。虽然拼写有点“小手一抖”,但逻辑很清楚:一个走 RGB + OpenPose,一个走 RGB-D + BodyStream。
5. 骨骼点统一:把不同算法转换成同一种语言 🗣️
不管骨骼点来自 Astra SDK 还是 OpenPose,后端识别都希望拿到统一格式。项目采用了 OpenPose 风格的 18 * 3 数组:每个关键点包含 x、y 与置信度/有效标记。
Astra SDK 的关节点命名体系与 OpenPose 不同,因此代码中专门写了转换函数,把头、颈、肩、肘、手、髋、膝、脚等点映射到统一序号。
int Processer::convert_joints_astra_to_openpose(float* jointArray)
{
memset(jointArray, 0, 18 * 3 * sizeof(float));
if (0 != jointsData.count(astra::JointType::Head))
{
jointArray[0] = jointsData[astra::JointType::Head].x;
jointArray[1] = jointsData[astra::JointType::Head].y;
jointArray[2] = 1;
}
if (0 != jointsData.count(astra::JointType::Neck))
{
int i = 1;
jointArray[3 * i] = jointsData[astra::JointType::Neck].x;
jointArray[3 * i + 1] = jointsData[astra::JointType::Neck].y;
jointArray[3 * i + 2] = 1;
}
}
这一步的意义很大:前端可以自由切换骨骼提取算法,后端则不用关心骨骼点来自哪里。对系统来说,“谁给的点”不重要,“点的格式一致”才重要。
6. 连续帧识别:步态不是照片,是一段动作 🎞️
步态识别不能只看某一帧。一个人站着的姿态可能很像另一个人,但走起来的节奏、摆臂、步幅、关节变化会更有区分度。因此采集端会缓存连续骨骼帧,并在合适时机发送给服务端。
项目中定义了两个关键常量:
#define MAX_QUEUE 1
#define MAX_FRAME 60
MAX_QUEUE 用于限制视频采集队列,避免处理堆积;MAX_FRAME 表示最多保留 60 帧骨骼步态数据。检测模式下,系统每积累若干帧就会尝试发起身份查询;录入模式下,达到一定帧数后将骨骼序列保存到数据集。
if (this->jointFrameData.size() % 5 == 0 &&
this->currentScore < 0.7 &&
!this->collectMode)
{
((TcpClient*)this->socket_client)
->query_skeleton_label(this->jointFrameData, this->messOut, this->messIsNew);
}
else if (this->jointFrameData.size() == MAX_FRAME &&
this->collectMode &&
this->collectBegin)
{
((TcpClient*)this->socket_client)
->collect_skeleton_label(this->jointFrameData, this->collectLabel,
this->messOut, this->messIsNew);
}
这段逻辑有点像“边走边看,越看越准”:如果当前相似度不够高,就继续攒帧;如果进入录入模式,则把一段完整步态存入身份库。
7. TCP 通信协议:骨骼点坐上“小火车”去服务端 🚆

采集端与服务端通过 TCP 通信。协议设计很轻量:
| 消息类型 | 前缀 | 含义 |
|---|---|---|
| 查询身份 | Q |
上传一段骨骼序列,服务端返回身份标签 |
| 录入身份 | L<label>S |
上传带标签的骨骼序列,服务端保存到数据集 |
| 返回结果 | L<label>:<score> |
服务端返回识别标签与相似度 |
采集端将每帧 18 * 3 个浮点数用逗号连接,不同帧之间用分号分隔:
int TcpClient::query_skeleton_label(std::queue<float*> skeletonFrames,
std::string& mess,
bool& mess_new)
{
std::string sendData;
while (0 != skeletonFrames.size())
{
float* skeletonArray = skeletonFrames.front();
skeletonFrames.pop();
for (int i = 0; i < 18 * 3 - 1; i++)
{
sendData += std::to_string(*skeletonArray) + ",";
skeletonArray++;
}
sendData += std::to_string(*skeletonArray) + ";";
}
sendData = "Q" + sendData;
send(this->socketClient, sendData.c_str(), strlen(sendData.c_str()), 0);
return 0;
}
服务端则在 listener.py 中监听 127.0.0.1:1996,解析消息前缀后进入查询或录入流程:
if func_type == 'Q':
_thread.start_new_thread(detect_person, (data[1:], conn, vectors))
elif func_type == 'L':
collect_data(data[1:], conn, vectors)
vectors = load_dataset(DATASETDIR)
8. 特征提取:把“走路姿势”翻译成数学向量 🧮

项目的特征提取主要由 readjsonApi.py 完成。它从骨骼点序列中提取四类信息:
| 特征类型 | 函数 | 描述 |
|---|---|---|
| 相对关节点位置 | computeRJP |
以颈部等关键点为参考,计算其他关节相对坐标与方向 |
| 肢体角度 | computeAngle |
计算肩、肘、髋、膝等肢体夹角和长度比例 |
| 全局帧间变化 | computeGlobal |
描述关节点在相邻帧之间的整体位移 |
| 局部帧间变化 | computeLocal |
去除人体整体平移后,描述局部动作变化 |
| 轨迹特征 | traj |
每 8 帧为一组,记录关键点运动距离和方向 |
相对关节点位置的核心思想如下:
def computeRJP(s_p):
feature = []
x = s_p[1 * 3]
y = s_p[1 * 3 + 1]
for i in range(st._SKNUM + 1):
if i == 1:
continue
x1 = s_p[i * 3] - x
y1 = s_p[i * 3 + 1] - y
r1 = math.atan2(y1, x1)
feature.append(x1)
feature.append(y1)
feature.append(r1)
return np.array(feature)
这相当于问:以人体躯干附近的点为基准,头、肩、手、腿分别在哪里?这种相对表达能削弱人在画面中左右移动带来的影响,更关注身体结构本身。
肢体角度则通过向量夹角计算实现:
limb12 = np.array([s_p[2 * 3] - s_p[1 * 3],
s_p[2 * 3 + 1] - s_p[1 * 3 + 1]])
limb10 = np.array([s_p[0 * 3] - s_p[1 * 3],
s_p[0 * 3 + 1] - s_p[1 * 3 + 1]])
lx = np.sqrt(limb12.dot(limb12))
ly = np.sqrt(limb10.dot(limb10))
cos_angle = limb12.dot(limb10) / (lx * ly + 0.001)
angle = np.arccos(cos_angle)
为了避免身高、距离镜头远近等因素造成影响,代码还会用身体尺度对部分长度特征进行归一化。这样系统关注的是“比例和动作模式”,而不是简单地把高个子和矮个子分开。
9. 向量化与匹配:谁的步态更像谁?🧠
提取到每帧特征后,histogramApi.py 会将特征映射到预先保存的聚类中心文件,例如:
| 聚类中心文件 | 对应特征 |
|---|---|
180_vars_point.txt |
相对关节点位置 |
180_vars_angle.txt |
肢体角度 |
180_vars_delta_point.txt |
点位帧间变化 |
180_vars_delta_angle.txt |
角度帧间变化 |
180_vars_traj.txt |
轨迹变化 |
向量化时,系统使用 scipy.cluster.vq.vq 将特征分配到最近的聚类中心,再统计直方图。直方图可以理解为:这一段步态里,各类骨骼姿态和运动模式出现了多少次。
X = vars_point
label, dist = vq(X, center_point)
for j in label:
if dist[m] < thres:
histogram_point[0][j] += 1
histogram_point[0] = standardization(histogram_point[0])
histogram_angle[0] = standardization(histogram_angle[0])
temp = np.hstack((histogram_point, histogram_angle))
histogram_final.append(temp)
最终匹配使用余弦相似度。它关注两个向量方向是否接近,适合比较“特征分布形态”而不是绝对数值大小。
def cos_sim(a, b):
a_norm = np.linalg.norm(a)
b_norm = np.linalg.norm(b)
return np.dot(a, b) / (a_norm * b_norm)
def get_sample_label(v_r, vs):
sims = []
labels = []
v_r = np.array(v_r[0])
for (vec, label) in vs:
vec = np.array(vec[0])
sim = cos_sim(v_r, vec)
sims.append(sim)
labels.append(label)
sim_args = np.argsort(-np.array(sims))
return labels[sim_args[0]], sims[sim_args[0]]
一句话总结:系统把一段步态变成一个向量,再从身份库里找“最像的那个人”。如果相似度足够高,就认为身份匹配;如果不够高,那就先别开门,门禁表示自己很有原则。🚪
10. 数据采集与实验验证 📊
根据项目总结,实验采集采用约 90 度视角的骨骼步态信息:人体距离摄像头约 3 米,以正常速度走过指定路段,摄像头平均采集约 30 帧骨骼信息。每个人采集约 5 到 10 段骨骼序列,用于建立身份数据集。
| 实验要素 | 项目记录 |
|---|---|
| 采集设备 | Astra Pro 摄像头 |
| 采集内容 | RGB-D 信息与骨骼点序列 |
| 采集距离 | 人体距离摄像头约 3 米 |
| 采集角度 | 约 90 度侧向步态 |
| 每人样本 | 约 5 到 10 段骨骼序列 |
| 验证规模 | 7 人小规模数据集 |
| 识别表现 | Top-1 准确率达到 90% 以上 |
需要注意的是,这个结果体现的是原型验证价值,而不是大规模商业系统结论。样本人数较少、场景较固定,因此后续若要部署到真实门禁,需要继续扩充人员规模、采集角度、衣着变化、行走速度和环境条件。
11. 交互端:能看骨骼,也能录入身份 🖥️

项目的 Windows 端界面支持以下操作:
| 功能 | 代码入口 | 说明 |
|---|---|---|
| 启动/停止识别 | OnBnClickedBtnstart |
创建或销毁 Processer |
| 选择算法 | RadioOpenpose、RadioAstra |
OpenPose 或 Astra SDK |
| 显示/隐藏骨骼 | OnBnClickedBtndisplayjoint |
控制画面中是否绘制骨骼 |
| 录入身份 | OnBnClickedBtnlogin |
输入标签并进入采集保存模式 |
| 实时显示 | display_opencv.cpp |
将 OpenCV 画面嵌入 MFC 界面 |
录入身份的代码很直接:
void CMy3DSkeletonIdentifyDlg::OnBnClickedBtnlogin()
{
if (editLabel.GetWindowTextLengthW() == 0)
return;
if (NULL == this->processer)
return;
CString label;
editLabel.GetWindowTextW(label);
this->processer->begin_collect(std::string(CW2A(label.GetString())));
}
用户输入身份标签后,系统会采集一段骨骼序列并保存到服务端数据集中。后续再有人走过摄像头前,系统就可以把当前步态与库中样本进行匹配。
12. 技术优势:低打扰、少隐私、高自然度 ✨
| 优势 | 说明 |
|---|---|
| 无感识别 | 行人自然走过即可完成身份判断 |
| 隐私友好 | 服务端只需骨骼点,不必上传 RGB 肖像 |
| 抗遮挡特性 | 面部遮挡或更换衣服时仍可能保留步态差异 |
| 适合门禁 | 可在走廊、入口等空间中自然部署 |
| 可扩展 | 后续可拓展到行人重识别、区域追踪等任务 |
13. 当前限制与改进方向 🛠️
项目总结和代码都显示,这是一个完成了核心链路验证的研究原型。它已经能演示骨骼采集、特征提取、身份匹配和界面交互,但距离稳定落地仍有不少可打磨空间。
| 方向 | 当前状态 | 建议 |
|---|---|---|
| Zora P1 部署 | 项目目标支持,但当前主要在 Windows 端验证 | 继续完成 ARM Linux 适配与性能测试 |
| OpenPose 路线 | 精度较好但计算量大 | 增加轻量化姿态估计或模型压缩 |
| Astra SDK 路线 | 速度快但有噪声和距离限制 | 增加骨骼点平滑、异常点剔除和多帧滤波 |
| 数据规模 | 7 人小规模验证 | 扩充人员、角度、速度、衣着和场景 |
| 通信安全 | 本地 TCP 明文传输 | 增加认证、加密和任务日志 |
| 交互体验 | 功能可用但界面较朴素 | 优化可视化、状态提示和历史记录 |
| 识别策略 | Top-1 相似度匹配 | 增加阈值拒识、多样本融合和置信度校准 |
14. 结语:身份识别也可以“不盯着脸看”🎯
这个系统的有趣之处在于,它把身份识别从“看脸”转向“看身体运动模式”。Astra Pro 提供 RGB-D 感知能力,采集端提取骨骼序列,服务端将关节空间结构、角度、运动和轨迹转化为特征向量,再通过相似度判断身份。整个流程既体现了 3D 视觉的工程价值,也回应了隐私友好型识别系统的现实需求。
当然,步态识别不是魔法。它需要稳定的骨骼点、足够的数据、合理的阈值和持续的工程优化。但作为一个竞赛级原型,它已经把“人在走廊里自然走过,系统自动判断能不能开门”这件事跑通了。门还没开,算法先点了点头:嗯,是熟人。🔓
