首页IT科技广东省职工工业机器人职业技能竞赛官网(广东省工科赛 智能终端配送机器人:创作心路历程,踩过的坑和解决方案)

广东省职工工业机器人职业技能竞赛官网(广东省工科赛 智能终端配送机器人:创作心路历程,踩过的坑和解决方案)

时间2025-06-21 01:28:33分类IT科技浏览4721
导读:前言 最近在准备广东省工科赛的智能配送组赛项,在这里记录一下我和队友的创作心路历程和踩过的坑。配送小车的运行流程及赛道如下所示:...

前言

最近在准备广东省工科赛的智能配送组赛项            ,在这里记录一下我和队友的创作心路历程和踩过的坑            。配送小车的运行流程及赛道如下所示:

配送小车在黄色出发区等待出发                  。小车扫描二维码                  ,根据二维码信息打开对应仓门      ,同时语音播报      ,放入物料后关闭仓门                  ,配送小车出发      。 配送小车出发前往紫色等待区            ,识别即将进入紫色等待区时配送小车减速      ,识别到即将离开紫色等待区时停车                  ,确保精准停车            。 在紫色等待区配送小车抬头识别红绿灯            ,当绿灯亮起时小车出发,并根据二维码信息左右平移至目标收货区(棕色)                  。 配送小车到达指定收货地点                  ,停车并语音播报                  ,扫描二维码,打开对应仓门并语音播报            ,拿取物料侯关闭仓门                  ,小车自转180°      。 配送小车平移至回到等待区前方      ,停车并向前出发            ,直到识别到黄色出发区                  ,减速并控制黄色出发区在视角内居中      。 配送小车回到出发区      ,配送任务完成                  。

1            、配送小车的成品图

2                  、作品视频演示

Jetson nano智能物流配送小车

一      、控制方案

1      、器件选择

小车构成 名称 上位机 Jetson nano 下位机 STM32F103 STM32F407 舵机 DS3120数字舵机 底盘 四麦克纳姆轮移动底盘 电机 1:30减速比 带编码器减速电机 电机驱动 双路直流电机驱动 摄像头 USB工业摄像头 陀螺仪 JY901S九轴陀螺仪 HWT101CT航向角陀螺仪 语音模块 SYN-6288语音模块 扫码模块 Scanner V3.0嵌入式扫码模块

2                  、总体创作历程

既然是小车      ,那自然是从底盘开始干起                  ,一开始我们采用的是STM32F103最小系统板作为底盘控制板            ,然后在实验室找了一块电源板      ,准备开始测试电机驱动            。我们使用的电机是1:30减速比减速电机                  ,电机驱动是韦恩斯塔克科技的双路直流电机驱动      。

于是有了下面这张我们小车的雏形:

浅浅地落地试了一下            ,效果还不错                  。测试完毕后,我开始着手设计我的上位机UI界面                  ,并编写我的上位机程序            。我采用Jetson nano作为上位机                  ,与单片机进行通信,控制单片机完成各种动作。测试上下位机通信正常后            ,我再加入了颜色识别和扫码模块的程序                  ,而后搭载于车上      ,进行了测试                  。可能由于配重问题或场地不平稳问题            ,导致小车无法正常直行和平移                  ,东倒西歪      ,于是我加上了PID算法      ,使得小车能够稳定行驶                  ,这个时候整块STM32F103系统板引脚基本都被插满了            ,看着也很乱      ,于是我换成了STM32F407作为底盘控制板使用                  。

测试后发现小车虽然能够正常直行                  ,但是平移的时候还是会有些许问题。经过反复测试后            ,发现是因场地不平稳导致麦轮悬空,从而走歪                  ,于是我加了陀螺仪对小车进行矫正                  ,一开始使用的是九轴陀螺仪JY901S,但是实际上路时发现效果不是很好            ,会有-3°至3°的误差            。而且最难受的是                  ,当你开始怀疑陀螺仪不精准是不是受到了磁场影响      ,于是你去掉了可能存在的磁场干扰                  。但是当你做完这些后准备重新校准时            ,你就麻烦大了                  ,你需要将整辆车抬起来      ,绕两个方向旋转360°      。我的这辆小车还是挺重的      ,转个一两次直接裂开            。于是后面换了HWT101CT单轴陀螺仪解决了这个问题                  。后面又继续调试了一些视觉方面的程序                  ,修改了一些bug后            ,也算是基本完成了      。

到这里你可能会觉得总体思路挺顺畅的      ,其实在实现的过程中还有很多奇奇怪怪的问题和一些值得一提的坑                  ,我会在下文一一道来      。

二            、SMT32下位机控制程序和逻辑分析

我的STM32下位机控制采用的是STM32F407ZGT6最小系统板            ,基于HAL库编写了减速电机      、编码器                  、陀螺仪            、语音模块灯模块的驱动程序,以及PID速度环的闭环系统                  。

1、电机驱动

首先放上一些简单的电机驱动程序                  ,包括电机PWM设置和电机的使能设置            。

/*---------------------------- 设置电机PWM ----------------------------*/ void Motor1_pwm_Set(int motor1_n) // 电机1设置PWM { HAL_TIM_PWM_Stop(&htim1, TIM_CHANNEL_1); Motor_PWM_sConfigOC.Pulse = motor1_n; HAL_TIM_PWM_ConfigChannel(&htim1, &Motor_PWM_sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); //Motor1_pwm_num = (motor1_n / (float)1000)*100; } /*---------------------------- 电机使能 ----------------------------*/ void Motor1_up_enable(void) // 电机1正转使能 { HAL_GPIO_WritePin(Motor_INA_GPIOx, Motor1_IN_GPIO, GPIO_PIN_RESET); HAL_GPIO_WritePin(Motor_INB_GPIOx, Motor1_IN_GPIO, GPIO_PIN_SET); } void Motor1_lost_enable(void) // 电机1反转使能 { HAL_GPIO_WritePin(Motor_INA_GPIOx, Motor1_IN_GPIO, GPIO_PIN_SET); HAL_GPIO_WritePin(Motor_INB_GPIOx, Motor1_IN_GPIO, GPIO_PIN_RESET); } void Motor1_disable(void) // 电机1失能 { HAL_GPIO_WritePin(Motor_INA_GPIOx, Motor1_IN_GPIO, GPIO_PIN_RESET); HAL_GPIO_WritePin(Motor_INB_GPIOx, Motor1_IN_GPIO, GPIO_PIN_RESET); }

2                  、速度环PID

如上总体创作历程所述                  ,为了解决配送小车行走偏移的问题,我先采用的是增量式PID算法            ,利用编码器实现配送小车4轮的速度闭环      。

(1)                  、编码器

我们选购的编码器为AB双向增量式磁性霍尔编码器                  ,其会将电机运转时的位移信息转变成一段连续的脉冲信号      ,利用脉冲个数可算得位移量                  。

在开始上手之前需要注意的事项

1、首先第一步还是要把传感器搞定            ,确保单位时间内获取的编码器脉冲值是正确的            。

2            、我们购买的编码器是霍尔编码器                  ,即为磁电转换的编码器      ,而且我在装车前就已经将编码器外壳拆卸掉了      ,因此需要确保编码器不会受到外界磁场过强的干扰。

3                  、给编码器供电的电源板切记要和单片机共地                  ,我就是因为没有和单片机共地导致信号不稳定            ,耗费了很长时间                  。

4      、注意好CubeMx配置的几倍频                  。

下面是我做的一些编码器的笔记      ,记录下以后方便查阅                  ,就是字有点小丑😅:

如上所示            ,假如我50ms获取一次编码器的当前总值,那么用当前编码器的值除以编码器单圈脉冲数                  ,就能得到50ms内电机转了几圈。

编码器单圈脉冲数的值 = 倍频数 × 电机线数 × 减速比

编码器值分析程序如下: float girth = 77*3.1415f; // 轮周长(mm) float encoder_mc = 4*11*30; // 编码器单圈脉冲数 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) // 定时器中断回调函数 { if (htim == &htim7) //50ms调用一次 { enc_1 = -(int32_t)__HAL_TIM_GET_COUNTER(&htim3); // 获取编码器1值(enc_1/50ms) __HAL_TIM_SetCounter(&htim3, 0); // mcu编码器接口置0 Motor1_now_Speed = (((float)enc_1 / encoder_mc)*girth/50)*1000; //计算轮1当前速度(mm/s) } }

(enc_1 / encoder_mc)求出了50ms内的圈数                  ,乘以轮周长girth则得到50ms内走的位移,再除以50乘以1000则得到一秒单位时间内的位移            。

(2)            、增量式PID算法

增量式PID公式如下图所示:

关于PID算法我是在b站up主421施公队的从不懂到会用!PID从理论到实践视频学的            ,视频里讲的PID算法个人认为通俗易懂                  ,是个相当不错的视频                  。

关于增量式PID算法程序如下所示: int threshold = 800; // 我电机定时器的ARR设置为1000      ,所以我这里限幅在-800至800的范围内 Error = target_Speed - now_Speed; // 用目标速度减去当前速度得到当前的误差值 P_Error = Error - Last_Error; I_Error = Error; D_Error = Error - 2*Last_Error + Last_Last_Error; add = KP * P_Error + KI * I_Error + KD * D_Error; // 对应上图公式 PWM += add; // 增加正数或负数的PID输出 Last_Last_Error = Last_Error; // 上一个误差在下一次变为上上个误差 Last_Error = Error; // 当前误差在下一次变为上一个误差 if(PWM > 800) PWM = 800; // 限幅 else if(PWM < -800) PWM = -800; // 限幅

如上程序所示            ,限幅为800                  ,那么为什么会有一个-800呢?其实这也是PID的特性之一      ,后面程序当判断为负值时反向旋转轮子      ,并把值置为正数      。如下程序所示(以电机1为例):

if(motor1_pwm >= 0){Motor1_up_enable();} // 判断PWM为正数                  ,正转且可直接赋值 else if(motor1_pwm < 0){Motor1_lost_enable();motor1_pwm = -motor1_pwm;} // 判断PWM为负数            ,反转且将PWM正负翻转 Motor1_pwm_Set(motor1_pwm); // 电机1赋pwm

(3)                  、野火上位机PID参数整定

关于最重要的PID参数的整定      ,我请教了师兄后了解到了野火的上位机和正点原子的无线调试器                  ,于是我参考野火手册上手写了一套野火上位机的远程调试程序            ,但是不知道什么原因,无线调试器一直无法实现电脑端向单片机端发送数据😅            。因此无线调试时只能看到波形                  ,然后改代码远程烧录                  。

具体的上位机介绍和部分例程在野火的上位机文档里也有                  ,有需要的也可以去参考野火上位机的文档      。如下为野火的上位机程序: /*==============================↓上下位机通信↓============================== */ /** * @brief 设置pid */ void set_p_i_d(float p, float i, float d) { KP = p; // P KI = i; // I KD = d; // D } /** * @brief 计算校验和 * @param ptr:需要计算的数据 * @param len:需要计算的长度 * @retval 校验和 */ uint8_t check_sum(uint8_t init, uint8_t *ptr, uint8_t len) { uint8_t sum = init; while(len--) { sum += *ptr; ptr++; } return sum; } /** * @brief 设置上位机的值 * @param cmd:命令 * @param ch: 曲线通道 * @param data:参数指针 * @param num:参数个数 * @retval 无 */ void set_computer_value(uint8_t cmd, uint8_t ch, void *data, uint8_t num) { uint8_t sum = 0; // 校验和 num *= 4; // 一个参数4个字节 static packet_head_t set_packet; set_packet.head = FRAME_HEADER; // 包头 0x59485A53 set_packet.len = 0x0B + num; // 包头 set_packet.ch = ch; // 设置通道 set_packet.cmd = cmd; // 设置命令 sum = check_sum(0, (uint8_t *)&set_packet, sizeof(set_packet)); // 计算包头校验和 sum = check_sum(sum, (uint8_t *)data, num); // 计算参数校验和 HAL_UART_Transmit(&huart1, (uint8_t *)&set_packet, sizeof(set_packet), 0xFFFFF); // 发送数据头 HAL_UART_Transmit(&huart1, (uint8_t *)data, num, 0xFFFFF); // 发送参数 HAL_UART_Transmit(&huart1, (uint8_t *)&sum, sizeof(sum), 0xFFFFF); // 发送校验和 } /** * @brief 读取上位机的值 * @param cmd:命令 * @param ch: 曲线通道 * @param data:参数指针 * @retval 无 */ void Read_Computer(UART_HandleTypeDef *husart) { packet_head_t packet; packet.cmd = rx_buffer[CMD_INDEX_VAL]; packet.len = COMPOUND_32BIT(&rx_buffer[LEN_INDEX_VAL]); packet.head = COMPOUND_32BIT(&rx_buffer[HEAD_INDEX_VAL]); if (packet.head == FRAME_HEADER) { switch(packet.cmd) { case SET_P_I_D_CMD: // 设置PID值 { uint32_t temp0 = COMPOUND_32BIT(&rx_buffer[13]); uint32_t temp1 = COMPOUND_32BIT(&rx_buffer[17]); uint32_t temp2 = COMPOUND_32BIT(&rx_buffer[21]); float p_temp, i_temp, d_temp; p_temp = *(float *)&temp0; i_temp = *(float *)&temp1; d_temp = *(float *)&temp2; set_p_i_d(p_temp, i_temp, d_temp); // 设置P I D } break; case SET_TARGET_CMD: { // 设置各电机期待值 } break; case START_CMD: // 开始按键 { car_forward(); //car_left(); //car_right(); run_ornot = 1; } break; case STOP_CMD: // 停止按键 { run_ornot = 0; Motor1_disable(); Motor2_disable(); Motor3_disable(); Motor4_disable(); Motor1.ESC_Output_PWM = 0; Motor1.Last_Error = 0; Motor1.Last_Last_Error = 0; Motor2.ESC_Output_PWM = 0; Motor2.Last_Error = 0; Motor2.Last_Last_Error = 0; Motor3.ESC_Output_PWM = 0; Motor3.Last_Error = 0; Motor3.Last_Last_Error = 0; Motor4.ESC_Output_PWM = 0; Motor4.Last_Error = 0; Motor4.Last_Last_Error = 0; } break; case RESET_CMD: // 复位单片机 { // HAL_NVIC_SystemReset(); } break; } } }

然后只需在获取编码器后调用set_computer_value发送当前速度值,和在串口接收的回调函数中添加Read_Computer函数获取上位机指令就ok了      。

3      、HWT101CT单轴陀螺仪

为了解决麦轮悬空而引起的偏移问题            ,我决定加上一个陀螺仪来矫正                  。

值得注意的点是                  ,一开始我们选用了九轴陀螺仪JY901S      ,但是其实这类型的陀螺仪并不适用于我的这辆配送小车            。首先我的需求只有单纯的Z轴上的角度            ,其次这款陀螺仪要矫正起来非常麻烦                  ,而且容易受磁场干扰      ,车上的空间非常有限      ,磁场强的东西又非常多                  ,比如Jetson nano和电机            ,还有个磁场最猛的扬声器在车上      。后面选用了HWT101CT单轴陀螺仪解决了这一问题      ,首先这块陀螺仪可以满足我单Z轴上的需求                  ,其次它有一个较好的外壳            ,包括线也是有比较好的保护,而且它受磁场干扰后                  ,只要离开磁场就能自动回到正常状态                  ,不用我搬着整台车翻来覆去地矫正                  。

链接为陀螺仪说明书:HWT101CT通信协议

从0x52开始,以0为初始值算起的16位和17位为角度输出的低8位和高8位

           。如下图所示:

陀螺仪角度获取程序如下所示:

// 陀螺仪串口接收 uint8_t value_IMU; // 串口输入值 uint8_t getBuffer[22]; // 存放数据的数组 uint8_t Enter[] = "\r\n"; float IMU_DATA; // 角度值 uint8_t IMU_DATAL; // 数据位低8位 uint8_t IMU_DATAH; // 数据位高8位 volatile float IMU_Z = 0.00f; volatile float IMU_init_data = 0.00f; // 陀螺仪初始角度 volatile char first_flag = 0; // 判断第一次录入陀螺仪角度 uint8_t countOfGetBuffer = 0; // 位数累计 char flag = 0; // 判断接收开始 char now_uart_flag = 0; // 当前输入值 char last_uart_flag = 0; // 上一个输入值 void USART3_IRQHandler(void) { /* USER CODE BEGIN USART3_IRQn 0 */ /* USER CODE END USART3_IRQn 0 */ HAL_UART_IRQHandler(&huart3); /* USER CODE BEGIN USART3_IRQn 1 */ last_uart_flag = now_uart_flag; // 上一次的串口输入值 now_uart_flag = value_IMU; // 当前的串口输入值 if(last_uart_flag == 0x55 && now_uart_flag == 0x52) // 协议判断输入角度值 { //printf("IMU\r\n"); flag = 1; // 开启接收程序 } //printf("%c", value_IMU); if(flag == 1) { getBuffer[countOfGetBuffer++] = value_IMU; if(countOfGetBuffer == 18) // 位长 { flag = 0; //while(HAL_UART_Transmit(&huart3, (uint8_t*)getBuffer, countOfGetBuffer, 5000)!= HAL_OK); //while(HAL_UART_Transmit(&huart3, (uint8_t*)Enter, 4, 5000)!= HAL_OK); IMU_DATAL = getBuffer[16]; IMU_DATAH = getBuffer[17]; IMU_DATA = (((short)IMU_DATAH<<8) | IMU_DATAL); // 高8位低8位 IMU_Z = ((float)IMU_DATA/32768)*180-180; // 把0°至360°范围改为-180°至180° if(first_flag == 0) // 单片机复位第一次获取陀螺仪值设置为初始正向 { IMU_init_data = IMU_Z; first_flag = 1; printf("Start:IMU_init_data:%.2f\r\n", IMU_init_data); } //printf("IMU_init_data:%.2f\r\n", IMU_init_data); memset(getBuffer, 0, countOfGetBuffer); // 清空数组 countOfGetBuffer = 0; // 位数清零 } } HAL_UART_Receive_IT(&huart3, (uint8_t *)&value_IMU,1); /* USER CODE END USART3_IRQn 1 */ }

4      、SYN-6288语音模块

语音模块一样也是用串口通信            ,程序如下                  ,商家提供的库函数的协议程序      ,不过是寄存器操作            ,所以影响不大:

/*---------------------- 语音模块串口播放 ----------------------*/ void USART2_SendData(u8 data) { while((USART2->SR & 0X40) == 0); USART2->DR = data; } void USART2_SendString(u8 *DAT, u8 len) { u8 i; for(i = 0; i < len; i++) { USART2_SendData(*DAT++); } } /*---------------------- 语音播报API定义 ----------------------*/ //Music:选择背景音乐                  ,0:无背景音乐      ,1~15:选择背景音乐 //v[0~16]:朗读音量 //m[0~16]:背景音乐音量 //t[0~5]:语速 void SYN_FrameInfo(u8 Music, u8 *HZdata) { /****************需要发送的文本**********************************/ unsigned char Frame_Info[50]; unsigned char HZ_Length; unsigned char ecc = 0; //定义校验字节 unsigned int i = 0; HZ_Length = strlen((char*)HZdata); //需要发送的文本长度 /*****************帧固定配置信息**************************************/ Frame_Info[0] = 0xFD ; //构造帧头FD Frame_Info[1] = 0x00 ; //构造数据区长度的高字节 Frame_Info[2] = HZ_Length + 3; //构造数据区长度的低字节 Frame_Info[3] = 0x01 ; //构造命令字:合成播放命令 Frame_Info[4] = 0x01 | Music << 4 ; //构造命令参数:背景音乐设定 /*******************校验码计算***************************************/ for(i = 0; i < 5; i++) { ecc = ecc (Frame_Info[i]); } for(i = 0; i < HZ_Length; i++) { ecc = ecc (HZdata[i]); } memcpy(&Frame_Info[5], HZdata, HZ_Length); Frame_Info[5 + HZ_Length] = ecc; USART2_SendString(Frame_Info, 5 + HZ_Length + 1); } void YS_SYN_Set(u8 *Info_data) { u8 Com_Len; Com_Len = strlen((char*)Info_data); USART2_SendString(Info_data, Com_Len); }

只需要简单的调用即可      ,如下所示:

void speaker0(void) { SYN_FrameInfo(1,(u8 *)"[v6][m6][t5]¸高德地图持续为您导航"); HAL_Delay(3000); }

5                  、舵机

本俩配送小车共搭载了3个舵机                  ,一个用于摄像头的抬头低头            ,两个用于打开和关闭仓门。

首先要将定时器周期配置为20ms      ,然后给出的PWM高电平应为0.5ms至2.5ms                  ,即PWM占空比为2.5%至12.5%                  。我的ARR值设置为1000            ,因此修改范围为25~125                  。

舵机PWM占空比设置程序如下所示: /*---------------------- 舵机PWM设置 ----------------------*/ // PWM占空比设置范围:25 ~ 125 void Servo1_pwm_Set(int Servo1_n) { HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1); Servo_PWM_sConfigOC.Pulse = Servo1_n; HAL_TIM_PWM_ConfigChannel(&htim2, &Servo_PWM_sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); }

三            、Jetson nano上位机程序和可视化界面

对于Jetson nano上位机程序的编写,我使用的是Python语言                  ,主要功能视觉用的是OpenCV                  ,界面设计用的是PyQt5。先从我使用Jetson nano编写程序时遇到的问题介绍起吧            。

0      、遇到的问题和解决方案

(1)                  、Serial

在安装python的serial库后遇过一个问题,在引用serial            ,即import serial时出现了报错:

AttributeError: module serial has no attribute Serial

解决方案

一开始只使用了pip3 install serial安装serial库                  ,少安装了一个pyserial                  。

正确的安装方法: pip3 install serial pip3 install pyserial

(2)            、端口号问题

如果按照nano系统默认的端口配对      ,当接入多个端口时            ,拔插一个端口后名字就会改变                  ,非常麻烦      ,需要一直改程序      ,或者不拔插端口                  ,很不现实      。

于是我决定定死端口            ,给要用的端口配对好名称      ,对应摄像头ID配对好摄像头接口名称                  ,只要除摄像头外的接口插在对应的端口上就可以防止以上问题发生            。

因此            ,解决方案

(1)、打开终端 udevadm info /dev/videoxx(/dev/ttyUSBxx) # 查看当前端口所在接口的信息 cd /etc/udev/rules.d sudo gedit serial.rules

进入文档,以下为我的配置案例                  ,可参考使用:

ACTION=="add",KERNEL=="video*",ATTRS{idVendor}=="0bc8", ATTRS{idProduct}=="5880", MODE:="0777", SYMLINK+="car_video" ACTION=="add",KERNELS=="1-2.1.1:1.0",SUBSYSTEMS=="usb",MODE:="0777",SYMLINK+="car_scanner" ACTION=="add",KERNELS=="1-2.2:1.0",SUBSYSTEMS=="usb",MODE:="0777",SYMLINK+="car_mcu" ACTION=="add",KERNELS=="1-2.4:1.0",SUBSYSTEMS=="usb",MODE:="0777",SYMLINK+="car_gyro"

退出后输入以下命令完成配置                  。

sudo /etc/init.d/udev restart

重新拔插一次端口就ok了      。

(3)                  、调节摄像头曝光度问题

一开始我在windows使用opencv能够正常切换摄像头自动和手动曝光                  ,但是来到nano的ubuntu系统上就不能用了,也可能是opencv的版本问题            ,后面我解决的方法是在终端上对摄像头模式进行调节      。

首先需要安装pexpect库: pip3 install pexpect

在程序里调用如下:

# 自动调节 child = pexpect.spawn(v4l2-ctl -d /dev/car_video -c exposure_auto=3) time.sleep(1) # 手动调节 child = pexpect.spawn(v4l2-ctl -d /dev/car_video -c exposure_auto=1) time.sleep(1) child = pexpect.spawn(v4l2-ctl -d /dev/car_video -c exposure_absolute=50) time.sleep(1)

v4l2-ctl -d /dev/car_video -c exposure_auto=xx为终端命令                  。

exposure_auto=3为自动调节                  ,1为手动调节      ,exposure_absolute=50指曝光值为50            。

以下为摄像头改变为手动曝光模式: def video_modeltoled(self): self.video.release() child = pexpect.spawn(v4l2-ctl -d /dev/car_video -c exposure_auto=1) time.sleep(1) child = pexpect.spawn(v4l2-ctl -d /dev/car_video -c exposure_absolute=1) time.sleep(1) while True: self.video = cv2.VideoCapture("/dev/car_video") # 定义识别红绿灯摄像头对象 if self.video.isOpened() is True: print(摄像头已打开      。) break elif self.video.isOpened() is False: print(未接入摄像头                  。)

1                  、扫码模块

首先需要配置好扫码模块的功能            ,配置完成后                  ,则开始编写扫码模块调用程序            。

如下所示      ,我的扫码模块是串口通信的      ,因此在运行主程序前先要判断扫码模块是否连接成功。 while True: # 判断串口是否正常连接                  ,且防止报错 try: self.scanner_ser = serial.Serial("/dev/car_scanner", 115200, timeout=10) # 定义串口对象 if self.scanner_ser.isOpen(): # 判断串口是否成功打开 print("扫码模块连接成功                  。") break else: print("扫码模块连接失败                  。") except: print("扫码模块未接入 或 扫码模块串口端口号错误。") pass

进入while后将判断扫码模块是否连接成功            ,如果连接成功则退出while      ,否则将不停地循环while                  ,直至确认连接成功为止            。当终端打印出 扫码模块连接失败                  。扫码模块未接入 或 扫码模块串口端口号错误      。 时            ,试着拔插扫码模块,即可成功连接                  ,程序退出while                  ,正常运行            。

而后开始扫码工作: print(进入扫码模式                  。) self.textEdit.setPlainText("进入扫码模式      。") while True: # 等待扫描二维码 ret, img = self.video.read() # 使用摄像头获取图像 scanner_data = self.scanner_run() # 运行扫码模块信息获取函数 if scanner_data == b11: print(二维码信息为 11       。) self.textEdit.append("二维码信息为 11                   。") self.QR_label.setText(11) self.mcu_ser.write($a.encode(utf-8)) self.end_num = 1 break elif scanner_data == b12: print(二维码信息为 12            。) self.textEdit.append("二维码信息为 12       。") self.QR_label.setText(12) self.mcu_ser.write($a.encode(utf-8)) self.end_num = 2 break elif scanner_data == b21: print(二维码信息为 21                   。) self.textEdit.append("二维码信息为 21            。") self.QR_label.setText(21) self.mcu_ser.write($b.encode(utf-8)) self.end_num = 1 break elif scanner_data == b22: print(二维码信息为 22 。) self.textEdit.append("二维码信息为 22                   。") self.QR_label.setText(22) self.mcu_ser.write($b.encode(utf-8)) self.end_num = 2 break self.textEdit.append("打开Qt界面                  。")

该程序为主程序中的一部分,当中有些是和扫码模块无关的            ,在这里稍微解释一下:

关于self.scanner_run()函数下文会挂上程序。 self.textEdit和self.QR_label都为配送小车主界面的控件                  ,分别用于显示终端打印信息和二维码信息            。 self.mcu_ser则为单片机串口对象      ,此处发送$a为打开1号仓门            , $b为打开2号仓门                  。 self.end_num定义目的地变量      。 使用ret, img = self.video.read()目的是确保摄像头图像获取通道不会一直堵塞着                  ,导致后续获取的图像有所延时            。当然这里也可以在前面使用self.video.release()直接关闭摄像头      ,也可防止出现这种问题      ,后面完成扫码后再次打开摄像头即可                  。因为此处资源占用也不多                  ,不会导致程序运行过慢            ,所以我也是直接调取摄像头来解决该问题      。

scanner_run程序如下所示:

def scanner_run(self): # 扫码模式 scanner_data = self.scanner_ser.read(4) scanner_data = scanner_data.splitlines() if scanner_data: scanner_data = scanner_data[0] return scanner_data

2、可视化界面

(1)            、视觉调试界面

赛场调试时间紧迫      ,如果视觉方面出现什么错误                  ,每次都要重新跑一遍才能发现错误出在哪里那将浪费很多的时间            ,因此我编写了一个上位机视觉调试qt界面,如下所示      。

视觉调试界面功能: 右侧第二行3个按键分别为3种识别或者说摄像头的模式                  ,自动曝光则是摄像头切换自动曝光模式                  ,手动曝光则是人为调节曝光度,颜色识别模式则是识别色块或是红绿灯的模式                  。 右侧第三行可实现在手动曝光模式下切换曝光值            ,输入曝光值后按确认即可            。 右侧颜色区块选择是我在写程序的时候默认保存了一些阈值范围                  ,方便快速切换到某个区域      ,再进行微调      。 下面6行拖动条则是调节HSV阈值的            ,H范围为0-180                  ,S范围为0-255      ,V范围为0-255                  。

左边为摄像头拍摄的图像      ,右边为颜色识别后的图像            。

(2)                  、配送小车主界面

如下图所示                  ,当扫描完二维码            ,并获得下位机反馈回来的出发信号时      ,上位机打开此配送小车主界面。

配送小车主界面功能: 界面左上角图像为小车的颜色识别图像                  ,在其右侧为摄像头拍摄后进行一定比例缩小的原图像                  。这两张图像伴随小车行进实时变换            ,方便在运行中和编写逻辑程序后确认程序是否存在bug,也方便修改bug时的调试                  。 界面左下角为配送小车扫描到的二维码信息                  ,进行可视化便于调试。 界面右下角为配送小车主程序运行时的信息打印                  ,同样也是便于调试和检测程序是否存在bug            。

3      、颜色识别

以下程序可调转至该链接有详细的解析:OpenCV python(三)【图像预处理:颜色空间转换】 && 颜色识别

(1)            、区块识别

挂几个函数:

def transform(self, image): # 颜色空间转换函数 # 颜色空间转换,image为导入的RGB图像 image_g = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # GRAY图像 image_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) # HSV图像 return image_g, image_hsv def color(self, image_hsv, color_mod): # 颜色识别函数 if color_mod is 1: image_hsv = image_hsv[142:192 , 0:255] image_color = cv2.inRange(image_hsv, (109, 78, 77), (157, 180, 180)) # HSV图像二值化(紫色) elif color_mod is 2: image_hsv = image_hsv[72:182 , 0:255] image_color = cv2.inRange(image_hsv, (60, 70, 95), (96, 255, 255)) # HSV图像二值化(绿色) elif color_mod is 3: if self.brown_model is 1: if self.end_num is 1: image_hsv = image_hsv[122:192 , 155:255] elif self.end_num is 2: image_hsv = image_hsv[122:192 , 0:100] else: pass #                   。      。            。                  。      。      。等等 image_color = cv2.inRange(image_hsv, (0, 95, 36), (13, 255, 255)) # HSV图像二值化(棕色1) image_color = cv2.erode(image_color, (3, 3), iterations=5) # 腐蚀 image_color = cv2.dilate(image_color, (3, 3), iterations=5) # 膨胀 return image_color

(2)                  、红绿灯识别

上述的调节曝光度则是为现在红绿灯识别做准备            ,一般环境下看红绿灯效果如下:

上图亮的是红灯                  ,但是完全看不出来红色      ,后面和师兄聊过后            ,师兄建议我试试看调节曝光度看看                  ,于是我尝试了一下      ,如下图所示:

识别效果很不错      ,容错率很高                  ,效果如下:

四      、电路设计

以下为我们设计的单片机扩展板和电源板                  。

以上为智能终端配送机器人的创作思路和部分代码            ,可能或多或少也会存在一些问题      ,如有错误的话还请指正            。过段时间整理好后再开源      。

声明:本站所有文章                  ,如无特殊说明或标注            ,均为本站原创发布                  。任何个人或组织,在未征得本站同意时                  ,禁止复制      、盗用                  、采集            、发布本站内容到任何网站      、书籍等各类媒体平台            。如若本站内容侵犯了原著者的合法权益                  ,可联系我们进行处理。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
python中idle怎么用(python idle 是什么)