机器人巡检实战:CornerNet_Lite 在 Ubuntu 16.04 与 ROS 中的部署 1. 项目整体设计与核心思路去年接手一个机器人巡检项目工控机上装的是 Ubuntu 16.04显卡是一张老款 GTX 1080系统里已经有一套跑得好好的 ROS kinetic。需求其实很直白在 ROS 里实时做目标识别把检测框、类别、置信度发布出来供导航节点和抓取模块直接消费。我试过几种方案最终选定 CornerNet_Lite。不是因为它花哨而是综合评估下来CORNERNET_LITE 这类无锚框检测模型在老平台上的编译成本可控部署难度相对低识别效果也确实够用。这篇就把我踩过的坑从头到尾捋一遍给正在折腾同样组合的同学做个参考。1.1 CornerNet_Lite 到底是什么为什么值得选CornerNet_Lite 是密歇根大学推出的一个轻量化目标检测项目里面包含两个检测器CornerNet-Saccade 和 CornerNet-Squeeze。这个名字拆开看就很有意思——Saccade 取自人眼的“扫视”机制会先快速瞄一眼全局找出可能存在目标的候选区域再对候选区域做精细检测Squeeze 则更像传统网络轻量化思路把费算力的骨干网络换成紧凑结构换取更高的推理速度。它们共同的底层创新是“角点检测”。传统检测模型比如 YOLO、SSD大部分要设计一堆先验框anchor预测时去修正这些框的位置和大小。CornerNet 的思路完全不同把目标检测看作“找左上角关键点”和“找右下角关键点”两个角点确定了矩形框就出来了。这个思路放在机器人场景里有个额外的好处——检测小目标或长宽比极端的物体时不容易被先验框的预设比例束缚我在实测里对远处细长目标的召回确实更好。1.2 为什么选它而不是 YOLO 或 SSD我在定方案之前其实先试过 YOLOv3也试过 SSD-MobileNet。YOLO 在 1080 上跑得飞快但有两个问题一个是老版本的 Darknet 源码在 Ubuntu 16.04 上需要自己改一堆编译选项另一个是我需要的是“能嵌进 ROS 的 Python 接口”Darknet 的 Python 绑定写得比较糙传图像数据频繁出幺蛾子。SSD-MobileNet 倒是轻但小目标精度一言难尽对我们巡检场景里的远距离零件检测明显不够。CornerNet_Lite 的好处在于它的仓库本身就带了一个干净的 Python 推理接口结构清晰改造成 ROS 节点非常顺畅。而且它的作者在做模型轻量化时已经考虑了部署问题不像一些学术仓库丢出个训练代码就完事了。综合比较下来CORNERNET_LITE 在原项目目录下只需要简单改动就能被外部脚本调用这一点对 ROS 开发是决定性的加分项。1.3 整体架构图像流、推理、消息发布一条线整个系统的数据流不复杂摄像头话题图像进来经过 cv_bridge 转成 OpenCV 的 Mat 或 numpy 数组送入 CornerNet_Lite 模型推理得到 bboxes、scores、clses 三组数据最后把画了检测框的图像重新发布回去同时把结构化结果发布到另一个话题。这样 ROS 里的任何节点都能订阅检测结果话题拿bbox不需要关心图像本身长什么样整个架构是解耦的。这里面有个设计细节值得提一下。我没有把模型推理逻辑硬塞进回调函数里而是在节点里单独开辟了一条处理链路采用“拿最新帧、丢弃积压帧”的策略。原因很简单ROS 回调是单线程串行执行的如果推理需要 100ms那回调期间其他消息全都会被堵住。后面我会专门讲这部分怎么处理。2. 环境部署Ubuntu 16.04 的编译与依赖Ubuntu 16.04 是个有点尴尬的版本。它发布于 2016 年系统自带的软件源里很多包已经过时了而 CornerNet_Lite 需要的是 Python 3.6 和 PyTorch 0.4.1这两个东西在 16.04 的默认源里都没有。如果你直接拿系统默认的 Python 3.5 去装依赖基本会走入死胡同。所以环境准备这一步我的建议是老老实实按下面这条路走。2.1 显卡驱动、CUDA、cuDNN 的版本匹配先看显卡。GTX 1080 在 Ubuntu 16.04 上装驱动还算省心用系统自带附加驱动源装 384 或 390 版本都可以。需要注意CornerNet_Lite 官方对应的是 PyTorch 0.4.1而 PyTorch 0.4.1 官方预编译包只支持到 CUDA 9.0。所以 CUDA 版本不要贪新就装 CUDA 9.0 cuDNN 7.0 的搭配。这一步如果你装成 CUDA 10后面导入 torch 时就会直接报找不到 libcudart 之类的错误非常痛苦。驱动装好之后验证一下 nvidia-smi 是否正常输出显存信息再验证 nvcc -V 是否能看到 CUDA 9.0。如果 nvcc 报找不到命令说明 CUDA 的 PATH 环境变量没有写入记得在 ~/.bashrc 里加上export PATH/usr/local/cuda-9.0/bin${PATH::${PATH}} export LD_LIBRARY_PATH/usr/local/cuda-9.0/lib64${LD_LIBRARY_PATH::${LD_LIBRARY_PATH}}建议整篇操作都在一个固定目录下进行我习惯在 /home/用户名/workspace 下建一个 corner_ros 文件夹把所有源码、权重、脚本集中管理。后续的路径引用都以这个结构为前提能少很多磕磕绊绊。2.2 Python 3.6 与 PyTorch 0.4.1 安装Ubuntu 16.04 默认 Python 是 3.5.2这个版本跑不了 CornerNet_Lite。我尝试过直接用 deadsnakes PPA 装 Python 3.6但 16.04 的 PPA 兼容性时好时坏。最稳的办法是源码编译安装 Python 3.6.8。你可能觉得编译 Python 很麻烦但其实只要按下面三步走问题不大sudo apt-get install -y build-essential libssl-dev libffi-dev zlib1g-dev wget https://www.python.org/ftp/python/3.6.8/Python-3.6.8.tgz tar -xzf Python-3.6.8.tgz cd Python-3.6.8 ./configure --enable-optimizations --prefix/usr/local/python368 make -j8 sudo make install sudo ln -s /usr/local/python368/bin/python3.6 /usr/local/bin/python3.6 sudo ln -s /usr/local/python368/bin/pip3.6 /usr/local/bin/pip3.6装完之后用python3.6 -V确认输出 3.6.8。接着创建一个虚拟环境避免污染系统环境。我习惯用 venvpython3.6 -m venv ~/corner_venv source ~/corner_venv/bin/activate pip install --upgrade pip pip install torch0.4.1 pip install torchvision0.2.1这里的教训是不要用系统自带 pip 直接装 torch因为 16.04 的 pip 版本太老容易下载到不兼容的 wheel。2.3 编译 CornerNet_Lite 源码从 GitHub 拉取 CornerNet_Lite 源码之后先安装项目依赖再编译扩展模块。官方 requirements.txt 里有 numpy、opencv-python、Cython 这些常规包直接装就行pip install -r requirements.txt cd CornerNet-Lite python setup.py build_ext --inplace编译这一步是第一个大坑点。这里的 setup.py 负责编译 deformable conv可变形卷积等 CUDA 扩展算子编译过程中需要 CUDA 和 NCCL。如果你的环境里没有装 NCCL会报找不到 nccl.h 的错误。NCCL 需要去 NVIDIA 官网下载 deb 包我当时下的版本是 2.2.13装完后再编译就通过了。编译过程还经常遇到 GCC 报错。因为 CUDA 9.0 自带的 nvcc 编译器对 GCC 版本有要求Ubuntu 16.04 默认 GCC 5.4 是可以兼容的但如果你系统里之前升级过 GCC 到 7.x就一定要降回来否则 nvcc 会报 unrecognized command line option 一类的问题。排查起来很费事不如一开始就用干净的工具链。2.4 权重文件与目录结构CornerNet_Lite 的权重文件不在 GitHub 仓库里需要单独下载放到项目的 cache/nnet 目录下。按照默认路径CornerNet-Saccade 的权重放在cache/nnet/CornerNet_Saccade/下文件名一般是CornerNet_SaccadeCornerNet-Squeeze 类似。默认的 Demo 类构造函数会硬编码这两个路径如果你想让代码更灵活可以改一下demo.py里的 model_path 参数手动指定为绝对路径。我在项目里就是直接改的绝对路径这样不管从哪里启动节点都不会因为工作目录不对而加载失败。3. 模型推理验证与参数调优环境搭好之后不要急着往 ROS 里塞先在命令行把推理跑通。这一步能帮你确认三件事模型权重是否下载完整、CUDA 环境是否正常、推理速度是否在自己能接受的范围内。3.1 单张图片与视频测试官方仓库的 demo.py 直接可以传图片目录python demo.py --image_dir /path/to/images python video.py --video_path /path/to/video.mp4我第一次运行的时候模型加载足足卡了快半分钟还以为死机了。后来发现这是正常的Saccade 模型要加载 ResNet、Hourglass 等多个子模块加上 CUDA 上下文初始化慢是常态。模型加载完成后单张 511x511 输入的推理时间在 GTX 1080 上大约 80-120ms对应的 FPS 是 8-12 帧。如果你用的是 Saccade 模式由于有注意力筛选机制实际平均推理时间会略低一点运行更偏向于稳态速度。提示demo.py 默认的输出是把检测框画在图片上保存下来。调试阶段建议先跑一张图片看目标框是否准确贴合边缘。如果框有偏大偏小的问题后面需要调阈值或检查预处理部分。3.2 置信度阈值怎么调推理结果里每类目标都带一个得分超过阈值的才算有效检测。官方默认是 0.5但实测在不同的场景里差别很大。如果你检测的目标普遍是小物体建议降到 0.35 左右能多召回一些弱特征目标但代价是也会引入一些误检。如果是空旷场景检测大型物体0.5 就能过滤得很干净。我调试时习惯把阈值参数单独放在一个配置文件里比如config.yaml每次跑不同场景就改阈值而不是在源码里来回改。这样最清晰。阈值调优的另一个技巧是观察混淆的误检目标比如机器人巡检中经常把配电箱上的指示灯误检成按钮这时候可以针对某些特定类别单独提高阈值保持其他类别不变。另外注意 NMS非极大值抑制参数。CornerNet_Lite 使用的 NMS 方式是基于角点配对完成的如果场景中物体特别密集每个角点会匹配多个可能的配对组合实际表现是同一个目标出现很多互相重叠的框。官方默认的 NMS 阈值是 0.5密集场景可以调到 0.4能减少重复框数量。3.3 CPU 与 GPU 的取舍如果工控机上没有 NVIDIA 显卡CornerNet_Lite 也能跑 CPU 模式但速度完全不是一个量级。我在一台不带 GPU 的 i7 工控机上测试过单张图片推理耗时接近 2 秒完全无法满足实时要求。所以如果你打算在机器人上长期使用它GPU 是硬条件。不过即便有 GPU也要注意显存占用。Saccade 模型加载后大约占用 4-5GB 显存跟输入尺寸和注意力候选数量有关如果你的显卡只有 2G 或 4G 显存跑起来会非常吃力甚至直接 OOM。GTX 1060 6GB 勉强够用GTX 1080 8GB 就能跑得比较从容了。显存不够还有一个权宜之计把输入图像从 511 缩小到 383推理速度能提升不少精度损失也不算大。官方虽然没有直接提供这个参数但你可以改 demo.py 里处理输入尺寸的部分。4. ROS 节点设计与消息流封装模型跑通只是第一步真正的工程难点在 ROS 节点设计。这个环节要处理三件事图像话题订阅与 OpenCV 转换、模型推理与图像同步、检测结果发布格式。4.1 创建 ROS 工作空间与功能包先确认 ROS kinetic 已经装好。如果没有建议装完整版sudo apt install ros-kinetic-desktop-full然后创建工作空间mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src catkin_create_pkg corner_detector rospy roscpp sensor_msgs std_msgs cv_bridge cd ~/catkin_ws catkin_make source ~/catkin_ws/devel/setup.bash这里有一个坑ROS kinetic 自带的 cv_bridge 是依赖 OpenCV 3.3.1 编译的。很多人在 Ubuntu 16.04 上会先 apt 装一个 libopencv-dev这个包的版本是 OpenCV 2.4.9装完之后 cv_bridge 跟 OpenCV 2.4 的头文件冲突编译时会报各种 undefined reference 的错误。解决办法很简单不要手动 apt 安装 OpenCV 2.4直接用 ROS 自带的 OpenCV 3.3catkin_make 时自动解析依赖即可。4.2 图像消息转换的细节ROS 摄像头话题一般是sensor_msgs/Image图像格式可能是bgr8、rgb8或者压缩后的compressed。cv_bridge 负责转换def image_callback(self, msg): try: cv_img self.bridge.imgmsg_to_cv2(msg, desired_encodingbgr8) except Exception as e: rospy.logerr(cv_bridge convert error: {}.format(e)) return这里注意一个性能问题imgmsg_to_cv2会拷贝整幅图像。如果你订阅的是高分辨率图像比如 1920x1080一秒钟 30 帧就是每帧 6MB 的内存拷贝本来不算什么但如果推理线程和回调线程同时处理旧帧内存占用会快速上涨。我的做法是只订阅需要的分辨率或者在相机驱动端直接设置低分辨率输出比如 640x480。对于目标检测任务640x480 足够了还省显存。关于buff_size参数订阅大图像话题时默认缓冲区可能不够数据量大时会出现Unable to receive message的报错。订阅时需要手动增大缓冲self.sub rospy.Subscriber(/camera/image_raw, Image, self.image_callback, queue_size1, buff_size2**24)这个 2**24 就是 16MB基本能解决图像消息接收失败的问题。4.3 推理线程与话题发布结构ROS 回调默认是单线程的如果直接在回调里做推理话题订阅会阻塞。我在工程里用的方案是回调只负责拿图像帧存到全局变量然后一个独立线程不断取出最新帧去推理、发布。类似import threading class CornerDetector: def __init__(self): self.lock threading.Lock() self.latest_frame None self.running True self.detector Demo() self.bridge CvBridge() self.image_pub rospy.Publisher(/detection/image, Image, queue_size1) self.det_pub rospy.Publisher(/detection/bboxes, Detection2DArray, queue_size10) self.sub rospy.Subscriber(/camera/image_raw, Image, self.image_callback, queue_size1, buff_size2**24) threading.Thread(targetself.process_loop).start() def image_callback(self, msg): try: frame self.bridge.imgmsg_to_cv2(msg, bgr8) with self.lock: self.latest_frame frame except Exception as e: rospy.logerr(str(e)) def process_loop(self): while not rospy.is_shutdown() and self.running: with self.lock: if self.latest_frame is None: time.sleep(0.005) continue frame self.latest_frame self.latest_frame None self.run_inference(frame)这个结构有几个好处。一是摄像头帧率高于推理帧率时队列里永远只保留最新帧不会累积旧图片造成处理延迟越来越大二是阻塞操作不会影响 ROS 内部的 topic 管理等任务三是后续如果要多线程并发处理这个结构天然支持。4.4 检测结果如何发出去我采用的是两种发布方式并存一是发布画好框的图像话题/detection/image方便 rviz 可视化二是发布结构化检测结果到/detection/bboxes供导航和上层逻辑使用。结构化消息选用了vision_msgs/Detection2DArray这个消息类型在机器人视觉项目中很常用字段里包含一个 Detection2D 数组每个 Detection2D 又包含 bounding_box、results 等子字段。如果你想完全避免引入新依赖也可以用 std_msgs/String 直接发 JSON 字符串下游节点自己解析。我最初就是这样做的因为上手最快。后来发现 JSON 字符串在跨语言节点传递时没类型检查调试麻烦就换成了正式消息。建议一开始就用正式消息省得后面返工。推理检测框画图时我额外叠加了一个类别名称这个信息来自模型输出的 cls id需要和 COCO 类名表对应。CornerNet_Lite 默认在 COCO 数据集上训练所以有 80 类。你的场景如果只关心其中几类可以在发布前先做一次类别筛选。比如我只发布 person、car、truck 这三类的结果其他都过滤掉既减少下游节点压力也减少干扰。5. 实跑效果与问题排查速查整套流程跑起来之后我在机器人平台上做了一轮实际运行记录了一些数据也总结了不少踩坑经验。这里直接整理成表格方便对照。5.1 实际运行效果评估用 640x480 输入Saccade 模型GTX 1080实测稳态推理速度大约 9-10 FPS。对于巡检机器人来说这个帧率够用因为底盘移动速度控制在 1m/s 左右10FPS 意味着每 10cm 左右就有一帧检测不会漏掉关键目标。如果是车载高速摄像头场景这个帧率就会偏紧张那就需要考虑换成 Squeeze 模型或者缩小输入尺寸。资源的开销方面一个节点占用显存约 4.5GBCPU 占用约 60%主要来自图像解码和发布内存约 1.5GB。工控机上跑它 导航 激光建图全套压力不算大。但在同一个 ROS 主节点上同时跑多个视觉节点就吃不消了建议如果有多个视觉任务尽量分到不同机器上。发布频率和话题带宽也值得关注。画框图像话题在 rviz 里显示时带宽占用很高640x480 的彩色图像大约 900KB/帧10FPS 就是 9MB/s 的网络消耗。如果机器人是无线通信建议发布 JPEG 压缩格式。cv_bridge 支持直接把 cv::Mat 编码为 compressed 类型self.image_pub.publish(self.bridge.cv2_to_compressed_imgmsg(cv_img, dst_formatjpg))这样带宽能降到 1MB/s 以下质量损失在可视化场景里可以接受。5.2 常见问题排查表现象可能原因解决办法torch 导入报 CUDA 错误PyTorch 版本与 CUDA 版本不匹配确认 torch 0.4.1 CUDA 9.0 搭配setup.py 编译找不到 nccl.h未安装 NCCL下载 NCCL 2.x deb 包安装模型加载后推理全为 0 结果权重路径错误加载了未训练模型检查 cache/nnet 目录路径与权重文件名ROS 订阅图像失败或丢帧buff_size 太小设置 buff_size2**24画框图像发布后 rviz 不显示消息没有持续更新检查发布频率确认队列 queue_size1推理线程 CPU 占用 100%使用了 CPU 模式检查 torch.cuda.is_available() 返回是否为 True目标框重复大量重叠NMS 阈值不合适将 NMS 阈值调到 0.4ROS 节点启动卡在模型加载模型文件未加载到内存等待即可首次加载约 30 秒是正常现象编译时 CUDA 找不到环境变量未设补充 PATH 和 LD_LIBRARY_PATH 并重新 source5.3 从 demo.py 到 ROS 改造的三个关键区别直接把官方 demo.py 塞进 ROS 节点里也能跑但有几个关键区别必须处理。第一个是图像来源不同官方 demo 是从文件读图ROS 是从话题实时拿帧所以要改造 extract 接口让它接受 numpy 数组而不是文件路径。第二个是生命周期不同官方 demo 一次性跑完就退出ROS 节点要常驻运行模型对象要存成节点成员变量而不是每次重新初始化。第三个是输出形式不同官方 demo 打印到控制台ROS 要发布到话题中间还需要考虑时间戳对齐。这三个区别处理好了整个改造就顺畅了。我改造 extract 接口的思路很简单把官方 Demo 类的 extract 方法复制一份改成 accept 一个 numpy 数组内部去掉 cv2.imread 那一步直接从数组开始处理。核心的预处理和后处理逻辑完全不动这样既能保证推理效果和官方一致又能满足 ROS 数据流的输入方式。注意模型的预处理逻辑里包含一个对图像做 resize 和归一化的过程。改成 numpy 输入后务必确认输入图像是 BGR 还是 RGB 格式。官方 extract 接口在读取图片后会自动做一次颜色转换如果你直接传 numpy 数组就要自己保证转换和官方保持一致否则检测结果会差很多。我在这上面栽过一次当时检测出来的目标类别完全错乱最后发现是 RGB 和 BGR 顺序反了。5.4 排查问题的一些通用思路排查这些问题时我最大的体会是别急于改代码先确认每一层链路是否正常。我的排查顺序固定是这样的先看 nvcc -V 和 nvidia-smi确认 CUDA 层没问题再写一个简单的 Python 脚本用官方 demo 跑一张图片确认模型层没问题然后订阅一个固定 bag 包里的图像话题确认 ROS 消息层没问题最后才把三个环节串起来调。这样每一步的错误都是可控的不会出现三个环节同时出问题导致无从下手的情况。如果你在运行时看到 CUDA 显存不够的错误优先检查是不是有别的小程序也占着显存。用nvidia-smi看一下进程列表就知道是谁在占用。训练任务和一些可视化工具往往会占掉 1-2GB这在小显存的卡上有可能是压死骆驼的最后一根稻草建议全部关掉再跑推理。6. 写在最后的一些体会我现在回过头看这个项目觉得最耗费时间的其实不是模型本身而是 Ubuntu 16.04 的依赖兼容性处理。但如果你严格照着上面的步骤走从零开始到 ROS 节点跑通一个周末的时间足够了。我在实际项目里最想提醒的一点是不要以 demo 里的检测效果为最终验收标准。demo 里用的是官方 COCO 数据集权重一旦切到你的巡检场景类别分布、目标尺寸、光照条件全变了检测效果会有明显波动。后面还是要做针对性微调哪怕是拿自己的数据只做少量训练轮次效果提升都会非常明显。不过那是另一个话题了。这一篇能帮你把 CORNERNET_LITE 在 Ubuntu 16.04 ROS 环境里稳稳跑起来目标就算达到了。