既然是小车 那自然是从底盘开始干起 一开始我们采用的是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 pyserial2 、端口号问题
如果按照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 b 11 : 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 b 12 : 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 b 21 : 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 b 22 : 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_data2、可视化界面 1 、视觉调试界面
赛场调试时间紧迫 如果视觉方面出现什么错误 每次都要重新跑一遍才能发现错误出在哪里那将浪费很多的时间 因此我编写了一个上位机视觉调试qt界面 如下所示。

视觉调试界面功能

左边为摄像头拍摄的图像 右边为颜色识别后的图像。
如下图所示 当扫描完二维码 并获得下位机反馈回来的出发信号时 上位机打开此配送小车主界面。

配送小车主界面功能
以下程序可调转至该链接有详细的解析 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)、红绿灯识别
上述的调节曝光度则是为现在红绿灯识别做准备 一般环境下看红绿灯效果如下

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

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

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


以上为智能终端配送机器人的创作思路和部分代码 可能或多或少也会存在一些问题 如有错误的话还请指正。过段时间整理好后再开源。
本次参加了广东省工科赛智能终端配送组,记录了自己的创作历程和一些踩过的坑,包括解决方案,包括上位机视觉和界面、扫码模块、语音模块、下位机PID速度环、陀螺仪等。 复制链接
STM32控制步进电机:基于定时器中断的ULN2003驱动器/步进电机驱动程序
16183
STM32控制步进电机:基于HAL库定时器中断的闭环步进电机驱动+精准控制脉冲数
11592
STM32控制步进电机:工作原理及库函数(标准库) / HAL库控制程序(不定期更新)
6673
OpenCV python(四)图像预处理:二值化 滤波操作 Open-AI: import cv2 import os # 图像文件夹路径 img_folder = ./imgs/ output_folder = ./output/ # 获取文件夹中所有图像文件的路径 # 如果输出文件夹不存在,则创建它 if not os.path.exists(output_folder): os.makedirs(output_folder) img_paths = [os.path.join(img_folder, img) for img in os.listdir(img_folder) if img.endswith(( .jpg , .png , .jpeg ))] # 循环处理每个图像 for img_path in img_paths: # 读取图像 img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 二值化处理 t, img_bin = cv2.threshold(img, 180, 255, cv2.THRESH_BINARY)
base_name = os.path.basename(img_path) output_path = os.path.join( ./output/ , f binary_{base_name} ) cv2.imwrite(output_path, img_bin) print( Binary conversion completed. ) # 循环处理每个图像 for img_path in img_paths: # 读取图像 img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 均值滤波 img_blur = cv2.blur(img, (1, 5)) # # 均值滤波 cv2.imshow( Binary Ima
抵扣说明:
1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。
更多文章请关注《万象专栏》
转载请注明出处:https://www.wanxiangsucai.com/read/cv182562