手上有块 ARM 板子,rootfs 分区只剩不到 10MB 可用空间,产品经理还要求把摄像头抓拍、缩放、转灰度这套流程放到设备上跑。翻出之前编译的 OpenCV 动态库一看,libopencv_world.so十九兆多,光libopencv_core.so就五兆出头,塞进去直接爆分区。于是就有了这次"裁剪 opencv 库到 2Mb"的折腾。
先把结论放前面:2MB 这个数字,绝大多数情况下指的不是某一个 .so 文件的体积,而是最终可执行文件里 OpenCV 实际占用的那部分。如果你非要一个 2MB 的libopencv_core.so还要带 dnn 和 gapi,那基本是做不到的;但如果你只留core+imgproc+imgcodecs三件套,配合编译期和链接期的一系列取舍,把 OpenCV 在成品里的净占用压到 2MB 以内,是完全可复现的事情。下面这套流程我在 ARMv7 和 x86_64 上都跑过,尺寸数字会随版本和工具链浮动,但思路和开关是一样好使的。
1. 先把体积账本翻开再动手
很多人裁 OpenCV 的第一反应是去翻 CMake 选项列表,看到一个WITH_XXX就关一个,关完发现体积只掉了几百 KB。原因是 OpenCV 的体积分布非常不均衡,大头集中在少数几个地方,你没砍到点子上,关一百个边角开关也没用。
1.1 用 size 和 nm 找出真正的"胖子"
默认构建完成后,先别急着改配置,先量一遍。对静态库可以直接看归档里每个目标文件的大小:
# 按体积排序,看清楚谁占地方 size -A lib/libopencv_core.a | sort -k2 -nr | head -30 # 按符号大小排序,定位具体的函数 nm --print-size --size-sort --radix=d lib/libopencv_core.a | tail -40如果是动态库,bloaty更好用,它能直接告诉你每个.o贡献了多少字节:
bloaty -d compileunits,symbols libopencv_imgproc.so我在 4.x 版本上量下来的典型分布是这样:imgproc的滤波和几何变换占了很大一块,core里的矩阵运算和并行框架占一块,真正让人意外的是指令集分派代码和 IPP 的静态库——这两个加起来经常能占整个库里三分之一以上的体积,而且它们藏在编译选项里,不翻构建日志根本看不见。
1.2 三个层级要分开算账
把 OpenCV 的体积拆成三层看,后面每一步操作都能对号入座:
| 层级 | 典型占比 | 主要手段 |
|---|---|---|
| 源码模块层 | 40% 到 60% | BUILD_LIST只保留必要模块 |
| 三方依赖层 | 20% 到 40% | WITH_IPP、WITH_FFMPEG、各图像编解码器 |
| 编译分派层 | 15% 到 35% | CPU_BASELINE、CPU_DISPATCH、编译优化等级 |
只看第一层是新手最常见的误区。我见过有人把模块从十几个砍到三个,库从 19MB 掉到 11MB,然后卡住了,觉得"再怎么砍也到不了 2MB"。其实把第二层的 IPP 关掉、第三层的 dispatch 关掉,剩下的 11MB 能直接掉到 4MB 左右。
1.3 明确你的 2MB 到底是哪个 2MB
这一点必须先跟需求方对齐,否则后面全是无用功。三种常见口径差别巨大:
- 静态库总和的 2MB:
libopencv_core.a+libopencv_imgproc.a+libopencv_imgcodecs.a三个归档文件的体积之和。这个目标偏紧,需要动 dispatch 和 LTO。 - 动态库的 2MB:单个
.so文件 2MB。如果只保留 core,勉强能做;带上 imgproc 就很吃力。 - 最终可执行文件里 OpenCV 的净占用:这是最合理也最常被实际使用的口径。因为静态链接加
--gc-sections之后,你代码里没调用的那 90% 函数根本不会被链进去,最终算下来常常比库文件小一个数量级。
提示:谈指标的时候一定要说清楚是"库文件体积"还是"链接后占用",这两个数字在实际项目里能差三四倍,先确认再开工,能省掉大量返工。
2. WITH 系列开关里,哪些是真省空间
cmake -L能列出几百个选项,但真正影响体积的没几个。我按实测的收益从高到低排一遍,你可以直接照着优先级砍。
2.1 IPP 是第一个必须关掉的东西
WITH_IPP默认是开的,构建时 CMake 会去下载一个叫ippicv的预编译静态库,然后静态链进libopencv_core。这个东西在 x86 上动辄二三十兆,是整份代码里最大的一块外部依赖。对体积敏感的场景没有任何犹豫余地:
-DWITH_IPP=OFF -DBUILD_IPP_IW=OFF代价是部分滤波和矩阵运算会走 OpenCV 自己的通用实现,性能可能有 10% 到 30% 的下降。但在嵌入式设备上,你的瓶颈通常在内存带宽而不是指令吞吐,这点损失基本感知不到。如果你的场景是 x86 服务器上跑高并发图像处理,那要另算账。
2.2 视频和 GUI 后端:嵌入式场景一律关
这类开关的特点是——只要你不用,关掉就是纯赚:
| 选项 | 作用 | 关掉的理由 |
|---|---|---|
WITH_FFMPEG | 视频编解码 | 拉进来一堆 codec,只做单帧抓拍完全不需要 |
WITH_GSTREAMER | 流媒体管道 | 同上,还会拖进 GStreamer 的头文件依赖 |
WITH_V4L | Linux 摄像头采集 | 如果你的相机走自己的驱动接口,用不上 |
WITH_GTK/WITH_QT | 桌面 GUI | 无显示设备,imshow本来也用不了 |
WITH_WIN32UI | Windows GUI | 交叉编译时无意义 |
WITH_OPENCL | GPU 加速接口 | 无 GPU,且运行时会加载内核源码字符串 |
WITH_OPENCL值得单独说一句。它不只是关掉一个调用接口,OpenCV 会把一堆 OpenCL kernel 源码以字符串形式编译进库里,运行时才去编译。关掉之后core能小几百 KB 到 1MB 不等,具体看版本。
2.3 并行框架:省得不多但值得关
WITH_TBB、WITH_OPENMP、WITH_PTHREADS_PF这三个是并行后端。TBB 会引入外部依赖并且体积可观,OpenMP 依赖编译器运行时,WITH_PTHREADS_PF是 OpenCV 自己基于 pthread 的实现,藏在core里。
-DWITH_TBB=OFF -DWITH_OPENMP=OFF -DWITH_PTHREADS_PF=OFF关掉WITH_PTHREADS_PF之后cv::parallel_for_会退化成单线程串行执行。对单核嵌入式芯片来说这本来就是事实,对多核芯片则要看你的处理流水线是不是真的靠parallel_for_撑性能。我的建议是:先关掉把体积压下来,如果实测发现某个 resize 慢得离谱,再单独把WITH_PTHREADS_PF=ON打开,它带来的体积增量相对可控。
2.4 图像编解码器:留一个就够
WITH_JPEG、WITH_PNG、WITH_TIFF、WITH_WEBP、WITH_OPENEXR、WITH_JASPER这一串默认基本全开。每关一个都能省下几十到几百 KB,全关能省一两兆。但你的业务总要读图,所以策略是只留你真正用到的那一种:
-DWITH_JPEG=ON -DWITH_PNG=OFF -DWITH_TIFF=OFF \ -DWITH_WEBP=OFF -DWITH_OPENEXR=OFF -DWITH_JASPER=OFF另外还有个容易漏的WITH_PROTOBUF和WITH_QUIRC(二维码识别用的),如果不用 dnn 和二维码功能,一起关掉。WITH_PROTOBUF在带 dnn 的构建里会拖进一大坨,不带 dnn 时影响不大,但关掉总没坏处。
3. BUILD_LIST 精确到模块的取舍
BUILD_LIST是 OpenCV 4.x 提供的一个很好的机制,它让你直接列出想要的模块,CMake 会自动把依赖补齐。注意这里写的是不带opencv_前缀的模块名。
3.1 模块依赖链要心里有数
OpenCV 模块之间有明确的依赖关系,你写了上层模块,下层会被自动带进来:
imgcodecs(读写图片)依赖imgprocimgproc(滤波、几何变换、颜色空间转换)依赖corecore(cv::Mat、基础运算、内存管理)不依赖任何其他模块
所以如果你想保留imread、resize、cvtColor这套最常用的组合,最小集合就是:
-DBUILD_LIST=core,imgproc,imgcodecs三个模块各自负责什么,用一张表说清楚:
| 模块 | 提供的能力 | 能不能省 |
|---|---|---|
core | cv::Mat、逐像素运算、矩阵运算、FileStorage | 不能,一切的基础 |
imgproc | resize、cvtColor、threshold、filter2D、形态学 | 只要做图像变换就必须要 |
imgcodecs | imread、imwrite、编解码器封装 | 只处理裸内存数据时可以省 |
如果你的数据来源是相机直接给的 YUV/RGB 缓冲,全程在内存里处理,那imgcodecs可以不要,只留core,imgproc,能再省掉几百 KB 到 1MB。我当时为了支持本地 JPEG 抓拍落盘,把它留下了。
3.2 那些"看着没用其实拖着大尾巴"的模块
有几个模块特别容易在默认构建里偷偷占地方:
ts:测试支持模块。BUILD_TESTS=OFF之后它本身不会构建测试用例,但这个模块的骨架代码仍然可能被编译进去。显式加-DBUILD_opencv_ts=OFF更保险。gapi:图计算框架,在 4.x 里体积相当可观,还依赖一个叫ade的第三方库。除非你在做流水线式的图计算,否则一定关掉。dnn:深度学习推理。这是 4.x 里除了 IPP 之外最大的单体模块,还拖着 protobuf。不做推理就关。world:把全部模块打包成一个库,方便但和"裁剪"是相反的方向,保持-DBUILD_opencv_world=OFF。
gapi和dnn这两个尤其要注意,它们的体积在 4.x 各版本里增长得很快。你只写BUILD_LIST=core,imgproc,imgcodecs时它们不会被构建,但如果你的构建脚本是从别处抄来的、用的是逐个BUILD_opencv_xxx=OFF的写法,很容易漏掉一两个。用白名单(BUILD_LIST)比用黑名单(逐项 OFF)安全得多,这是我从一次线上事故里换来的教训——当时漏关了一个模块,固件多出六兆,产线刷机包直接超限。
3.3 顺带把测试和示例全关掉
这部分对库体积影响有限,但对构建时间和磁盘占用影响巨大,而且不关的话经常会出意外:
-DBUILD_TESTS=OFF \ -DBUILD_PERF_TESTS=OFF \ -DBUILD_EXAMPLES=OFF \ -DBUILD_opencv_apps=OFF \ -DBUILD_DOCS=OFF \ -DBUILD_JAVA=OFF \ -DBUILD_opencv_python2=OFF \ -DBUILD_opencv_python3=OFFBUILD_opencv_apps会生成一堆演示可执行文件,交叉编译环境下它们占的空间经常比库本身还大,务必关掉。Java 和 Python 绑定在嵌入式上基本用不上,关掉还能避免 CMake 去找 JDK 和 Python 头文件。
4. CPU_DISPATCH:藏得最深的那个体积黑洞
这一节是我认为最值得单独讲的部分,因为绝大多数裁剪教程都不会提它,但它往往是关掉 IPP 之后下一个能砍出大块体积的地方。
4.1 运行时分派机制是怎么吃体积的
OpenCV 为了保证同一份二进制能在不同年代的 CPU 上跑出性能,实现了一套运行时分派机制。编译时它会为SSE4.2、AVX、AVX2、AVX512这些指令集各生成一份函数实现,程序启动时检测当前 CPU 支持哪套指令,再把函数指针指向对应版本。
这套机制的代价是同一段算法被复制多份。imgproc里的滤波、resize、cvtColor这些热点函数,每一个都有好几个变体编译进库里。默认情况下CPU_DISPATCH会打开一串指令集,在 x86_64 上实测imgproc的体积能因为这部分翻倍。
处理方式很直接:
-DCPU_BASELINE=SSE2 \ -DCPU_DISPATCH=CPU_DISPATCH赋空值就是不分派,只编译一份基线实现。CPU_BASELINE指定基线指令集,x86_64 用SSE2(这是 x86_64 的架构最低要求,必然支持),ARMv7 用NEON,ARMv8 用NEON或NEON_FP16。
4.2 性能损失有多大
这是必须诚实面对的问题。在我测过的 ARMv7 平台上,把 dispatch 关掉、只保留 NEON 基线,cv::resize双线性插值在 640x480 到 320x240 这个量级上耗时增加不到 15%,cvtColor的 RGB 转灰度基本没变化。原因很简单——NEON 的向量化实现还在,被去掉的是那些针对更高指令集的额外变体。
x86 上的影响会大一些,因为 SSE2 到 AVX2 的差距确实存在。如果你跑的是服务器端的批量图像处理,我建议保留CPU_DISPATCH=SSE4_2,AVX2这两档,别全关。
4.3 交叉编译时的 baseline 陷阱
交叉编译 ARM 平台时要注意工具链的默认配置。有些 CMake 工具链文件里会写-DCPU_BASELINE=NEON,但如果你的目标芯片是 ARMv7-A 又不带 FPU 的版本(比如某些 Cortex-A5 配置),强行开 NEON 会在运行时直接触发非法指令。
安全做法是先确认目标 SoC 的-march和-mfpu参数,再决定 baseline:
# ARMv7-A 带 NEON -DCPU_BASELINE=NEON -DENABLE_NEON=ON # 没有 NEON 的 ARMv7 -DCPU_BASELINE= -DENABLE_NEON=OFF -DENABLE_VFPV3=ON还有一种更保守的玩法:-DCV_ENABLE_INTRINSICS=OFF把所有 SIMD 全部关掉,只留纯 C 实现。体积能再降一点,但性能会掉得很难看,我在实际项目里没用过,只在排查某次崩溃时临时验证过一次。
5. 编译期和链接期的最后一公里
模块砍完、依赖砍完、dispatch 砍完,库大概能到 3 到 5MB 这个区间。想再往下走,就得靠编译器和链接器的优化了。
5.1 优化等级和函数分段
CMAKE_BUILD_TYPE直接选MinSizeRel,它会用-Os而不是-O2。这一步通常能省 10% 到 20% 的体积。然后再手动加上函数级分段,为后续的链接期裁剪做准备:
-DCMAKE_C_FLAGS="-Os -ffunction-sections -fdata-sections" \ -DCMAKE_CXX_FLAGS="-Os -ffunction-sections -fdata-sections"-ffunction-sections让每个函数单独放在一个节里,-fdata-sections对数据同理。这两个参数本身会让库稍微变大(因为节变多了,对齐填充增加),但它们是--gc-sections能生效的前提。
顺带提一下-flto。链接时优化能把跨编译单元的调用内联掉,对小函数的体积收益明显,但 OpenCV 的代码规模下 LTO 链接会非常慢,而且在某些老版本 GCC 交叉工具链上会直接报错。我的建议是先把 LTO 放着,等其他手段都用完了再考虑,收益通常在 5% 到 10%。
注意:不要加
-fno-exceptions。OpenCV 内部大量使用cv::Exception做错误传递,虽然它有CV_TRY/CV_CATCH宏支持无异常模式,但代码路径覆盖得并不完整,实际构建出来运行时行为很难保证。我在一个项目里为了省那两百 KB 加过,结果imread读不存在的文件时直接 abort,排查了半天。
5.2 符号可见性
默认情况下,动态库会导出所有非 static 符号,.dynsym表本身就能占几百 KB。加-fvisibility=hidden让默认隐藏,OpenCV 自己的CV_EXPORTS宏会把需要导出的符号再显式放出来:
-DCMAKE_CXX_FLAGS="-Os -ffunction-sections -fdata-sections -fvisibility=hidden -fvisibility-inlines-hidden"静态库场景下这个参数主要影响的是链接期的符号合并效率,收益不算大;但如果你做的是动态库,这一步能省掉可观的符号表体积,还能顺带减少符号冲突风险。
5.3 gc-sections 才是最终体积的决定因素
如果你是静态链接到自己的程序里(强烈推荐这种方案),那真正决定最终体积的是链接命令:
-Wl,--gc-sections \ -Wl,--as-needed \ -Wl,-s--gc-sections会把没有被引用到的节全部丢掉。因为前面加了-ffunction-sections,粒度为单个函数,所以你在代码里没调用的 OpenCV 函数一个字节都不会进最终产物。这就是为什么我一直强调 2MB 应该按"最终可执行文件净占用"来算。
--as-needed确保不被使用的动态库依赖不会写进DT_NEEDED。-s在链接时就去掉符号表,和后面手动 strip 是一个效果,二选一即可。
6. 一份能直接跑的裁剪脚本
把上面所有开关拼起来,就是我在 ARM 项目里实际用的配置。你把工具链文件路径换成自己的就能用。
6.1 完整的 CMake 配置
cmake -S opencv-4.8.0 -B build-arm \ -DCMAKE_TOOLCHAIN_FILE=$PWD/arm-linux-gnueabihf.cmake \ -DCMAKE_BUILD_TYPE=MinSizeRel \ -DCMAKE_INSTALL_PREFIX=$PWD/out-arm \ \ -DBUILD_LIST=core,imgproc,imgcodecs \ -DBUILD_SHARED_LIBS=OFF \ -DBUILD_opencv_world=OFF \ \ -DBUILD_TESTS=OFF \ -DBUILD_PERF_TESTS=OFF \ -DBUILD_EXAMPLES=OFF \ -DBUILD_opencv_apps=OFF \ -DBUILD_opencv_ts=OFF \ -DBUILD_DOCS=OFF \ -DBUILD_JAVA=OFF \ -DBUILD_opencv_python2=OFF \ -DBUILD_opencv_python3=OFF \ \ -DWITH_IPP=OFF \ -DBUILD_IPP_IW=OFF \ -DWITH_ITT=OFF \ -DWITH_OPENCL=OFF \ -DWITH_OPENCLAMDBLAS=OFF \ -DWITH_OPENCLAMDFFT=OFF \ -DWITH_TBB=OFF \ -DWITH_OPENMP=OFF \ -DWITH_PTHREADS_PF=OFF \ -DWITH_EIGEN=OFF \ -DWITH_LAPACK=OFF \ -DWITH_CUDA=OFF \ -DWITH_FFMPEG=OFF \ -DWITH_GSTREAMER=OFF \ -DWITH_V4L=OFF \ -DWITH_GTK=OFF \ -DWITH_QT=OFF \ -DWITH_PROTOBUF=OFF \ -DWITH_QUIRC=OFF \ -DWITH_ADE=OFF \ \ -DWITH_JPEG=ON \ -DWITH_PNG=OFF \ -DWITH_TIFF=OFF \ -DWITH_WEBP=OFF \ -DWITH_OPENEXR=OFF \ -DWITH_JASPER=OFF \ -DBUILD_ZLIB=ON \ \ -DCPU_BASELINE=NEON \ -DCPU_DISPATCH= \ -DENABLE_NEON=ON \ \ -DCMAKE_C_FLAGS="-Os -ffunction-sections -fdata-sections -fvisibility=hidden" \ -DCMAKE_CXX_FLAGS="-Os -ffunction-sections -fdata-sections -fvisibility=hidden -fvisibility-inlines-hidden"配置完之后先别急着make -j,先看一眼 CMake 的输出摘要,确认To be built那一行列出的模块只有你想要的三个,以及IPP: NO、FFMPEG: NO、OpenCL: NO这些确实生效了。这一步花十秒钟,能避免编译四十分钟之后才发现某个开关没起作用。
6.2 构建和产物处理
cmake --build build-arm -j$(nproc) # 静态库只去调试信息,别用 --strip-unneeded arm-linux-gnueabihf-strip --strip-debug out-arm/lib/libopencv_core.a arm-linux-gnueabihf-strip --strip-debug out-arm/lib/libopencv_imgproc.a arm-linux-gnueabihf-strip --strip-debug out-arm/lib/libopencv_imgcodecs.a # 体积验收 du -h out-arm/lib/*.a这里要重点提醒静态库的 strip 参数。静态库必须用--strip-debug,不能用--strip-unneeded。归档文件里的重定位信息对链接器是必需的,--strip-unneeded在某些 binutils 版本上会把这类信息也一起去掉,链接时报一堆undefined reference,而且报错的符号看起来完全莫名其妙,很容易误判成自己代码的问题。动态库则可以放心用--strip-unneeded。
如果你需要保留调试能力,用分离符号的方式:
objcopy --only-keep-debug libopencv_core.a libopencv_core.a.debug objcopy --strip-debug libopencv_core.a objcopy --add-gnu-debuglink=libopencv_core.a.debug libopencv_core.a这样发布包只带精简版,出问题的时候把 debug 文件放到对应路径,gdb 就能恢复完整的调用栈。
6.3 验收:不只看体积,还要看能不能跑
体积压下来了不等于活儿干完了。写一个最小验证程序,把业务里真正会调用的函数都过一遍:
#include <opencv2/imgcodecs.hpp> #include <opencv2/imgproc.hpp> #include <cstdio> int main() { cv::Mat src = cv::imread("/tmp/test.jpg", cv::IMREAD_COLOR); if (src.empty()) { printf("imread failed\n"); return -1; } cv::Mat gray, small; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); cv::resize(gray, small, cv::Size(), 0.5, 0.5, cv::INTER_LINEAR); cv::threshold(small, small, 128, 255, cv::THRESH_BINARY); printf("ok: %dx%d\n", small.cols, small.rows); return 0; }用和产品一致的链接参数编译,然后看最终可执行文件里 OpenCV 贡献了多少:
arm-linux-gnueabihf-g++ -Os -ffunction-sections -fdata-sections \ verify.cpp -o verify \ -Iout-arm/include \ -Lout-arm/lib -lopencv_imgcodecs -lopencv_imgproc -lopencv_core \ -Wl,--gc-sections -Wl,--as-needed -Wl,-s size verify看text段的增长量,这才是你真正要汇报给项目的数字。我最近一次在 ARMv7 上的结果:三个静态库合计 3.6MB,上面这个验证程序链接出来总大小 1.8MB,减去空程序本身的一百多 KB,OpenCV 的净占用在 1.7MB 左右,目标达成。
7. 我踩过的几个坑
这一节写的都是实际发生过的、文档里不会告诉你的问题。
7.1 关掉 dispatch 之后某个函数直接崩
有一次在 ARMv8 平台上把CPU_BASELINE设成了NEON,结果cv::resize用INTER_AREA插值跑到一半触发 SIGILL。排查下来是某些内核函数在编译时选择了比 baseline 更高的实现路径,而运行时分派被关掉之后没有回退机制。解决办法是把CPU_BASELINE设成NEON_FP16或者干脆留空让 OpenCV 自己判断目标平台的默认值,同时确认工具链的-march参数和 baseline 是一致的。
这个坑的教训是:关掉 dispatch 之后,所有热点函数都要在真机上跑一遍,不能只在 x86 的模拟环境里验证。模拟器和真机的指令集支持情况经常不一致。
7.2 少了 codec 导致 imread 静默返回空
我第一次只留WITH_PNG=ON的时候,测试用的是 PNG 图片,一切正常。上线之后业务方传的是 JPG,imread返回空 Mat,而且不抛异常也不打日志。OpenCV 在找不到对应解码器时的行为就是这样,静默失败。
后来我做了一条硬性规定:裁剪构建的验收用例必须覆盖业务里出现的所有图片格式,哪怕只有一次例外。另外可以在程序初始化时加一段自检:
std::vector<uchar> buf; cv::imencode(".jpg", cv::Mat(4, 4, CV_8UC3), buf);如果业务要用 JPEG,这行代码在裁剪后的构建里能正常跑,说明编码器在。解码器可以用一个小尺寸的测试图提前验证。
7.3 BUILD_LIST 里模块名的写法
BUILD_LIST里写的是不带前缀的模块名,core,imgproc,imgcodecs,中间用逗号分隔且不能有空格。我见过有人写成opencv_core,opencv_imgproc,CMake 不报错,但结果是一个模块都没构建,库文件是空的,构建过程"成功"了。所以配置完一定要检查To be built那一行的输出。
7.4 别忽略磁盘和内存开销
裁剪之后构建时间会显著缩短,但编译imgproc时的内存占用依然不低。我之前在一台 4GB 内存的虚拟机上用-j8,直接被 OOM killer 干掉。安全做法是-j4或者用ninja配合负载限制:
cmake -G Ninja ... ninja -j4Ninja 在增量构建上的表现也比 Make 好不少,改一个开关重新编译的时候差别很明显。
8. 还有没有更极端的路可以走
如果你的目标不是 2MB 而是 500KB 甚至更小,上面的路就走到头了。还有两个方向可以考虑。
一个是直接找社区已经维护好的移动端裁剪分支。这类项目专门针对移动端做了深度裁剪,产物通常就是core+imgproc+imgcodecs三个静态库,体积控制在两兆上下,而且把很多平台适配的坑都填过了。用之前注意核对它的 OpenCV 基础版本和你的需求是否匹配,特别是 API 层面的差异。
另一个是彻底放弃完整库,只把你用到的算法摘出来。比如业务里只用resize+cvtColor+threshold,那完全可以去 OpenCV 源码里把这几个函数的实现抠出来,去掉模块封装和错误检查机制,自己维护一个小文件。这种做法体积能压到几十 KB,但代价是后面 OpenCV 升级、发现 bug、需要新功能的时候都得自己动手。我只在极端的 MCU 项目上这么干过,一般嵌入式 Linux 场景不值得。
我个人在实际操作中的体会是:裁剪的本质是"明确知道自己不需要什么",而不是"尽可能多关开关"。先把业务用到的 API 列清楚,再反推需要哪些模块、哪些编解码器、哪档指令集,剩下的全砍掉,这样出来的配置最干净也最好维护。从 19MB 砍到 2MB 以下这件事,我做过三四次,每次真正花时间的不是编译,而是前面那份"到底用了什么"的清单。清单列准了,剩下的就是照着上面这套开关填参数而已。