ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

显著性检测 sailencyFilter 编译实战:CMake 与 OpenCV 避坑指南

显著性检测 sailencyFilter 编译实战:CMake 与 OpenCV 避坑指南 简介面向显著图检测研究者的可编译源码包对应2012年CVPR论文《Saliency Filters: Contrast Based Filtering for Salient Region Detection》。作者修复了原始发布代码中的编译错误使用者只需搭建好OpenCV环境即可直接编译运行省去自行调试的麻烦。资源共17个文件含7个cpp源文件、6个h头文件、1个hpp头文件及3个txt说明压缩包仅37KB。源码结构围绕显著图生成展开permutohedral实现高效的边缘保持滤波saliency模块负责核心对比度计算superpixel提供超像素预处理test与evaluate程序可帮助验证结果。对希望快速复现经典显著性检测算法、或在其基础上做二次开发的读者来说这是一份轻量而完整的参考实现。目前已有536人学习下载适合具备OpenCV基础、正在研究显著性检测或图像滤波的开发者学习使用。 搞过视觉项目的人可能都有类似经历网上找到一个显著性检测的源码标题写得花团锦簇下载下来一看要么缺CMakeLists.txt要么OpenCV头文件路径对不上要么压根编译不过。我这次要说的sailencyFilter就是这么个项目——模块本身思路不复杂但要让它在自己的环境里顺利编译成可执行文件中间藏着不少讲究。这篇文章会把整个链路讲清楚它是什么、依赖什么、怎么组织源码、用什么姿势构建以及我在编译过程中真实踩过的几个坑。需要先说明一点sailencyFilter这个名字更常见的拼写是saliencyFilter指的就是显著性过滤器属于计算机视觉里显著性目标检测Salient Object Detection的一个基础模块。它的输入是一张普通图像输出是一张逐像素的显著性分值图分值越高的地方越吸引人眼注意。这类技术在智能截图、电商主图裁剪、视频浓缩、机器人避障预筛选等场景里用得很多。下面所有内容都基于一个我整理过的、可编译的C实现来展开。1.sailencyFilter到底是个什么模块——先弄清它解决什么问题1.1 显著性检测在视觉任务中的定位显著性检测简单说就是模拟人眼注意力机制从一幅图像里找出人最先关注到的区域。它和常见的目标检测不一样目标检测输出的是目标类别和矩形框而显著性检测输出的是逐像素的分值图Saliency Map分值越高表示该像素属于显著前景的可能性越大。在流水线里sailencyFilter通常作为预处理器存在。比如做图像裁剪时先算出显著性热力图然后只在显著区域周围寻找最佳构图做视频摘要时根据每帧的显著性分布判断关键帧做语义分割预处理时用显著性图把前景和背景粗分离减少后续网络计算量。和我实际接触过的几个项目相比它的定位介于传统算法和深度学习之间不依赖GPU单张CPU就能跑适合快速原型验证。1.2 为什么叫Filter而不是Detector叫Filter是有原因的。在数字图像处理里Filter强调的是一种逐像素的映射变换输入输出都是图像只是像素值被重新解释。sailencyFilter做的就是这件事输入一张三通道彩色图输出一张单通道灰度图每个像素的值不再表示亮度而表示该位置相对于整张图像的突出程度。这个设计有实际好处因为它输出的是图所以可以直接接阈值分割、连通域分析、形态学处理等后续操作不必为检测框的形状和数量提前做假设。和深度学习显著性检测模型相比它牺牲了一部分准确率但换来了编译简单、依赖少、推理快的优势在一块普通的嵌入式板子上也能跑得动。2. 编译前环境准备——工具链与依赖缺一不可2.1 我采用的编译环境这个项目不是纯标准库就能编译的它依赖OpenCV所以环境准备必须避开几个常见的坑。我实测通过的环境是操作系统Ubuntu 20.04 x64以及Windows 10 Visual Studio 2019编译器GCC 9.4Linux、MSVC v142Windows均开启C17标准OpenCV4.5.5Linux下通过apt安装的libopencv-devWindows下用的4.5.5预编译包构建工具CMake 3.16以上Linux下配合MakeWindows下配合Visual Studio生成器为什么把OpenCV版本单独强调因为这个模块的核心计算颜色空间转换、高斯模糊、矩阵归一化全都依赖OpenCV的数据结构和基础函数。不同版本的OpenCV在头文件组织上有差异比如3.x时代的opencv2/imgproc/imgproc.hpp到4.x就变成了opencv2/imgproc.hpp。如果代码按旧版本写新环境编译时很容易报找不到头文件。2.2 选老版本还是新版本这里给个实用建议不要追新。OpenCV 4.5.x是个比较稳妥的选择一方面它和C17兼容良好另一方面网上绝大多数的示例代码和问题解答都覆盖了这个版本。OpenCV 5.x如果在生产环境里用遇到问题可参考的资料相对少。同样OpenCV 3.x虽然也支持这个项目但部分API命名和4.x有差异比如cv::createTrackbar这类函数在4.x里就挪了位置。Windows用户还需要注意一点OpenCV预编译包里的opencv_world455.dll有release和debug两种版本对应的lib名分别是opencv_world455.lib和opencv_world455d.lib。链接时如果把这两个搞混了会出现一堆无法解析的外部符号这个问题我在第5节还会详细说。3. 源码结构与核心算法拆解——编译前先看懂再动手3.1 工程目录怎么组织一个可编译的源码工程至少要做到三件事有CMakeLists.txt、有明确的头文件包含路径、有清晰的源文件列表。这个项目的目录结构比一般的命令行脚本稍微复杂一点但比完整的大型应用简单很多sailencyFilter/ ├── CMakeLists.txt ├── README.md ├── include/ │ └── sailency_filter.h ├── src/ │ ├── main.cpp │ └── sailency_filter.cpp ├── samples/ │ └── input.jpg └── build/include目录放类的声明src目录放实现和程序入口samples放测试图build是编译输出目录。这样做的原因很直接把接口和实现分开编译时头文件搜索路径只需指向include源文件只需列出src下的.cpp模块边界清晰后续想把filter封装成动态库也很方便。3.2 核心类的接口设计头文件saliency_filter.h里我定义了一个非常精简的接口整个模块对外只暴露一个核心方法#pragma once #include opencv2/core.hpp class SailencyFilter { public: SailencyFilter(); ~SailencyFilter() default; // 输入分辨率输出单通道浮点显著性图值域[0, 1] cv::Mat filter(const cv::Mat src, double sigma 0.4); private: // 颜色量化把24位真彩图像压缩到更少的颜色桶 cv::Mat quantizeColor(const cv::Mat src, int binCount) const; // 构建颜色直方图并计算每种颜色的显著值 std::vectordouble computeColorSaliency(const cv::Mat quantized) const; // 根据颜色显著值回写每个像素得到最终显著性图 cv::Mat renderSaliencyMap(const cv::Mat quantized, const std::vectordouble colorSaliency) const; };接口设计上有个小细节filter函数返回值是cv::Mat类型是CV_32F值域在[0, 1]之间。为什么不直接返回一张8位图因为下游操作如阈值分割、自适应二值化往往需要更精确的浮点中间结果过早转成CV_8U会丢失精度。这个设计让过滤器更加通用调试起来也更方便。3.3 核心算法流程是啥这个项目的算法思路是典型的全局对比度Global Contrast方法和CVPR 2011年那篇经典的HCHistogram Contrast方法是一脉相承的。我来把它拆开说第一步颜色量化。一张普通图像的RGB颜色数量可能达到数百万种如果直接对每个像素两两比较颜色距离计算量是O(N^2)根本跑不动。所以先把每个通道从256级压缩到12级左右这样颜色总数从千万级别降到约1728种后面的统计才现实。代码里的quantizeColor函数就是干这个的。第二步统计颜色直方图。对于量化后的颜色统计每种颜色出现的像素个数。一种颜色出现的像素越多它在图像中的存在感越强。第三步计算每种颜色的显著值。这里用到一个关键思想一种颜色的显著值等于它和所有其他颜色之间的距离加权和。权重就是其他颜色出现的频率。为了加速还可以把颜色按照显著值做一次平滑消除量化带来的噪声。数学上颜色c的显著值公式可以写成S(c) Σ P(c_i) * D(c, c_i)其中P(c_i)是颜色c_i在整张图中出现的概率D(c, c_i)是颜色c和c_i在Lab颜色空间里的欧氏距离。为什么用Lab空间而不是RGB因为Lab空间里欧氏距离更接近人眼感知的颜色差异算出来的显著性更符合直觉。第四步回写像素值。量化后的每个像素都对应一种颜色而这种颜色已经算出了显著值直接把显著值赋值给像素就得到了一张浮点显著性图。4. 构建全过程从CMakeLists.txt到可执行文件4.1 CMakeLists.txt的编写要点CMakeLists.txt是这个项目能否一键编译的关键。很多源码编译不过就是因为它要么只有一个Makefile只适用于Linux要么根本没有构建脚本。我写了一个跨平台的CMake配置内容不多但每一行都有讲究cmake_minimum_required(VERSION 3.16) project(sailencyFilter LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(OpenCV REQUIRED COMPONENTS core imgproc) add_library(sailency_filter_lib STATIC src/sailency_filter.cpp ) target_include_directories(sailency_filter_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ) target_link_libraries(sailency_filter_lib PUBLIC ${OpenCV_LIBS} ) add_executable(sailencyFilter src/main.cpp ) target_link_libraries(sailencyFilter PRIVATE sailency_filter_lib )几个关键点find_package(OpenCV REQUIRED COMPONENTS core imgproc)指定只需要core和imgproc两个模块而不是把整个OpenCV都链接进来编译时间会短不少。有些机器上OpenCV还带了contrib模块没必要全拉进来。sailency_filter_lib单独编译成静态库这个设计是我特意保留的。把filter模块和主程序分开以后如果想把filter封装成Python接口或者其他语言的扩展直接复用这个静态库就行不用改主程序代码。CXX_STANDARD 17这个项目其实用C11就够但统一设成17是为了避免个别编译器默认标准太老导致std::filesystem或者其他现代特性不可用。当然了如果读者的代码里有结构化绑定那C17就是硬要求。4.2 Linux下的编译命令配置好CMakeLists.txt之后编译命令非常固定cd sailencyFilter mkdir -p build cd build cmake .. -DCMAKE_BUILD_TYPERelease make -j$(nproc)-j$(nproc)是让编译器用满所有CPU核心实测在这个小项目上4核机器大概十几秒就能编完。如果想编出调试版本把CMAKE_BUILD_TYPE换成Debug就行但注意Debug版本运行速度会慢不少显著性检测这种计算密集型的模块建议还是用Release。4.3 Windows下的编译命令Windows上不能直接用make但CMake生成Visual Studio工程也一样顺手。打开x64 Native Tools Command Prompt for VS 2019执行cd sailencyFilter mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease -G Visual Studio 16 2019 -A x64 cmake --build . --config Release和Linux版相比差异主要在生成器-G和架构参数-A x64上。这里特别注意-A x64不能省略。如果不指定CMake默认可能生成Win32工程到时候链接OpenCV 64位库会直接报架构不匹配的错。5. 编译实战中必然会踩到的坑——我踩过的都替你踩完了5.1 链接时一堆未定义的外部符号Windows下用MSVC编译时最常见的错误是error LNK2019: unresolved external symbol void __cdecl cv::imread(...)这种错误十有八九是库文件没链接对。检查思路是这样的先确认find_package(OpenCV)有没有成功CMake命令行输出里会打印OpenCV_DIR看它指向的那个路径下是不是真的有OpenCVConfig.cmake。再确认链接库是不是${OpenCV_LIBS}而不是手动写死的一个lib路径。最后确认Release配置链接的是opencv_world455.libDebug配置链接的是opencv_world455d.lib不能混用。混用的表现是release编译通过debug编译报一堆LNK2019。在Linux下如果遇到undefined reference to cv::imread往往是链接库顺序问题。把${OpenCV_LIBS}放在目标文件后面就行——CMake的target_link_libraries会自动处理依赖顺序所以这个现象已经很少见了真遇到的话检查下是不是手动改过链接命令。5.2 OpenCV头文件版本不匹配把项目从OpenCV 3.x迁移到4.x时会碰到经典的找不到头文件错误。比如fatal error: opencv2/imgproc/imgproc.hpp: No such file or directory原因就是4.x把各模块的头文件路径统一扁平化了应该改成#include opencv2/imgproc.hpp #include opencv2/core.hpp #include opencv2/highgui.hpp这个坑看起来很小但如果你是从旧教程复制代码过来的很容易卡在这里。更隐蔽的问题是函数签名变化比如cv::cvtColor的默认参数、cv::normalize的归一化类型枚举值4.x和3.x都有细微差别。我建议在代码里统一加一个编译期宏检查#if CV_MAJOR_VERSION 4 // 4.x 新写法 #else // 3.x 兼容写法 #endif5.3 Windows下宏定义冲突导致编译失败这个问题比较隐蔽但也特别典型。在Windows上用MSVC编译时min和max是预定义宏会跟标准库的std::min、std::max打起架来导致一堆莫名其妙的编译错误比如error C2589: ( : illegal token on right side of ::。解决办法有两种一种是在源文件顶部加#define NOMINMAX另一种是在CMakeLists里给目标加上编译定义target_compile_definitions(sailency_filter_lib PUBLIC NOMINMAX)这个坑在纯Linux环境下不会遇到但如果你打算跨平台编译一定要提前预防。5.4 Release和Debug模式的运行差异还有一类编译过了但运行表现不一样的坑。Debug模式下OpenCV的cv::Mat会自动填充0xCDCDCDCD这样的调试标记值显著性图看起来一片花。这不是算法问题是Debug版本的内存初始化行为。测试算法效果时务必用Release版本否则容易被表象误导白白浪费几个小时调参数。6. 运行验证与效果调试——拿到显著性图之后做什么6.1 命令行参数怎么设计主程序main.cpp的职责很简单读取图像、调用filter、保存结果。为了让调试方便我设计了两个命令行参数int main(int argc, char** argv) { if (argc 3) { std::cout Usage: sailencyFilter input_image output_image [sigma] std::endl; return -1; } std::string input argv[1]; std::string output argv[2]; double sigma (argc 4) ? std::stod(argv[3]) : 0.4; cv::Mat src cv::imread(input, cv::IMREAD_COLOR); if (src.empty()) { std::cerr Failed to load image: input std::endl; return -1; } SailencyFilter filter; cv::Mat saliency filter.filter(src, sigma); // 将浮点显著性图映射到0-255并保存 cv::Mat out8U; saliency.convertTo(out8U, CV_8U, 255.0); cv::imwrite(output, out8U); std::cout Saliency map saved to output std::endl; return 0; }为什么要单独提供sigma参数因为全局对比度方法对噪声比较敏感sigma越大颜色显著值的平滑程度越高输出图像就越柔和适合背景带纹理的图像sigma越小显著性图越锐利适合主体和背景对比本身就很强的图像。不调这个参数你可能会觉得算法对某些图效果平平。6.2 实测效果与性能基线我拿一张1080x720的实拍图做了测试量化bin设为12单线程Release模式下整条pipeline读取、量化、直方图统计、显著值计算、回写、保存大约耗时45毫秒。也就是说视频流场景下能做到20FPS以上作为预处理模块是够用的。对比输入原图和输出的显著性热力图能明显看到主体周围亮、背景暗的效果。如果某些图上效果不理想可以按以下顺序排查主体和背景颜色接近把量化bin调小比如从12调到8让相近颜色被合并得更彻底拉开对比度。背景有大面积高频纹理适当增大sigma或者先在源码的computeColorSaliency里对颜色距离做一次高斯加权。输出图整体偏灰可能是因为图像本身对比度低可以在renderSaliencyMap里对最终结果做一次直方图均衡化。6.3 从显著性图到实际应用拿到显著性图之后通常不会直接用而是做后续处理。最常用的链条是cv::Mat binary; cv::threshold(saliency, binary, 0.3, 1.0, cv::THRESH_BINARY); // 对 binary 做连通域分析得到显著区域的包围盒 std::vectorstd::vectorcv::Point contours; cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);阈值0.3这个值是我根据多次实验定的经验值。如果想更鲁棒可以用cv::threshold(saliency, binary, 0, 255, cv::THRESH_OTSU)自适应计算阈值省去手工调参的麻烦。这样从显著性过滤到目标区域提取的整条链路就串起来了。7. 后续扩展方向——这个模块还能怎么玩saliencyFilter的接口设计得足够简单所以扩展空间很大。我个人建议从这几个方向入手一是集成到视频流处理里。显著性检测天然适合做关键帧提取。对视频逐帧算显著性图然后统计每帧显著区域的面积和位置变化就能实现简单的镜头切换检测或者视频摘要。二是换一种显著值计算方法。现在的全局对比度方法对复杂背景鲁棒性中等偏下可以考虑在量化之后加入空间权重——距离图像中心越远的像素权重越低这样能显著提升对背景噪声的抑制能力。改动量不大主要是在第三步骤的颜色显著值公式里增加一个空间距离项。三是封装成动态库给其他语言调用。可以写一个extern C的C接口再用pybind11或者cffi封装。这样一来Python、Java甚至前端通过WebAssembly都能调用同一个编译好的显著性过滤器代码复用率瞬间拉高。我在实际使用中最大的体会是一个项目能顺利编译运行和算法本身同样重要。sailencyFilter这种经典模块算起来不复杂但把它包装成拿下来就能编译、跑起来就能用的工程还是要花点心思的。如果你照着这篇文里的步骤还遇到编译问题优先检查OpenCV版本和链接库配置这两块解决了其余基本都是细节问题。本文还有配套的精品资源点击获取
返回列表