
1. 为什么要裁剪 OpenCV 库到 2MB 以内做嵌入式视觉、边缘盒子、Android 端图像处理甚至是某些桌面小工具分发的时候绕不开一个很现实的问题opencv官方预编译包太大了。一个 Release 版、已经 strip 过的libopencv_world.so随随便便就是 40MB 到 60MB 的量级如果没关调试信息200MB 以上也不稀奇。这个体积丢进一个 Flash 只有 8MB 或者 16MB 的板子上连塞都塞不进去更别说还要留空间给模型、配置文件和业务代码。我最早碰到这个需求是给一款工业扫码设备做固件。设备用的是 Cortex-A7rootfs 分区一共就给了 32MB还要装 Python 解释器加一堆脚本。当时用官方包直接放进去编译出来镜像直接爆分区好几天都在做减法。后来把opencv从四十多兆砍到 1.8MB业务侧只用了imgproc里的十几来个函数整机跑得比我预想的还稳。这篇文章就把整个裁剪过程完整拆开讲从为什么大、砍哪里、怎么砍到砍完之后常见的坑全部落到位。适合看这篇的人大概是这么几类一是做嵌入式 Linux 或者 RTOS 方案的要给 rootfs 省空间二是做 Android / 小程序 / 桌面工具被安装包体积卡住上架标准的三是自己攒 SDK 做二次分发的不想把一个庞然大物塞给下游。基础要求不高会敲命令行、能编译 C 项目就行剩下的细节我会在每一步解释清楚。先说结论2MB 这个目标对只用到 core imgproc imgcodecs 的核心子集是完全可以做到的前提是你真的敢关依赖、敢开链接器垃圾回收、敢对符号动手。关键在于弄清楚每一层能省多少别把力气花错地方。1.1 完整版 OpenCV 的体积到底花在哪很多人第一反应是代码太多了其实代码本身没那么恐怖。以 OpenCV 4.x 为例源码编译出来的.so去掉调试信息之后的机器码部分如果只算core也就一两兆。真正把体积撑起来的主要是三块用不到的模块、被静态链进来的第三方库、以及调试符号和符号表。模块这块OpenCV 官方默认构建的模块有二十多个dnn、features2d、calib3d、video、stitching、objdetect、photo、ml全都在里面。dnn一个模块单独就可能是十几兆因为它是模板展开特别多的推理前端。calib3d和stitching也不小虽然你大概率根本没用到。你如果只是做图像读写加基础的滤波、缩放、颜色转换那这些模块纯属白带。第三方依赖这块更隐蔽。OpenCV 默认会去拉libjpeg-turbo、libpng、libtiff、libwebp、openexr、protobuf、zlib还有ffmpeg的封装。这些库如果以静态方式被编进去几个兆是轻轻松松的libtiff加openexr就能贡献小十兆。而且很多依赖是我没主动用但它默认开的状态必须手动一个个关掉。最后是符号表和调试信息。用默认 CMake 配置编 ReleaseOpenCV 往往会保留一部分调试信息和完整的动态符号表。strip一下通常能瘦掉 30% 到 50%strip --strip-unneeded或者-s链接选项效果更狠。很多人一上来就折腾编译参数其实先strip一刀收益比想象中大得多。1.2 2MB 这条线的现实意义为什么盯住 2MB因为这个量级刚好能塞进大多数低配场景的余量里。比如一个 8MB 的 Flash刨去 bootloader、内核、文件系统用户区可能就剩下三四兆1.8MB 的库加几百 KB 的业务代码正好。再比如 Android 的 ABI 拆分单个 so 控制在 2MB 以内整包体积你能算得很清楚不会因为某个 native 库突然超标被打回。还有做 CI 产物的制品越大拉取越慢2MB 的库下载几秒就完了几十兆的库在弱网环境下能把流水线拖成狗。注意这里说的 2MB 是最终交付形态的体积也就是 strip 之后、动态链接库形态下的.so大小。静态库.a看着也小但实际链接进你的程序之后会被按需抽取最终体积不一定更小而且会和你的主程序耦合升级维护反而更麻烦。除非你有特殊理由建议还是产出精简过的动态库。不过得说清楚一个前提2MB 是功能子集版的体积不是完整功能版的体积。想保留dnn做推理、保留videoio读摄像头、保留features2d做特征匹配同时还要压到 2MB微雕的空间就很有限了。这时候得换思路比如按业务实际调用把符号白名单列出来做更极端的符号级裁剪但那已经属于另一个话题了。下面所有的实操都建立在我用不到这么多模块这个前提上。2. 动手前先做一次体积解剖裁之前先称重这是做减法的基本纪律。凭感觉关模块很容易漏掉依赖也容易误关掉自己其实在用的东西。我一般的做法是先把全量版编出来然后从三个维度拆模块维度、依赖维度、符号维度。2.1 用 nm 和 size 快速看模块占用最直接的办法是编一个全量版本然后用nm -C --defined-only --print-size把每个.so里定义了哪些符号、各占多少字节统计出来按大小排序。对于你关注的模块大致就能看出谁是大头。命令大概长这样# 统计某个 so 中按符号大小排序的前 30 项 nm -C --defined-only --print-size libopencv_world.so.4.5 \ | sort -k2 -r \ | head -30另一个更粗粒度但更快的办法是直接看各模块单独编出来的.so大小。CMake 里把BUILD_SHARED_LIBS打开、不启world模式编完在lib目录下一个个ls -lh就能看到。我实测过一组典型数值编译器 GCC 9-O2未 strip仅供参考模块未 strip 体积strip 后体积说明opencv_dnn约 18MB约 5.5MB模板展开多符号密集opencv_calib3d约 4.5MB约 1.4MB相机标定、三维重建opencv_features2d约 4MB约 1.2MB特征点检测匹配opencv_imgproc约 7MB约 2.1MB滤波、形态学、几何变换opencv_core约 6MB约 1.6MB基础数据结构、矩阵运算opencv_imgcodecs约 1.5MB约 0.5MB图像编解码入口opencv_videoio约 2MB约 0.7MB视频/摄像头输入输出从这张表能看出一个很关键的事情imgproc和core是砍不掉的底座加起来 strip 后还有 3.7MB 左右,想压到 2MB说明还得继续往下抠光靠模块裁剪不够必须上编译选项和链接优化。2.2 第三方依赖才是真正的隐形大头core看着 1.6MB但你真正把它链进主程序时可能变成 4MB多出来的全是第三方库。imgcodecs依赖libjpeg-turbolibpng之类这些库 OpenAI 官方在构建时会优先用系统自带的动态版本找不到就自己下载源码静态编进去。一旦静态编进去你产出的.so就会把它们的机器码全部吞进去。要确认到底吞了哪些可以用readelf -d看动态依赖用nm看有没有带第三方前缀的符号或者直接strings搜一下版权字符串# 看库的动态依赖确认哪些是外部链接、哪些被静态吞进来了 readelf -d libopencv_world.so | grep NEEDED # 搜一下第三方库版权字符串能确认哪些被静态编译进来了 strings libopencv_world.so | grep -i -E png|jpeg|tiff|webp我一般的目标是图片编解码只保留libjpeg-turbo和libpng其余libtiff、openexr、libwebp、protobuf、ffmpeg全部关掉。这一个动作往往能省下 8MB 到 15MB是性价比最高的一刀。2.3 别忽略符号表和调试信息全量版 OpenCV 的.so里.symtab、.debug_*这些段占的比例不小。Release 构建一般不会有完整的.debug_info但.symtab和大量未使用的弱符号、模板实例化还是会留着。strip一把能砍掉三到五成具体取决于你开了多少模板重的模块。特别是dnn这种符号表本身可能就是几兆。所以我的建议是先全量编一次strip一下再决定砍什么。别拿着没 strip 的 60MB 去规划 2MB 的目标那样你会得出绝对不可能的错误结论然后在编译参数上白费力气。3. 裁剪方案的分层设计思路裁 OpenCV 不是一招鲜是四层叠加。只做其中一层要么体积下不来要么功能缺斤少两。下面按收益从大到小排顺序就是我实际操作的顺序。3.1 模块裁剪BUILD_LIST 是最大的一刀OpenCV 的 CMake 提供了BUILD_LIST直接指定只构建哪些模块其他一律不编译。这是收益最大、风险最低的一步。用法很直白cmake -DBUILD_LISTcore,imgproc,imgcodecs ..这三个模块的关系是imgproc依赖coreimgcodecs依赖core和imgproc。你只写imgproc,imgcodecsCMake 会自动把core带进来不用手动补。但如果你写imgcodecs,imgproc顺序反了也没关系依赖解析是自动的这点可以放心。我那台工业扫码设备实际只用到灰度化、二值化、缩放、旋转和 JPEG 解码所以BUILD_LIST就锁在core,imgproc,imgcodecs。这一步做完模块从二十多个降到三个未 strip 的体积直接从六十多兆掉到十几兆strip 之后大概 4MB 出头。离 2MB 还有距离但地基已经打好了。提示BUILD_LIST里别写world。world模式是把所有模块打包成一个.so跟裁剪的目标完全相反。想省体积就别碰它。3.2 特性开关WITH_* 和 BUILD_* 的区别很多人分不清这两类参数踩坑就踩在这。简单说BUILD_opencv_xxxOFF是这个模块别编WITH_xxxOFF是这个外部依赖别启用BUILD_xxxOFF有时候是内部工具不编比如BUILD_TESTS、BUILD_PERF_TESTS、BUILD_EXAMPLES、BUILD_DOCS。最容易漏的是BUILD_TESTS和BUILD_PERF_TESTS默认是ON。它们不直接增大最终.so但会拖慢编译时间、引入 gtest 依赖还会让某些模块为了测试打开额外的接口。一律关掉。依赖开关要一个个手动关。我常用的清单是这一组参数建议值理由WITH_IPPOFF闭源加速库体积大嵌入式用不上WITH_OPENCLOFFGPU 加速无 GPU 场景纯负担WITH_TBBOFF多线程后端可用标准线程替代WITH_EIGENOFF只有部分算法用关了无影响WITH_LAPACKOFF线性代数后端基础图像处理用不到WITH_FFMPEGOFF不读视频/摄像头就不需要WITH_GSTREAMEROFF同上WITH_OPENEXROFFHDR 格式绝大多数业务用不到WITH_TIFFOFFTIFF 解码不用就关WITH_WEBPOFFWebP 解码不用就关WITH_JPEGON需要读 JPEG 就保留WITH_PNGON需要读 PNG 就保留WITH_PROTOBUFOFF只有 dnn 用模块裁掉后自动失效WITH_1394 / WITH_V4LOFF摄像头采集相关按需这一套关完第三方静态库进不去.so从 4MB 往 2.x MB 掉。注意WITH_JPEG和WITH_PNG我建议留着因为imread读取这两种格式太常见了把它们关了之后imread对 JPEG 直接返回空 mat那种排查起来很折磨人。如果确实只需要处理无压缩的原始位图再考虑关。3.3 编译与链接优化把死代码真的删掉到这一步体积已经接近 2.5MB 了但还没到 2MB。剩下的空间要靠编译器帮忙。核心手段有三个第一是优化等级用-Os而不是-O2。-Os优先优化体积对图像处理这种计算密集但分支不复杂的代码性能损失通常在 5% 以内体积能再小 10% 到 15%。如果你的业务对帧率敏感也可以混合比如core用-O2、其余用-Os但那样 CMake 配置要改略微麻烦一般不值当。第二是打开函数级和变量级分段再让链接器做垃圾回收-DCMAKE_C_FLAGS-Os -ffunction-sections -fdata-sections -fvisibilityhidden -DCMAKE_CXX_FLAGS-Os -ffunction-sections -fdata-sections -fvisibilityhidden -DCMAKE_SHARED_LINKER_FLAGS-Wl,--gc-sections -Wl,--as-needed-ffunction-sections -fdata-sections让每个函数/变量单独占一个段--gc-sections就能把从未被引用的段整段丢掉。-fvisibilityhidden把默认符号可见性设成隐藏导出符号表会大幅缩小还能避免符号冲突。--as-needed则阻止链接器把没用到的外部库真的拉进来。第三是链接完再strip而且要用--strip-unneeded它会把重定位不需要的符号也干掉比普通strip更彻底aarch64-linux-gnu-strip --strip-unneeded libopencv_*.so*三招叠加实测能从 2.5MB 左右掉到 1.8MB 到 2.2MB 区间。具体落在哪跟你用的编译器版本、目标架构关系很大交叉编译工具链版本越新通常越省。3.4 符号级手术最后的精细化操作如果前三层做完还是差一点可以考虑符号级裁剪。原理是用nm导出一份被引用的符号列表再用objcopy --keep-symbols或链接器版本脚本把没被引用的符号丢掉。这个操作风险偏高主要因为它会破坏库的 ABI只适合业务代码和库一起编译、一起发布的场景不适合做通用 SDK 分发。我的建议是业务代码里只引入了少数几个头文件那就用这种办法。比如你只用cv::resize、cv::cvtColor、cv::threshold和imread这几个函数那把它们的符号名导出来用版本脚本--version-script只导出这几个其余全隐藏甚至删除体积还能再压 100KB 到 300KB。对 2MB 的目标来说可能刚好够用但这是见效不错的一类补救手段。4. 从源码到 2MB 的完整实操前面讲思路这里把完整命令走一遍。我以交叉编译到 aarch64 为例本地 x86_64 的流程一样把工具链参数换成native就是。4.1 环境准备与源码获取首先确认工具链齐全cmake建议 3.16 以上交叉编译器用对应的 GNU 工具链。源码我从官方仓库拉指定版本的分支别用 master免得碰上未发布的行为变更# 拉取指定版本的源码 git clone --depth 1 --branch 4.5.5 https://github.com/opencv/opencv.git cd opencv mkdir build cd build提示不需要opencv_contrib就别拉。它里面全是扩展模块默认会被BUILD_LIST过滤掉但拉下来会占磁盘、拖慢 CMake 配置纯属浪费时间。4.2 CMake 参数逐条拆解这是整个流程的核心我按重要性分三组。第一组是模块范围-DBUILD_LISTcore,imgproc,imgcodecs -DBUILD_SHARED_LIBSON -BUILD_TESTSOFF -BUILD_PERF_TESTSOFF -BUILD_EXAMPLESOFF -BUILD_DOCSOFF -BUILD_opencv_appsOFF -BUILD_opencv_python3OFF -BUILD_JAVAOFF第二组是依赖开关把上表里的开关全加上这里不重复列了。第三组是编译和链接优化-DCMAKE_BUILD_TYPERelease -DCMAKE_C_FLAGS-Os -ffunction-sections -fdata-sections -fvisibilityhidden -DCMAKE_CXX_FLAGS-Os -ffunction-sections -fdata-sections -fvisibilityhidden -DCMAKE_SHARED_LINKER_FLAGS-Wl,--gc-sections -Wl,--as-needed -Wl,-s -DCMAKE_INSTALL_PREFIX/opt/opencv-mini -DENABLE_PRECOMPILED_HEADERSOFF -DENABLE_SSEOFF -DENABLE_AVXOFF -DENABLE_AVX2OFF有几个点值得单独说一下。-DENABLE_PRECOMPILED_HEADERSOFF是为了避免预编译头把一堆用不到的头文件实例化进去实测能省一点。-DENABLE_SSE系列在 x86 上默认开如果你编的是 x86 且确定不用关掉能省一点但 aarch64 上这些开关不存在写了也没关系CMake 会忽略。-Wl,-s直接在链接期做 strip省得你编译完再执行一遍。不过我一般还是会在安装后再手动strip一次因为 CMake 的-s有时对某些工具链行为不一致人工来一遍更保险。完整的配置命令拼出来大概是这样cmake .. \ -DCMAKE_TOOLCHAIN_FILE../platforms/linux/aarch64-gnu.toolchain.cmake \ -DBUILD_LISTcore,imgproc,imgcodecs \ -DBUILD_SHARED_LIBSON \ -DBUILD_TESTSOFF -DBUILD_PERF_TESTSOFF \ -DBUILD_EXAMPLESOFF -DBUILD_DOCSOFF \ -DBUILD_opencv_appsOFF -DBUILD_opencv_python3OFF -DBUILD_JAVAOFF \ -DWITH_IPPOFF -DWITH_OPENCLOFF -DWITH_TBBOFF \ -DWITH_EIGENOFF -DWITH_LAPACKOFF \ -DWITH_FFMPEGOFF -DWITH_GSTREAMEROFF -DWITH_OPENEXROFF \ -DWITH_TIFFOFF -DWITH_WEBPOFF \ -DWITH_JPEGON -DWITH_PNGON \ -DWITH_PROTOBUFOFF -DWITH_1394OFF -DWITH_V4LOFF \ -DCMAKE_BUILD_TYPERelease \ -DCMAKE_C_FLAGS-Os -ffunction-sections -fdata-sections -fvisibilityhidden \ -DCMAKE_CXX_FLAGS-Os -ffunction-sections -fdata-sections -fvisibilityhidden \ -DCMAKE_SHARED_LINKER_FLAGS-Wl,--gc-sections -Wl,--as-needed \ -DCMAKE_INSTALL_PREFIX/opt/opencv-mini配置完之后记得看一眼输出里的模块清单摘要确认真的只有三个模块被选中。如果看到dnn、calib3d还在列表里多半是BUILD_LIST拼写有问题或者用了 SQLite 缓存没清干净。4.3 编译、strip 与体积验证编译直接make -j$(nproc)模块少了之后时间很短一两分钟的事。装到前缀目录后开始 strip同时把不需要的文件删掉make -j$(nproc) make install cd /opt/opencv-mini/lib du -sh . # 先看整体 ls -lh libopencv_*.so* aarch64-linux-gnu-strip --strip-unneeded libopencv_*.so* du -sh . # strip 之后对比验证分两步。第一步是体积ls -lh看每个.so的实际大小加起来乘上你要用到的数量。第二步是功能写个最小验证程序调一下你业务真正用到的函数交叉编好丢到板子上跑一遍// 验证最小可用性只测你业务实际调用的函数 #include opencv2/core.hpp #include opencv2/imgproc.hpp #include opencv2/imgcodecs.hpp #include cstdio int main() { cv::Mat src cv::imread(test.jpg, cv::IMREAD_GRAYSCALE); if (src.empty()) { printf(imread failed\n); return 1; } cv::Mat dst; cv::resize(src, dst, cv::Size(320, 240)); cv::threshold(dst, dst, 128, 255, cv::THRESH_BINARY); printf(ok: %dx%d\n, dst.cols, dst.rows); return 0; }注意验证一定要用真实业务函数覆盖别只测resize。我踩过的一个坑是imread能读灰度但读不了彩色原因是WITH_JPEG开了但某个内部解码器被-fvisibilityhidden隐藏了导出符号。这种问题不跑真实路径根本发现不了。4.4 实测配置与体积对照为了让目标更直观我把几组配置的实际结果列出来。测试环境是 GCC 9.3 交叉编译 aarch64-Os加分段加gc-sections最后strip --strip-unneeded数值是多次编译取中间值供参考。配置corecoreimgproccoreimgprocimgcodecs全依赖 未 strip约 6.0MB约 13MB约 15MB关依赖 未 strip约 3.2MB约 6.8MB约 7.6MB关依赖 strip约 1.1MB约 2.4MB约 2.9MB再加分段与 gc约 0.85MB约 1.7MB约 2.1MB再做符号白名单约 0.7MB约 1.5MB约 1.9MB从表里能读出一个结论2MB 的达成主要靠的是关依赖 strip 分段 gc这三板斧符号白名单只是锦上添花。如果你一开始就上符号手术往往会先被功能莫名其妙没了折腾很久得不偿失。5. 踩坑记录与问题排查裁剪过程中出问题基本都是砍太狠或者缓存污染。我把遇到过的典型情况和处理方式整理出来能省不少时间。5.1 链接期报 undefined reference 怎么查这类错误最典型症状是链接你的主程序时报某个cv::xxx未定义。原因不外乎三种一是BUILD_LIST里漏了模块比如用了cv::imwrite却没加imgcodecs二是模块在但函数被-fvisibilityhidden隐藏了三是--gc-sections把一个被反射或间接调用的段误删了。排查顺序我一般是这样先用nm -D libopencv_core.so | grep 符号名确认符号在不在注意一定要加-D看动态符号表因为strip之后.symtab就没了。如果在说明是链接参数问题检查-Wl,--no-as-needed或者是不是被--as-needed提前丢掉了如果不在就是BUILD_LIST漏模块补上重编。还有一种少见但恶心的情况gc-sections把某个只在static初始化里注册的函数段丢了。这时候可以在出问题的模块上局部关掉gc-sections或者用--undefined符号名强制保留。-Wl,--whole-archive也能解决但它会把静态库全塞进去跟省体积直接冲突只适合临时调试。5.2 运行时报错和 imread 静默失败编译链接都过了板子上跑却出问题通常是依赖缺失或 ABI 不匹配。常见的两类一类是error while loading shared libraries用readelf -d看.so的NEEDED项确认它依赖的第三方库在目标文件系统里都存在。裁的时候WITH_JPEGON但实际用的是动态 libjpeg那目标机上就得有对应的libjpeg.so少了就跑不起来。另一类是imread返回空 Mat但也不报错。这种情况多半是编解码器没编进去或者格式不支持。比如WITH_JPEGOFF却去读 JPEG就会静默失败。排查方法是换一张无压缩的 BMP 试如果 BMP 能读、JPEG 不能就是解码器问题回头把对应的WITH_xx打开重编。提示OpenCV 的imread失败不抛异常、只返回空 Mat这是很常见的坑。建议在自己的封装里加一句if (mat.empty())判断并打日志否则线上跑起来只觉得图没出来很难定位。5.3 常见问题速查表现象可能原因处理方式链接报 undefined referenceBUILD_LIST 漏模块补模块后重编符号在 .so 里但链接不到--as-needed 提前丢弃加 --no-as-needed 或调整顺序运行时找不到 .so第三方动态依赖缺失readelf -d 查 NEEDED补齐目标机库imread 返回空编解码器未编译打开对应 WITH_xx 重编体积比预期大很多CMake 缓存没清删 build 目录重新配置某函数行为异常-Os 或 gc 误删局部关优化或加 --undefined编译时间极长BUILD_TESTS 未关显式关 BUILD_TESTS、BUILD_PERF_TESTS自编库版本冲突系统里已有同名 .so设置 RPATH 或用 LD_LIBRARY_PATH 指定路径踩坑这件事说到底就是记录 复现。我习惯在裁剪每一层之后都留一份体积快照和功能验证结果出问题至少知道是哪一层引入的。很多人省掉这一步最后体积确实到了 1.8MB但业务跑起来缺东少西回头重新返工的代价比当初慢一点大得多。6. 还能再小吗几个进阶方向如果 2MB 还不够业务又实在必须用 OpenCV 的某些接口那还有几条路可以走但每条路都有代价得权衡。第一条路是编译期模板实例化收敛。OpenCV 的很多算法是模板函数core里Mat::at、Mat_T这类模板会为多种类型实例化。如果你的业务只处理uint8和float可以通过定制编译参数减少实例化类型但这要改源码不是标准做法适合长期维护自己 fork 的团队。第二条路是只编静态库并按需抽取。把core、imgproc编成.a你的主程序链接时链接器只会抽取被引用的目标文件。如果只用到少量函数最终可执行文件的增量会远小于动态库。这个方案的代价是升级 OpenCV 要重新编译主程序耦合度高。适合固件这种编一次烧一次的场景。第三条路是换更轻的替代库。如果只是做缩放、灰度、二值化很多 C 语言的小库或者自己写几十行 SIMD 就够了根本用不上 OpenCV。选 OpenCV 往往是因为生态和接口习惯但如果体积是第一约束那该换就换没必要硬扛。第四条路是版本选择。不同大版本的体积差异很可观3.x 比 4.x 在很多模块上都要小4.x 又比 5.x 稳。如果你的 API 兼容用低一档的大版本可能直接省下几百 KB这是几乎零成本的优化。最后说个我自己常做的取舍不要为了 200KB 的极致而牺牲可维护性。我见过不少团队为了压到 1.5MB把 CMake 参数堆到几十行、自己改了源码、维护了一套自定义编译脚本结果下一个同事接手时完全看不懂升级一次 OpenCV 要折腾一周。体积是硬指标没错但要在满足指标和团队能维护之间找平衡。我的经验是先把业务真正用到的函数列一张清单按清单裁裁到刚好够用就停手留个 10% 到 20% 的余量给未来加需求比一步压死要稳得多。这套方法我用在好几个项目上最低压到过 1.6MB最高也就 2.3MB业务侧从没因为库体积返工过。