项目地址:
yolo-object-detection-lab:基于 FastAPI 与 Ultralytics YOLO 的图像目标检测实验项目 - AtomGit
前面几篇里,这个项目已经从目标检测一路做到:
Single Image → Batch Images → Video → Tracking → Line Crossing → ROI Analysis → Trajectory Heatmap → Multi-ROI Flow
做到这里之后,已经可以回答:
目标是什么?
是不是同一个目标?
有没有经过某条线?
进入了哪个区域?
在区域里停留多久?
从哪个区域去了哪里?
但还有一个很自然的问题:
这个目标到底移动得有多快?
所以这一次,我继续在 YOLO + ByteTrack 的基础上加入第九种模式:
Speed Estimation
用 Track ID 的连续位置变化,估计目标在视频中的运动速度。
一、这次做的不是“直接算车速”
先说明一个很重要的问题。
普通单目视频里,如果没有真实世界尺度标定,就不能直接把像素位移写成:
30 km/h 50 km/h
因为图像里:
100 px
到底代表 1 米还是 10 米,取决于摄像机位置、透视关系和目标所在区域。
所以这次我把速度分成两种:
Pixel Speed
单位:
px/s
以及完成尺度标定之后才允许使用的:
m/s km/h
这次真实运行结果主要使用的是px/s 模式,这样不会为了得到“看起来像真实车速”的数字而强行换算。
二、速度怎么从 ByteTrack 轨迹算出来?
ByteTrack 已经能够给每个目标一个稳定的 Track ID。
例如:
person #13 car #15 car #16
每一帧都会得到这个目标新的位置。
和前面 ROI、Line Crossing 一样,这次仍然使用:
Bottom-center Anchor
也就是 Bounding Box 底部中心:
anchor_x = (x1 + x2) / 2 anchor_y = y2原因也很直接:
对于行人,它更接近脚落在地面的位置;
对于车辆,它也更接近车辆在道路上的实际位置。
三、Pixel Speed 的计算
假设同一个 Track 在两帧中的位置分别是:
P1 = (x1, y1) P2 = (x2, y2)
像素位移:
distance_px = sqrt((x2 - x1)^2 + (y2 - y1)^2)
再根据两帧时间差:
delta_time
计算:
speed_px_s = distance_px / delta_time
最终单位就是:
px/s
比如:
80 px/s
表示这个目标在图像平面中,每秒大约移动 80 个像素。
四、为什么不能直接显示单帧速度?
目标检测框不是完全稳定的。
即使目标实际运动很平滑,bbox 也可能出现:
左抖一下 右抖一下 突然跳几像素
如果直接拿每两帧的位置算速度,就很容易出现:
30 42 38 160 35 40
这种明显抖动。
所以这次加入了:
Smoothing Window
我实际使用:
5
并采用:
Moving Average
也就是用最近 5 个速度样本求平均。
最终页面主要展示的是:
Smoothed Speed
而不是完全依赖单帧瞬时值。
五、还需要过滤异常位移
除了普通抖动,还有一种更严重的情况:
Track 在某一帧突然跳到了很远的位置。
这种情况可能来自:
检测框变化 遮挡 Track 重关联 目标重新出现
所以这次增加了:
Max Pixel Jump
真实测试设置为:
200 px
如果单帧位置跳变超过这个范围,就不会直接把这次异常位移纳入正常速度统计。
这样可以避免一个异常帧把整个 Track 的平均速度拉得特别高。
六、真实测试参数
这次继续使用前面几篇一直测试的道路视频:
Resolution: 1920×1080 FPS: 29.97 Total Frames: 962 Duration: 32.10 s
实际参数:
Confidence Threshold: 0.55 Frame Interval: Every Frame Smoothing Window: 5 Smoothing Method: Moving Average Max Pixel Jump: 200 px Speed Threshold: 0 Display Unit: px/s Show Track Trails: ON Trail Length: 30
七、962 帧完整跑完
最终运行结果:
962 / 962 Frames 100%
处理时间:
74.9 s
Tracking 得到:
Unique Tracks: 37 Tracks With Speed: 34
也就是说:
整个视频一共识别到 37 个不同 Track,其中 34 个拥有足够连续的位置数据,可以计算速度。
八、真实速度统计结果
最终 Speed Summary:
Unique Tracks: 37 Tracks With Speed: 34 Average Pixel Speed: 80.8 px/s Max Pixel Speed: 2047.1 px/s
这里要注意:
2047.1 px/s
不是:
2047 km/h
它只是图像平面中的像素速度峰值。
像素速度会受到:
透视关系 目标距离摄像机远近 检测框尺寸变化 目标运动方向
等因素影响。
所以它更适合:
同一视频 同一区域 相同摄像机条件
下的相对速度分析。
九、每一个 Track 都能单独看速度
这次页面还增加了:
Track Speed Table
每个目标都会记录:
Track ID Class Frames Average Speed Max Speed Distance Duration
例如一个 Car Track 可以看到:
Average: xx px/s Max: xx px/s Distance: xxx px Duration: x.xx s
而 Person、Car 等不同类别也可以分别查看。
这就比单纯显示:
Current Speed
更有用,因为它能够看完整个 Track 生命周期中的运动情况。
十、结果视频里可以直接看实时速度
最终 Result Video 中仍然保留:
Bounding Box Track ID Class Confidence Trajectory Speed
这样在视频播放过程中,就能看到目标位置变化和速度统计同步更新。
相比只看最后的表格,视频更容易判断:
为什么这个 Track 某一段速度突然变快?
某个目标是不是因为靠近摄像机导致像素速度明显增加?
十一、为什么有 37 个 Track,但只有 34 个能算速度?
因为速度至少需要:
两个有效时间点
如果某个目标只出现很短时间,例如:
1 帧
或者缺乏连续有效位置,那么就没有办法计算:
Δdistance / Δtime
所以:
Unique Tracks
和:
Tracks With Speed
本来就不应该完全相同。
这次实际是:
37 ↓ 34
属于正常结果。
十二、还加入了两点尺度标定
除了 px/s,这次也实现了一个简单的:
Two-point Calibration
用户可以在视频首帧上点击:
P1 P2
然后输入这两个点在真实世界中的已知距离。
例如:
5 m
系统就可以得到一个局部近似比例:
meters_per_pixel
之后理论上可以把:
px/s
转换为:
m/s km/h
不过这种两点标定需要谨慎使用。
因为道路视频存在明显透视:
远处 100 px
和:
近处 100 px
通常并不代表相同的真实距离。
所以这种方法更适合:
在比较小的局部平面区域内做近似尺度估计。
如果需要更严格的真实速度,还应该进一步加入:
Homography Perspective Transform Bird's-eye View
做道路平面标定。
十三、为什么这篇主要展示 px/s?
这也是我这次刻意保留的设计。
如果没有足够可靠的标定信息,程序就只显示:
px/s
而不是自动生成一个:
xx km/h
因为后者看起来更“高级”,但很容易产生误导。
这次真实测试中:
Average Pixel Speed = 80.8 px/s
这个数字虽然不是实际道路车速,但它是从真实 Track 位置和真实视频 FPS 中计算出来的。
它可以用于:
同一场景下比较快慢 发现异常运动 轨迹行为分析 后续标定前的数据验证
十四、Speed Threshold
这次还预留了:
Speed Threshold
可以设置:
100 px/s
或者标定以后设置:
10 km/h
当 Track 超过阈值时,可以生成:
SPEED_THRESHOLD Event
不过这次实际测试中设置为:
0
所以:
Speed Events = 0
没有启用阈值事件。
这里也没有把它写成:
“超速事件”
因为除非用户提供真实道路限速和可靠标定,否则这里只能叫:
Speed Threshold Event
而不是交通意义上的“超速”。
十五、支持结果导出
Speed Estimation 也支持:
Export Speed JSON Export Tracks CSV Export Speed Events CSV
JSON 可以保存:
video metadata settings calibration speed summary tracks events
Tracks CSV 则适合后续用:
Excel Pandas
继续统计:
平均速度 最大速度 运动距离 持续时间
而且导出使用的是当前已经完成的结果,不会重新跑 YOLO。
十六、391 个测试全部通过
加入 Speed Estimation 以后,项目完整测试增加到:
391 pytest passed
前端:
257 assertions passed
新增测试主要覆盖:
Bottom-center Anchor Pixel Distance px/s delta_time Moving Average Median Outlier Filter Track History Average / Max Speed Distance Calibration m/s km/h Threshold Event 0 Track Stale Result Reset YOLODetectorError
而之前所有模式:
Single Image Batch Images Video Tracking Line Crossing ROI Heatmap Multi-ROI
都没有回归。
十七、现在这个项目已经有 9 种模式
做到这里以后,整个项目已经变成:
Single Image ↓ Batch Images ↓ Video Detection ↓ ByteTrack Tracking ↓ Line Crossing ↓ ROI Occupancy & Dwell ↓ Trajectory Heatmap ↓ Multi-ROI Flow ↓ Speed Estimation
每一步其实都在回答一个新的问题。
最开始:
这是什么?
后来:
它是不是刚才那个目标?
再后来:
它去了哪里?
经过了哪里?
在哪里停留?
最常走哪些区域?
从哪个区域去了哪个区域?
而现在:
它移动得有多快?
总结
这次真实运行结果:
962 Frames 37 Unique Tracks 34 Tracks With Speed Average Pixel Speed: 80.8 px/s Max Pixel Speed: 2047.1 px/s Processing Time: 74.9 s
整个速度估计链路可以概括成:
YOLO ↓ ByteTrack ↓ Track ID ↓ Bottom-center Anchor ↓ Frame-to-frame Distance ↓ delta_time ↓ Pixel Speed ↓ Smoothing ↓ Outlier Filtering ↓ Speed Statistics
同时还加入了可选的两点尺度标定,为后续:
px/s → m/s → km/h
预留了接口。
项目地址:
yolo-object-detection-lab:基于 FastAPI 与 Ultralytics YOLO 的图像目标检测实验项目 - AtomGit
下一步如果继续往下做,我准备进一步加入:
Perspective Calibration + Bird's-eye View
把现在的像素速度估计继续往更接近真实世界尺度的运动分析推进。