contribute上次修改:2026-09-29

4.3 视觉控制中的坐标与滤波

总目录:1.1 OpenCV介绍与学习路线 | 第四章:把知识连成项目 | 编号:4.3

← 上一节:4.2 摄像头检测与调参 | 返回总目录:1.1 OpenCV介绍与学习路线


4.3 视觉控制中的坐标与滤波

视觉检测给出 (cx, cy),云台却需要“往哪转、转多少”。中间隔着坐标系、镜头投影、控制方向和时延。这个地方一旦符号弄反,云台就会非常努力地逃离目标。

一、先分清图像、相机、云台和世界坐标

至少有四套概念:

图像坐标:像素 (x, y),右为正、下为正
相机坐标:以镜头为基准的三维方向
云台角度:yaw / pitch 的编码与正方向
世界坐标:固定在场地或靶面上的位置

它们不能靠“看起来差不多”直接相减。至少要做方向标定:

  1. 云台 yaw 增大时,画面中的固定目标向左还是向右?
  2. pitch 增大时,目标向上还是向下?
  3. 摄像头是否倒装、镜像或旋转?
  4. 单片机角度单位是度、弧度还是编码器计数?

二、从目标中心得到像素误差

画面中心:

height, width = frame.shape[:2]
image_center = (width / 2, height / 2)

目标误差:

error_x = target_x - image_center[0]
error_y = target_y - image_center[1]

按图像坐标:

error_x > 0 → 目标在画面右边
error_y > 0 → 目标在画面下边

发送给云台前是否取负,要由刚才的方向实验决定。

加入死区,避免中心附近来回抖:

def deadband(value, width):
    if abs(value) <= width:
        return 0.0
    return value - np.sign(value) * width

三、像素怎样粗略换成角度

若只知道水平视场角 HFOV,可先做小角度近似:

degrees_per_pixel_x = hfov_deg / width
angle_error_x = error_x * degrees_per_pixel_x

垂直方向同理。这个线性关系在画面中心附近较合理,越靠边误差越明显。

更准确的针孔模型使用焦距像素值:

angle_error_x = np.degrees(np.arctan2(error_x, fx))
angle_error_y = np.degrees(np.arctan2(error_y, fy))

fx、fy 来自相机标定矩阵。镜头畸变明显时,应先标定和去畸变。

四、一阶低通:给误差信号加减震器

一阶低通公式:

filtered = α × current + (1 - α) × previous
  • α 接近 1:更相信新值,反应快,平滑弱;
  • α 接近 0:更相信旧值,平滑强,延迟大。
class LowPassFilter:
    def __init__(self, alpha):
        if not 0 < alpha <= 1:
            raise ValueError("alpha 必须在 (0, 1] 内")
        self.alpha = alpha
        self.value = None

    def update(self, current):
        current = float(current)

        if self.value is None:
            self.value = current
        else:
            self.value = (
                self.alpha * current
                + (1 - self.alpha) * self.value
            )

        return self.value

    def reset(self):
        self.value = None

用 None 初始化,第一帧直接采用当前值,避免原笔记从 0 开始造成启动偏差。

帧率变化明显时,用时间常数比固定 alpha 更好解释:

alpha = dt / (tau + dt)

tau 越大,平滑越强。

五、滤波为什么也可能让系统更差

滤波能压噪声,却会引入滞后。闭环控制里,画面本来就晚于真实运动,再加很强滤波,控制器可能一直追着过去的位置跑。

震荡不一定只靠滤波解决,还要检查:

  • 控制增益是否过大;
  • 符号是否反了;
  • 摄像头与执行器延迟;
  • 串口命令是否积压;
  • 云台机械回差;
  • 目标检测是否间歇跳到干扰物;
  • 相机在云台上运动造成的画面反馈。
先修符号和时延
   ↓
再降低控制增益、加限幅与死区
   ↓
最后用适量滤波处理剩余噪声

图像的高斯滤波处理空间邻域;这里的一阶低通处理时间序列。都叫滤波,但对象完全不同。

六、固定物理中心与圆周轨迹

若要在距离 D 的平面上画物理半径 R 的圆,对应角半径可写为:

radius_angle = np.degrees(np.arctan2(R, D))

在世界或云台角度坐标里生成轨迹更清楚:

target_yaw = center_yaw + radius_angle * np.cos(omega * t)
target_pitch = center_pitch + radius_angle * np.sin(omega * t)

然后再让控制器追踪目标角度,而不是先在像素平面随便画个圆,再假设它一定对应真实世界的圆。

若必须预测固定世界点在当前画面中的像素位置,需要:

  • 当前 yaw / pitch;
  • 相机与云台轴之间的外参;
  • 相机内参 fx, fy, cx, cy;
  • 坐标轴方向约定;
  • 必要时考虑平移和镜头畸变。

原笔记中的 origin_x = Ox - yaw * K 可以作为小范围实验模型,但不应直接当完整相机投影模型。

七、一条更稳妥的控制链

检测目标中心
   ↓
检查目标是否可信、是否连续
   ↓
像素误差 → 角度误差
   ↓
死区 + 限幅 + 适量低通
   ↓
控制器计算命令
   ↓
发送并记录时间戳
   ↓
观察真实响应,重新调参

限幅示例:

command = np.clip(kp * filtered_error, -max_command, max_command)

目标丢失时不要继续拿最后一个误差无限控制:

if target is None:
    filter_x.reset()
    filter_y.reset()
    command_x = 0.0
    command_y = 0.0

到这里,OpenCV 入门的主线就走完了:像素怎样进来、怎样被分割成目标、怎样变成轮廓特征、又怎样交给控制系统。以后遇到新任务,先画流水线,再逐层替换工具,会比到处复制一整段代码可靠得多。


← 上一节:4.2 摄像头检测与调参 | 返回总目录:1.1 OpenCV介绍与学习路线

留言区

不收集任何个人信息;昵称只绑定浏览器里的随机 Cookie。