第 17 / 30 课。 前置:上一课。本篇的运行范围以实验说明和验证记录为准,构造样例不代表真实任务精度或板端性能。
先定义控制的物理意义
图像中心偏差是像素量,不是电机步数。本课把横向偏差归一化到约 [-1,1],输出归一化角速度意图。真实系统还需标定视场角、方向、减速比、步进细分和机械限位,再由驱动层转换。不能把本课数值直接写进电机寄存器。
最小控制使用比例项 u=Kp×e。误差在死区内时目标速度为零,外部再做速度和加速度限幅。先把 P 控制与延迟关系看明白,再考虑积分或微分;积分饱和与导数噪声不能靠堆参数解决。
目标选择不能每帧重来
本实现首次选择靠近画面中心的可见轨迹,然后锁定 ID。目标丢失时停止,不立即切换到其他人;重新选择需要显式 release。这样能区分“保持当前目标”与“自动挑选新目标”两种产品行为。
未匹配的预测框不产生控制命令。实际可允许短暂预测跟随,但必须明确最长时间和停止规则。本课采用更容易验证的观测缺失即停止策略。
时间戳与停止优先级
控制要看结果对应的采集时间。处理结束得很快但输入已排队一秒,仍然是过期结果。超过 timeout 时立即输出零速度,停止路径优先于普通加速度限幅;驱动如何实现机械安全减速应由实际控制层约定。
练习与硬件衔接
改变 Kp、死区和时间步,检查速度和相邻命令变化。模拟输入停顿,确认不会持续使用最后一条非零命令。进入海思云台系列时,先在电机禁能状态核对方向,再执行低速、小角度验证。
交付目标切换规则、单位说明、超时停止日志及机械边界。框跟得很稳不表示控制稳定,视频延迟与云台运动会构成反馈回路。
配套实验与运行记录
实验:目标跟随:从轨迹到云台控制接口
查看完整代码:main.py
"""实现目标锁定、死区、限速和失效停止,分离视觉输出与电机驱动。"""
from controller import Controller
def row(x, ident=1, matched=True):
return {"id": ident, "box": [x, 0, x + 20, 20], "matched": matched}
def main():
controller = Controller()
commands = []
for x in [70, 65, 60, 45]:
out = controller.step([row(x)], 100, 0.05, 0.02)
commands.append(out["speed"])
print(out)
assert all(abs(v) <= 0.6 for v in commands)
assert all(abs(a - b) <= 0.100001 for a, b in zip([0] + commands, commands))
stopped = controller.step([row(70)], 100, 0.05, 0.3)
assert stopped["speed"] == 0
lost = controller.step([row(70, 2)], 100, 0.05, 0.02)
assert lost["speed"] == 0 and lost["target_id"] == 1
controller.release()
assert controller.step([row(70, 2)], 100, 0.05, 0.02)["target_id"] == 2
print("stale stop, identity lock and explicit reacquisition passed")
if __name__ == "__main__":
main()
查看完整代码:controller.py
"""输出归一化角速度意图,不直接驱动电机。"""
import math
class Controller:
def __init__(
self, kp=1.2, deadzone=0.05, max_speed=0.6, max_accel=2.0, timeout=0.2
):
self.kp = kp
self.deadzone = deadzone
self.max_speed = max_speed
self.max_accel = max_accel
self.timeout = timeout
self.previous = 0.0
self.target = None
def step(self, rows, width, dt, age):
if (
width <= 0
or dt <= 0
or not all(math.isfinite(v) for v in [width, dt, age])
or age < 0
):
raise ValueError("invalid timing or dimensions")
visible = [r for r in rows if r["matched"]]
chosen = next((r for r in visible if r["id"] == self.target), None)
if chosen is None and self.target is None and visible:
chosen = min(
visible, key=lambda r: abs((r["box"][0] + r["box"][2]) / 2 - width / 2)
)
self.target = chosen["id"]
if age > self.timeout or chosen is None:
self.previous = 0.0
return {"target_id": self.target, "speed": 0.0, "reason": "stale_or_lost"}
center = (chosen["box"][0] + chosen["box"][2]) / 2
error = (center - width / 2) / (width / 2)
desired = (
0.0
if abs(error) <= self.deadzone
else max(-self.max_speed, min(self.max_speed, self.kp * error))
)
delta = self.max_accel * dt
speed = max(self.previous - delta, min(self.previous + delta, desired))
self.previous = speed
return {"target_id": self.target, "speed": speed, "reason": "tracking"}
def release(self):
self.target = None
self.previous = 0.0
在项目根目录执行;下载包解压后,在解压目录执行 python3 main.py。
cd 'content/posts/AI-深度学习/嵌入式视觉模型进阶/17-目标跟随/code' && python3 main.py
在本篇代码目录执行 python3 main.py。依赖安装、可选入口见包内 README.md;实际运行结果见 verification.md。下载包包含本课使用的辅助模块与样例。
参考与进一步阅读
原始论文或官方文档用于核对结构、接口和适用范围。本篇代码是围绕教学目标编写的最小实验;除明确注明的完整入口外,不视为对应论文模型的完整复现。