在 Linux 上配置树莓派 4B OpenCV 交叉编译

2660 字
13 分钟
在 Linux 上配置树莓派 4B OpenCV 交叉编译

这两天大家都在火热地准备电赛,我也不例外。去年我使用的摄像头是 MaixCam Pro,编程语言我选择了 Python,使用了 MaixPyPython OpenCV 库。苦于性能瓶颈,今年我打算用 C++ 重写图像处理逻辑,目标平台是树莓派 4B

为什么需要交叉编译#

树莓派 4B 用的是 ARM Cortex-A72,编译 C++ 本来就慢,加上 OpenCV 的头文件和模板展开量巨大,哪怕是一个小项目也可能需要编译几十秒。开发过程中改一行代码就要重新编译,十分影响开发效率。交叉编译让我们在 x86 PC 上直接生成 ARM 二进制文件,编译速度能快不少。

整个流程大致是:

  1. 在树莓派上安装 OpenCV 的开发包
  2. 将树莓派的根文件系统同步到 PC,这样 PC 上的交叉编译器就能够使用树莓派 ARM 架构的 OpenCV 头文件和库
  3. 在 PC 上配置交叉编译工具链
  4. 使用 CMake + 交叉工具链编译你自己的项目
  5. 将编译好的二进制部署到树莓派

准备工作#

首先确保树莓派已开启 SSH,并能从你的 PC 连接。

SSH 到树莓派,安装 OpenCV 开发包:

Terminal window
sudo apt update
sudo apt install -y --no-install-recommends \
libopencv-dev

libopencv-dev 会自动拉入所有 OpenCV 模块以及它们的头文件和 .so 链接。后续 rsync sysroot 时,这些头文件和库会被一起同步到 PC,编译器就能找到它们了。

获取树莓派 sysroot#

sysroot 是交叉编译的核心。它让 PC 上的编译器能识别目标设备的头文件和库。我们需要将树莓派的整个根文件系统复制到 PC。

在 PC 上创建 sysroot 目录,然后使用 rsync 同步:

Terminal window
mkdir -p ~/rpi-sysroot
rsync -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 即可获得交叉编译工具链:

Terminal window
sudo dpkg --add-architecture arm64
sudo apt update
sudo 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-* 交叉工具链包,但存在几个棘手的问题:

  1. 工具链版本不一致,不同发行版提供的 binutils/gcc 版本可能与树莓派 sysroot 中的库 ABI 不兼容。
  2. 库路径布局差异,非 Debian 系的 aarch64 工具链默认搜索路径与 Debian 的 multiarch 布局 /usr/lib/aarch64-linux-gnu/ 不同,导致链接器找不到库。
  3. glibc 版本不匹配,Arch 等滚动发行版的 glibc 版本往往更新,链接到树莓派旧版 glibc 时可能出现符号未定义的问题。

使用容器是非常好的一种解决方案。在 Docker/Podman 容器中运行一个完整的 Debian 环境,将树莓派 sysroot 挂载进去,所有编译操作都在容器内完成。这样工具链和 sysroot 同属 Debian 生态,自然兼容。

容器方案详解#

我选择 debian:trixie 作为基础镜像(Trixie 是 Debian 13,也是当前的 testing 分支,包版本较新且稳定)。以下是我的 Dockerfile:

FROM debian:trixie
ENV DEBIAN_FRONTEND=noninteractive
ENV TARGET_TRIPLE=aarch64-linux-gnu
ENV 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}-ar
ENV PKG_CONFIG_SYSROOT_DIR=${SYSROOT_MOUNT}
ENV PKG_CONFIG_PATH=\
${SYSROOT_MOUNT}/usr/lib/${TARGET_TRIPLE}/pkgconfig:\
${SYSROOT_MOUNT}/usr/share/pkgconfig
WORKDIR /workspace
ENTRYPOINT ["/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 的依赖库至关重要。

构建和运行容器#

Terminal window
# 构建镜像
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.1libblas.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 ONLYINCLUDE ONLYPACKAGE ONLY 表示库、头文件、包都只从 sysroot 中查找,避免污染宿主机的头文件

编译#

Terminal window
# 在容器内执行
cd /workspace
mkdir build && cd build
cmake -GNinja \
-DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release \
..
ninja -j$(nproc)

编译完成后,build/ 目录下就是可以在树莓派上直接运行的 ARM64 二进制文件。用 file 命令确认一下:

Terminal window
file my_app
# my_app: ELF 64-bit LSB executable, ARM aarch64, dynamically linked, ...

部署到树莓派#

编译产物只是一个可执行文件,直接 scp 过去就行:

Terminal window
scp build/my_app pi@<树莓派IP>:~/my_app

然后在树莓派上运行:

Terminal window
ssh pi@<树莓派IP> ./my_app

如果程序跑起来输出了预期结果,恭喜你,交叉编译成功了!

总结#

整个方案的核心思路是统一编译环境:让交叉工具链和 sysroot 来自同一个生态,避免跨发行版的 ABI 不兼容。对于使用 Fedora、Arch 等非 Debian 系发行版的开发者,Docker 容器以极低的额外开销解决了这个问题。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

在 Linux 上配置树莓派 4B OpenCV 交叉编译
https://blog.blackcyan.top/posts/rpi-cross-compile-opencv/
作者
墨青BlackCyan
发布于
2026-07-18
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
墨青BlackCyan
三点几了,饮茶先啦
公告
欢迎来到墨青的博客!
音乐
封面

音乐

暂未播放

0:000:00
暂无歌词
分类
标签
站点统计
文章
12
分类
2
标签
25
总字数
7,351
运行时长
0
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.13.10
文章许可
CC BY-NC-SA 4.0