ESP32-CAM AI视觉小车:从硬件选型到算法实现的嵌入式机器视觉实践

📅 2026/8/19 12:04:19
ESP32-CAM AI视觉小车:从硬件选型到算法实现的嵌入式机器视觉实践
1. 项目概述当ESP32-CAM遇见AI视觉小车如果你玩过Arduino小车大概率做过循线和避障但多半是靠红外对管或者超声波模块。那种玩法稳定但总觉得少了点“智能”的味道——传感器告诉你“前面有东西”但它不知道那东西是墙、是猫、还是另一个小车。今天聊的这个项目就是把一块带摄像头的ESP32-CAM模块塞进一个小车底盘里让它真正用“眼睛”去看路、去识别障碍实现基于视觉的循线和避障。这不仅仅是换个传感器那么简单它意味着你的小车从依赖触觉的“盲人摸象”升级成了拥有视觉感知能力的“智能体”。这个“ESP32-CAM AI视觉小车”的核心就是利用ESP32-CAM这颗集成了Wi-Fi、蓝牙和OV2640摄像头的芯片在本地进行实时的图像处理与决策。我们不再需要把图像数据一股脑传到电脑或者云端去分析所有的“思考”过程都在小车上的这块微控制器里完成。它能看到前方的赛道线能分辨出障碍物的大致轮廓和位置然后自主决定是沿着线走还是绕开障碍。这听起来像是机器人竞赛里的高级项目但实际上随着开源库和开发工具的成熟它的门槛已经降低了很多非常适合有一定单片机基础想向嵌入式AI和机器视觉迈出第一步的爱好者。我自己折腾这个项目的初衷就是想验证在ESP32这种资源受限的设备上到底能跑多“智能”的算法。结果令人惊喜通过一些巧妙的算法裁剪和优化它不仅能流畅运行反应速度也足以应对室内环境下的移动需求。接下来我会把从硬件选型、算法原理到代码调试的完整过程拆开揉碎了讲尤其是那些在官方例程里不会提的坑和技巧。2. 核心硬件选型与电路设计解析玩嵌入式项目硬件是骨架选对了事半功倍。这个视觉小车的硬件系统可以分成三大部分大脑主控与视觉、身体底盘与驱动、以及能量供给电源。2.1 主控与视觉核心为什么是ESP32-CAM市面上带摄像头的开发板不少为什么偏偏是ESP32-CAM这得从它的几个不可替代的优势说起。首先性价比与集成度。ESP32-CAM模块通常不到50元却集成了ESP32双核处理器、4MB PSRAM、OV2640摄像头模组和一个TF卡槽。ESP32的主频高达240MHz双核设计允许我们用一个核心专门处理图像AI推理另一个核心负责控制电机和通信这是实现流畅视觉处理的基础。而4MB的PSRAM外部RAM是关键中的关键OV2640摄像头输出的图像数据量很大ESP32芯片自带的几百KB内存根本不够看PSRAM就是用来缓存这些图像数据的“高速仓库”没有它高分辨率图像处理无从谈起。其次OV2640摄像头。这颗摄像头支持最高200万像素1600x1200但在这个项目里我们通常不会用到这么高。高分辨率意味着更多的数据处理起来更慢。我们一般会将其配置为QVGA320x240甚至更低的分辨率在保证能看清赛道线和障碍物的前提下最大化处理速度。OV2640通过DVP并行接口与ESP32连接数据传输速率快驱动程序成熟在Arduino和ESP-IDF环境下都有很好的支持。注意市面上有些便宜的ESP32-CAM模块为了省成本用的可能是OV7670摄像头30万像素或者没有焊接PSRAM芯片。务必确认你购买的是带有“OV2640”和“4MB PSRAM”的版本否则后续的AI模型部署可能会失败。2.2 动力与底盘电机、驱动与结构考量小车底盘的选择决定了项目的稳定性和扩展性。对于视觉小车我强烈推荐使用四轮麦克纳姆轮底盘。虽然价格比普通的两轮差速底盘贵一些但它能实现全向移动前后左右平移以及原地旋转这在复杂的避障和路径规划中带来巨大的灵活性。例如当摄像头识别到正前方有障碍时普通小车只能左转或右转而麦克纳姆轮小车可以直接横向平移动作更高效、更“聪明”。驱动部分ESP32-CAM的GPIO引脚驱动能力很弱无法直接驱动电机。我们需要一个电机驱动板。最常用的是基于TB6612或DRV8833芯片的双路电机驱动模块。它们体积小、效率高、带有内置的保护电路。一个驱动模块可以驱动两个直流电机我们的小车有四个电机所以需要两个这样的驱动模块。电源是另一个容易忽略但至关重要的部分。电机启动瞬间电流很大可能超过1A而ESP32-CAM和摄像头工作时也需要稳定的5V和3.3V供电。如果用一个普通的9V电池串联一个线性降压模块给整个系统供电电机动作时很容易造成电压骤降导致ESP32重启俗称“掉电复位”。可靠的方案是使用两路独立供电一路用大容量锂电池如7.4V 2S锂电通过大电流BEC电池消除器电路即开关降压模块降压到6V左右直接给电机驱动供电另一路则通过一个独立的、干净的5V稳压模块如LM2596为ESP32-CAM和摄像头供电。确保两地“共地”即可。这样可以有效隔离电机负载对控制电路的干扰。2.3 电路连接要点与避坑指南连接电路时逻辑要清晰。将ESP32-CAM的GPIO引脚分为两组一组用于控制电机驱动另一组用于摄像头本身通常固定不需要我们动。对于电机驱动以TB6612为例每个驱动模块需要两个PWM引脚控制速度两个数字引脚控制方向。例如将ESP32的GPIO12、GPIO13、GPIO14、GPIO15分别连接到两个驱动模块的PWMA、AIN1、AIN2、PWMB等引脚。务必查阅你所用驱动模块的数据手册确认其逻辑电平。大多数模块兼容3.3V但确认一下更保险。对于ESP32-CAM模块本身除了常规的5VVCC、GND连接外最关键的是要把GPIO0引脚通过一个按钮接到GND。这是因为ESP32-CAM在上电时会检查GPIO0的电平来决定启动模式高电平为正常启动低电平为下载模式。我们需要一个按钮在需要烧录程序时按住它再上电进入下载模式正常运行时则确保它是高电平通常板子上有下拉电阻不按按钮就是高电平。摄像头排线插入时要对准卡扣轻轻按紧避免接触不良导致图像花屏或无法初始化。一个常见的坑是电源噪声导致图像干扰。如果你在屏幕上看到的图像有横条纹或者抖动很大概率是电源问题。检查你的5V电源线是否和电机电源线捆扎在一起尝试为ESP32-CAM的电源输入端并联一个100μF的电解电容和一个0.1μF的瓷片电容可以很好地滤除低频和高频噪声。3. 视觉算法核心从像素到决策硬件搭好了相当于给了小车眼睛和腿。接下来就是最核心的“大脑”部分算法。我们要在ESP32上实现两个功能循线和避障。这本质上都是图像处理问题。3.1 循线算法简化与效率的平衡传统的PC端OpenCV循线可能会用到Canny边缘检测、Hough变换找直线等复杂操作。但在ESP32上我们必须做极致的简化。一个经过实战检验的高效方法是灰度化二值化底部扫描法。图像采集与预处理首先我们让OV2640输出一幅QVGA320x240的灰度图像。灰度化直接在摄像头初始化时配置可以节省彩色转灰度的计算时间。得到灰度图后我们对其进行二值化处理。这里的阈值不是固定的因为环境光线会变。一个更好的方法是使用大津法Otsu‘s Method在每帧图像上动态计算阈值。虽然大津法本身有点计算量但对于240行的图像我们可以只取中间几十行来计算或者每10帧计算一次阈值平衡效果和速度。赛道线提取底部扫描法我们并不需要分析整幅图像。对于循线来说小车只需要关心眼前靠近车头那一部分地面的情况。因此我们只在图像底部划出一个“感兴趣区域”ROI例如从第200行到第240行总共240行。在这个ROI里我们从左到右扫描每一行像素找到从黑赛道到白地面或从白到黑的跳变点。将这些跳变点的中心位置记录下来。中线计算与偏差计算假设我们扫描到了两条线左边缘和右边缘那么赛道中心线就是这两条线中心的平均值。如果只扫描到一条线比如弯道我们可以根据历史数据或预设的赛道宽度来估算另一条边。最终我们得到一个代表当前赛道中心位置的“目标点”。将这个目标点与图像的中心点即小车当前的理论前进方向进行比较其水平偏移量就是我们的偏差Error。PID控制转向得到了偏差就可以用经典的PID控制器来生成转向指令。P比例项根据当前偏差大小决定转向幅度I积分项累计历史偏差消除静态误差比如长期偏向一侧D微分项根据偏差变化率预测趋势让转向更平滑。将PID的输出映射到左右电机的速度差上小车就能自动修正方向沿着线走了。实操心得在ESP32上实现PID时要注意数据类型的处理。使用浮点数计算虽然方便但速度慢。可以先将误差放大1000倍用整型数进行PID运算最后再缩小回电机速度值能显著提升速度。另外PID参数Kp Ki Kd需要实地调试。一个快速的方法是先调Kp让小车能快速响应但又不振荡然后调Kd来抑制振荡最后根据需要加入较小的Ki。3.2 避障算法基于深度估计与区域分割避障比循线更复杂因为我们需要判断障碍物的远近和位置。在单目摄像头上实现精确测距很难但我们可以用一些视觉线索来估算。运动光流法简易版一种简单有效的方法是帧间差分法。让小车在正常寻迹时以固定频率比如10Hz捕获图像并将当前帧与上一帧进行灰度差分。在静止背景下差分图像中移动的物体障碍物会显现出来。我们可以计算差分图像中“白色”有变化像素的区域和重心。如果这个区域出现在图像中下部分且面积较大就认为有障碍物靠近。这种方法计算量小但对静止的障碍物无效且需要背景相对静止。基于特征区域的障碍物假设更鲁棒的方法是结合场景先验知识。我们假设赛道在图像中占据一个梯形区域透视效应而障碍物通常是出现在这个梯形区域内、高于地面的团块。我们可以通过边缘检测或颜色分割如果障碍物颜色鲜明找出图像中的连通区域。然后计算这些区域的几何属性底部位置区域底部在图像中的纵坐标。越靠下纵坐标值越大说明障碍物离小车越近。高度与宽度比一个立体的障碍物如瓶子在图像中通常高大于宽而地面的影子或色块可能宽大于高。区域面积面积越大障碍物可能越近或越大。通过给这些特征设定阈值我们可以过滤出可能是障碍物的区域并标记出它们的“危险等级”和位置左、中、右。决策逻辑当识别出障碍物后小车的决策逻辑可以是一个简单的状态机状态1循线。默认状态执行上述循线算法。状态2障碍评估。检测到正前方图像中间区域有高危险等级障碍物。减速并分析障碍物偏左还是偏右。状态3绕行。根据障碍物位置生成一个绕行路径。例如障碍物偏右则向左转一定角度然后直行一段距离再尝试回到原来的赛道上。这里可以引入一个简单的“虚拟目标点”让PID控制器驱动小车走向该点从而实现绕行。状态4回归寻迹。绕过后重新开启底部扫描寻找赛道线切换回状态1。3.3 引入微型AI模型TensorFlow Lite Micro上述算法都是基于传统图像处理虽然高效但泛化能力有限比如对复杂光线、不同颜色的线适应差。如果你想玩点更“AI”的可以尝试在ESP32上部署微型神经网络模型实现更智能的识别。TensorFlow Lite for MicrocontrollersTFLite Micro允许你将训练好的轻量级模型部署到ESP32上。例如你可以训练一个简单的图像分类模型识别“直线”、“左弯”、“右弯”、“交叉口”、“障碍物”等几种场景。或者训练一个目标检测模型直接框出赛道线和障碍物。流程简述数据收集用你的ESP32-CAM在不同光照、不同赛道背景下拍摄几千张图片并手动标注。模型训练在PC上使用TensorFlow或PyTorch训练一个微型CNN模型如MobileNetV1的极简版或自建几个卷积层的小网络。模型转换与量化将模型转换为TFLite格式并进行整型量化INT8 Quantization。这是关键步骤它将模型权重和激活值从浮点数转换为8位整数极大地减少了模型大小和计算量使其能在ESP32上实时运行。部署与推理将量化后的模型文件一个.tflite文件放入ESP32的SPIFFS文件系统或直接编译进程序。在代码中调用TFLite Micro解释器输入预处理后的图像如下采样到96x96的灰度图即可得到分类结果。注意事项AI模型虽然强大但会消耗大量计算资源。一帧推理可能需要几百毫秒这会导致控制周期变慢小车反应迟钝。因此通常不会每帧都做AI推理而是每5-10帧推理一次中间帧依然用快速的传统算法控制。同时模型输入分辨率要尽可能低网络层数要尽可能少以平衡精度和速度。4. 软件框架与代码实现详解有了算法思路我们需要一个可靠的软件框架把它们组织起来。对于ESP32双核芯片充分利用其多核特性是提升性能的关键。4.1 双核任务划分与FreeRTOS应用ESP32的Core 0和Core 1可以并行执行任务。一个合理的划分是Core 0通常负责Arduino的loop()承担高优先级、实时性要求高的任务。这里我们放置电机控制任务。它以一个固定的高频率如100Hz运行读取当前的目标速度、转向指令通过PWM控制电机驱动板。这个任务必须稳定、准时确保小车运动平滑。Core 1承担计算密集型的任务。我们创建一个视觉处理任务放在Core 1上。这个任务负责从摄像头获取一帧图像。执行图像预处理灰度化、二值化等。执行循线或避障算法计算出控制指令目标速度、转向角。将控制指令通过队列FreeRTOS的Queue发送给Core 0的电机控制任务。这样即使视觉处理某一帧因为计算复杂而稍微耗时比如做了次AI推理也不会影响到电机控制的实时性小车不会出现卡顿式的运动。在Arduino环境下我们可以使用xTaskCreatePinnedToCore函数来创建绑定到特定核心的任务。记得为任务分配足够的栈空间图像处理任务需要较大的栈例如8192字以上。4.2 核心代码模块拆解下面我勾勒出几个关键代码模块的结构和要点并非完整代码但提供了实现的骨架和思路。1. 摄像头初始化与图像捕获#include “esp_camera.h” // 摄像头引脚定义根据你的ESP32-CAM模组型号调整 #define PWDN_GPIO_NUM -1 #define RESET_GPIO_NUM -1 #define XCLK_GPIO_NUM 21 #define SIOD_GPIO_NUM 26 #define SIOC_GPIO_NUM 27 #define Y9_GPIO_NUM 35 #define Y8_GPIO_NUM 34 #define Y7_GPIO_NUM 39 #define Y6_GPIO_NUM 36 #define Y5_GPIO_NUM 19 #define Y4_GPIO_NUM 18 #define Y3_GPIO_NUM 5 #define Y2_GPIO_NUM 4 #define VSYNC_GPIO_NUM 25 #define HREF_GPIO_NUM 23 #define PCLK_GPIO_NUM 22 void setupCamera() { camera_config_t config; config.ledc_channel LEDC_CHANNEL_0; config.ledc_timer LEDC_TIMER_0; config.pin_d0 Y2_GPIO_NUM; config.pin_d1 Y3_GPIO_NUM; config.pin_d2 Y4_GPIO_NUM; config.pin_d3 Y5_GPIO_NUM; config.pin_d4 Y6_GPIO_NUM; config.pin_d5 Y7_GPIO_NUM; config.pin_d6 Y8_GPIO_NUM; config.pin_d7 Y9_GPIO_NUM; config.pin_xclk XCLK_GPIO_NUM; config.pin_pclk PCLK_GPIO_NUM; config.pin_vsync VSYNC_GPIO_NUM; config.pin_href HREF_GPIO_NUM; config.pin_sscb_sda SIOD_GPIO_NUM; config.pin_sscb_scl SIOC_GPIO_NUM; config.pin_pwdn PWDN_GPIO_NUM; config.pin_reset RESET_GPIO_NUM; config.xclk_freq_hz 20000000; // XCLK频率20MHz较稳定 config.pixel_format PIXFORMAT_GRAYSCALE; // 直接输出灰度图节省处理时间 config.frame_size FRAMESIZE_QVGA; // 320x240 config.jpeg_quality 0; // 非JPEG模式此参数无效 config.fb_count 2; // 双缓冲一帧处理时摄像头可填充下一帧 // 初始化摄像头 esp_err_t err esp_camera_init(config); if (err ! ESP_OK) { Serial.printf(“Camera init failed with error 0x%x”, err); return; } } // 在视觉任务中捕获一帧 camera_fb_t * fb esp_camera_fb_get(); if (!fb) { Serial.println(“Camera capture failed”); return; } // 此时fb-buf中就是灰度图像数据fb-len是数据长度fb-width和fb-height是宽高 // ... 处理图像 ... esp_camera_fb_return(fb); // 处理完后必须归还缓冲区2. 图像处理与循线计算简化示例// 假设图像是320x240的灰度图数据在fb-buf中 uint8_t *image fb-buf; int width fb-width; int height fb-height; // 1. 动态阈值二值化使用Otsu法这里简化为一维OTSU计算实际可优化 int threshold calculateOtsuThreshold(image, width, height); // 2. 定义ROI底部50行 int roi_start_y height - 50; int left_edge[50] {0}; int right_edge[50] {0}; int valid_lines 0; for (int y 0; y 50; y) { int row_start (roi_start_y y) * width; bool found_left false; bool found_right false; // 从左向右扫描找左边缘黑到白 for (int x 0; x width - 1; x) { if (image[row_start x] threshold image[row_start x 1] threshold) { left_edge[y] x; found_left true; break; } } // 从右向左扫描找右边缘白到黑 for (int x width - 1; x 0; x--) { if (image[row_start x] threshold image[row_start x - 1] threshold) { right_edge[y] x; found_right true; break; } } if (found_left found_right) { valid_lines; } } // 3. 计算平均中线位置 if (valid_lines 10) { // 有效扫描线足够多才计算 long sum_center 0; for (int i 0; i 50; i) { if (left_edge[i] 0 right_edge[i] 0) { sum_center (left_edge[i] right_edge[i]) / 2; } } int current_center sum_center / valid_lines; int image_center width / 2; int error current_center - image_center; // 偏差正数表示中线偏右小车需右转 // 4. 将error送入PID控制器计算转向量 // ... }3. FreeRTOS任务与队列通信#include “freertos/FreeRTOS.h” #include “freertos/task.h” #include “freertos/queue.h” QueueHandle_t controlQueue; // 用于从视觉任务向控制任务发送指令 typedef struct { int speed; int steering; // 转向角或速度差 } ControlCommand; void visionTask(void *pvParameters) { setupCamera(); while(1) { // 捕获并处理图像... ControlCommand cmd; cmd.speed baseSpeed; cmd.steering pidOutput; // 假设pidOutput是PID计算出的转向量 // 发送指令到队列如果队列满则等待10ms xQueueSend(controlQueue cmd pdMS_TO_TICKS(10)); vTaskDelay(pdMS_TO_TICKS(20)); // 控制视觉处理频率例如50Hz } } void controlTask(void *pvParameters) { // 初始化电机驱动引脚等... ControlCommand cmd; while(1) { // 从队列接收指令如果队列空则等待最长等待一个控制周期10ms if (xQueueReceive(controlQueue cmd pdMS_TO_TICKS(10)) pdTRUE) { // 根据cmd.speed和cmd.steering计算左右轮PWM值并输出 setMotorSpeed(leftMotor, cmd.speed - cmd.steering); setMotorSpeed(rightMotor, cmd.speed cmd.steering); } else { // 超时未收到新指令可以保持上次速度或停止 // keepLastSpeed(); } vTaskDelay(pdMS_TO_TICKS(10)); // 控制任务以100Hz运行 } } void setup() { Serial.begin(115200); controlQueue xQueueCreate(5 sizeof(ControlCommand)); // 创建队列深度为5 // 创建任务视觉任务绑定到Core 1控制任务绑定到Core 0 xTaskCreatePinnedToCore(visionTask, “Vision” 8192, NULL, 1, NULL, 1); xTaskCreatePinnedToCore(controlTask, “Control” 4096, NULL, 2, NULL, 0); // Arduino的loop()函数现在空闲了 } void loop() { // 可以在这里放置一些低优先级的任务如串口调试信息打印 vTaskDelay(pdMS_TO_TICKS(1000)); }4.3 参数调试与性能优化技巧代码跑起来只是第一步让小车跑得稳、跑得顺需要精细调试。PID参数调试准备一个简单的串口命令接口可以在小车运行时实时调整PID参数并观察效果。例如通过串口发送“kp 1.5”来更改比例系数。在直道上观察小车对扰动的响应比如轻轻推它一下目标是快速修正且不超调、不振荡。图像处理优化降低分辨率如果QVGA320x240处理起来还是吃力可以尝试降到QQVGA160x120。速度会快很多但对细线的识别能力会下降。缩小ROI进一步减小底部扫描区域的高度和宽度只关注最关键的区域。固定阈值在光线可控的环境下可以放弃动态阈值使用一个固定的二值化阈值能省下计算Otsu阈值的时间。使用查找表LUT对于像二值化这样的像素级操作可以预先计算好所有可能的灰度值对应的二值结果0或255存储在一个256字节的数组里。处理时直接查表比if判断快得多。内存与速度监控使用ESP.getFreeHeap()、ESP.getPsramSize()等函数监控内存使用情况确保没有内存泄漏。使用micros()函数对关键函数进行计时找出性能瓶颈。5. 实战问题排查与进阶思考即使按照上述步骤操作在实际组装和调试中你几乎一定会遇到各种问题。这里我整理了一份常见问题速查表以及对应的排查思路。问题现象可能原因排查步骤与解决方案摄像头初始化失败1. 电源供电不足或噪声大。2. 摄像头排线接触不良。3. GPIO引脚定义错误。1. 用万用表测量摄像头VCC引脚电压应在4.8V-5.2V之间稳定。并联滤波电容。2. 重新拔插摄像头排线确保卡扣锁紧。3. 核对代码中的引脚定义与你的模块原理图是否完全一致。图像出现横条纹或抖动电源噪声干扰。电机驱动与主板共用电源电机启停引起电压波动。1. 为ESP32-CAM的5V输入增加LC滤波电路电感电容。2.最有效采用独立双电源供电方案彻底隔离电机电源与控制电源。小车循线时剧烈振荡画龙PID参数不合适通常是比例系数Kp太大或微分系数Kd太小。1. 先将Ki和Kd设为0从小到大地调整Kp直到小车能对偏差有反应但开始轻微振荡。2. 然后逐渐增加Kd观察振荡是否被抑制。Kd是“阻尼”项能预测趋势防止过冲。3. 最后加入很小的Ki用于消除长期偏向一侧的静态误差。无法识别白色背景下的白线或黑色背景下的黑线二值化阈值设置不当或环境光变化导致对比度低。1. 实现动态阈值算法如Otsu。2. 在摄像头前增加偏振片或遮光罩减少环境光干扰。3. 考虑使用边缘检测代替阈值分割。例如使用Sobel算子检测线的边缘对光照变化更鲁棒但计算量稍大。避障时误触发频繁障碍物检测的阈值如区域面积、底部位置设置太敏感。1. 通过串口打印出检测到的区域面积、位置等参数观察正常行驶和遇到障碍物时的数值差异。2. 引入延时确认机制连续多帧如3-5帧都检测到障碍物才触发避障动作避免因图像噪声误判。3. 结合多传感器融合例如在车头加装一个超声波模块作为“最终仲裁”当视觉和超声波都认为有障碍时才行动。程序运行一段时间后死机或重启1. 内存泄漏如摄像头缓冲区未归还。2. 看门狗定时器WDT超时。1. 确保每次esp_camera_fb_get()后都对应有esp_camera_fb_return(fb)。2. 在长时间循环的任务中适时调用vTaskDelay()或delay()释放CPU并喂看门狗esp_task_wdt_reset()。3. 检查栈空间是否足够尤其是图像处理任务适当增加xTaskCreate中的栈大小参数。Wi-Fi图传卡顿影响控制如果开启了Wi-Fi视频流会占用大量CPU和带宽影响主控逻辑。1. 降低图传分辨率如降至QQVGA和帧率如5fps。2.将图传任务放在最低优先级确保控制任务和视觉任务能抢占CPU。3. 若非必要调试完成后可关闭图传功能。当你的基础循线避障小车跑顺之后就可以考虑一些进阶玩法了1. 赛道元素识别识别十字路口、起止线、路标等。可以通过识别特定颜色的色块、或者使用训练好的轻量级AI模型如TFLite Micro来实现。识别到十字路口后小车可以结合预设的路径规则进行决策比如直行、左转、右转。2. 建图与路径规划让小车在第一次运行时一边走一边记录摄像头看到的特征或者结合廉价的编码器估算里程在内存中构建一个简单的环境地图。第二次运行时它就可以根据地图进行简单的路径规划而不是简单的“遇到障碍就绕”。3. 多车协同与通信利用ESP32内置的Wi-Fi或蓝牙让多辆小车之间可以通信。可以玩“跟随领航车”、“编队行驶”或者简单的“避免碰撞”游戏。这需要设计一套简单的通信协议如MQTT over Wi-Fi或者ESP-NOW这种低功耗协议。这个项目就像一把钥匙打开了嵌入式视觉和边缘AI的大门。从调通第一个摄像头画面到小车颤颤巍巍地跟着线走再到它能聪明地绕开你故意放在路上的水杯整个过程充满了挑战和成就感。最重要的是它让你亲身体会到在资源有限的微控制器上实现“智能”需要的是对算法极致的优化和对硬件深入的理解这是一种与在PC上写代码完全不同的、更贴近物理世界的编程乐趣。