0%

MATLAB 跑不动了:用 C++ 复刻 Itti–Koch 注意机制工具箱

我的毕业课题需要在零件图片上做注意机制(视觉显著性)实验,用的工具箱是 Caltech 的 SaliencyToolbox(Itti–Koch 模型,MATLAB 实现,2006–2013)。但我装的 MATLAB R2025b 和这个老工具箱不兼容,根本跑不起来。于是我用 C++(MSVC x64)把工具箱的默认处理路径逐行复刻了一遍,约 2000 行,脱离了 MATLAB 环境,还顺手把课题下一步要用的 top-down 模板匹配也做了。本文记录这件事的来龙去脉、模型原理、实现中的坑和最终效果。

1 为什么做这件事

课题的实验流程是:对一批零件照片(86 张)计算显著图,得到人眼可能的注视点序列;后续还要把”标准模板”的匹配结果作为先验,做 top-down 引导的注意实验。这些都依赖 SaliencyToolbox。

问题出在环境上:这个工具箱最后一次更新是 2013 年,而我的 MATLAB 是 R2025b,直接运行报错,改兼容性问题等于在一个十三年的代码库里考古。权衡之后我决定换条路:MATLAB 只用来读原始实现当”金标准”参考,真正的批量计算用 C++ 重写。好处很实际:

  • 不依赖 MATLAB 运行时,任何一台装了 VS 的 Windows 机器都能跑;
  • 命令行批处理,86 张图一条命令出全部结果;
  • top-down 接口自己设计,后面接模板匹配不用受 MATLAB 代码结构约束。

2 原理:Itti–Koch 注意机制模型

模型的核心是模拟人类视觉系统的 bottom-up 注意机制,数据流如下:

1
2
输入图像 → 高斯金字塔 → 特征提取 → 中央-周边差 → 特征图归一化 → 显著图
→ WTA 神经网络选注视点 → 返回抑制(IOR) → 下一注视点

分几步说:

  1. 高斯金字塔:把原图逐层降采样,得到多个尺度的表示。
  2. 特征提取:在每个尺度上分别计算三类特征——强度、颜色对立(红-绿、蓝-黄)、方向(Gabor 滤波,0/45/90/135 度)。
  3. 中央-周边差(center-surround):模拟视网膜感受野,用粗尺度(周边)与细尺度(中央)做差,突出”和周围不一样”的区域。
  4. 归一化与合成:每张特征图做迭代归一化,再按权重合成一张显著图(saliency map),图上越亮的位置越”显眼”。
  5. WTA(winner-take-all):一个 LIF(leaky integrate-and-fire)神经元网络在显著图上演化,最先达到阈值发放的位置就是下一个注视点。
  6. 返回抑制(IOR):选中一个注视点后,对该区域施加抑制,让下一轮注意跳到别的地方——这和人类扫视行为一致。

top-down 扩展是在第 4 步做文章:把外部先验(比如模板匹配的热力图)当作一路额外特征,按权重混入显著图合成,注意力就会被先验”拉”向目标区域。

3 做了哪些工作

3.1 逐行复刻默认处理路径

用 VS 2022 的 MSVC(x64,/O2 /std:c++17)复刻了工具箱默认路径,约 2000 行,第三方依赖只有 stb 两个单头文件(图像读写)。文件分工:

文件 职责
saliency.hpp 公共类型与声明(Map/Image/SalParams/LIF/WTA)
salmap.cpp 金字塔、颜色/强度/方向特征、center-surround、迭代归一化、显著图合成、top-down 混入
wta.cpp LIF 神经元、WTA 演化、disk/shape 两种 IOR、坐标转换
segment.cpp fastSegmentMap、estimateShape、二值形态学
main.cpp 命令行、图像 IO、overlay 绘制、selftest、批量处理

3.2 调试中的坑

一个真 bug:sepConv2PreserveEnergy 里的反向累加我一开始写错了。MATLAB 的 cumsum(v(end:-1:1)) 语义是”末尾 j+1 个元素的和”,不是后缀和。写错的表现是常数图金字塔的底边和右边直接爆炸,修正后恢复平坦。

一个假 bug:在完美均匀图上,显著图出现了条纹。查了一圈发现是降采样核 [1,5,10,10,5,1,1]/33 求和不严格等于 1,产生 1e-16 量级的浮点尾差,normalizeImage 把它放大到 [0,10],再被迭代归一化放大成可见条纹。对照 MATLAB 原版,一模一样的行为——这是工具箱固有的病态边界,真实图片不会触发,所以 selftest 里这一项只检查”输出有限”。

一堆编码坑:build.bat 里的中文注释会让 cmd 解析爆炸(改纯 ASCII);git-bash 中文路径下 cmd 找不到批处理(用绝对路径);MSVC 用 GBK 读 UTF-8 源文件报 C4819(加 /utf-8);windows.h 的 min/max 宏冲突(NOMINMAX)。Windows 上写 C++ 的日常。

3.3 top-down 模板匹配

工具箱本身没有模板匹配,这是我加的。做法是对每个”标准模板”在零件图上做零均值归一化互相关(NCC)滑窗匹配,多个模板的热力图逐像素取 max 融合成一张,然后走已有的 top-down 通道混入显著图。

NCC 的公式:

1
r(x,y) = Σ(I−Ī)(T−T̄) / sqrt( Σ(I−Ī)² · Σ(T−T̄)² )

窗口的均值和方差用积分图 O(1) 算,互相关项直接滑窗。86 张零件图 × 8 个模板几分钟跑完,不需要 FFT。负相关(反匹配)不是注意目标,直接裁为 0;比图大的模板自动跳过。

3.4 验证

内置 --selftest 六项全过:白方块/红方块的显著图峰值定位、首注视点位置、两张均匀图的输出有限性、NCC 峰值精确定位(合成图贴图案,峰值落在窗口中心,相关度 1.0000)。真图上注视点落在连杆小头孔、杆身、大头孔等高对比区,返回抑制衰减后的重访行为与 MATLAB 一致。

4 软件使用

构建(需要 VS 2022):

1
build.bat

build.bat 内部调 vcvarsall.bat x64 设置环境,然后:

1
cl /O2 /std:c++17 /EHsc /W3 /utf-8 /Fe:saliency.exe main.cpp salmap.cpp wta.cpp segment.cpp

命令行用法:

1
2
3
4
5
6
7
8
9
10
11
saliency.exe <图片或目录> [选项]
--fixations N 注视点个数(默认 5,0 = 只算显著图)
--out DIR 输出目录(默认 ./out)
--ior MODE disk|shape|none(默认 shape)
--norandom 关闭坐标 jitter 与噪声(完全可复现)
--seed N 随机种子(默认 42)
--topdown IMG 外部 top-down 注意图(加权混入显著图)
--template PATH 模板文件或目录,NCC 匹配热力图作为 top-down(与 --topdown 互斥)
--tdweight W top-down 权重(默认 1)
--masks 额外保存每个注视点的形状 mask PNG
--selftest 运行内置自检

每张输入图输出三个文件:*_salmap.png(显著图)、*_overlay.png(原图叠加注视点轨迹和形状轮廓)、*_fixations.csv(注视点坐标和时间);用 --template 时多一个 *_tmap.png(模板匹配热力图)。

实际跑过的命令:

1
2
3
4
5
:: 批量 bottom-up,86 张零件图
saliency.exe 零件 --fixations 10 --out out_batch

:: 单图 + 模板匹配 top-down
saliency.exe 零件\lg2.jpg --template 标准模板 --tdweight 2 --fixations 5 --norandom --out out_tpl

5 处理结果

5.1 bottom-up:连杆

显著图上最亮的位置在连杆小头孔附近:

连杆显著图

对应的前几个注视点依次落在小头孔、杆身、大头孔——都是图中对比度最高的结构:

连杆 bottom-up 注视轨迹

5.2 top-down:模板匹配引导

用 8 个标准模板对同一张连杆图做 NCC 匹配,热力图的亮带准确落在杆身:

模板匹配热力图

把它以权重 2 混入显著图后,注视点序列被明显调制,第一轮就锁定小头孔区域(纯 bottom-up 时第一注视点在大头孔边缘):

连杆 top-down 注视轨迹

5.3 法兰

注视点分布在螺栓孔和内圈边缘:

法兰注视轨迹

5.4 活塞

销孔和顶部边缘是最显著区域:

活塞注视轨迹

5.5 轴承

滚子与保持架的高对比纹理吸引了大部分注视点:

轴承注视轨迹

6 小结

一次环境兼容性问题,最后变成了一次完整的重写:C++ 复刻的注意机制工具箱不依赖 MATLAB,一条命令批量出结果,还顺带把课题下一步的 top-down 模板匹配打通了。复刻老代码最大的收获是搞清楚了哪些”bug”是真 bug,哪些是原作者留下的固有行为——后者要原样保留,否则就谈不上”复刻”了。