news 2026/9/14 3:28:45

Python步态识别实战:基于GEI与CNN的行人身份识别

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python步态识别实战:基于GEI与CNN的行人身份识别

简介:这份步态识别行人项目资料包围绕 Python 实现展开,适合希望入门或进阶学习步态识别、传感器行为分析的学习者,也可直接用于毕业设计、课程设计、大作业、工程实训或初期项目立项。包内提供两组实验数据,data 与 data2 各含 10 个 txt 文件,分别记录 10 位受试者的加速度与陀螺仪信号,并配套 RNN+GRU(128)+全连接(10)、PCA+KNN、CNN、SVM 四类算法实现,便于横向对比模型效果。压缩包共 510 个文件、约 38.11MB,以 291 个 Python 源码为主,同时包含模型权重、训练检查点、配置与依赖文件、说明文本及少量可执行组件,目录结构能支撑数据读取、特征处理、模型训练与推理验证的完整流程。已有 233 人学习,读者可对照四套算法的训练脚本、检查点与说明文件,复现传感器时序数据的预处理、特征提取、分类评估等关键环节,也可在此基础上替换数据或调整网络层数,用于毕设与课程设计。

1. 步态识别不是刷脸换皮:Python 生态里远距离行人身份识别的最小闭环

步态识别行人的核心假设是:每个人走路的节奏、步幅和摆臂幅度,和指纹一样有区分度,而且不需要配合。摄像头放在十米开外,人低着头、背对着镜头,刷脸和指纹全失效,步态依然能给出一个候选身份。基于 Python 做这件事,难点从模型前移到了数据:能不能把一段监控视频稳定地切出单人的轮廓序列,直接决定最终识别率的上下限。这不是一篇 python 入门教程,默认你已经配好 python 3.8+ 环境和一块能跑 PyTorch 的 GPU。下文沿着「视频到轮廓、轮廓到周期、周期到特征、特征到身份」这条链路展开,把每一步的代码、参数和常见误区讲透,适合做安防、智慧养老和零售客群分析的工程师参考。

2. 步态识别行人的数据准备:从视频里切出干净轮廓序列

2.1 CASIA-B 与自采视频:步态数据从哪来

做步态识别,第一步永远是搞清楚数据长什么样。公开数据集里,CASIA-B 是中文语料中最常见的步态识别基准:124 个受试者、11 个视角(0° 到 180°)、三种行走条件(正常、背包、穿大衣)。学术界几乎所有步态论文都在这个数据集上报告准确率,拿它验证算法是否有效最省事,而且它提供预分割好的轮廓图,按序列编号组织目录,申请后直接下载即可跳过视频切割这一步。OU-ISIR 系列规模更大,受试者上千,但采集条件更杂,直接上手跑基线时往往分不清是算法问题还是数据问题。

实际落地大多没有公开数据可用,得自己架摄像头。我的经验是固定机位、镜头高度 1.5 米左右、让行人尽量垂直于镜头光轴行走,这样轮廓最完整,背景差分效果也最好。镜头正对行人走来(0° 视角)时,轮廓几乎不含摆动信息,识别率会明显下降,这个反直觉现象在第 5 章专门展开。

数据来源受试者规模视角条件/干扰适用阶段
CASIA-B12411背包、大衣算法验证与对比
OU-ISIR数千多视角多种大规模训练、跨视角研究
自采视频自定义1~2现场真实光照、遮挡工程落地与验收

2.2 用 OpenCV 背景差分把行人轮廓从视频里抠出来

固定摄像头场景下,背景差分是性价比最高的前景提取方式。OpenCV 内置的 MOG2 模型对光线渐变和树叶晃动有一定自适应能力,适合室外监控。环境上只需要 opencv-python 4.x 和 numpy 1.21 以上,IDE 用 vscode 把 Python 解释器指到虚拟环境即可开始。

import cv2 import numpy as np cap = cv2.VideoCapture("walk.mp4") fgbg = cv2.createBackgroundSubtractorMOG2( history=500, # 背景建模参考帧数 varThreshold=24, # 像素方差阈值,越大越不敏感 detectShadows=True # 标记阴影为 127,便于剔除 ) frames = [] while True: ret, frame = cap.read() if not ret: break fg = fgbg.apply(frame) # MOG2 的阴影标记值为 127,二值化后直接剔除 _, fg = cv2.threshold(fg, 200, 255, cv2.THRESH_BINARY) fg = cv2.morphologyEx(fg, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) fg = cv2.morphologyEx(fg, cv2.MORPH_CLOSE, np.ones((11, 11), np.uint8)) contours, _ = cv2.findContours(fg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: continue largest = max(contours, key=cv2.contourArea) x, y, w, h = cv2.boundingRect(largest) if w < 20 or h < 60: continue # 过滤树叶晃动产生的碎前景 frames.append(fg[y:y + h, x:x + w])

几个参数按场景调整:varThreshold 越大越不容易把阴影误判成前景,但也会漏掉与背景颜色接近的衣服;history 控制背景更新速度,人流密集的地方调到 200 左右,让背景尽快适应频繁变化。最后用连通域面积过滤小噪点是关键一步,否则后面的周期检测会被碎片前景干扰得面目全非。如果摄像头会移动,背景差分直接失效,需要换成目标检测加跟踪的方案,比如用检测器出框、按跟踪 ID 切序列,代价是检测框抖动会给后续对齐增加工作量。

2.3 步态周期检测:从轮廓宽度序列里找节奏

轮廓序列必须按周期切分,原因有二:GEI 特征依赖完整周期,不同视频的长度也不能直接比较。正常行走时,双支撑相(两只脚同时着地)轮廓最宽,单支撑相最窄,于是轮廓宽度呈近似正弦的周期性波动,这个信号就是切周期的依据。

import numpy as np def estimate_period(widths, min_frames=15, max_frames=60): """widths: 每帧行人轮廓宽度,返回周期长度(帧)""" x = widths - np.mean(widths) # 去直流分量 n = len(x) acorr = np.correlate(x, x, mode="full")[n - 1:] window = acorr[min_frames:max_frames] # 只在生理范围内找峰值 return int(np.argmax(window)) + min_frames

自相关的含义是序列与自身错位若干帧后的相似度,位移为 0 处必然最大,跳过前 15 帧就是避开这个无意义峰值。正常人步频约每分钟 100 到 120 步,一个完整周期包含两步,在 25fps 视频里对应 25 到 30 帧,min_frames 和 max_frames 按这个范围圈定,避免把半个周期或瞬时噪声当成周期。拿到周期长度后,从宽度序列的局部最小值开始,每隔一个周期切一段,每段就是一个步态周期样本。切完建议把每段帧数打印出来看一眼,周期不稳的片段宁可丢弃也不要塞进模型。

2.4 轮廓归一化与对齐:把人放进统一的 64×64 画布

不同行人离镜头距离不同,轮廓尺寸差异巨大,必须归一化到固定分辨率,同时保留重心位置信息。直接对整个外接矩形做 resize 会破坏横向位移信息,正确做法是先按高度缩放,再以水平质心对齐,最后底边对齐踩在同一水平线上。

def align_silhouette(sil, target_h=64, target_w=64): """缩放轮廓到统一画布:水平按质心对齐,垂直底边对齐""" h, w = sil.shape scale = target_h / h nw = max(1, min(target_w, int(w * scale))) resized = cv2.resize(sil, (nw, target_h), interpolation=cv2.INTER_NEAREST) m = cv2.moments(resized) cx = int(m["m10"] / (m["m00"] + 1e-6)) if m["m00"] > 0 else nw // 2 canvas = np.zeros((target_h, target_w), dtype=np.uint8) x0 = max(0, min(target_w // 2 - cx, target_w - nw)) canvas[:, x0:x0 + nw] = resized return canvas

用最近邻插值而不是双线性,是为了保持轮廓边缘的二值性,避免插值产生灰色过渡带。质心对齐保证不同身高的行人中轴落在画布同一列,底边对齐让身高差异转化为头顶到画布顶部的空白高度,这本身就成了身份线索。归一化结束后按subject_id/sequence_id/frame_idx.png组织目录,训练时直接按目录读,省去重复切割。

3. 用 Python 生成步态能量图 GEI:轮廓到特征的关键一跳

3.1 GEI 为什么是步态识别特征的首选

步态能量图(Gait Energy Image, GEI)的思路极其朴素:把一个周期的所有二值轮廓逐像素取平均,得到一张灰度图。动作幅度大的区域(摆臂、迈腿)在平均后灰度值低,躯干灰度值高,整张图同时编码了体型和运动能量分布。相比直接输入轮廓序列,GEI 把时序信息压进单帧,模型输入从T×H×W变成1×H×W,计算量小一个量级,并且对单帧切割噪声天然鲁棒,某帧轮廓破损只影响平均结果里的一个像素。代价是丢失帧间顺序信息,两个节奏不同但平均姿态接近的人可能在 GEI 上难以区分,这个局限到 3.4 节再讲对应的改进方向。

3.2 计算 GEI 的最小 Python 实现

import numpy as np def compute_gei(cycle_frames): """cycle_frames: 一个周期内归一化后的二值轮廓列表 (T, H, W)""" stack = np.stack(cycle_frames, axis=0).astype(np.float32) return stack.mean(axis=0)

这段代码的工作量集中在入参。cycle_frames 里每一帧都应该是 2.4 节对齐后的 64×64 二值图,且帧数恰好等于一个周期。实际使用中一个行人会采集多个周期,建议对每个周期分别算 GEI,再按身份取平均作为注册特征,匹配时取最近距离,比把整段行走压成一张图更稳。GEI 算完务必可视化检查:理想情况下腿部区域能看到明显的灰度渐变带(摆动形成的半透明影),如果整张图要么纯黑要么纯白,回头查二值化和周期切分,不要急着进模型。

注意:可视化 GEI 时统一用灰度色图上色,不要用伪彩色。伪彩色会放大灰度差异,容易让你误判特征质量。

3.3 GEI 的四个关键参数:周期、分辨率、阈值与对齐

参数经验值作用调参方向
周期帧数 T25~40决定平均的样本量T 小则噪声大,T 大则动作被平滑
输入分辨率64×64 / 128×64决定特征粒度数据量小用小分辨率防过拟合
二值化阈值200(剔除阴影)过滤 MOG2 阴影残留前景噪声多就调高
对齐方式质心 + 底边消除身高与距离干扰视角变化大时改整体外接框对齐

周期帧数是最值得调试的参数。同样是 25fps 的视频,一个人快步走周期只有 22 帧,慢步走要 35 帧;固定 T 切帧的话,慢走的人在平均时会引入半个多余步伐,GEI 腿部区域糊成一片。稳妥做法是先用 2.3 节的自相关估计实际周期,再对轮廓序列做时间维线性插值,统一重采样到 32 帧,让跨视频特征天然可比。

3.4 GEI 之外的序列建模思路

GEI 是步态识别的第一代特征,2019 年之后的 SOTA 算法基本转向序列建模:GaitSet 把周期内多帧轮廓当作无序集合做最大池化,GaitPart 用卷积在时间维做局部匹配。这些方案能把换装条件下的 Rank-1 再拉高 10 个点以上,但训练样本需求和调试成本也成倍上涨。理解 GEI 的价值在于它是最简单的可运行基线:先用它把整条工程链路跑通,拿到一个可复现的数值,再考虑替换成序列模型。直接把 SOTA 模型搭在没有验证过的数据管线上,出了问题根本分不清是数据错误还是模型错误。

4. 用 Python 训练步态识别算法:网络结构、损失函数与相似度度量

4.1 三类主流步态识别算法的选型逻辑

GEI + CNN 胜在简单,序列模型卡在数据量,姿态关键点方案看场景,选型的判断标准不是谁论文分数高,而是你手里有多少标注数据、摄像头部署在什么位置。

方案类型输入训练成本换装鲁棒性适用规模
GEI + CNN单帧100 人以内
序列 + 时空卷积T×H×W 张量100~1000 人
姿态关键点关节序列最好人少、遮挡少

GEI + CNN 在几十人的小规模场景完全够用,也是把整条管线跑通的最短路径;序列模型适合有 GPU 集群和标注团队的中大项目;姿态关键点在密集人群里会因遮挡大面积丢失关节点,安防场景要谨慎选。下文按 GEI + CNN 这条线展开,其他两类方案在参数设计上可以平移参考。

4.2 用 PyTorch 定义步态识别网络

下面的 GaitNet 接收单通道 GEI,输出 128 维归一化特征向量和一个分类 logits。训练阶段用分类头学身份,推理阶段只保留特征向量。

import torch import torch.nn as nn class GaitNet(nn.Module): def __init__(self, num_classes, emb_dim=128): super().__init__() self.backbone = nn.Sequential( nn.Conv2d(1, 32, 3, stride=2, padding=1), # 64×64 -> 32×32 nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.Conv2d(32, 64, 3, stride=2, padding=1), # 32×32 -> 16×16 nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 128, 3, stride=2, padding=1), # 16×16 -> 8×8 nn.BatchNorm2d(128), nn.ReLU(inplace=True), ) self.pool = nn.AdaptiveAvgPool2d((1, 1)) self.embedding = nn.Linear(128, emb_dim) self.classifier = nn.Linear(emb_dim, num_classes) self.ce = nn.CrossEntropyLoss() def encode(self, x): x = self.backbone(x) x = self.pool(x).flatten(1) return nn.functional.normalize(self.embedding(x), dim=1) def forward(self, x, labels=None): e = self.encode(x) logits = self.classifier(e) if labels is not None: return logits, self.ce(logits, labels), e return logits

两个细节值得说明。embedding 输出后做了一次 L2 归一化,把特征向量投影到单位球面,这样欧氏距离和余弦距离在排序上完全等价,后续算相似度矩阵可以直接用矩阵乘法。分类头只在训练时参与前向,推理阶段丢弃,避免特征向量与类别标签耦合。如果数据预处理时把 GEI 存成了三通道 PNG,在 Dataset 里要转灰度并做(x / 255.0 - 0.5)标准化,否则模型收敛速度会明显变慢。

4.3 损失函数组合:交叉熵保收敛,三元组拉结构

只靠交叉熵,模型学到的是区分身份,不保证同类特征聚拢、异类特征散开;只靠三元组,训练又容易震荡。常见做法是两者加权,交叉熵提供稳定梯度,三元组负责拉近同类、推开异类。

def batch_hard_triplet_loss(emb, labels, margin=0.3): """emb 已 L2 归一化,labels 是身份 ID""" B = emb.size(0) sim = emb @ emb.t() # B×B 余弦相似度 pos = labels.unsqueeze(1) == labels.unsqueeze(0) pos.fill_diagonal_(False) # 去掉自身匹配 pos_sim = sim.masked_fill(~pos, -1.0) # 只看正样本对 neg_sim = sim.masked_fill(pos, -1.0) # 只看负样本对 hardest_pos = pos_sim.max(dim=1).values # 最像的正样本 hardest_neg = neg_sim.max(dim=1).values # 最像的负样本 return torch.relu(hardest_neg - hardest_pos + margin).mean()

Batch-hard 三元组只取每个 anchor 最像的负样本和最像的正样本,梯度信号比随机三元组稳定。margin=0.3 的含义是:负样本与 anchor 的相似度至少要低于正样本 0.3,差距不足才计损失。margin 太小类间边界松,太大训练初期损失降不下去。完整训练的损失写成loss = ce_loss + 0.5 * triplet_loss,权重从 0.5 起步,观察验证集 Rank-1 再微调。

4.4 训练协议与超参:gallery/probe 别混、batch 怎么配

步态识别的评测协议和普通分类不同:注册集(gallery)和查询集(probe)必须来自不同拍摄序列。如果训练时把同一个人不同序列混在一起随机切分,模型会通过背景或拍摄时间偷学,测试准确率虚高。我的做法是给样本加一个VideoID字段,训练集按受试者划分,同一段视频的帧只出现在训练集或验证集之一,保证测试时模型没见过同一段视频的任何帧。

训练超参从这些值起步:Adam 优化器、学习率 1e-3、batch size 64,每个 batch 至少包含 8 个身份、每人 8 帧 GEI;学习率每 20 个 epoch 衰减 0.1,总共训练 80 到 100 个 epoch。数据增强只加随机水平翻转和 ±2 像素平移,不要做随机裁剪,GEI 对空间位置极其敏感,裁剪会破坏对齐,反而掉点。

5. 步态识别行人的验证与上线:Rank-1、跨视角鲁棒性和落地技巧

5.1 用 CMC 曲线评估步态识别的 Rank-1

CMC(Cumulative Match Characteristic)是步态识别最常用的评估曲线,横轴是候选名单长度,纵轴是真实身份出现在前 k 个候选里的概率,Rank-1 就是 k=1 时的命中率,也是论文里最常报的单个数字。

from sklearn.metrics.pairwise import cosine_similarity def cmc_curve(gallery_emb, gallery_ids, probe_emb, probe_ids, max_rank=20): sim = cosine_similarity(probe_emb, gallery_emb) cmc_arr = np.zeros(max_rank) for i in range(len(probe_ids)): order = np.argsort(-sim[i]) for r in range(max_rank): if gallery_ids[order[r]] == probe_ids[i]: cmc_arr[r:] += 1 break return cmc_arr / len(probe_ids)

实现上有两个容易出错的地方。一是必须排除同源样本,probe 和 gallery 如果来自同一段视频的相邻帧,相似度天然接近 1,会把 Rank-1 抬高好几个点;二是cmc_arr[r:] += 1的写法,因为「前 r 位命中」意味着所有更大的 k 也命中。把不同视角的 probe 分开算 CMC,你会发现 90° 侧视角最高、0° 正对视角最低,这是步态识别行人的固有特性,不是模型 bug。

5.2 换装、跨视角、步速变化:三个高频坑的补救方案

换装是最头疼的问题。穿大衣会让腿部轮廓糊成筒状,GEI 里腿部能量几乎全部丢失。补救思路是分部件处理:把 GEI 按比例切成头、躯干、腿部三个区域,给腿部更高的分类权重,或者训练时随机裁剪掉轮廓的一部分,强迫网络同时依赖多个部位。跨视角问题来自轮廓投影的本质,同一人从 0° 和 90° 看剪影完全不同,工程上最常见的妥协是分别训练两个固定视角的独立模型,视角差异超过 60° 时 Rank-1 掉 20 个点以上,这个预期要在项目立项时就和业务方对齐。步速变化影响的是周期切分,对策是估计周期后对轮廓序列做时间维线性插值,统一重采样到 32 帧再算 GEI,这个小操作通常能带回 3 到 5 个点的 Rank-1。

5.3 上线前的低成本验证:三人群体验收法

模型上线前,先做一次成本极低的实物验证:找三个人,每人拍两段不同日期的行走视频,第一段注册进 gallery,第二段作为 probe。整个流程跑通后,如果 Rank-1 命中率不是 100%,先别碰模型,把预处理管线里每个中间结果可视化——背景差分掩码、周期切分点、对齐后的轮廓序列逐帧存成图片核对。VSCode 里直接看图片目录比打印日志直观得多。我见过的大多数失败案例,最后都落在「切周期把两段走路拼在一起」和「阴影没滤干净」上,模型反而是最不需要动的零件。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 3:28:28

Python+OpenCV轻量级车道线检测系统开发实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 3:27:41

Pascal VOC标签转YOLO训练全指南:小目标数据集格式转换与调参

简介&#xff1a;面向YOLO目标检测的小番茄检测数据集&#xff0c;聚焦农业生产中的果实识别与成熟度判断&#xff0c;为计算机视觉方向的开发者和农业自动化研究者提供可直接使用的数据基础。压缩包共1790个文件&#xff0c;包含895张png原始图像与895个xml标签文件&#xff0…

作者头像 李华
网站建设 2026/9/14 3:25:42

OpenClaw+腾讯云:广告营销Agent基础设施部署与成本优化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 3:25:17

dshvm 架构拆解:用版本隔离终结 dsh 破坏性更新

如果你的工作流里已经离不开 dsh&#xff0c;大概率迟早会撞上这么一幕&#xff1a;一个平淡无奇的上午&#xff0c;你顺手执行了 dsh 的升级命令&#xff0c;下午开始&#xff0c;插件集体报错&#xff0c;CLI 参数变了&#xff0c;连对话历史的数据结构都对不上了。明明一行代…

作者头像 李华