AI跳绳系统从零实现:姿态识别、计数算法与多端架构实战
AI跳绳系统从零实现:姿态识别、计数算法与多端架构实战
AI跳绳是一类通过摄像头或惯性传感器采集人体运动数据,借助姿态估计模型识别跳绳动作并自动计数的应用系统。它的技术链路通常分为四层:端侧采集层(摄像头抽帧或蓝牙 IMU 数据)、算法推理层(人体关键点检测 + 计数状态机)、业务服务层(Spring Boot 提供接口与统计逻辑)、数据存储层(MySQL 落库与聚合查询)。如果只做单人娱乐场景,纯端侧推理就能闭环;如果要支持班级体测、多人同屏、历史曲线,则必须补上服务端与多端适配。本文按工程实现的顺序,把这条链路完整拆一遍。
一、先选路线:视觉方案与传感器方案的取舍
在动手写代码之前,需要先确定计数信号的来源,这决定了后续 70% 的工作量。
视觉方案:用手机摄像头拍摄,通过 BlazePose、MoveNet 等轻量姿态模型输出 17 或 33 个人体关键点,再基于腕部或踝部的周期性位移做计数。优点是零额外硬件,小程序、H5、App 都能跑;缺点是受光照、遮挡、背景干扰影响大,且推理功耗不可忽视。
传感器方案:跳绳手柄内置六轴 IMU(加速度计 + 陀螺仪),通过加速度模值的周期峰值计数,再经蓝牙 BLE 上报。优点是计数稳定、延迟低、几乎不耗手机算力;缺点是需要专用硬件,且只能计数,拿不到姿态是否标准的判断。
混合方案在实践中更常见:传感器负责高频计数,视觉负责姿态质量评估(例如判断是否出现"甩绳不跳"或"双摇误判")。两者数据在服务端按时间戳对齐。
二、视觉计数链路:关键点检测 + 峰值状态机
2.1 端侧抽帧与推理调度
不要在 requestAnimationFrame 里逐帧推理,手机端会迅速发热降频。合理做法是按固定间隔抽帧,例如 12~15 FPS,用 setInterval 或 cameraContext.onCameraFrame 控制节奏。
// uniapp:按固定间隔从摄像头取帧,送入本地推理
const INFER_INTERVAL = 66; // 约 15 FPS
let timer = null;
function startCapture(cameraContext, onKeypoints) {
const ctx = uni.createCameraContext();
timer = setInterval(() => {
ctx.takePhoto({
quality: 'low',
success: (res) => {
// res.tempImagePath 交给 WASM 版姿态模型推理
inferPose(res.tempImagePath).then(onKeypoints);
}
});
}, INFER_INTERVAL);
}
function stopCapture() {
if (timer) clearInterval(timer);
timer = null;
}
推理侧建议把模型量化为 int8,并用 WebAssembly + SIMD 后端;在小程序里可以走 .createInferenceSession,H5 端则用 TensorFlow.js 的 WASM 后端。模型输入分辨率压到 256×256 通常就够用,再大对计数精度几乎没有提升。
2.2 用腕部纵坐标做峰值检测
关键点里适合做计数的是左右手腕(索引 9、10)。腕部随绳的摆动呈明显周期性,且比踝部更不容易被腿部自遮挡。直接对 y 坐标做"过零检测"会抖动误判,正确做法是带滞回的峰值状态机:
class JumpCounter {
constructor({ high = 0.42, low = 0.30, minInterval = 180 }) {
this.high = high; // 归一化 y 的上升阈值
this.low = low; // 回落阈值
this.minInterval = minInterval;
this.state = 'down'; // down -> up -> down 记一次
this.lastTs = 0;
this.count = 0;
}
// y 为归一化到 [0,1] 的腕部纵坐标,越小越靠上
push(y, ts) {
if (ts - this.lastTs < this.minInterval) return this.count;
if (this.state === 'down' && y < this.high) {
this.state = 'up';
} else if (this.state === 'up' && y > this.low) {
this.state = 'down';
this.count += 1;
this.lastTs = ts;
}
return this.count;
}
}
三个参数的作用:high/low 构成滞回带,避免关键点抖动在单帧内反复触发;minInterval 限制计数频率,防止把绳子的高频旋转误算成起跳。实际调试时把这两个阈值做成可视化曲线叠加,比盲调快得多。
2.3 多人场景的 ID 绑定
多人同屏时,先按关键点包围盒做 IoU 匹配,把上一帧的轨迹 ID 分配给当前帧的人体,再对每个人体独立维护一个 JumpCounter 实例。轨迹丢失超过 1 秒就销毁计数器,避免 ID 漂移导致计数串号。
三、服务端与多端架构设计
端侧负责实时反馈,服务端负责沉淀与统计。技术栈上,后端用 Spring Boot + MyBatis-Plus + MySQL,用户端用 uni-app(Vue 语法)一次编写、编译到小程序 / H5 / 公众号 / 安卓 / iOS,管理后台用 Vue + Element UI 做数据看板与规则配置,是比较成熟的组合。
3.1 表结构
CREATE TABLE jump_session (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
source TINYINT NOT NULL COMMENT '1=视觉 2=蓝牙 3=混合',
started_at DATETIME NOT NULL,
ended_at DATETIME NULL,
total_count INT DEFAULT 0,
valid_count INT DEFAULT 0 COMMENT '姿态合格次数',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_user_time (user_id, started_at)
);
CREATE TABLE jump_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
session_id BIGINT NOT NULL,
seq INT NOT NULL COMMENT '端侧序号,用于幂等去重',
ts_offset INT NOT NULL COMMENT '相对会话开始的毫秒偏移',
confidence FLOAT DEFAULT 0,
KEY uk_session_seq (session_id, seq)
);
uk_session_seq 这个索引很关键:端侧网络抖动时会重发批次,靠它做幂等插入,避免同一次跳跃被计两遍。
3.2 上报接口
@RestController
@RequestMapping("/api/jump")
public class JumpController {
private final JumpService jumpService;
public JumpController(JumpService jumpService) {
this.jumpService = jumpService;
}
@PostMapping("/session/{id}/records")
public R<SyncResult> sync(@PathVariable Long id,
@RequestBody @Valid BatchSyncDTO dto) {
// 单批限制条数,超限直接拒绝,防止端侧异常刷量
if (dto.getRecords().size() > 500) {
return R.fail("batch too large");
}
return R.ok(jumpService.saveBatchIdempotent(id, dto.getRecords()));
}
}
saveBatchIdempotent 内部用 MyBatis-Plus 的 saveBatch 配合 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE,一次事务落库。
3.3 离线优先的端侧策略
跳绳场景常在户外、无网环境。端侧必须本地先存(uni.setStorage 或 SQLite),联网后按会话批量补传。补传时带上客户端生成的会话 UUID,服务端用它做跨端去重。
四、蓝牙智能跳绳的数据接入
如果采用硬件方案,通常走 BLE 的 GATT 通道:连接设备后订阅 Notify 特征值,手柄按固定频率推送打包好的数据帧。
uni.notifyBLECharacteristicValueChange({
deviceId,
serviceId: SERVICE_UUID,
characteristicId: NOTIFY_UUID,
state: true
});
uni.onBLECharacteristicValueChange((res) => {
const buf = new Uint8Array(res.value);
const view = new DataView(buf.buffer);
const count = view.getUint16(2, true); // 小端读取
const battery = view.getUint8(4);
updateRealtime(count, battery);
});
这里容易踩的坑是字节序。不同厂商固件的多字节字段大小端不一致,拿到协议文档后先用固定测试值验证一遍再解析,不要凭经验写 getUint16(2)。另外 Notify 频率往往高于 UI 刷新需求,建议在端侧做一次节流,只在计数变化时才触发状态更新。
五、落地过程中的优化建议
- 控制推理频率而不是分辨率。把推理降到 12 FPS,准确率几乎不变,但功耗下降明显。
- 阈值自适应。固定
high/low在不同身高、不同拍摄距离下表现差异很大,可以根据前 3 秒的腕部坐标极差动态初始化阈值。 - 姿态质量而非只计数。用膝角、躯干倾角判断动作规范性,比单纯计总数更有训练价值。
- 数据对齐。 混合方案中视觉与传感器时间戳来自不同时钟源,统一用服务端接收时间做一次校准。
- 灰度发布模型。 端侧模型更新要走版本号 + 回滚开关,避免新模型上线后计数异常。
FAQ
Q1:AI跳绳一定要用摄像头吗?
不一定。纯 IMU 的智能跳绳只能靠加速度峰值计数,稳定性好但拿不到姿态数据。摄像头方案能同时做计数和动作规范判断,代价是功耗和对光照敏感。
Q2:为什么计数会出现多计或少计?
多计通常来自绳子空转被误识别为起跳,可通过提高 minInterval 和加入踝部关键点交叉校验解决;少计多因关键点被遮挡导致轨迹断裂,可加入卡尔曼或简单线性预测补帧。
Q3:端侧推理和云端推理怎么选?
计数必须端侧做,因为需要实时反馈且涉及隐私视频流。云端更适合做长周期统计、排名聚合和模型迭代训练,不要把逐帧推理放到服务端。
Q4:多端适配成本高吗?
用 uni-app 编写用户端后,小程序、H5、安卓、iOS 可复用大部分逻辑;差异主要集中在摄像头调用、文件系统与蓝牙 API 上,建议把这些能力抽成统一的适配层。
Q5:数据上报丢了怎么办?
采用会话级 UUID + 记录序号的双重幂等设计,端侧离线缓存、联网补传,服务端索引兜底,即可保证终一致。
更多推荐


所有评论(0)