本帖最后由 韩亚楠 于 2024-9-29 14:32 编辑
前言
非常有幸参与到本次的双路电机扩展板试用活动,收到板子后第一感觉做工还是一如既往的精致,各种功能接口也是非常丰富,那既然是双路电机扩展板,那就优先做一个巡线小车项目来测试一下。
项目目录- 硬件组成
- 双路电机驱动方法
- 视觉识线
- PID巡线
- 完整程序及效果视频
- 总结
硬件组成
材料清单:
- 行空板 * 1
- 行空板双路电机驱动IO扩展板 * 1
- USB摄像头 * 1、3D打印摄像头支架*1
- 小车底盘( 直流减速电机* 2、轮子 * 2、五金若干)
- 减速电机尽量选择减速比大一些的,初步搞视觉巡线,速度一开始不能太快,需要能够慢下来且扭矩大一些的。
- 电源 * 1 (6-12V)
组装示意:ps:由于是使用之前的小车底盘,而且后续要继续测试高速光电巡线,所以图中的光电传感器并未拆除,本项目中所有光电传感器均处于未连接状态。
双路电机驱动
扩展板两路电机管脚控制方法:
M1 方向管脚 P5 (为1前进,为0后退) 速度管脚 P8 (数值范围0-1023) M2 方向管脚 P6 (为1前进,为0后退) 速度管脚 P16 (数值范围0-1023)
自己写了一个两轮驱动函数,把M1和M2的速度范围映射为-255~255,speed函数两个参数分别对应M1速度和M2速度。
- def numberMap(x, in_min, in_max, out_min, out_max):
- return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
- def nm(x):
- return int(numberMap(x, 0, 255, 0, 1023))
- def speed(x, y):
- if x>0:
- p_p5_out.write_digital(1)
- else:
- p_p5_out.write_digital(0)
- if y>0:
- p_p6_out.write_digital(1)
- else:
- p_p6_out.write_digital(0)
- x = nm(abs(x))
- y = nm(abs(y))
- p_p8_pwm.write_analog(x)
- p_p16_pwm.write_analog(y)
复制代码
视觉识线
视觉巡线的话还是首推opencv,使用方便。
大致思路:
1.首先对摄像头获取到的图片转换为HSV色彩空间,这样有助于找寻红色,然后根据找到的颜色来创建掩码(0(黑色)和255(白色)组成),在进行膨胀和腐蚀操作,改善图像特征并消除噪声,我们能得到如下图效果。 2.取掩码中 第320行和380行的像素点(下图中绿色线代表第320行,黄色线代表380行),统计并计算出画面中红线中心点的位置坐标(下图中红色十字星位置)。
代码: - #获取线中心位置函数
- def process_frame(img):
- global lp1
- global lp2
- global lcs
- #转换为hsv色彩空间
- hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
- # 定义红色的HSV范围
- lower_red = np.array([0, 100, 100])
- upper_red = np.array([10, 255, 255])
- # 创建红色掩码
- mask = cv2.inRange(hsv, lower_red, upper_red)
- # 形态学操作:膨胀和腐蚀
- kernel = np.ones((5, 5), np.uint8)
- mask = cv2.dilate(mask, kernel, iterations=4)
- mask = cv2.erode(mask, kernel, iterations=4)
-
- colorp1=mask[lp1]
- colorp2=mask[lp2]
- try:
- lineccp1 = np.sum(colorp1==lcs)
- lineccp2 = np.sum(colorp2==lcs)
- lineip1 = np.where(colorp1==lcs)
- lineip2 = np.where(colorp2==lcs)
- if lineccp1 == 0:
- lineccp1 = 1
- if lineccp2 == 0:
- lineccp2 = 1
- leftp1 = lineip1[0][lineccp1-1]
- rightp1 = lineip1[0][0]
- centerp1 = int((leftp1+rightp1)/2)
- leftp2 = lineip2[0][lineccp2-1]
- rightp2 = lineip2[0][0]
- centerp2 = int((leftp2+rightp2)/2)
- center = int((centerp1+centerp2)/2)
- except:
- center = None
- return mask,center
复制代码
PID巡线
拿到准确的线中心坐标后,就可以代入PID公式计算控制信号了,关于PID原理部分这里不再赘述,然后就可以调整Kp和Kd来让小车达到理想的巡线效果。 ①kp是PID控制器中的比例调节系数,它决定了系统对误差的响应速度。kp越大,系统对误差的响应越敏感,调整速度越快,但是过大的kp会造成小车疯狂摆头,就是过冲、震荡。 ②kd是PID控制器中的微分调节系数,它决定了系统对误差变化率的响应。kd越大,系统对误差变化的抑制作用越强,有助于减少过冲和震荡。 ③在实际应用中,通常通过“试凑法”来调整kp、kd的值。就是从小变大逐步调整每个参数,并观察小车巡线的响应状态,直到找到最佳组合。 ④除了以上因素外,小车减速电机的一致性与摄像头的角度也是影响效果的重要因素,这个因车而异,大家自行调整。
代码 - def pid(x):
- global kp,kd,old_err
- err = x-320
- offset = int(err*kp+(err-old_err)*kd)
- old_err = err
- return offset
复制代码
效果展示
完整代码
- # -*- coding: UTF-8 -*-
-
- # MindPlus
- # Python
- import cv2
- import numpy as np
- from pinpong.board import Board
- from pinpong.board import NeoPixel
- from pinpong.board import Board,Pin
- from pinpong.extension.unihiker import *
-
- def numberMap(x, in_min, in_max, out_min, out_max):
- return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
- def nm(x):
- return int(numberMap(x, 0, 255, 0, 1023))
- def speed(x, y):
- if x>0:
- p_p5_out.write_digital(1)
- else:
- p_p5_out.write_digital(0)
- if y>0:
- p_p6_out.write_digital(1)
- else:
- p_p6_out.write_digital(0)
- x = nm(abs(x))
- y = nm(abs(y))
- p_p8_pwm.write_analog(x)
- p_p16_pwm.write_analog(y)
-
- #获取线中心位置函数
- def process_frame(img):
- global lp1
- global lp2
- global lcs
- #转换为hsv色彩空间
- hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
- # 定义红色的HSV范围
- lower_red = np.array([0, 100, 100])
- upper_red = np.array([10, 255, 255])
- # 创建红色掩码
- mask = cv2.inRange(hsv, lower_red, upper_red)
- # 形态学操作:膨胀和腐蚀
- kernel = np.ones((5, 5), np.uint8)
- mask = cv2.dilate(mask, kernel, iterations=4)
- mask = cv2.erode(mask, kernel, iterations=4)
-
- colorp1=mask[lp1]
- colorp2=mask[lp2]
- try:
- lineccp1 = np.sum(colorp1==lcs)
- lineccp2 = np.sum(colorp2==lcs)
- lineip1 = np.where(colorp1==lcs)
- lineip2 = np.where(colorp2==lcs)
- if lineccp1 == 0:
- lineccp1 = 1
- if lineccp2 == 0:
- lineccp2 = 1
- leftp1 = lineip1[0][lineccp1-1]
- rightp1 = lineip1[0][0]
- centerp1 = int((leftp1+rightp1)/2)
- leftp2 = lineip2[0][lineccp2-1]
- rightp2 = lineip2[0][0]
- centerp2 = int((leftp2+rightp2)/2)
- center = int((centerp1+centerp2)/2)
- except:
- center = None
- return mask,center
-
- def pid(x):
- global kp,kd,old_err
- err = x-320
- offset = int(err*kp+(err-old_err)*kd)
- old_err = err
- return offset
-
- Board().begin()
- p_p5_out=Pin(Pin.P5, Pin.OUT)
- p_p8_pwm=Pin(Pin.P8, Pin.PWM)
- p_p6_out=Pin(Pin.P6, Pin.OUT)
- p_p16_pwm=Pin(Pin.P16, Pin.PWM)
- cv2.namedWindow("n", cv2.WINDOW_NORMAL)
- cv2.moveWindow("n", 0, 0)
- cv2.resizeWindow("n", 240, 320)
- vd = cv2.VideoCapture()
- vd.set(cv2.CAP_PROP_FRAME_WIDTH,240)
- vd.set(cv2.CAP_PROP_FRAME_HEIGHT,320)
- vd.open(0)
-
- if vd.isOpened():
- lp1 = 320 #检测线1
- lp2 = 380 #检测线2
- lcs = 255 #检测点为白色=255 黑色=0
- bs = 60 #基础速度
- kp = 0.010
- kd = 0.040
- old_err = 0
- while True:
- ret, cvi = vd.read()
- # 获取图像 h = 480 w = 640
- h,w,c = cvi.shape
- res,center=process_frame(cvi)
- cv2.line(cvi, (0,lp1), (640,lp1), (0,255,0), 3, cv2.FILLED)
- cv2.drawMarker(cvi, (center, int(lp1+((lp2-lp1)/2))), (0,0,255), cv2.MARKER_CROSS, 20, 5, cv2.FILLED)
- cv2.line(cvi, (0,lp2), (640,lp2), (0,255,255), 3, cv2.FILLED)
- cv2.imshow("n", cvi)
- f = 0
- if center!= None:
- offset = pid(center)
- if center>320 :
- f = 1
- else:
- f = 2
- ls = bs + offset*10
- rs = bs - offset*10
- else:
- if f==1:
- for i in range(10):
- ls,rs = bs,-1*bs
- time.sleep(0.1)
- elif f==2:
- for i in range(10):
- ls,rs= -1*bs,bs
- time.sleep(0.1)
- else:
- ls,rs= bs,bs
- speed(ls,rs)#设置两轮速度
- if cv2.waitKey(20) & 0xff== 97:
- speed(0,0)
- break
- print(ls,rs,center)
- vd.release()
- cv2.destroyAllWindows()
复制代码
总结
这块扩展版在使用过程中感觉是非常好用的,后续还会去用它做一些智能识别的项目,另外如果这个板子能扩展出四路电机控制的话,做一个麦轮小车应该会更好玩。
|