这是一个前段时间做的一个智能小车,主要是想用小车识别带颜色的线路,以及路上可能遇到的二维码或apirltag标签码。在程序编写方面可能有一些不成熟,但是功能还是有的,已实验通过。
一、所需硬件
MaixCAM、STM32开发板(使用串口1)
二、MaixCAM端
直接上代码:
代码如下(示例):
- '''
- # 功能:同时实现AprilTag标记跟踪和巡线,并读取AprilTag标记(同二维码)中心点,作为巡线的标识
- #       巡线时是黄线,停车点是蓝线。
- # 串口发送数据:0x2C 0x12 X_H  X_L y Z R 0XB5 
- # V1.0
- # 2024.6.23
- # 修改
- # V1.1
- # 2024.6.26
- # 修改内容:
- # 2024.7.16
- # 修改内容:增加标签的旋转角度识别,并把值通过串口发送
- # 2024.8.28
- # 修改内容:修改识别标签的类型为AprilTag_36H10,这个可以从0到2319,使用的标签可能多,所以用两们8位数据发送线单片机。
- # 串口发送数据格式:0x2C 0x12 X_H  X_L y Z_H  Z_L R_H  R_L 0XB5
- '''
- from maix import image, camera, display
- from maix import app, uart, pinmap, time
- import sys
-  
- # 默认使用串口,也就是A16---TX,A17---RX
- device = "/dev/ttyS0"
- serial0 = uart.UART(device, 115200)   # 串口波特率是115200
-  
- #serial0.write("hello 1\r\n".encode()) # 初始化先发送hello 1  和hello 2
- #serial0.write_str("hello 2\r\n")
-  
- cam = camera.Camera(320,240,)
- cam.skip_frames(30)           # 跳过开头的30帧
- disp = display.Display()
-  
- families = image.ApriltagFamilies.TAG36H10
- x_scale = cam.width() / 160     # 图像缩放
- y_scale = cam.height() / 120
-  
- # 基本初始化--巡线参数
- # 寻找到对应的直线
- #thresholds = [[0, 80, 40, 80, 10, 80]]      # red
- #thresholds1 = [[0, 80, -120, -10, 0, 30]]    # green
- #thresholds = [[0, 80, 30, 100, -120, -60]]  # blue
- thresholds1 = [[33,46,-18,53,-104,-33]]  #蓝色
- thresholds = [[74,94,-23,-3,22,42],[50,70,-12,8,52,72],[77,97,-29,-9,32,52]]    # 不同时间段的黄色值,可以提高因为白天和晚上不同光线的影响。
-  
- '''
- # 串口发送函数
- # 向STM32F103单片机发送数据包
- # 数据包格式为2个帧头,5个数据和一个帧尾,x:上位控制舵机值,发送时先发高8位,再发低8位,
- # y:是否遇到十字路口标志1为遇到十字路口,
- # z:是Apriltag内容(和二维相似),表示标志点,TAG36H10 → 0 to 2319  0x00ff & z:是低8位,z>>8,高8位。
- # R:是旋转角度, 是标签的旋转角度,从0~360度。0x00ff & R:是低8位,R>>8:高8位。
- '''
- def sending_data(x,y,z,R):
-     global uart;
-     FH = [0x2C,0x12,x>>8,0x00ff & x,y,z>>8,0x00ff & z,R>>8,0x00ff & R,0x5B]
-     serial0.write(bytes(FH));
-     serial0.write_str("\r\n");
-  
- # 巡线函数与二维码识别函数结合
- def Lines_function():
-     roi1            = [0,100,320,16]       #巡线敏感区
-     roi2            = [0,80,320,8]        #关键点敏感区
-     expectedValue   = 160                  #巡线位置期望,拍摄的画面是320X240
-     err             = 0                    #本次误差
-     old_err         = 0                    #上次误差
-     Kp              = 3.2                    #PID比例系数
-     Kd              = 0.5                    #PID微分系数
-     Flag            = 0                    #用于关键点标志
-     servo_data      = 1500                 #发送给上位机的值,用于控制舵机
-     servo_Value     = 1500                 #期望转角值
-  
-     # 基本初始化--巡线参数
-     roi3                     = [0,0,160,80]         #标签识别敏感区,由于图片是缩小到了(160,120),取上半部分
-     AprilTag_expectedValue   = 160                  #巡线位置期望,拍摄的画面是320X240
-     AprilTag_err             = 0                    #本次误差
-     AprilTag_old_err         = 0                    #上次误差
-     AprilTag_Kp              = 3.2                  #PID比例系数
-     AprilTag_servo_data      = 1500                 #发送给上位机的值,用于控制舵机
-     AprilTag_servo_Value     = 1500                 #期望转角值
-     AprilTag_payload         = 0                    #标签的id
-     rotation_jiaodu          = 0                    #标签的旋转角度
-     
-     img = cam.read()
-     # 以下是Apriltag标签相关
-     new_img = img.resize(160, 120) #将图像缩放得更小,用更小的图像来让算法计算得更快
-     '''
-     寻找apriltag标签,并将查询结果保存到apriltags,
-     以供后续处理。
-     其中families用来选择apriltag族,默认为image.ApriltagFamilies.TAG36H10
-     '''
-     apriltags = new_img.find_apriltags(roi=roi3,families = families) # (roi=roi3,families = families)
-     # 以下是巡线相关
-     blobs1 = img.find_blobs(thresholds,roi=roi1, pixels_threshold=100,merge=True)    # 寻找色块方法实现巡线。
-     blobs2 = img.find_blobs(thresholds1,roi=roi2, pixels_threshold=100,merge=True,margin=40)
-     for a in apriltags:
-         corners = a.corners()
-         for i in range(4):
-             corners[i][0] = int(corners[i][0] * x_scale)
-             corners[i][1] = int(corners[i][1] * y_scale)
-         x = int(a.x() * x_scale)
-         y = int(a.y() * y_scale)
-         w = int(a.w() * x_scale)
-         h = int(a.h() * y_scale)
-  
-         #x_rota = int(a.x_rotation())
-         rotation_jiaodu = int(180 * a.rotation() // 3.1415)
-         #print(rotation_jiaodu)
-  
-         for i in range(4):
-             img.draw_line(corners[i][0], corners[i][1], corners[(i + 1) % 4][0], corners[(i + 1) % 4][1], image.COLOR_RED)
-             img.draw_string(x + w, y, "id: " + str(a.id()), image.COLOR_RED)
-         #img.draw_string(x + w, y + 15, "family: " + str(a.family()), image.COLOR_RED)
-             img.draw_string(x + w, y + 30, "rotation : " + str(180 * a.rotation() // 3.1415), image.COLOR_RED)
-            
-         #print(str(180 * a.rotation() // 3.1415))
-         
-         #PID计算
-         AprilTag_X_center =int(x+w/2)                       # 标签中心点x值
-         AprilTag_err=AprilTag_X_center-AprilTag_expectedValue # 得到线与摄像头中心的偏移量err
-         AprilTag_servo_data = AprilTag_servo_Value+(AprilTag_Kp*AprilTag_err+Kd*(AprilTag_err-AprilTag_old_err))     # 得到的偏差值err,可以进行简单PID计算。
-         AprilTag_old_err = AprilTag_err
-         servo_data = int(AprilTag_servo_data)
-         AprilTag_payload = int(a.id())        # 标签的id
-         '''
-         if(AprilTag_payload>256):
-             AprilTag_payload = 0xff;
-         '''
-         print(AprilTag_payload) 
-         #sending_data(servo_data,0,int(qr.payload()))
-         
-     if blobs1:
-             #servo_data_tmp.clear()   # 清空列表
-         for blob in blobs1:
-             img.draw_rect(blob[0], blob[1], blob[2], blob[3], image.COLOR_GREEN)# 画一个框,在兴趣区roi1的区域
-             img.draw_cross(blob[5],blob[6],image.COLOR_GREEN)   # 画一个十字                           
-             #PID计算
-             actualValue=int(blob[5])             # 实际色块的外框的中心x值
-             err=actualValue-expectedValue   # 得到线与摄像头中心的偏移量err       
-             # 得到的偏差值err,可以进行PID计算。
-             # 当偏差小于零时,说明小车向右偏,偏差大于零时,说明小车向左偏(原理是这样,要按实际模块安装在小车的方向)
-             servo_data = servo_Value+(Kp*err+Kd*(err-old_err))
-             old_err= err
-             #servo_data = servo_Value+Kp*err
-             servo_data = int(servo_data)  # 转换为整数
-             #print(servo_data)
-             #sending_data(servo_data,0,0)
-     # 当侦测关键点
-     if blobs2:
-         for blob in blobs2:
-             img.draw_rect(blob[0], blob[1], blob[2], blob[3], image.COLOR_WHITE)
-             img.draw_cross(blob[5], blob[6],image.COLOR_WHITE)
-             if blob[2] >50:  # 是否大于正常巡线时寻找到的色块外框宽度w值。
-                 Flag = 1
-             else: Flag = 0
-             #print(Flag)
-             #sending_data(0,Flag,0)
-         #img.draw_string(0, 0, "theta: " + str(theta) + ", rho: " + str(rho), image.COLOR_BLUE,2)     
-         #print(a.x1(),a.y1(),a.x2(),a.y2())
-     sending_data(servo_data,Flag,AprilTag_payload,rotation_jiaodu)
-     disp.show(img)
-  
- while 1:
-     Lines_function();
-     time.sleep_ms(200)
注:
1、与STM32硬件连接时要注意串口顺序,使用交叉连接,还有MaixCAM模块提供了一个typec_USB接口,使用时注意方向,具体参考MaixCAM使用说明。
2、波特率设置是115200(与STM32相同)
3、程序中识别的标签码是AprilTag_36H10,他的范围是0~2319,这个也是根据个人项目而定,如果使用标签量少,可以使用AprilTag_36H11,具体可以查看相关说明。
4、程序中,做了一个简易PID的控制,主要是把模块识别的线或标签中心点的值转换为小车舵机的控制数据。我的小车是和舵机控制转向的,中心值是1500,最左边是1200,最右边是1800。
5、程序中,还可以识别到标签的ID,和角度值,这个功能是MaixCAM模块自带的,只要调用他的方法就可以,还是比较方便的。
6、程序中,还设置了十字路口的识别,主是是当前面的色块大于一个数值时,就发送1到STM32,做为标志。
三、STM32端
main函数主要是怎么利用串口传回来的线或标签中心点值,标签的角度值,十字路口的标志位,这里就不做说明。
这里主要说一下STM32的串口怎么设置,这里我使用的是串口1,配置如下:
- void usart_init(uint32_t bound)
- {
-     /*UART 初始化设置*/
-     uartx_handler.Instance = USART_UX;                                       /*USART_UX*/
-     uartx_handler.Init.BaudRate = bound;                                     /*波特率*/
-     uartx_handler.Init.WordLength = UART_WORDLENGTH_8B;                      /*字长为8位数据格式*/
-     uartx_handler.Init.StopBits = UART_STOPBITS_1;                           /*一个停止位*/
-     uartx_handler.Init.Parity = UART_PARITY_NONE;                            /*无奇偶校验位*/
-     uartx_handler.Init.HwFlowCtl = UART_HWCONTROL_NONE;                      /*无硬件流控*/
-     uartx_handler.Init.Mode = UART_MODE_TX_RX;                               /*收发模式*/
-     HAL_UART_Init(&uartx_handler);                                           /*HAL_UART_Init()会使能UART1*/
-     HAL_UART_Receive_IT(&uartx_handler, (uint8_t *)aRxBuffer, RXBUFFERSIZE); /*该函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量*/
- }
-  
- /**
-  * @brief       UART底层初始化,时钟使能,引脚配置,中断配置
-                                 此函数会被HAL_UART_Init()调用
-  * @param       huart:串口句柄
-  * @retval      无
-  */
- void HAL_UART_MspInit(UART_HandleTypeDef *huart)
- {
-     /*GPIO端口设置参数存放位置*/
-     GPIO_InitTypeDef gpio_initure;
-  
-     if (huart->Instance == USART_UX)                    /*HAL调用此接口进行串口 MSP初始化*/
-     {
-     /* IO 及 时钟配置 */
-     USART_TX_GPIO_CLK_ENABLE();                         /* 使能串口TX脚时钟 */
-     USART_RX_GPIO_CLK_ENABLE();                         /* 使能串口RX脚时钟 */
-     USART_UX_CLK_ENABLE();                              /* 使能串口时钟 */
-     gpio_initure.Pin = USART_TX_GPIO_PIN;               /*串口发送引脚号*/
-     gpio_initure.Mode = GPIO_MODE_AF_PP;                /*复用推挽输出*/
-     gpio_initure.Pull = GPIO_PULLUP;                    /*上拉*/
-     gpio_initure.Speed = GPIO_SPEED_FREQ_HIGH;          /*IO速度设置为高速*/
-     HAL_GPIO_Init(USART_TX_GPIO_PORT, &gpio_initure);
-         
-     gpio_initure.Pin = USART_RX_GPIO_PIN;               /* 串口RX脚 模式设置 */
-     gpio_initure.Mode = GPIO_MODE_AF_INPUT;    
-     HAL_GPIO_Init(USART_RX_GPIO_PORT, &gpio_initure);   /* 串口RX脚 必须设置成输入模式 */
- #if USART_EN_RX
-     HAL_NVIC_EnableIRQ(USART_UX_IRQn);                  /*使能USART1中断通道*/
-     HAL_NVIC_SetPriority(USART_UX_IRQn, 3, 3);          /*组2,最低优先级:抢占优先级3,子优先级3 */
- #endif
-     }
- }
-  
- /**
-  * @brief       UART数据接收回调接口
-                 数据处理在这里进行
-  * @param       huart:串口句柄
-  * @retval      无
-  */
- void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
- {
-     static uint8_t RxState = 0;
-     static uint8_t RxCounter1=0;
- 	static uint16_t RxBuffer1[11]={0};
-     uint8_t i;
-     
-     if (huart->Instance == USART_UX)                   /*如果是串口1*/
-     {
- 		if(RxState==0 && aRxBuffer[0]==0x2C)  //0x2c帧头
- 				{
- 					RxState=1;
- 					RxBuffer1[RxCounter1++]=aRxBuffer[0];
- 				}
- 		
- 				else if(RxState==1&&aRxBuffer[0]==0x12)  //0x12帧头
- 				{
- 					RxState=2;
- 					RxBuffer1[RxCounter1++]=aRxBuffer[0];
- 				}
- 		
- 				else if(RxState==2)
- 				{
- 					RxBuffer1[RxCounter1++]=aRxBuffer[0];
-  
- 					if(RxCounter1==10 && aRxBuffer[0] == 0x5B)       //RxBuffer1接收满了,接收结束。
- 					{
- 						
- 						rwm_x_value_H = RxBuffer1[RxCounter1-8];
- 						rwm_x_value_L = RxBuffer1[RxCounter1-7];
- 						K210_Flag = RxBuffer1[RxCounter1-6];
- 						data_rwm_H = RxBuffer1[RxCounter1-5];
-                         data_rwm_L = RxBuffer1[RxCounter1-4];
- 						jiaodu_H = RxBuffer1[RxCounter1-3];
- 						jiaodu_L = RxBuffer1[RxCounter1-2];
- 						RxCounter1 = 0;
- 						RxState = 0;	
- 					}
- 					else if(RxCounter1 > 10)            // 接收异常
- 					{
- 						RxState = 0;
- 						RxCounter1=0;
- 						for(i=0;i<10;i++)
- 						{
- 								RxBuffer1[i]=0x00;      //将存放数据包数组清零
- 						}
- 					
- 					}
- 				}
- 				else   // 接收异常
- 				{
- 						RxState = 0;
- 						RxCounter1=0;
- 						for(i=0;i<10;i++)
- 						{
- 								RxBuffer1[i]=0x00;      // 将存放数据包数组清零
- 						}
- 				}
- 			g_usart_rx_sta = 0;
-     }
- }
注:
1、这里使用是HAL库函数版本
2、在接收端使用了一个简单的报文,有包头,有包尾。
这是我第一次在网上分享自己做的东西,做的不好,后面多多努力,请各位大神多多指教。
 
                                    
评论记录:
回复评论: