发布日期: 2024/02/04 09:08

花费200元,我用雪糕棒手搓了一台可UI交互的视觉循迹小车


常见的视觉循迹小车都具备有路径识别、轨迹跟踪、转向避障、自主决策等基本功能,如果不采用红外避障的方案,那么想要完全满足以上这些功能,摄像头、电机、传感器这类关键部件缺一不可,由此一来小车成本也就难以控制了。

但如果,有这样一款视觉循迹小车,它可以完全自己手搓,并用成本极低的雪糕棒来搭建车体架构,不仅保留了传统循迹小车具备的所有功能,还额外适配上一块小屏幕并配上UI界面用于升级人机交互方式。



更重要的是,它的器件成本被压缩到200元左右,这样的视觉循迹小车能让你心动吗~


核桃派视觉循迹小车简介

核桃派H616视觉循迹小车的循迹功能和人机交互界面整体代码由Python+Qt实现,它通过摄像头获取周围环境的图像信息,并利用图像处理算法识别出特定的标记或路径,然后根据标记或路径的形状和方向信息,自动控制小车的行驶方向和速度,以实现沿着预定轨迹自动行驶的目的。


手搓一台视觉循迹小车所需要用到的基础硬件材料如下:

  • 核桃派H616开发板+LCD屏幕≈178元;
  • 四个电机+车轮≈16元;
  • 电机驱动模块≈4元;
  • 摄像头≈50元;
  • 移动电源≈20元;
  • 雪糕棒若干≈4元(也可以≈不要钱);

循迹功能实现

要让小车实现循迹自运动的操作,其实也可以说是一个在教小车如何精准识别线路并做出判断的过程,想要小车的摄像头实现对路线的准确判断,就需要用到一个目前循迹小车最广泛采用的技术手段——二值化。


二值化是图像分割的一种方法,用于将图像中的像素点矩阵的灰度值设置为0或255,也就是将整个图像呈现出明显的只有黑和白的视觉效果。

在二值化过程中,将大于某个临界灰度值的像素灰度设为灰度极大值(通常是255),将小于这个值的像素灰度设为灰度极小值(通常是0),从而实现二值化。

# 根据不同模式,用不同的hsv上下限值
upper_hsv = (180,255,100)
lower_hsv = (0,0,0)
grayImage = cv2.inRange(hsvImage, np.array(lower_hsv), np.array( upper_hsv)) # 颜色二值化

二值化图像后,整个画面会被区分为黑白分明的两种颜色,之后就需要进行路线轮廓的描绘以及质心的标注,这个操作的目的是让小车知道该往左拐还是往右拐,进而控制两边车轮的速度。

获取最大轮廓:

# 获取所有轮廓,画出所有轮廓
contours, hierarchy = cv2.findContours(grayImage, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
areas = [cv2.contourArea(c) for c in contours]]
cv2.drawContours(rgbImage,contours, -1,(0,255,0),3)

计算质心:

# 计算面积最大轮廓的质心
areas = [cv2.contourArea(c) for c in contours]
   try:
      M = cv2.moments(contours[areas.index(max(areas))])
   except:
      pass
   M10=M.get("m10")
   M01=M.get("m01")
   M00=M.get("m00")
   if M00 <= 0 :
      continue
   cX = int(M10 / M00)
   cY = int(M01 / M00)
# 绘制质心
cv2.circle(rgbImage, (cX, cY), 15, (255, 0, 255), -1)

在绘制轮廓与质心后就需要进行质心坐标的判断,这里的原理很简单,就是质心偏左就往左转,质心偏右就往右转,在判断的同时通过电机来控制两边车轮的速度进而控制小车的行驶方向。




根据质心的位置计算车轮速度的控制参数postion,根据具体的计算公式可以得知,postion的取值范围为-50到+50,其中0代表质心在正中间,+越大质心越往左,-越大质心越往右,则进行下面的速度计算和控制,否则将速度设置为0,再根据postion和delta_sum的值以及其他参数的调整,计算并控制左右车轮的速度。

# 车轮速度控制
if self.flag_start.status() == True :
    # -50 ~ +50  :0为正中间,+越大则越往左,-越大则越往右,
    postion = int(int(center_x - cX) / int(center_x / 50)) 
    # postion += self.delta_sum 

    step = self.postion_last - postion
    self.delta_sum += (postion - step)*0.01
    if self.delta_sum > 100:
        self.delta_sum =  100
    elif self.delta_sum < -100:
        self.delta_sum =  -100
    if abs(postion) < 5 :
        self.delta_sum = 0
       
    # print("self.delta_sum", self.delta_sum)
    self.postion_last = postion
         
    speed_l = 50 - postion - int(self.delta_sum) 
    speed_r = 50 + postion + int(self.delta_sum) 

    motor.L.speed(speed_l)
    motor.R.speed(speed_r)
    self._slider_l.setValue(speed_l)
    self._slider_r.setValue(speed_r)

人机交互界面实现

核桃派H616开发板上预装了PyQt,所以可以使用Qt自带的设计器软件来画窗口,在设计好后通过命令一键转化为Python代码,再去核桃派的开发文档复制一段显示案例的代码,就可以轻松在电脑上预览到刚刚的窗口画面。


为了在远程服务器上运行图形界面应用程序,通过设置os.environ["DISPLAY"] = ":0.0"允许Thonny远程运行。

# 允许Thonny远程运行
import os
os.environ["DISPLAY"] = ":0.0"

定义了一个名为event_press的函数,用于处理QPushButton按钮的released事件。当按钮被释放时,切换work.flag_start的状态,并根据状态改变按钮的文本。

def event_press():
    if work.flag_start.status():
        work.flag_start.disable()
        ui.pushButton.setText("点击开始")
    else :
        work.flag_start.enable()
        ui.pushButton.setText("点击结束")

为了处理三个不同按钮的released事件,定义了change_to_mode三个函数,这些函数用于将work.flag_mode的模式设置为不同的值。


def change_to_mode0():
    work.flag_mode.set_mod( 0 )
def change_to_mode1():
    work.flag_mode.set_mod( 1 )
def change_to_mode2():
    work.flag_mode.set_mod( 2 )
ui.pushButton.released.connect(event_press)
ui.pushButton_auto.released.connect(change_to_mode0)
ui.pushButton_black.released.connect(change_to_mode1)
ui.pushButton_white.released.connect(change_to_mode2)

以上这些代码片段是构成一个由PyQt所创建GUI的关键部分。它通过创建一个窗口,并在窗口中显示了一些UI元素,同时定义了一些事件处理函数,这个应用程序根据用户的操作来控制某些功能,并使用定时器来让解释器每隔一段时间运行一次,以保持界面响应性能,最后进入主循环等待事件的触发和处理。

开源资料获取

本文所有内容均转载自原作者本人的B站视频账号及核桃派论坛开源文章,文章内所提到的小车部件和源代码均公开在帖子和视频中,感兴趣的小伙伴可以复制下方链接或者戳文末的“阅读原文”获取。

B站视频链接:https://www.bilibili.com/video/BV1TK411i7Bb