首页 / 资讯中心 / 文章详情

ARM Linux板从零移植OpenCV 4.x:交叉编译、依赖管理与性能调优

ARM Linux板从零移植OpenCV 4.x:交叉编译、依赖管理与性能调优 ★ FEATURED ARTICLE
做嵌入式视觉、尤其是拿到一块陌生的 ARM 开发板时“移植 OpenCV”一定避不开。这一篇循序渐进的实操记录人数我不单独展开直接从我在一块 Cortex-A 级别 ARM Linux 板子上移植 OpenCV 4.x 的过程中抽出的核心步骤按环境、依赖、编译、上线运行、性能调优的顺序写。先交代两个前提宿主机器是 x86_64 的 Linux 环境目标板是 ARM 架构的 Linux 系统这是目前最常见的“宿主机编译、目标板运行”的嵌入式开发场景。如果你目标是 AArch64也就是 arm64 架构除了工具链前缀和个别参数不同流程完全一样。想强调的是移植不等于把 OpenCV 源码放到板子里直接 make。ARM 板大多受限于存储空间和处理能力在板上编译既慢又容易卡死更可靠的手段是交叉编译在性能强大的宿主机器上生成目标板可执行的库文件与可执行程序最后拷贝到板子上验证。这篇文章会陪你走完全部流程最后一小节还有我自己反复踩过几轮的调优心得。系列前一篇已经提过几处通用“经验包”这一篇则是完整版以一次独立的板级移植作为眼下唯一主线。1. 移植前必须确认的硬件与工具链事实拿到一台 ARM 板子多数人第一反应是找 datusheet看处理器能跑多快、内存多大这当然对但开始动 CMake 之前还有更关键的问题工具链与板子是否匹配。工具链版本、处理器架构、跟浮点运算相关的 ABI 约定任何一项对不上都会让后续的编译过程出现一系列怪异错误。1.1 交叉编译是移植的起点不是唯一选项交叉编译的原理很简单在主机上用一套针对 ARM 目标的编译器生成代码目标板只负责运行。OpenCV 本身是 C/C 项目源码用到的编译器特性不会特别新但依赖的第三方库在交叉编译时是真正的难点因为每个库都需要用目标层的工具链重新编译一遍这是很多人开始没有心理准备的地方。 为什么不直接到板子上编译我在某型号 ARM 板子试过4 核处理器2GB 内存编译一个完整 OpenCV 大概需要 poke 近半小时而且编到中途内存耗尽常见得很。板子系统里还没有完整 build-essential 工具链的也大有人在所以交叉编译“更大几率”是对的做法。当然如果板子内存充足、系统里已有可用的编译环境直接在板上编也是可行的但如果你做的是产品预研后续需要反复改配置、加模块交叉编译的整个流程更可控。1.2 建立符合目标板的根文件系统交叉编译的 OpenCV 编译期环境需要基础 C/C 库和系统头文件这些来自目标板的系统根目录。建议构造一个专门的主机目录例如 /opt/arm-rootfs用板子的根文件系统内容填充它至少要包含 lib、usr/lib、usr/include 这几个关键目录。在这个目录里目标板的 libc.so、pthread、dl 等基础库和头文件都会使用到。 很多供应商发布的 SDK 包里已经自带根文件系统或工具链目录用起来也比较稳妥。如果没有现成的就从板子上直接把整个根文件系统打包过来再解包到这个目录。拿板子系统时要注意不要只拷贝 /lib 和 /usr/lib因为很多库和头文件在 /usr/local 下尤其是你自己之前装过的依赖。干脆把整块根文件系统镜像挂载或解包避免遗漏。1.3 工具链接口与处理器特性的常见误区我犯过也用一件事编译器用 arm-linux-gnueabihf 开头代表 ARMv7 硬浮点如果板子其实是 AArch64那么必须使用 aarch64-linux-gnu 前缀。多个工具链前缀不要乱挑最好的办法是检查板子的内核信息或者供应商 SDK 文档一次性给自己记录下面这些参数。检查项命令/方法对 OpenCV 的影响处理器架构板内执行uname -m决定工具链前缀与 -march 参数是否支持 NEON查看 /proc/cpuinfo 里的 Features包含 neon 才行影响 CPU_BASELINE 与 NEON 使能浮点 ABI工具链名称含 hf 通常是 hardfp但最好用readelf -A验证决定链接时是否报 VFP 寄存器错误根文件系统 libc 版本板内执行ldd --version工具链 sysroot 的 libc 不能比板子新太多工具链和系统根目录还有两个容易踩进去的坑。第一个是工具链自带的 sysroot 与板子的 libc 版本不匹配通常会出现 cannot find -lstdc 或 GLIBC_2.34 not found 这类运行时报错解决方法是尽量采用供应商配套 SDK。第二个是不要用主机的 /usr/lib 来满足交叉编译的查找种下的错误会在链接后期才爆发后面第四部分会专门展开。2. 依赖库的顺序排列与版本锁定刚开始接触 OpenCV 时我一直以为它是个自包含的库直到有一天在板子上跑 imread 读取常规图片返回的 Mat 是空的才明白它如果想读 jpg/png就必须在编译期看到对应的解码后端。这个道理在交叉编译场景里更严峻主机的这些后端库不能直接复用必须在编译 OpenCV 前为目标板准备一套 ARM 版本。2.1 先弄清楚完整依赖图谱再动手OpenCV 的模块化结构决定了依赖“按需引入”的魅力。不开 DNN 就不用碰 protobuf不开 GUI 窗口就不用编译高层的图形工具库不加视频采集功能就不需要外接视频后端。先做一个清单再决定每个模块要不要。 我日常最常保底的一组是核心、图像处理、图像编码、视频采集和窗口显示模块。围绕它们依赖可分为以下几类基本压缩库任何带压缩性质的图片格式都绕不开的底层库先编它后面的依赖都直接或间接需要它。图片格式解码库PNG、JPEG、TIFF 三个常用格式的解码库编码解码都靠它们完成。摄像头采集后端V4L2 相关接口库Linux 下摄像头帧采集的标准入口。并行库选项TBB 这类库属于可选项不开启也可以用 pthread 完成并行但板子核多且性能关键时往往会加它。我的建议是为了减少环境不确定第一版编译只保留核心、图像处理、图像编码、视频采集这四个模块将界面和 DNN 全部关闭。等版子在板上能跑通最小 Demo再往里加其他模块。编译期减少依赖验证周期短出问题时定位也大幅度快于一次性全量。2.2 交叉编译依赖库的通用操作流程依赖库的构建方式各不相同但大体遵循一个固定姿势先 configure再 make再 install 到根文件系统目录。以常见的图片格式解码库为例通常你会见到这样的功能步骤tar -xzf libxxx.tar.gz cd libxxx ./configure --prefix/opt/arm-rootfs/usr --hostarm-linux-gnueabihf --enable-shared make -j4 make install有一部分库用 cmake 构建的也很常见只要注意两点指定 CMAKE_INSTALL_PREFIX 指向系统根目录并让 cmake 找到同一套交叉工具链。在做这一步时我习惯用一个临时环境变量把工具链地址带出来既避免反复写活活的路径也能保证依赖库之间使用同一条编译链。 编译顺序基本按依赖箭头反向来最底层的基础压缩库最先编图片格式解码库在上层OpenCV 最后。贪方便打乱顺序常常导致链接阶段 libxxx.so 引用了一堆 undefined reference。如果你发现一个库链接时依赖另一个库就回头重新排一下顺序没有捷径。2.3 版本选择的三个原则依赖库版本与 OpenCV 版本之间的兼容问题比大多数人想象的更关键。某次我为了性能直接选了很新的图片格式解码库结果 OpenCV 的接口调用方式跟不上报了一堆隐晦的 undefined symbol。后来换回板子上常用的大版本旧一点的分支立刻安静下来。总结出三条可行的原则第一依赖库版本不要超过 OpenCV 源码官方分支里对应的 README 推荐版本。第二以板子系统里已有库的版本为中心优先匹配主版本一致如果板子已经有人验过某个版本你直接沿用是最安全的。第三编译依赖库时保留窗口的configure参数日志避免日后需要重建时无据可依。环境变量也可以帮上大忙。交叉编译时把 PKG_CONFIG_LIBDIR 指向 /opt/arm-rootfs/usr/lib/pkgconfig并且让它只定位到系统根目录里的 .pc 文件这样 OpenCV 在查依赖时就不会撞到主机上的同名文件。在我某次操作中这个变量一度是脚本里最关键的一行少了它再标准的前缀设置都可能前功尽弃。3. 用构建系统配置 OpenCV 的关键参数OpenCV 从 3.x 时代开始全面转向 CMake到了 4.x 几乎只有 CMake 一条路。交叉编译时CMake 的配置参数直接决定生成的库是给 x86 用的还是给 ARM 用的。很多第一次搞的人打开 CMake 界面就蒙我就把最关键的一组参数与一个可复用的配置文件写下来这套配置翻过整个 ARM 团队后还会继续用到。3.1 一份可用的交叉编译配置文件在 OpenCV 源码目录之外放一个单独的文件名字就叫 toolchain.arm.cmake内容大致如下set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) set(TOOLCHAIN_PREFIX arm-linux-gnueabihf) set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g) set(CMAKE_SYSROOT /opt/arm-rootfs) set(CMAKE_FIND_ROOT_PATH /opt/arm-rootfs) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)也许你会问这第九行是什么意思前三行明确目标系统是 ARM Linux告诉 CMake 不要走 x86 的文件路径第四到第六行把编译器切换成交叉工具链第七行设置系统根目录最后四行限定查找程序、库、头文件、包时的路径模式。PROGRAM NEVER 保证它去找主机命令而不是在根文件系统里找开发工具后面三个 ONLY 则强制所有库和头文件只从目标根文件系统里找隔离主机干扰。 如果用 AArch64 板子只需要把 TOOLCHAIN_PREFIX 换成 aarch64-linux-gnu。关于 arm-linux-gnueabihf 和 arm-linux-gnueabi 的区别简单讲gnueabi 对应软浮点gnueabihf 对应硬浮点绝大多数现代 ARM Linux 系统都用硬浮点工具链但老版本内核和旧板子可能只有软浮点版本这从 boot 时打印的 CPU 信息能看出来。3.2 决定最终产物的模块选项配置的时候OpenCV 默认会尝试把能编的模块都编上这可能不是你想要的。用 BUILD_LIST 压缩模块集合往往能省下大量时间与磁盘空间还能减少被依赖的第三方库范围。我的这组参数一直拿来反复用cmake -S . -B build-arm \ -DCMAKE_TOOLCHAIN_FILE../toolchain.arm.cmake \ -DCMAKE_BUILD_TYPERelease \ -DCMAKE_INSTALL_PREFIX/usr/local/opencv-arm \ -DBUILD_LISTcore,imgproc,imgcodecs,videoio,highgui \ -DWITH_OPENCLOFF \ -DWITH_OPENMPOFF \ -DWITH_TBBOFF \ -DWITH_GTKOFF \ -DWITH_V4LON \ -DBUILD_SHARED_LIBSON关键选项的含义我列个表比事后猜含义方便参数取值作用与理由BUILD_LISTcore,imgproc,...只编译必要的模块减少依赖与体积WITH_GTKOFF不编 GUI 后端绝大多数无屏幕板子用不到WITH_V4LON打开 Linux 视频采集后端摄像头场景需要BUILD_SHARED_LIBSON默认生成 .so方便排查问题后面可再改静态方案WITH_OPENMP按需板子核心多且追求速度才开多核小内存慎用WITH_TBB按需引入并行库会带来额外依赖先 OFF跑通后再试如果你之后需要用 OpenCV 的 DNN 模块跑神经网络还得把 DNN 加入 BUILD_LIST同时准备 protobuf 依赖。建议第一版全部关闭稳字当头。3.3 指令集优化NEON 与 VFPv4 的启用边界ARM 平台的性能优化往往绕不开 NEON 指令集OpenCV 4.x 在 CMake 里对 ARM 优化有比较直观的控制方式。局部板子若是 ARMv7通常会在命令行加上 -DCPU_BASELINENEON,VFPV3 或类似参数如果板子支持 VFPv4则写成 NEON,VFPV4。过早地全局泛化会出问题尤其当你手里的板子其实不支持某些指令时贸然打开 NEON 会让编译出的库在运行阶段直接非法指令。 我有一个稳妥做法先在板内看 /proc/cpuinfo 的 Features 是否包含 neon再决定要不要开启。若包含在 CMake 参数里加上 NEON若不包含宁可什么也别动用通用编译参数。对于 AArch64 架构NEON 是默认启用且没有选择的余地再去手动加反而多余。 还要特别提醒一个坑不要在交叉编译环境里使用 -mcpunative它会把编译器所在的主机 CPU 特性当成目标 CPU 特性酿成的在板子上必崩无疑。目标板具体用什么 CPU就写什么 CPU比如明确指定 -mcpucortex-a7而不是依赖宿主机自动探测。4. 编译链接阶段最容易踩的坑依赖就绪、CMake 参数入湖进入真正 make 阶段好戏才刚开始。在 x86 上非常正常的命令到了 ARM 交叉编译环境却可能冒出一整套秀存在感的报错。这一节记下我自己遇到的三类真问题不看报错本身先看环境因素。4.1 编译崩溃不一定是内存不足我第一次在笔记本电脑上跑 OpenCV 全量交叉编译时顺手用了 make -j12结果编到中段 GCC 进程莫名被杀死终端输出一栏一栏的 Killed。当时下意识觉得是磁盘空间问题后来用 free -h 看到是内存耗尽大量并行编译进程占满内存系统开始把编译进程杀掉控制台却不报详细的编译错误。 交叉编译对主机内存的要求往往被低估尤其 OpenCV 这类 C 模板大户每个编译单元都可能让人内存达到一两百 MB。解围办法很简单先降到 make -j4如果还是被杀死就降 -j2同时确认 temp 分区剩余内存足够。后续我组包里特意保留自主板的多次 -j4极少再出问题。附加建议关闭其他大量占用内存的应用再用 make -j$(nproc) 直接胡来的情况必须忍住只在你确认内存充裕时才用。4.2 链接 error 时先检查文件属性而非看错信息提示错误大多数是 undefined reference to xxx或者 cannot find -lxxx。这俩时候第一反应是去补什么包那就容易误判。真正原因经常是它在系统根目录里找到的库文件其实是 x86 版本或者是两个不同架构的库被 CMake 串在一起。 我的排查顺序先定位报错里涉及的 .so 文件在哪个目录然后执行 file /路径/libxxx.so看输出的架构描述。它应该是类似 ELF 32-bit LSB shared object, ARM, EABI5 这样的结果。如果看到 x86-64就说明库是从主机带进去的交叉编译过程根本不该用。此时回到依赖库编译安装那一步重新在系统根目录里安装 ARM 版本。 第二个反复遇到的链接问题是某个依赖库确实编好了但 OpenCV 仍然说找不到。这通常是 CMAKE_FIND_ROOT_PATH 没设置或者设置了配置里少加了 ONLY 模式。系统根目录里明明有对应版本却找不到优先回看 3.1 节里的配置文件把查找模式的四个参数一个一个对齐。4.3 运行时非法指令常见诱因编译链接顺利通过并不代表万事大吉。复制到板子上一分钟跑程序直接崩掉后台常常给出 Illegal instruction (core dumped)这就是典型的编译时启用了目标板不支持指令的表现。我经历过一次狼狈。某块核芯其实支持 Cortex-A7但我在编译参数里写了更适合更新处理器的 NEON 变体运行时直接打到非法指令上。 这类问题的排查逻辑是反向的先确认编译时开过哪些指令优化参数再核对板子 /proc/cpuinfo 里的 Features。如果编译时启用 NEON但板子的 Features 里连 neon 都没有那么程序大概率就是这样崩去掉相关参数重编即可。另一个更省事的办法在板子上执行 readelf -A 可执行文件名查看 Tag_CPU_arch 和 Tag_NEON_preserved 之类的标记对比板子的实际能力你很快就知道自己编出了什么水平的产物。 有时候非法指令与可用变量不够有关系比如低版本内核配高版本编译库但这很少见。先从指令集、从工具链和板子的匹配度排查是永远用在最前面的方向。5. 部署到 ARM 板上的验证方法等 make 完成你的构建目录里会生成一批 .so 和可执行程序。不要急着把整个目录复制到板子上更好的思路是设定一个清晰的目标目录结构把需要的东西统一收集起来再同步过去。这一节讲讲安装阶段和上板验证的细节。5.1 两个安装路径的区别在 CMake 配置里有两个容易弄混的概念CMAKE_INSTALL_PREFIX 决定安装到哪个前缀目录DESTDIR 是临时把一个安装过程重定向到指定根目录的手段。 我的习惯是先指定 CMAKE_INSTALL_PREFIX/usr/local/opencv-arm然后在 make 完成后的安装阶段使用临时目录make install DESTDIR/tmp/opencv-arm-target/这会在 /tmp/opencv-arm-target/usr/local/opencv-arm 下生成完整的头文件与动态库。然后我只需要把 /tmp/opencv-arm-target/ 里的内容按目录结构拷贝到板子根目录再在板子上把 /usr/local/opencv-arm/lib 加入动态库搜索路径即可。 当初一开始用的是直接把安装前缀指向系统根目录 /usr后来发现这样极易污染宿主机的环境也不方便多版本管理。用 DESTDIR 的临时目录做法宿主机完全隔离板子端也随时可以删掉整个目录不需要重装系统验证。5.2 编译一个最小验证程序到了这一步最急迫的就是用一段足够简单、又能覆盖基础功能的程序验证 OpenCV 是否真正可用。我多次用来验证的就是图像读取加灰度转换#include stdio.h #include opencv2/opencv.hpp int main(int argc, char** argv) { if (argc 2) { printf(usage: %s image.jpg\n, argv[0]); return 1; } cv::Mat img cv::imread(argv[1]); if (img.empty()) { printf(imread failed\n); return 1; } cv::Mat gray; cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY); printf(image size: %d x %d\n, img.cols, img.rows); printf(gray pixel[0,0]: %d\n, gray.atuchar(0, 0)); return 0; }编译时直接指定 OpenCV 的头文件和库目录至少这种明确路径不依赖板子上的 pkg-config便于排查问题arm-linux-gnueabihf-g demo.cpp \ -I/tmp/opencv-arm-target/usr/local/opencv-arm/include/opencv4 \ -L/tmp/opencv-arm-target/usr/local/opencv-arm/lib \ -lopencv_core -lopencv_imgproc -lopencv_imgcodecs \ -o vision_demo把 vision_demo 与一张普通图片 copy 到板子执行 /vision_demo test.jpg。如果打印 output 正常说明最核心的 core、imgproc、imgcodecs 模块二次链接无误到这一步Arm 移植流程就基本跑通了。5.3 运行时依赖是否齐全怎么判断上板运行也常有看到 error while loading shared libraries: libopencv_core.so.408: cannot open shared object file 之类输出。这并不一定是 OpenCV 没安装到位而是系统动态链接器没找到库路径。 在板子上执行 ldd vision_demo会列出程序依赖的每个动态库及搜索到的位置。像 libopencv_core.so.408 或 libstdc.so.6 显示 not found就证明路径配置不完整。简单直接的方案是在板子上的 /etc/ld.so.conf 里加入一行 /usr/local/opencv-arm/lib再执行 ldconfig让系统重新扫描并建立链接缓存。 用 ldd 时顺带看一下 librt、libpthread 等依赖的指向如果某些基础库指向宿主机的 x86 版本抓取到板子上无意义需要回根文件系统里替换。判定是否标准首先要看目标板系统根目录里的同名库版本与工具链自带的 sysroot 是否同源如果版本不一致重新编译对应依赖库是稳妥路线。6. 围绕性能与二次开发的实际调优建议一旦最小 Demo 跑通剩下的问题就转到“怎么更快、更省地方”。OpenCV 在 ARM 上有不少可以挤压的余地但前提是要尊重板子本身的能力不要盲目跟风开优化选项。6.1 先测量再开启优化我有一次非常典型的经历在项目里听说 NEON 对图像处理加速效果明显直接全量 NEON 编译满心以为板子上的图像缩放性能会翻倍结果实测发现提升比预期低很多因为算法大部分时间卡在内存拷贝上甚至有些场景因随机访问反而更慢。 所以任何优化都要先有一个基准数据。在板子上执行一轮简单的计时比如对同一张 720x480 图片做 100 次缩放或滤波记录平均耗时。之后开启优化选项再跑同一套数据对比才有效。不要依赖直观感受图像缩放这类操作在 ARM 上用 NEON 确实能快不少但如果瓶颈在内存带宽盲目 NEON 不会带来质变。 并行方面也有同样的原则。TBB 或 OpenMP 都要产生线程管理成本若板子是二核四核且算法图像较小你需要考虑开并行带来的收益。我一般先关掉并行测试一次再打开并行对比一次确认真正快了才会保留配置。这一原则对嵌入式视觉相当有效。6.2 裁剪模块与控制动态库数量嵌入式产品发布时最关心可执行产物体积与依赖数量。完整版 OpenCV 动态库非常多安装目录动辄几百 MB 甚至更大这在医疗、工业级设备上可以接受但在存储紧张的板子上就遭罪了。我的建议根据项目实际算法逻辑精确设计模块剪纸凡是没用到的模块一律不要编进去。 BUILD_LIST 就是裁剪的有效入口。我自己的例子只留 core、imgproc、imgcodecs、videoio、highgui 五个模块后安装目录体积能下降到几百 MB 的四分之一左右。若后续不需要视频捕获把 videoio 去掉还能再少一截。DNN 如果不涉及神经网络推理坚决不纳入。 动态链接库太多也影响启动时间。产品最终形态可以把 OpenCV 与主程序一起静态链接生成单个可执行文件方便拷贝部署但开发阶段保持动态库方式更舒服出错时不必反复重编。我在验证阶段用动态库出产品时再默认调成静态是相对平衡的方案。6.3 值得固化的移植流程走过的弯路都折腾完后建议把所有步骤沉淀成脚本否则三个月后再升级版本又要重新踩一遍。参考下列一个极简脚本结构把主干留下来#!/bin/bash export ROOTFS/opt/arm-rootfs export TOOLCHAINarm-linux-gnueabihf export PKG_CONFIG_LIBDIR${ROOTFS}/usr/lib/pkgconfig build_dep() { cd $1 ./configure --prefix${ROOTFS}/usr --host${TOOLCHAIN} make -j4 make install } build_dep zlib build_dep libpng build_dep libjpeg # 其余依赖类似 cmake -S opencv -B build-arm \ -DCMAKE_TOOLCHAIN_FILEtoolchain.arm.cmake \ -DCMAKE_BUILD_TYPERelease \ -DBUILD_LISTcore,imgproc,imgcodecs,videoio,highgui \ -DWITH_V4LON -DWITH_GTKOFF cmake --build build-arm -j4 make install DESTDIR/tmp/opencv-arm-target再配合拉一页简单的记录写明各依赖库的版本号、CMake选项、测试结果以后在不同板子间复用摩目时会顺很多。我上一轮完全照着这套脚本从一个平台复刻到另一个相同架构的板子除了工具链路径略改几乎没有再花一个下午一起去调环境。如果你手头还有几块不同板子正等着部署可以把这个流程当作继续往下走的模板。
阅读完成 · 觉得有帮助?
咨询建站