在 Linux 上配置树莓派 4B OpenCV 交叉编译
这两天大家都在火热地准备电赛,我也不例外。去年我使用的摄像头是 MaixCam Pro,编程语言我选择了 Python,使用了 MaixPy 和 Python OpenCV 库。苦于性能瓶颈,今年我打算用 C++ 重写图像处理逻辑,目标平台是树莓派 4B。
为什么需要交叉编译
树莓派 4B 用的是 ARM Cortex-A72,编译 C++ 本来就慢,加上 OpenCV 的头文件和模板展开量巨大,哪怕是一个小项目也可能需要编译几十秒。开发过程中改一行代码就要重新编译,十分影响开发效率。交叉编译让我们在 x86 PC 上直接生成 ARM 二进制文件,编译速度能快不少。
整个流程大致是:
- 在树莓派上安装 OpenCV 的开发包
- 将树莓派的根文件系统同步到 PC,这样 PC 上的交叉编译器就能够使用树莓派 ARM 架构的 OpenCV 头文件和库
- 在 PC 上配置交叉编译工具链
- 使用 CMake + 交叉工具链编译你自己的项目
- 将编译好的二进制部署到树莓派
准备工作
首先确保树莓派已开启 SSH,并能从你的 PC 连接。
SSH 到树莓派,安装 OpenCV 开发包:
sudo apt updatesudo apt install -y --no-install-recommends \ libopencv-devlibopencv-dev 会自动拉入所有 OpenCV 模块以及它们的头文件和 .so 链接。后续 rsync sysroot 时,这些头文件和库会被一起同步到 PC,编译器就能找到它们了。
获取树莓派 sysroot
sysroot 是交叉编译的核心。它让 PC 上的编译器能识别目标设备的头文件和库。我们需要将树莓派的整个根文件系统复制到 PC。
在 PC 上创建 sysroot 目录,然后使用 rsync 同步:
mkdir -p ~/rpi-sysrootrsync -avz --copy-unsafe-links \ --exclude=/proc --exclude=/sys --exclude=/dev \ --exclude=/tmp --exclude=/run --exclude=/mnt \ pi@<树莓派IP>:/ ~/rpi-sysroot/注意:将
<树莓派IP>替换为你的树莓派 IP 地址。--copy-unsafe-links会将绝对路径符号链接转换为相对路径的副本,避免链接指向宿主机路径。
配置宿主机交叉编译环境
这里出现了分水岭:你的 PC 用的是什么 Linux 发行版?
Debian 系(Debian、Ubuntu、Mint 等)
如果你用的是 Debian 系,事情很简单。Debian 的 multiarch 支持非常成熟,直接 apt install 即可获得交叉编译工具链:
sudo dpkg --add-architecture arm64sudo apt updatesudo apt install -y --no-install-recommends \ gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \ binutils-aarch64-linux-gnu然后配置 CMake 工具链文件,指定 sysroot 路径和交叉编译器即可开始编译。
非 Debian 系(Fedora、Arch、openSUSE 等)
如果你用的是 Fedora、Arch Linux、openSUSE 等非 Debian 系发行版,问题就复杂了。这些发行版虽然也提供 aarch64-linux-gnu-* 交叉工具链包,但存在几个棘手的问题:
- 工具链版本不一致,不同发行版提供的 binutils/gcc 版本可能与树莓派 sysroot 中的库 ABI 不兼容。
- 库路径布局差异,非 Debian 系的 aarch64 工具链默认搜索路径与 Debian 的 multiarch 布局
/usr/lib/aarch64-linux-gnu/不同,导致链接器找不到库。 - glibc 版本不匹配,Arch 等滚动发行版的 glibc 版本往往更新,链接到树莓派旧版 glibc 时可能出现符号未定义的问题。
使用容器是非常好的一种解决方案。在 Docker/Podman 容器中运行一个完整的 Debian 环境,将树莓派 sysroot 挂载进去,所有编译操作都在容器内完成。这样工具链和 sysroot 同属 Debian 生态,自然兼容。
容器方案详解
我选择 debian:trixie 作为基础镜像(Trixie 是 Debian 13,也是当前的 testing 分支,包版本较新且稳定)。以下是我的 Dockerfile:
FROM debian:trixie
ENV DEBIAN_FRONTEND=noninteractiveENV TARGET_TRIPLE=aarch64-linux-gnuENV TOOLCHAIN_PREFIX=/usr/${TARGET_TRIPLE}ENV SYSROOT_MOUNT=/sysroot
RUN apt update && apt install -y --no-install-recommends \ build-essential \ gcc-${TARGET_TRIPLE} g++-${TARGET_TRIPLE} binutils-${TARGET_TRIPLE} \ cmake ninja-build make git rsync wget pkg-config dpkg-dev \ && rm -rf /var/lib/apt/lists/*
RUN echo '#!/bin/bash' > /entrypoint.sh \ && echo 'mkdir -p ${TOOLCHAIN_PREFIX}/{include,lib}' >> /entrypoint.sh \ && echo 'rm -rf ${TOOLCHAIN_PREFIX}/include ${TOOLCHAIN_PREFIX}/lib/*' >> /entrypoint.sh \ && echo 'ln -sf ${SYSROOT_MOUNT}/usr/include ${TOOLCHAIN_PREFIX}/include' >> /entrypoint.sh \ && echo 'ln -sf ${SYSROOT_MOUNT}/usr/lib/${TARGET_TRIPLE} ${TOOLCHAIN_PREFIX}/lib' >> /entrypoint.sh \ && echo 'exec "$@"' >> /entrypoint.sh \ && chmod +x /entrypoint.sh
ENV CC=${TARGET_TRIPLE}-gcc --sysroot=${SYSROOT_MOUNT}ENV CXX=${TARGET_TRIPLE}-g++ --sysroot=${SYSROOT_MOUNT}ENV AR=${TARGET_TRIPLE}-arENV PKG_CONFIG_SYSROOT_DIR=${SYSROOT_MOUNT}ENV PKG_CONFIG_PATH=\${SYSROOT_MOUNT}/usr/lib/${TARGET_TRIPLE}/pkgconfig:\${SYSROOT_MOUNT}/usr/share/pkgconfig
WORKDIR /workspaceENTRYPOINT ["/entrypoint.sh"]各部分说明
ENV 环境变量定义了交叉编译的目标三元组 aarch64-linux-gnu,以及工具链和 sysroot 的挂载路径。将这些路径抽取为变量,方便使用者替换为自己需要的版本。
apt 安装的软件包分为几类:
build-essential:编译基础设施gcc-aarch64-linux-gnu:ARM64 交叉编译器、汇编器和链接器cmake ninja-build:Ninja 构建系统rsync:从树莓派同步文件pkg-config dpkg-dev:用于查找依赖库和生成正确的 pkg-config 路径
entrypoint.sh是容器启动时自动执行的脚本。它的任务是建立符号链接,让交叉工具链的默认搜索路径指向我们挂载的树莓派 sysroot:
- 将
${SYSROOT_MOUNT}/usr/include链接为工具链的 include 目录 - 将
${SYSROOT_MOUNT}/usr/lib/${TARGET_TRIPLE}链接为工具链的 lib 目录
这样当你使用 #include <opencv2/core.hpp> 时,编译器实际上会从树莓派的 sysroot 中寻找这个头文件。
CC/CXX 环境变量指定了交叉编译器,并通过 --sysroot 参数将 sysroot 设置为编译的根目录。GCC 的 --sysroot 选项会改变所有标准搜索路径(/usr/include、/usr/lib 等)的根前缀,效果等同于将 sysroot 模拟为 /。
PKG_CONFIG_SYSROOT_DIR 告诉 pkg-config 在 sysroot 中查找 .pc 文件,而 PKG_CONFIG_PATH 指定了 .pc 文件的具体搜索路径。这对于 CMake 找到 OpenCV 的依赖库至关重要。
构建和运行容器
# 构建镜像docker build -t rpi-cross-compiler .
# 运行容器,将 sysroot 挂载进去docker run --rm -it \ -v ~/rpi-sysroot:/sysroot:ro \ -v $(pwd):/workspace \ rpi-cross-compiler \ bash说明:
/sysroot以只读:ro方式挂载,防止编译过程意外修改树莓派系统文件。工作目录/workspace以读写方式挂载当前目录,用于存放源码和编译输出。
Sysroot 预处理:为什么需要这一步
此前我在尝试交叉编译时,发现从树莓派 rsync 下来的 sysroot 不能直接使用,需要经过预处理。
1. 修复 merged-usr 目录结构
Debian 从 Buster 开始采用了 merged-usr 方案(即 /bin、/sbin、/lib 统一迁移到 /usr/bin、/usr/sbin、/usr/lib 下,原路径保留为符号链接)。树莓派的 Raspberry Pi OS 基于 Debian,自然也遵循此布局。
但 rsync 在复制符号链接时,如果目标路径的解析出现问题(例如 --copy-unsafe-links 的处理时机),可能导致 sysroot 根目录下的 /include 或 /lib 符号链接变成空目录或指向错误位置。编译器查找头文件时,会期望 /sysroot/usr/include 是真实目录。如果 rsync 产物的结构不对,预处理脚本会将其修正为:
/sysroot/├── include -> usr/include # 符号链接├── lib -> usr/lib # 符号链接└── usr/ ├── include/ # 真实头文件目录 └── lib/ └── aarch64-linux-gnu/ # multiarch 库目录2. 修复 C++ 标准库的多架构符号链接
GCC 的 C++ 标准库头文件按架构组织,结构如下:
/usr/include/c++/14/├── <平台无关头文件>└── aarch64-linux-gnu/ # 平台相关头文件 → 链接到 ../../aarch64-linux-gnu/c++/14在树莓派上,/usr/include/c++/14/aarch64-linux-gnu/ 应该是一个符号链接,指向 /usr/include/aarch64-linux-gnu/c++/14/,即 Debian multiarch 的 C++ 头文件存放位置。
这个符号链接使用的是绝对路径。当你用 rsync 拉取 sysroot 时,如果链接目标没有被完整保留,或者链接断掉了,编译 C++ 代码时就会找不到 <bits/c++config.h> 等平台相关头文件,报错:
fatal error: bits/c++config.h: No such file or directory预处理会检查这个链接,如果断裂就重新创建。
我就是在这一步踩了坑。如果树莓派上没有
/usr/include/c++/14/aarch64-linux-gnu也没关系,预处理会自动创建链接的。
3. 修复 alternatives 机制的断裂符号链接
Debian 使用 update-alternatives 系统来管理同一软件的多个实现。BLAS 和 LAPACK(线性代数库,OpenCV 的核心依赖)就是典型例子:
/usr/lib/aarch64-linux-gnu/├── libblas.so.3 -> blas/libblas.so.3.12.1 # alternatives 管理的链接├── libblas.so -> blas/libblas.so # 开发用的 .so 链接├── liblapack.so.3 -> lapack/liblapack.so.3.12.1└── liblapack.so -> lapack/liblapack.so这些符号链接指向 blas/ 和 lapack/ 子目录下的实际库文件。但 rsync 同步时,这些相对路径链接可能因为 rsync 的 --copy-unsafe-links 选项或者文件系统差异而断裂或变成悬空链接。
后果就是链接阶段 CMake 找不到 -lblas 和 -llapack,链接器直接报 cannot find -lblas。
预处理脚本遍历 sysroot 中的库目录,检查这些关键的 libblas.* 和 liblapack.* 链接,如果断裂就手动修复,重新指向正确的目标文件。虽然逻辑看起来是重复的 case 分支,但每个链接的目标版本号可能不同(liblapack.so.3.12.1、libblas.so.3.12.1),必须逐一处理。
编译你自己的项目
进入挂载了 sysroot 的容器后,就可以编译你自己的 C++ 项目了。
项目的 CMakeLists.txt
跨平台编译的第一步是把 CMakeLists.txt 写对。下面是一个典型的 OpenCV 项目配置:
cmake_minimum_required(VERSION 3.16)project(MyCVProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(OpenCV REQUIRED)
add_executable(my_app main.cpp)target_link_libraries(my_app PRIVATE ${OpenCV_LIBS})find_package(OpenCV REQUIRED) 会去 sysroot 里搜索 OpenCV 的 CMake 配置文件(OpenCVConfig.cmake),找到后自动设置好 include 路径和链接库。
交叉编译工具链文件
CMake 靠工具链文件来知道你要交叉编译。创建一个 toolchain.cmake:
set(CMAKE_SYSTEM_NAME Linux)set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_SYSROOT /sysroot)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
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)配置说明:
CMAKE_SYSROOT:告诉 CMake 所有搜索都以此为根CMAKE_C_COMPILER/CMAKE_CXX_COMPILER:指定交叉编译器。由于容器的 ENV 已经设了CC/CXX(带--sysroot),如果你想省略这两个变量,CMake 会自动从环境变量中获取CMAKE_FIND_ROOT_PATH_MODE:控制find_*命令的查找策略。PROGRAM NEVER表示不从 sysroot 中查找程序,而LIBRARY ONLY、INCLUDE ONLY、PACKAGE ONLY表示库、头文件、包都只从 sysroot 中查找,避免污染宿主机的头文件
编译
# 在容器内执行cd /workspacemkdir build && cd build
cmake -GNinja \ -DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake \ -DCMAKE_BUILD_TYPE=Release \ ..
ninja -j$(nproc)编译完成后,build/ 目录下就是可以在树莓派上直接运行的 ARM64 二进制文件。用 file 命令确认一下:
file my_app# my_app: ELF 64-bit LSB executable, ARM aarch64, dynamically linked, ...部署到树莓派
编译产物只是一个可执行文件,直接 scp 过去就行:
scp build/my_app pi@<树莓派IP>:~/my_app然后在树莓派上运行:
ssh pi@<树莓派IP> ./my_app如果程序跑起来输出了预期结果,恭喜你,交叉编译成功了!
总结
整个方案的核心思路是统一编译环境:让交叉工具链和 sysroot 来自同一个生态,避免跨发行版的 ABI 不兼容。对于使用 Fedora、Arch 等非 Debian 系发行版的开发者,Docker 容器以极低的额外开销解决了这个问题。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!



湘公网安备43130202000333号