☰
机器人巡检实战:CornerNet_Lite 在 Ubuntu 16.04 与 ROS 中的部署
2026/10/17 12:51:16 网站建设 项目流程

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。接着创建一个虚拟环境,避免污染系统环境。我习惯用 venv:

python3.6 -m venv ~/corner_venv source ~/corner_venv/bin/activate pip install --upgrade pip pip install torch==0.4.1 pip install torchvision==0.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_Saccade,CornerNet-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.3,catkin_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_encoding='bgr8') 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_size=1, buff_size=2**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_size=1) self.det_pub = rospy.Publisher('/detection/bboxes', Detection2DArray, queue_size=10) self.sub = rospy.Subscriber('/camera/image_raw', Image, self.image_callback, queue_size=1, buff_size=2**24) threading.Thread(target=self.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.5GB,CPU 占用约 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_format='jpg'))

这样带宽能降到 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_size=2**24
画框图像发布后 rviz 不显示消息没有持续更新检查发布频率,确认队列 queue_size=1
推理线程 CPU 占用 100%使用了 CPU 模式检查 torch.cuda.is_available() 返回是否为 True
目标框重复大量重叠NMS 阈值不合适将 NMS 阈值调到 0.4
ROS 节点启动卡在模型加载模型文件未加载到内存等待即可,首次加载约 30 秒是正常现象
编译时 CUDA 找不到环境变量未设补充 PATH 和 LD_LIBRARY_PATH 并重新 source

5.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 环境里稳稳跑起来,目标就算达到了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询