资讯详情

基于AlphaPose与JAAD的行人意图识别:从姿态估计到机器学习落地

📅 2026/9/16 5:57:09 | 华诺云谱 👁 阅读
基于AlphaPose与JAAD的行人意图识别:从姿态估计到机器学习落地
简介面向计算机视觉与自动驾驶场景这份资源以JAAD行人数据集为基准包含基于Alpha Pose的姿态识别、行人边界框与姿态结果匹配、SGD/SVM/Random Forest等分类模型训练及多种评价指标实现。代码结构围绕数据处理、模型训练与结果评估展开适合正在研究行人意图识别、需要可运行代码框架的开发者参考。整个压缩包共34个文件以21个Python脚本为主辅以XML配置、Git忽略文件、Markdown说明及项目配置文件整体仅42KB轻量易部署便于快速阅读核心逻辑。目前已有346人学习浏览。除了完整工程代码外还包含项目结构说明与许可文件目录清晰尤其适合需要基于JAAD数据集进行行人行为分类、头部方向估计或行人感知扩展的初学者与进阶者可在此基础上直接进行二次开发与算法对比实验。1. 基于行人感知的意图识别从AlphaPose到JAAD的落地挑战行人意图识别是个典型的“差一步”问题等到行人迈出半步再刹车系统响应已经慢了半拍而如果提前预判“这个人要横穿”车辆就能在更早的时间点开始减速。这个项目把 AlphaPose 姿态估计结果与 JAAD 行车数据集对齐用传统机器学习模型SGD、SVM、随机森林从骨架关键点中提取特征做意图分类。它的价值在于不依赖大规模动作视频也不需要高成本的行为标注靠姿态变化和行人位置关系就能得到一个可解释、可快速迭代的意图预测基线。适合正在搭建行人行为感知原型、希望先验证“姿态信息到底有没有用”的工程师和研究人员。整个工程的文件结构也对应了“数据解析 → 姿态匹配 → 特征计算 → 模型训练 → 工程接入”这条最直接的技术链路。2. 数据对齐与预处理JAAD注释规范与AlphaPose关键点的匹配实现2.1 JAAD的注释组织方式JAAD数据集的核心是“以视频剪辑为单位的注释”。它把行人分成三类pedestrian带行为注释的样本、peds较远且不与人互动的旁观者、people行人群体。每个行人有一个唯一id形如0_video_id_pedestrian_number。如果该行人带有行为注释id末尾会加字母b比如0_1_3b如果属于路人则末尾是p。这种命名方式直接影响了训练样本的组织代码里通常解析出id后再决定这条样本是否进入“意图识别”的训练集。所有边界框都用两点坐标表示即左上角和右下角[x1, y1, x2, y2]。每个框有对应的遮挡标签0表示无遮挡1表示部分遮挡25%2表示完全遮挡75%。遮挡标签在训练时非常重要完全遮挡的样本姿态估计结果不可靠盲目加入训练会严重干扰模型。我一般会先把遮挡为2的样本剔除或者标记成独立类别让模型学到“未知”状态。2.2 解析XML与mat文件项目里toolkit目录下的xml_read.py和mat_read.py就是负责把这套注释读进内存的。JAAD的原始标注以XML形式存放每个文件对应一个视频片段。下面的代码展示了解析XML并提取行人id、边界框和遮挡等级的逻辑import xml.etree.ElementTree as ET import glob import os def parse_jaad_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() records [] # JAAD的标签层级通常是 frame - object for frame in root.findall(.//frame): frame_id int(frame.get(id)) for obj in frame.findall(object): ped_id obj.get(id) # 例如 0_1_3b # 边界框信息 bbox obj.find(box) x1 int(float(bbox.get(left))) y1 int(float(bbox.get(top))) x2 int(float(bbox.get(right))) y2 int(float(bbox.get(bottom))) occl int(obj.find(attributes).get(occlusion)) records.append({ frame: frame_id, id: ped_id, bbox: (x1, y1, x2, y2), occlusion: occl }) return records这段代码用了findall(.//frame)来遍历所有帧再在每帧下循环object节点。这里的occlusion直接对应JAAD里0/1/2的遮挡等级。解析后得到的记录列表会作为后续匹配的基础。需要注意JAAD某个视频里同一行人可能出现很多帧所以每条记录都带着frame编号后续做跟踪或时序特征时会用到。mat_read.py则是读取coco17_data这类MATLAB格式的姿态结果。AlphaPose的输出有时会转成.mat保存使用scipy.io.loadmat读取即可from scipy.io import loadmat def load_pose_mat(mat_path): data loadmat(mat_path) # AlphaPose保存的字段通常是 keypoints 或 preds keypoints data[keypoints] # shape: (batch, 17, 3) boxes data[boxes] # shape: (batch, 4) return keypoints, boxes读取后data的维度是“帧数 × 17个关键点 × 3”第3维分别是x、y和置信度。boxes则是检测到的行人边界框。到这里两边的数据都进内存了下一步是解决“AlphaPose的框”和“JAAD的框”如何配对。2.3 AlphaPose输出与JAAD框的匹配AlphaPose独立检测出的行人框和JAAD标注框往往不在同一坐标系其实两者都在原图坐标系下但检测框数量、顺序和JAAD不一定对得上。常见做法是计算交并比IoU并用最大匹配策略将检测框与标注框关联。下面是我在工程项目里常用的暴力匹配版本def match_detections_to_jaad(gt_boxes, det_boxes, iou_thresh0.5): matches [] # (gt_idx, det_idx) for g_idx, gt in enumerate(gt_boxes): best_iou 0.0 best_d_idx -1 for d_idx, det in enumerate(det_boxes): iou compute_iou(gt, det) if iou best_iou: best_iou iou best_d_idx d_idx if best_iou iou_thresh: matches.append((g_idx, best_d_idx)) return matches匹配完成后再把对应检测框的关键点坐标取出来和该行人的ID、遮挡等级拼到一起形成一条完整的训练样本。这里有个很容易踩的坑AlphaPose在完全遮挡时仍可能给出一个置信度很低的关键点结果。所以在匹配后要再做一次置信度过滤把平均关键点置信度低于0.4的样本丢弃。实际效果是训练数据量减少了约10%~20%但模型准确率能提升3~5个百分点。3. 特征工程与头部姿态辅助从关键点到意图特征向量3.1 COCO17关键点结构与行人框归一化AlphaPose默认输出COCO17格式的17个关键点顺序为鼻子、左眼、右眼、左耳、右耳、左肩、右肩、左肘、右肘、左腕、右腕、左髋、右髋、左膝、右膝、左踝、右踝。这个顺序决定了所有特征计算都必须按下标访问。例如第0点是鼻子第5点和第6点分别是左右肩。原始关键点坐标受行人距离和图像分辨率影响很大直接输入分类器会让模型学到“离镜头近的人意图更明显”这种假规律。因此第一步是按行人框归一化。常见做法是把关键点坐标减框左上角再除以框宽高使得所有坐标落在[0,1]区间。这样同一个动作在不同距离下的特征向量基本一致。3.2 人脸关键点与头部方向头部方向是行人意图的核心线索。项目中有get_face_point_test.py专门做人脸点检测然后结合肩膀线估算头部朝向。一个简单有效的方法是通过鼻子、耳朵和肩膀的相对位置判断偏航角。例如行人向左看时右耳到鼻子的距离会比左耳到鼻子的距离短。下面这段代码演示了如何从COCO17关键点里提取头部方向特征def compute_head_feature(pose): nose pose[0] left_ear pose[3] right_ear pose[4] left_shoulder pose[5] right_shoulder pose[6] # 肩膀宽度用于归一化 shoulder_width np.linalg.norm(right_shoulder[:2] - left_shoulder[:2]) if shoulder_width 1e-6: shoulder_width 1.0 # 鼻子到左耳/右耳的距离差正值表示头偏右 dist_left np.linalg.norm(nose[:2] - left_ear[:2]) dist_right np.linalg.norm(nose[:2] - right_ear[:2]) head_yaw (dist_left - dist_right) / shoulder_width # 鼻子相对于肩线中点的偏移表示抬头/低头 shoulder_center (left_shoulder[:2] right_shoulder[:2]) / 2 head_pitch (nose[1] - shoulder_center[1]) / shoulder_width return np.array([head_yaw, head_pitch])这里的head_yaw其实就是两个距离差除以肩宽能够大致反映头部左右转动幅度head_pitch通过鼻子与肩线中点的纵向偏移反映低头/抬头。它们都做了尺度归一化因此不同远近的行人可以直接比较。不过这个方法在行人侧对镜头时误差较大因为左右耳距离差会饱和。项目中后续TODO里提到的“实现头部方向分类”就是想把这种连续量改成离散的“左/中/右/前方”四分类减少噪声影响。3.3 构建完整意图特征向量把关键点、框信息和时序信息拼成一条特征向量这步在intention_calculate.py里完成。我建议的特征表如下特征名计算方式说明box_center_x / y(x1x2)/2归一化行人在画面中的水平位置box_w/h(x2-x1), (y2-y1) / 图像宽高行人尺度反映距离keypoints_norm关键点坐标归一化到框内17个点的相对位置shoulder_slope左右肩连线与水平线夹角判断倾斜/侧身head_yaw / pitch见3.2节代码头部朝向avg_conf17个关键点置信度均值姿态估计置信度bbox_speed(当前框中心-上一帧框中心)/帧间隔横向移动速度intention_calculate.py里会把当前帧的静态特征和前面若干帧的速度特征拼接起来形成长度为“17×2 8”左右的向量。代码中的示例函数如下def build_intention_feature(pose, bbox, prev_center, timestamp): kp np.array(pose)[:, :2].flatten() / [bbox_w, bbox_h] head compute_head_feature(pose) speed_x (bbox_center_x - prev_center[0]) / (timestamp 1e-6) return np.hstack([kp, head, [speed_x, bbox_w / image_w]])参数说明prev_center是上一帧行人中心坐标timestamp是两帧间隔秒数。这里的速度其实只是一个粗略估计因为视频帧率不一定稳定。更稳健的做法是使用JAAD自带的活动标签做平滑或直接计算连续三帧的中值速度。4. 模型训练与评估SGD、SVM与随机森林在意图识别上的对比4.1 样本组织与标签划分意图识别本质上是一个二分类问题过马路 / 不过马路。但JAAD自带的活动标签很多比如 walking, standing, looking。需要先做标签映射。项目里把“walking looking”这类会与驾驶员发生交互的样本视为正样本其余为负样本。同时要剔除遮挡为2的样本避免姿态特征不可靠。在组织数据时train_image_ml.py处理静态图像特征train_video_ml.py处理带时序信息的特征。两者的训练集和测试集都按视频剪辑划分不能随机切分否则同一视频的多个连续帧会同时出现在训练和验证里造成数据泄露。JAAD数据集的官方做法是以视频名划分例如用video_id的最后一位数字来切分保证同一个行人的轨迹不会被拆开。4.2 三种模型训练项目里用到的SGD、SVM和RandomForest都是scikit-learn实现。train_image_ml.py里的训练逻辑大致是from sklearn.linear_model import SGDClassifier from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import GridSearchCV def train_models(X_train, y_train): # SGD分类器适合大数据量 sgd Pipeline([ (scale, StandardScaler()), (clf, SGDClassifier(losslog_loss, max_iter1000, tol1e-3)) ]) # SVM使用RBF核需要先标准化 svm Pipeline([ (scale, StandardScaler()), (clf, SVC(kernelrbf, probabilityTrue, class_weightbalanced)) ]) # 随机森林不需要缩放直接训练 rf RandomForestClassifier(n_estimators300, max_depth12, min_samples_leaf5) models { sgd: sgd, svm: svm, rf: rf } for name, model in models.items(): if name rf: param_grid {n_estimators: [200, 400], max_depth: [8, 12, 16]} else: param_grid {clf__C: [0.01, 0.1, 1, 10]} # sgd和svm在pipeline里参数带clf__前缀 gs GridSearchCV(model, param_grid, cv3, scoringf1) gs.fit(X_train, y_train) models[name] gs.best_estimator_ return models参数说明SGD用的log_loss让模型输出概率方便后续阈值调优SVC设置了probabilityTrue虽然会降低训练速度但能够拿到predict_proba结果RandomForest的class_weightbalanced对不均衡数据更友好。GridSearchCV的cv3只做快速筛选最终还是要留出一个独立验证集来评估泛化性。4.3 评价指标与结果对比项目要求“实现各种评价指标”这里不能只看准确率。行人行为通常正负样本比例在1:3到1:5之间准确率很容易虚高。我一般会同时输出精确率、召回率、F1、AUC和混淆矩阵。test_joint_image_video.py里对应代码是from sklearn.metrics import precision_recall_fscore_support, roc_auc_score, confusion_matrix def evaluate_model(model, X_test, y_test): y_pred model.predict(X_test) y_prob model.predict_proba(X_test)[:, 1] precision, recall, f1, _ precision_recall_fscore_support( y_test, y_pred, averagebinary) auc roc_auc_score(y_test, y_prob) cm confusion_matrix(y_test, y_pred) print(fPrecision: {precision:.3f}, Recall: {recall:.3f}, F1: {f1:.3f}, AUC: {auc:.3f}) print(Confusion Matrix:) print(cm)从实际工程项目经验看随机森林在静态特征上往往比SGD好因为关键点归一化后存在大量非线性关系但随机森林容易过拟合需要把max_depth限制在12左右。SVM的RBF核在特征维度低时效果最好一旦特征维度超过200训练时间会明显上升。SGD的优势是训练快速适合后续加入视频流做在线学习。表格可以这样对比模型F1AUC训练时间约SGD0.710.783sSVM0.740.8225sRandomForest0.760.8412s这个结果只是示意具体数值取决于特征质量和样本量。但趋势是明确的在中小规模数据上树模型的表达力更强且对特征尺度不敏感。5. 工程化流水线main.py的数据流、日志与MySQL存储5.1 main.py的整体流程工程根目录下的main.py把前面所有模块串了起来。它的设计思路是“视频输入 → 关键点提取 → 数据对齐 → 特征计算 → 模型推理/训练”。整个流程需要考虑内存和速度视频不能一次性全部读入而是逐帧处理。def main(config_path): cfg load_config(config_path) # 1. 初始化日志 logger setup_logger(cfg[log]) # 2. 创建数据集处理链 data_loader VideoToImage(cfg[input_dir], cfg[output_dir]) data_loader.convert_all() # video_to_image.py # 3. 姿态估计 alpha_pose AlphaPoseWrapper(cfg[alpha_pose]) pose_results alpha_pose.infer(cfg[image_dir]) # 4. 匹配JAAD标注 jaad_records parse_jaad_xml(cfg[jaad_xml]) matches match_detections_to_jaad(jaad_records, pose_results) # 5. 特征计算与训练 X_train, y_train build_dataset(matches, cfg[features]) models train_models(X_train, y_train) # 6. 保存模型与特征信息 save_artifacts(models, cfg[output_dir])参数说明cfg是集中管理所有路径和超参数的配置字典AlphaPoseWrapper是对AlphaPose推理接口的封装matches里不仅包含索引对还附带了帧号、行人id和遮挡等级。build_dataset函数会把连续若干帧的特征拼接成时序特征这一步在train_video_ml.py里也有类似实现。5.2 日志与异常处理数据处理链路很长任何一步失败都会导致整条处理链中断。项目中的log_config.py和log.py解决了这个问题。setup_logger会同时把日志输出到控制台和文件并带上时间戳和模块名这样排错时能快速定位到是数据加载还是推理阶段出了问题。import logging def setup_logger(log_cfg): logger logging.getLogger(intention) logger.setLevel(logging.DEBUG) file_handler logging.FileHandler(log_cfg[path]) console_handler logging.StreamHandler() formatter logging.Formatter(%(asctime)s - %(name)s - %(levelname)s - %(message)s) file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) logger.addHandler(file_handler) logger.addHandler(console_handler) return logger正常情况下日志会记录每个阶段处理了多少帧、匹配成功多少样本、训练精度多少。一旦某帧关键点全部为0日志会显示“frame 1024: pose empty, skip”。这种异常在视频存在镜头切换或快速运动时经常出现直接跳过比强行填补更安全。5.3 推理结果写入MySQL工程里专门有一个mysql目录说明真实场景下需要把意图识别结果持久化。write.py负责把每条预测记录写入数据库方便后续做可视化或者事故复盘。以下是一个最小插入函数import pymysql def insert_intention(conn, video_id, frame_id, ped_id, prob, label): cursor conn.cursor() sql INSERT INTO intention_results (video_id, frame_id, ped_id, probability, label, create_time) VALUES (%s, %s, %s, %s, %s, NOW()) cursor.execute(sql, (video_id, frame_id, ped_id, prob, label)) conn.commit() cursor.close()这里的probability是模型输出的连续值label是阈值化后的0/1结果。保留概率而不是只存标签是为了后续做阈值调整和时序平滑。如果每帧都写数据库IO会成为瓶颈通常的做法是每隔10帧写入一次或者用消息队列异步落库。5.4 视频与图像双通道验证test_joint_image_video.py的作用是联合测试图像模型和视频模型。图像模型只依赖当前帧关键点速度快但噪声大视频模型依赖多帧时序特征更稳定但延迟高。工程里常用“早期融合”策略先引入图像模型的结果作为初始判断再用视频模型结果和它平均。这样既保留了实时性又提高了最终准确性。def joint_predict(image_model, video_model, feature_seq): image_prob image_model.predict_proba(feature_seq[-1:])[0, 1] video_prob video_model.predict_proba(feature_seq[None, :])[0, 1] return 0.6 * video_prob 0.4 * image_prob这个比例可以调整视频帧太少时video_prob不可靠可以把权重调低。6. 意图识别阈值优化利用错误分析调整决策边界模型默认的决策阈值是0.5但在行人意图识别里这样设定往往并不合理。负样本正常行走/站立远多于正样本准备横穿且两类风险代价不对称把行人横穿漏判成“安全”比把正常行判成“横穿”危险得多。因此最后一节要讲从验证集上学习最优阈值。首先绘制ROC曲线找到离左上角最近的点。这个点对应的阈值能最大化“真阳性率 真阴性率”。实际代码可以这样写import numpy as np def find_optimal_threshold(y_valid, prob_valid): fpr, tpr, thresholds roc_curve(y_valid, prob_valid) # 计算每个阈值点与左上角(0,1)的欧氏距离 dist np.sqrt((fpr - 0.0) ** 2 (tpr - 1.0) ** 2) best_idx np.argmin(dist) return thresholds[best_idx]参数说明这里用的是验证集而非测试集防避过拟合。验证集最好来自不同的视频片段这样得到的阈值才具有泛化能力。其次如果业务上对“漏报”零容忍可以结合代价敏感调整。比如设定假阴性的代价是假阳性的5倍那么目标函数就不是F1最大化而是代价最小化。这时可以把阈值往低调比如降到0.3同时观察验证集的精确率是否还能接受。还有一种做法是直接对正样本过采样或对负样本降采样但代价是模型输出概率会有偏差最佳阈值也会变化所以调整样本权重后必须重新搜索阈值。最后一个小技巧是时序平滑。单个帧的意图概率波动很大可以对连续5帧的概率做中值滤波消除单帧姿态估计失败导致的毛刺。配合前面找到的阈值系统在真实视频上的稳定性会有明显提升。这一步是所有模块都完成后最容易产生正向收益的改动值得最先尝试。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。