/ 消息 / 线性回归线跟踪

线性回归线跟踪

大家好,

OpenMV Cam 能够在 2017 年 7 月 15 日举行的DYI Robocars Meetup上参赛,使用一种新算法来跟踪路线,使用线性回归与斑点跟踪。虽然使用线性回归速度较慢,但​​它可以实现更好的跟踪控制,因为它可以在视野中的任何位置找到直线。无论如何,这是视频:

代码

现在来看看它是如何工作的。首先,我们将相机设置为灰度 QQQVGA (80x60) 模式,并垂直翻转和水平镜像相机,因为我们将相机倒置安装在机器人上。我们使用灰度而不是 RGB565 来加快速度,同时将分辨率缩小到 80x60,这是正确跟踪线条所需的最小分辨率。使用更大的分辨率和稳健的线性回归是可能的,但很危险。请注意,由于我们不跟踪颜色,因此不需要关闭自动增益或自动白平衡。

传感器.reset()
传感器.set_pixformat(传感器.灰度)
传感器.set_framesize(传感器.QQQVGA)
传感器.set_vflip(True)
传感器.set_hmirror(True)
 sensor.skip_frames(time = 2000) # 在使用图像之前运行自动增益/自动白平衡

接下来,对于主循环,我们将拍摄 snapshot(),然后使用 histeq() 进行直方图均衡,这会提高对比度以克服白线和灰色混凝土地面之间的低对比度差异。运行 histeq() 后,图像中的所有亮线将被推高到灰度值接近 255。然后,我们使用阈值为 [240:255] 的二进制来利用这一事实,将图像分割为图像的亮线部分。最后,侵蚀操作可以清除所有噪音。

而真实:
 img =sensor.snapshot().histeq()
 img.binary([GRAYSCALE_THRESHOLD])
 img.侵蚀(1)

现在我们准备使用强大的线性回归算法来处理图像。该算法在运行 binary() 后计算所有设置像素对之间的斜率,然后找到中值斜率。使用这种技术,它可以在返回错误结果之前处理图像中大约 25% 的异常像素。然而,鲁棒线性回归是一种 NxN 算法……因此,如果设置了太多像素,它将运行很长时间。这就是我们使用 80x60 分辨率的原因。避免在设置太多像素时花费几秒钟来处理图像。

行 = img.get_regression([(255, 255)], 健壮=True)

请注意,get_regression()不需要binary()来对图像进行二值化。它会自己做这件事。但是,如果我们这样做,我们将无法看到二进制图像输出,我们将该操作移到外部并仅使用 get_regression([(255, 255)]) 查找白色像素...

无论如何,get_regression() 返回使用具有theta()、rho() 和magnitude() 值的线对象。在使用这些值之前,我们首先需要了解它们的含义。 Magnitude() 是直接的。这是线路检测的强度。一个简单的阈值就足以接受或拒绝该线路。弄清楚如何使用 theta() 和 rho() 有点困难。下图显示了 theta() 和 rho() 是什么:

Theta() 和 Rho()

那么,首先,我们为什么在这里使用极坐标数学?好吧,我们这样做是因为当 mx 变小时,您在学校学到的常规 my/mx 数学就不起作用了。通过极坐标数学,我们可以处理任何坡度。无论如何,theta() 是从原点定义的。它与 rho() 一起旋转,rho() 描述了从原点开始的线的长度。它们都用于在图像区域上投影一条垂直线。因此, get_regression() 返回的 theta() 和 rho() 值是用于查找通过图像中要跟踪的点的垂直线的值。请注意,笛卡尔坐标空间是颠倒的,因为图像是随着 Y 向下增加而索引的。上面的图像区域是实际图像在笛卡尔坐标空间中的位置。

现在,我们如何使用 theta() 和 rho() 来跟踪一条线?这真是令人费解。我花了大约6个小时思考才想出这个:

 def line_to_theta_and_rho(线):
 if line.rho() < 0: # 象限 3/4 
if line.theta() < 90: # 象限 3(未使用)
返回 (math.sin(math.radians(line.theta())),
 math.cos(math.radians(line.theta() + 180)) * -line.rho())
 else: # 象限 4
返回 (math.sin(math.radians(line.theta() - 180)),
 math.cos(math.radians(line.theta() + 180)) * -line.rho())
 else: # 象限 1/2
 if line.theta() < 90: # 象限 1
如果 line.theta() < 45:
 return (math.sin(math.radians(180 - line.theta())),
 math.cos(math.radians(line.theta())) * line.rho())
别的:
返回 (math.sin(math.radians(line.theta() - 180)),
 math.cos(math.radians(line.theta())) * line.rho())
 else: # 象限 2
 return (math.sin(math.radians(180 - line.theta())),
 math.cos(math.radians(line.theta())) * line.rho())

 def line_to_theta_and_rho_error(line, img):
 t, r = line_to_theta_and_rho(线)
 return (t, r - (img.width() // 2))

那么,这是怎么回事?好吧,我们基本上是在尝试根据图像中线条的位置来映射机器人要遵循的不同行为。首先,作为背景说明......为了模仿 OpenCV 输出(现在看来这可能不是一个好主意),theta() 和 rho() 值的定义类似于来自霍夫线检测器的 OpenCV 输出。也就是说,theta() 从 0 到 179,rho() 从负到正。然而,负数 rho() 没有任何意义。 rho() 为负数实际上意味着您应该将 180 添加到 theta(),然后使 rho() 为正数。我认为将所有 OpenMV 代码更改为仅输出从 0 到 359 的 theta() 和 rho() 可能很有意义,这只是正数,而不是按照 OpenCV 的方式做事......并且很快改变这一点......无论如何...

继续,基本上有四个区域,我们希望在给定线的位置的情况下,机器人有不同的行为。另外,请注意 get_regression() 永远不会返回小于 90 的负 rho() 和 theta()。因此,上面的第一种情况实际上永远不会执行(如果您查看上面的图像,您会发现没有可以绘制的线)通过象限 3) 的图像。这样就剩下象限 1、2 和 4。

现在,例如,当从上图中的象限 4 绘制线路时,这意味着机器人通常在线上,因此您希望它直线行驶并在返回线路时进行微妙的自我修正。

返回 (math.sin(math.radians(line.theta() - 180)),
 math.cos(math.radians(line.theta() + 180)) * -line.rho())

上面的代码通过将问题分为两部分来帮助实现这一目标。首先,我们希望将驱动 theta() 映射到零。因此,上面返回的元组的第一部分使用 sin() 将 theta() 映射到值 [-1:1]。当 theta() 为零时,sin() 为零。但是,随着 theta() 的增加,sin() 也会增加。由于每个象限只有 90 度,因此我们不必担心 sin() 再次开始减小。除此之外...上面的负 180 部分只是凭经验猜测并用于反转操作以使机器人朝正确的方向行驶。

无论如何,如果只是将上述值放入 PID 循环中,它不会帮助机器人驱动并沿着路线行驶。这是因为机器人只会尝试用这部分来使线变直。但是,它不会尝试将线置于机器人下方的中心。第二个方程实现了这一点。

 math.cos(math.radians(line.theta() + 180)) * -line.rho())

所以,上面的方程就是“cos(theta()) * rho()”。这是由 theta() 和 rho() 创建的向量的 X 长度。上面的负号和加 180 只是为了撤消前面提到的负 rho() 内容。随后,从图像的 X 中心减去 X 长度,(80 / 2) = 40,以创建另一个误差向量,我们将尝试使用 PID 循环将其最小化,以使机器人上线。

对于以下情况,上面的其余象限遵循类似的逻辑:您位于线的左侧、线的右侧、从左侧垂直地朝向线、以及从右侧垂直地朝向线。如果您想更多地了解发生了什么,我建议使用 theta() 和 rho() 在纸上画出可能的情况,如上图所示,然后看看数学是如何计算的。最后,请注意,与象限 1 相比,象限 2/4 就像半强度象限。这就是我们将象限 1 分为两部分的原因。

接下来,为了最终驱动机器人,我们只需获取两个误差函数输出并将它们输入 PID 循环:

 t,r = line_to_theta_and_rho_error(线,img)
新结果 = (t * THETA_GAIN) + (r * RHO_GAIN)
 delta_结果 = 新结果 - 旧结果
旧结果 = 新结果

new_time = pyb.millis()
增量时间 = 新时间 - 旧时间
旧时间 = 新时间

p_output = 新结果
i_output = max(min(i_output + new_result, I_MAX), I_MIN)
 d_输出 = (delta_结果 * 1000) / delta_时间
pid_output = (P_GAIN * p_output) + (I_GAIN * i_output) + (D_GAIN * d_output)

输出 = 90 + 最大值(最小值(int(pid_output),90),-90)

因此,在上面我们线性组合每个可以不同加权的误差函数输出,执行标准 PID 魔法,然后在 0 到 180 度之间驱动伺服。我的机器人的完整代码如下。只有92行!

 BINARY_VIEW = 真
灰度阈值 = (240, 255)
 MAG_THRESHOLD = 4
西塔增益 = 40.0
 RHO_增益 = -1.0 
P_增益 = 0.7
 I_增益 = 0.0
 I_MIN = -0.0
 I_MAX = 0.0
 D_增益 = 0.1
转向伺服索引 = 0

导入传感器、图像、时间、数学、pyb
从机器导入I2C、引脚
从伺服导入伺服

i2c = I2C(sda=引脚('P5'), scl=引脚('P4'), 频率=10000)
伺服=伺服(i2c,地址= 0x40,频率= 50,min_us = 1000,max_us = 2200,度= 180)
伺服系统.位置(STEERING_SERVO_INDEX, 90)

传感器.reset()
传感器.set_pixformat(传感器.灰度)
传感器.set_framesize(传感器.QQQVGA)
传感器.set_vflip(True)
传感器.set_hmirror(True)
传感器.skip_frames(时间= 2000)
时钟 = time.clock()

 def line_to_theta_and_rho(线):
如果 line.rho() < 0:
如果 line.theta() < 90:
返回 (math.sin(math.radians(line.theta())),
 math.cos(math.radians(line.theta() + 180)) * -line.rho())
别的:
返回 (math.sin(math.radians(line.theta() - 180)),
 math.cos(math.radians(line.theta() + 180)) * -line.rho())
别的:
如果 line.theta() < 90:
如果 line.theta() < 45: 
return (math.sin(math.radians(180 - line.theta())),
 math.cos(math.radians(line.theta())) * line.rho())
别的:
返回 (math.sin(math.radians(line.theta() - 180)),
 math.cos(math.radians(line.theta())) * line.rho())
别的:
 return (math.sin(math.radians(180 - line.theta())),
 math.cos(math.radians(line.theta())) * line.rho())

 def line_to_theta_and_rho_error(line, img):
 t, r = line_to_theta_and_rho(线)
 return (t, r - (img.width() // 2))

旧结果 = 0
 old_time = pyb.millis()
 i_输出 = 0
输出=90

而真实:
时钟.tick()
 img =sensor.snapshot().histeq()
如果是 BINARY_VIEW:
 img.binary([GRAYSCALE_THRESHOLD])
 img.侵蚀(1)

行 = img.get_regression([(255, 255)], 健壮=True)
打印字符串=“”

如果线且 (line.magnitude() >= MAG_THRESHOLD):
 img.draw_line(line.line(), color=127)

 t,r = line_to_theta_and_rho_error(线,img) 
新结果 = (t * THETA_GAIN) + (r * RHO_GAIN)
 delta_结果 = 新结果 - 旧结果
旧结果 = 新结果

new_time = pyb.millis()
增量时间 = 新时间 - 旧时间
旧时间 = 新时间

p_output = 新结果
i_output = max(min(i_output + new_result, I_MAX), I_MIN)
 d_输出 = (delta_结果 * 1000) / delta_时间
pid_output = (P_GAIN * p_output) + (I_GAIN * i_output) + (D_GAIN * d_output)

输出 = 90 + 最大值(最小值(int(pid_output),90),-90)
 print_string = "行确定 - 转 %d - 行 t: %d, r: %d" % (输出, line.theta(), line.rho())
别的:
 print_string = “线路丢失 - 转 %d” % 输出

伺服系统.位置(STEERING_SERVO_INDEX,输出)
 print("FPS %f, %s" % (clock.fps(), print_string))

Servos.py

PCA9685.py

现在,以上所有内容中最重要的是 line_to_theta_and_rho() 函数。虽然代码可以使用一些优化来使其更简单......但它正确地映射了如何驱动机器人为您走线!这意味着我们可以将它与 find_lines() 一起使用,它比 get_regression() 快得多,并且可以处理 160x120 或更高的图像来跟踪多条线,并线性组合检测到的多条线来驱动机器人!

驴

不管怎样,如果你已经读到这里了,感谢你的阅读!我和 Chris Anderson 将继续致力于 OpenMV Donkey Robocar 平台的工作。一旦我们解决了所有问题并使系统完美运行,我们将发布有关如何自行制作系统的说明。