Autoware 感知模块学习笔记到第五篇了。前面几篇我把相机驱动、点云处理、检测框这些基础内容都过了一遍这次终于把 vision_segment_enet_detect 这个节点正式跑通了。它做的是图像语义分割输入一张车辆前置相机的 BGR 图像输出一张把道路、人行道、车辆、行人、交通灯等不同语义区域涂成不同颜色的分割图。说得再直白一点它在回答一个目标检测回答不了的问题“画面里到底哪一块是可以开过去的路”。这个节点在 Autoware 感知链路里算是元老级语义分割组件虽然名字很长拆开看其实很清晰vision 表示输入来自相机图像segment 表示逐像素分类enet 指代轻量级分割网络 ENetdetect 是 Autoware 内部对所有感知算法节点的统一叫法。整个节点适合三类读者一是刚接触自动驾驶感知 Stack 的工程师想搞明白图像分割跟下游规划之间是怎么衔接的二是已经在用检测类模型、想补像素级分割能力的人三是准备在嵌入式设备上做轻量分割部署想找一个真实可参考案例的开发者。我花了两个晚上把它的代码、模型授权、话题链路和启动参数全部梳理了一遍中间还踩了几个不算难但很折磨人的坑。这篇笔记不打算写成文档复读机而是把“为什么要这样设计”“实际怎么调”“出了问题怎么排查”一起讲清楚。1. 感知模块里为什么必须放一个语义分割节点1.1 只有矩形框是不够的目标检测模型给的是一个矩形框和类别比如“这里有辆车置信度 0.92位置在图像右下角”这对决策来说非常有价值但框覆盖不到的地方完全没有信息。真实驾驶场景里车要走的路不是一个框能框住的它可能横贯整个画面底部形状不规则中间还夹杂着阴影、水渍、井盖和车道线。一旦把关注点从“有什么物体”换成“哪些区域能通行”就必须把每个像素分类。这就是语义分割存在的根本原因。图像里每个像素都被赋予一个标签比如“路面”“人行道”“车辆”“天空”“植被”整张图变成一块块语义区域规划模块才能根据这些区域画出可行驶空间。1.2 分割结果是连接 2D 图像和 3D 空间的中间层有人会问相机图像是二维的分割结果也是一张二维图它怎么帮到车辆控制关键在于它可以和空间数据做映射。拿到分割掩膜之后配合相机内参可以把“属于路面”的像素投影到点云上或者用逆透视映射把像素位置投到地平面生成鸟瞰视角的可行驶区域栅格。没有这层像素级分类后续的局部路径规划就只能靠点云几何去猜哪里是路效果会糟糕很多。所以在 Autoware 的感知模块里语义分割不是终点它是连通“视觉原始数据”和“空间规划输入”的关键中转环节。1.3 为什么选 ENet 而不是 FCN 或 SegNet早期语义分割模型里FCN 是开山之作结构直观但推理速度慢在车载工控机上很难跑实时SegNet 是对称的编码器-解码器结构效果好但参数多、计算量大。ENet 的设计目标非常明确实时且轻量在保证可用的精度前提下尽量削减计算量。它发布时主打的就是嵌入式场景模型文件体积小推理速度比当时的主流分割网络快一个量级正适合 Autoware 这种要在车里同时运行定位、感知、规划多个节点的系统。所以 Autoware 把这个节点命名为 enet_detect既是模型选择的体现也是在告诉后人感知节点要考虑的不只是准不准还有在整车上跑不跑得动。2. 节点运行链路与功能拆解2.1 输入端与输出端的话题结构这个节点本身不负责获取图像它的输入来自相机驱动或数据集回放。典型的话题连接是这样的方向话题名消息类型说明订阅/image_rawsensor_msgs/Image相机原始图像BGR 编码8UC3发布/semantics/image_rawsensor_msgs/Image彩色分割掩膜每个像素的颜色代表一个类别发布/semantics/canvassensor_msgs/Image原图与分割结果叠加的可视化图方便调试实际工程里有的数据包图像话题不叫 /image_raw可能是车辆自带相机的 /camera/color/image_raw。启动前务必用rostopic list或ros2 topic list先确认一遍。这个话题名不匹配是最常见的第一坑后面会细说。2.2 一个完整推理周期里发生了哪些事把代码逻辑压缩成伪代码其实是这样的void imageCallback(const sensor_msgs::ImageConstPtr msg) { // 1. 用 cv_bridge 把 ROS Image 转成 cv::Mat cv::Mat frame cv_bridge::toCvCopy(msg, bgr8)-image; // 2. 按网络配置把图像缩放到固定尺寸比如 640x360 cv::Mat resized; cv::resize(frame, resized, cv::Size(net_width, net_height)); // 3. 转成网络输入 Blob并做归一化 float* blob frameToBlob(resized); // 4. 前向推理得到每个像素属于各类别的分数 float* output network_-forward(blob); // 5. 对每个像素取分数最大的类别得到 argmax 索引图 cv::Mat labels argmax(output, height, width, num_classes); // 6. 根据颜色表把类别索引映射成彩色像素 cv::Mat mask applyColorMap(labels); // 7. 发布分割图、叠加可视化图 sem_image_pub_.publish(mask); canvas_pub_.publish(overlay(frame, mask)); }这个流程看起来简单但代码实现里藏了很多细节图像缩放用什么插值算法、归一化时用的 mean 和 scale 是多少、网络输出 layout 是 NCHW 还是 NHWC、argmax 之后颜色表顺序是否和训练类别索引一一对应。任何一个环节对不上最终结果就会变成全黑、全亮或者颜色完全错乱的图。2.3 分割结果在感知链路里的下游去向跑起来之后分割掩膜并不仅仅为了好看。在 Autoware 的典型 demo 里分割结果会被进一步用来生成可行驶区域把“道路”和“人行道”像素设为可通行把“车辆”“行人”“建筑”“植被”设为不可通行再结合相机投影或栅格形成一个局部代价地图交给规划模块做避障和路线选择。还有一个常见玩法是停车场语义检测把停车位和路面分割出来车辆就能低速倒进一个没有画线标注的车位。理解了这条链路再看这个节点它就不再是孤零零一个分割模型而是感知规划生态里的一个标准组件。3. ENet 网络背后的设计逻辑3.1 故意不对称的编码器-解码器ENet 整体上还是编码器-解码器框架前半部分不断下采样把分辨率从高变小提取越来越抽象的语义特征后半部分不断上采样把低分辨率的特征图恢复到原图尺寸生成逐像素分类结果。但它跟 SegNet 这类模型最大的不同是把解码器做得非常轻。SegNet 的编码器和解码器几乎是对称的参数多、计算量大ENet 的编码器占了绝大多数计算资源解码器只有轻量的反卷积和上采样模块。这样设计的逻辑很朴素分割的关键是把图像内容“看懂”而不是把一个模糊的特征图硬糊回原图所以算力应该集中在前半程。类比一下就像压缩文件编码器负责找规律压信息解码器负责解压还原ENet 把压缩阶段做得很重解压阶段做得很省。3.2 瓶颈模块和空洞卷积的取舍ENet 的基础构建单元是 bottleneck一个瓶颈模块内部通常有 1x1 卷积、5x5 卷积或空洞卷积且不同尺度的特征会在模块里相加。这里面有个容易被忽略的设计大量使用空洞卷积dilation。空洞卷积是在卷积核内部“插洞”让卷积核不用变大也能覆盖更大的感受野看到的上下文范围更广。对分割任务来说判断一个像素是路面还是车顶需要看它周围比较大的范围空洞卷积正是对症下药的手段。代价也是有的空洞卷积更容易丢失局部细节所以小目标经常分割得不够好。实际测试里远处的小行人和细杆状物体经常出现漏分或者断成几截这是 ENet 这类轻量模型的典型短板只能靠硬件的更强算力/后处理补一补。3.3 从 19 类到自动驾驶能用的语义常见 ENet 模型是在 Cityscapes 数据集上训练的多类分割模型类别包括道路、人行道、建筑、围墙、栅栏、电线杆、交通灯、交通标志、植被、地形、天空、人、骑手、车、卡车、巴士、火车、摩托车、自行车一共 19 类。但工程上直接输出 19 类往往不是最舒服的。规划模块对“车、卡车、巴士”之间的差异没那么敏感它更关心“能不能碾过去”。所以实际使用中经常做一次类别映射把 19 类合并成三类或四类语义原始类别合并语义是否可行驶道路、人行道、地形可通行地面是车辆、卡车、巴士、火车、摩托车、自行车、行人、骑手动态障碍物否建筑、围墙、栅栏、电线杆、植被静态障碍物否天空、未知不可通行否这个映射表不是写死的具体怎么合并取决于下游模块的输入要求。有的场景要把交通灯单独拎出来方便红绿灯状态识别有的泊车场景需要把人行道和道路区分因为倒车需要骑上人行道时不能把路面全当成障碍。理解类别映射比单纯跑通节点更重要。4. 实操从环境准备到跑出第一张分割图4.1 编译环境的准备老规矩先把环境理顺。无论你用的是 ROS 1 版的 Autoware 还是新版模块化环境都要保证三样东西没问题一套可用的 ROS 环境按官方文档完成安装和 sourceGPU 驱动和推理框架比如 CUDA 相关依赖这样 ENet 推理才能跑在 GPU 上而不是 CPU 空转Autoware 整套代码已经完成过至少一次编译。我自己的习惯是编译前先确认基础环境有没有导入成功export CUDA_VISIBLE_DEVICES0 source /opt/ros/distro/setup.bash cd autoware_workspace colcon build --cmake-args -DCMAKE_BUILD_TYPERelease --packages-select vision_segment_enet_detect构建过程会遇到不少坑最常见的是依赖版本没对齐编译到一半报某个头文件找不到。这时候别硬着头皮改源码先查一下是不是某个依赖没包全优先用官方脚本重装依赖。4.2 模型权重文件的准备与检查这个节点要工作必须有训练好的 ENet 权重。不同仓库提供的权重格式可能不一样有的是 Caffe 风格的 .caffemodel deploy.prototxt有的是推理引擎格式的 .engine 或 .trt 文件。下载完之后至少做四件事确认模型输入尺寸。很多坑都是模型输入是 640x360代码里却写死成 512x256跑出来的分割结果要么变形要么直接崩溃。确认权重文件的路径权限。Autoware 节点通常以普通用户身份运行如果权重文件放在 root 才能读的目录下启动节点时会报加载失败。确认类别数。如果你的模型输出是 19 类但颜色表代码里只有一个 12 色列表运行时大概率出现奇怪的越界问题。用文件管理器看一眼模型大小。文件大小完全不对大概率是下载中断直接换下载地址重来。注意权重文件路径里不要放中文和空格也不要放在桌面这种容易带特殊字符的路径里。老版本节点对路径的解析非常死板路径有问题时会直接闪退连日志都不给。4.3 用 launch 文件把节点拉起来跑节点最简单的办法是写一个 launch 文件集中配置图像话题和模型路径。一个典型配置长这样launch node pkgvision_segment_enet_detect typeenet_ros namevision_segment_enet_detect outputscreen param namemodelfile value/PATH/TO/enet.caffemodel/ param namenet_height value360/ param namenet_width value640/ param nameuse_gpu valuetrue/ param nameimage_topic value/image_raw/ /node /launch参数名因仓库版本而异不要照抄后就不管。最稳妥的做法是打开仓库里的 sample 配置文件看一眼把 modelfile、image_topic、GPU 开关这三项确认好。其余参数保持默认先把节点跑起来再逐步调。启动之后用rosnode list和rostopic list检查节点是否注册成功、话题是否正在发布。接着在接收端开两个工具rqt_image_view rvizrqt_image_view 直接订阅 /semantics/image_rawRviz 里添加一个 Image 显示这两个窗口能帮你在十秒内判断输出是否正常。4.4 用 rosbag 回放替代真实相机测试如果没有现成的车载相机或者不想在真车上调试可以用相机数据集回放来替代。先录一段自己的测试数据或找一份公开自动驾驶图像数据包然后按顺序启动回放和节点rosbag play xxx.bag数据包开始发布图像后节点会自动进入推理状态。这里有个经验用 bag 回放时先rosbag play --pause暂停等 rqt_image_view 窗口打开并订阅上话题之后再按空格键开始播放。因为节点刚启动的前几帧图像往往被用于加载模型和建缓存很容易错过第一帧暂停播放可以避免“启动看起来没反应”的错觉。5. 参数调节与效果优化5.1 关键参数这样调才有方向分割节点最值得调的地方其实不是算法内部而是输入分辨率和运行平台。我整理了一个参数权衡清单参数调小调大影响net_width / net_height推理变快小目标易丢细节保存更好帧率下降直接决定处理速度与精度平衡GPU 开关强制 CPU速度骤降GPU 推理帧率可能差几十倍类别数语义更粗误分减少区分更细影响后续映射和规划输入图像缩放方式容易损失细节保留细节多耗时变长对分割边缘影响明显我实测下来同一个模型在低功率工控机上跑 640x360 时约 20 帧上下如果把分辨率提到 1280x720帧率会掉到 8~10 帧对直接驾驶决策来说已经相当紧张了。所以做项目前先想清楚模型给规划提供的是粗粒度可行驶区域还是需要精细到远距离小目标。前者建议控制在 640 宽度级别后者则必须上量化加速。5.2 推理链路的优化不是只看网络很多人在这个节点上卡住以为问题出在模型太大其实真正的瓶颈在图像数据和显存的频繁拷贝。有一段时间我把图像话题换成高分帧结果 CPU 占用暴涨GPU 占用却没起来后来发现是 cv_bridge 在回调里反复做格式转换再加上发布时连续生成好几份图像副本都在抢内存带宽。这里给三个很实用的优化方向在回调之外做好图像预处理。比如先缩放到网络输入尺寸再送入推理避免每一帧都执行昂贵的 resize。减少发布冗余。如果下游节点不订阅 canvas 叠加图就把 /semantics/canvas 的发布关掉节省的不只是 CPU还有可观的带宽。如果平台支持优先把模型转成推理引擎格式或低精度版本。ENet 本身参数就少量化后几乎不影响语义覆盖质量但推理速度能再上一个台阶。换句话说先看数据通路有没有多余搬运再考虑换更大的 GPU效果往往更明显。5.3 用后处理把分割结果变成能用的地图原样输出的分割图噪点很多尤其是摄像头动态范围大的时候阴影交界处会出现大量像素级闪跳。实际使用时我建议加三道简单的后处理第一道中值滤波或形态学开闭运算。把单个零散像素和椒盐噪点清掉让区域边缘更平滑。第二道类别过滤和连通域分析。因为单帧误分割很难完全避免可以把面积小于某个阈值的孤立“路面”块剔除防止规划模块误以为那里能通过。第三道时序平滑。上一帧是路面、这一帧突然变成障碍物的像素大概率是误判建议做一帧缓冲连续多帧判决后再更新最终语义。这个操作对长期稳定性的提升非常明显比单纯换大模型性价比高得多。6. 常见问题与排查技巧实录6.1 问题速查表现象直接原因解决方向启动后 rqt_image_view 黑屏话题名不匹配用rostopic list查实际发布话题再订阅输出图全是一种颜色模型类别数与颜色表不一致检查 prototxt/模型输出通道和颜色表长度推理很慢GPU 占用低实际跑在 CPU 上检查 GPU 开关、推理框架是否 GPU 版节点启动就闪退权重路径错或模型文件损坏确认路径、重新下载、检查文件大小分割图与原图错位图像尺寸/翻转设置不对确认输入分辨率、禁用不必要的图像翻转小目标频繁丢失输入分辨率不够调大 net_width/net_height 后重新评估帧率6.2 用日志和系统命令把问题定位到具体环节遇到“输出不对”这类模糊问题时我习惯画一条链路逐段排查图像话题有没有数据节点有没有执行推理输出话题有没有数据每一段都用一个命令单独验证。图像输入正常却全图一个色的优先怀疑模型加载是否成功而不是马上改代码。可以先看节点启动日志里有没有“model load successfully”之类的关键字没有说明模型文件没加载上后续所有输出自然是空的或错的。GPU 有没有真正跑起来用nvidia-smi看进程时的 GPU 利用率最直接。如果节点在跑但 GPU 利用率接近 0而 CPU 飙高基本可以断定推理走了 CPU 路径优先查框架版本和编译选项。6.3 容易踩碎的下意识小坑这个节点的调试让我记住了一个教训别迷信默认参数。默认颜色表顺序跟 19 类类别顺序未必一致有些版本的输出还会先把 19 类做一次索引互换直接拿原始索引去查颜色表出来的一定是张“调色板出问题”的图。正确做法是把某个类别的像素单独提取出来生成一张黑白掩膜与原图对比确认该类别区域是否对应正确再决定要不要调整颜色表。另外图像通道顺序也很坑。相机驱动输出的图像不一定是 BGR有的设备是 RGB。ENet 模型训练时如果用 BGR 输入现在直接喂 RGB 图模型看到的颜色分布完全不同分割结果轻则边缘抖动重则大片类别错分。启动时用图像查看器看一眼颜色是否鲜艳正常就能快速排除这个问题。最后想提一个经常被忽略的环境变量问题。某些部署脚本会设置单帧推理的超时时间如果车机负载高节点经常会在推理超时后被强杀表现就是运行几分钟后无征兆掉线。遇到这种“跑着跑着消失”的情况建议把超时时间调大一些同时把图像发布频率降下来往往比更换推理框架更见效。我自己把这套节点完整调完后的感受是vision_segment_enet_detect 虽然挂着老模型的名字但把整个感知链路打通之后你对“像素级分类到底解决什么问题、怎么和规划衔接”的理解会上一个台阶。后续我还想把这个分割结果接一个逆透视映射到地平面生成真正给规划用的局部栅格图到时候再写一篇实操记录。也建议你把类别映射表改一改换成自己场景的语义组合试试很容易跑出很有意思的效果。