19f21b03a3470.png

1. 项目概览:用骨骼和步态做身份识别 🦴

本项目面向“基于 3D 人体建模的身份识别”场景,核心思想是:不直接依赖人脸、指纹等显性生物特征,而是利用人体行走时的骨骼点空间信息与运动信息,提取具有个体差异的步态特征,从而完成身份判断。

这件事听起来像电影里的“走两步就暴露身份”,但工程上并不玄学。每个人走路时,肩、肘、髋、膝、脚等关节的相对位置、摆动幅度、帧间变化都有一定习惯性。系统要做的,就是把这些“走路签名”变成可计算的特征向量,再与已录入人员的数据集进行匹配。

项目条目 内容
来源 第一届 3D 视觉创新应用竞赛 + 王泽鹏
相机型号 Astra Pro
支持设备 Zora P1 开发板
识别依据 人体骨骼点、步态运动、关节空间结构
典型应用 门禁系统、禁区报警、无感通行、隐私友好型身份识别
源码链接 https://github.com/3DCVdeveloper/3DReconstruction_identification

2. 为什么不用“刷脸”,而是看步态?😎

传统身份识别常见方案包括人脸、指纹、虹膜、门禁卡等。它们各有优势,但也存在配合度、隐私、遮挡、伪造或佩戴设备等问题。步态识别的特点是“人在自然行走时就能完成识别”,不需要停下来摆姿势,也不需要把脸凑到摄像头前。

项目总结中提到:体型与步态信息相比面部、指纹等特征更难模仿;同时,骨骼点数据比 RGB 彩色图像更少暴露肖像隐私。采集端只需要上传骨骼信息到服务端,而不是传输完整彩色画面,这一点对门禁、走廊、办公区等场景尤其友好。

识别方式 主要依据 优势 局限
人脸识别 面部纹理 精度高、应用成熟 受遮挡、光照、隐私影响
指纹识别 指纹纹路 设备便宜、速度快 需要主动接触或配合
门禁卡 物理凭证 部署简单 可转借、可遗失
骨骼步态识别 骨骼结构 + 行走运动 无感、隐私友好、较难模仿 需要连续帧与稳定采集环境

3. 系统架构:边端采骨骼,服务端认身份 🧩

19f21b6a983fe.png

从代码结构看,系统由两大部分组成:

模块 代表文件 职责
采集终端 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 的双路线设计 📷

19f21bb12fa26.png

项目采集端使用 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 通信协议:骨骼点坐上“小火车”去服务端 🚆

19f21c3039a29.png

采集端与服务端通过 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. 特征提取:把“走路姿势”翻译成数学向量 🧮

19f21c986b18e.png

项目的特征提取主要由 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. 交互端:能看骨骼,也能录入身份 🖥️

19f21cd4adcf1.png

项目的 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 视觉的工程价值,也回应了隐私友好型识别系统的现实需求。

当然,步态识别不是魔法。它需要稳定的骨骼点、足够的数据、合理的阈值和持续的工程优化。但作为一个竞赛级原型,它已经把“人在走廊里自然走过,系统自动判断能不能开门”这件事跑通了。门还没开,算法先点了点头:嗯,是熟人。🔓

19f21d259bb86.png

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注