简介:这是一份基于Gabor小波的多尺度图像纹理特征提取与分析系统,采用MATLAB实现,主要面向图像处理、模式识别方向的初学者与研究人员,用于解决纹理特征表达中多尺度与多方向信息难以统一描述的问题,系统支持旋转与尺度不变性,便于直接嵌入分类或检索任务中。压缩包共2个文件,包含1个m脚本与1个md说明文档,m文件为完整可运行的算法主程序,md文档用于说明实现思路与使用方式,整包仅4KB,轻量简洁,适合快速阅读与移植。已有60人浏览学习,兼具入门参考与算法验证价值。通过该资源,读者可以获得Gabor滤波器组构建、多尺度多方向特征提取、特征向量组织与可视化分析的具体代码实现,并了解如何在MATLAB环境下快速搭建纹理分析流程。适合作为课程设计、论文实验或项目预研阶段的起步工具,具有较强的参考意义。
1. 为什么纹理特征提取要选Gabor小波做多尺度分解
一批带纹理的织物在流水线上高速通过,人工目检漏检率始终压不下去;一组病理切片等着分型,医生希望在移动光标的同时得到纹理相似度排序。这两类任务都依赖同一个技术底座:基于Gabor小波的多尺度图像纹理特征提取。Gabor 滤波器本质上是高斯窗调制的复正弦波,在频域里同时保留了位置和频率信息,因此能精确捕捉纹理的周期和走向。把多个尺度、多个方向的 Gabor 滤波器组合起来,图像中“粗糙”“细腻”“横纹”“斜纹”这些主观感知就变成了一组可计算的数值向量。MATLAB 的 Image Processing Toolbox 直接提供了 gabor 和 imgaborfilt 函数,整套系统的落地成本比多数人想象的低。这篇博文按滤波器组设计、特征提取、特征后处理、参数调优的顺序展开,所有代码都可以直接落到自己的函数里。
2. Gabor滤波器组设计与参数选择
2.1 从数学定义看它为什么能提取纹理
二维 Gabor 滤波器的空间域形式可以写成:
h(x,y)=exp(-(x'^2+γ^2 y'^2)/(2σ^2)) · exp(j(2π f x'+φ))
其中 x'、y' 是原坐标绕方向角 θ 旋转后的值;f 是空间频率,单位通常写为周期/像素,数值上等于 1/波长;σ 是高斯窗的宽度;γ 是高斯包络的长宽比;φ 是相位偏移。这个公式可以拆成两部分理解:复正弦分量决定了滤波器对哪个频率、哪个方向最敏感,高斯窗则把它的感受野限制在局部,让它不像傅里叶变换那样把全图信息混在一起。所以 Gabor 滤波器在频域和空间域都有定位能力,这正是它能做纹理识别的原因。
单一尺度的 Gabor 滤波器只能看到一种粗细程度的纹理:短波长对细密纹理响应强,长波长对粗大的周期性纹理响应强。把多个不同频率的滤波器并行铺开,就形成多尺度分解;再叠加上方向维,同一个滤波器组就能区分“横纹”“斜纹”“各种朝向密集线”。实际工程中,尺度数和方向数做笛卡尔积,生成的每个滤波器都对应一个“频率方向通道”。
常见误用是把 g 的实数核直接当模板做相关匹配,或者把所有 mag 输出堆成一个高维向量直接训练分类器。前者忽略了滤波器组的频率覆盖设计,后者忽略了统计压缩的必要性。正确做法是先设计一组在 log 尺度上等距的波长,再对每个响应图做统计量压缩。
2.2 用 MATLAB 内置函数生成滤波器组
早期实现 Gabor 滤波器组要自己构造旋转高斯调制核,还要处理复数卷积。从 R2015b 开始,Image Processing Toolbox 内置了 gabor 函数,声明滤波器组的代码只有两行:
wavelength = [3 6 12 24]; orientation = [0 45 90 135]; g = gabor(wavelength, orientation);wavelength 的单位是“像素/周期”,orientation 的单位是“度”。两行代码生成 4×4=16 个滤波器。每个 g(p) 对象里保存了方向、波长、空间长宽比和 SpatialKernel 属性,SpatialKernel 是可直接查看的实数卷积核。如果安装 MATLAB 时没勾选 Image Processing Toolbox,运行到这行会直接报“未定义函数”,需要去附加功能里补装。
把滤波器组画出来能直观确认参数没有设错:
figure; for p = 1:numel(g) subplot(4, 4, p); imshow(real(g(p).SpatialKernel), []); title(sprintf('\\lambda=%d, theta=%d°', round(g(p).Wavelength), g(p).Orientation)); end图里可以看到,短波长的核比较小,长波长的核明显更大;实部核呈现条纹状,条纹方向对应设定的 orientation。如果图像规模是 256×256,而波长上限已经超过 64,那这个滤波器几乎只覆盖局部角点,提取出来的响应没有统计意义。
2.3 参数表与选参原则
| 参数 | 作用 | 常见取值 | 调整方向 |
|---|---|---|---|
| wavelength(波长) | 感知的纹理周期 | [3 6 12 24] 或 [2 4 8 16 32] | 减小可捕捉细纹理,增大可捕捉粗大周期 |
| orientation(方向) | 纹理走向选择性 | [0 45 90 135] 或每30°一个 | 方向越多特征维度越高,对旋转越敏感 |
| SpatialAspectRatio | 高斯窗长短比 | 0.5 或 1 | 0.5 增强方向选择性,1 为各向同性 |
| 滤波器组规模 | 特征维数的决定性因素 | 16 到 30 个 | 纹理类别复杂时加大,样本量少时减小 |
波长序列公比为 2 是常用经验:纹理周期在视觉上接近对数均匀分布,等比数列能以最少的滤波器覆盖最大的频率范围。方向步长 45° 在多数任务里够用;如果目标是布匹横纹检测,可以把方向缩到 [0 90] 两点,先跑通再增密。要注意的是,gabor 函数不会校验 wavelength 是否大于图像边长,这个约束要使用方自己把关。
3. 用 MATLAB 实现多尺度 Gabor 纹理特征提取
3.1 预处理:灰度化、类型归一化与尺寸归一化
Gabor 卷积要求图像是灰度图,且数值类型最好为 double。uint8 在卷积时会引入整数截断,幅度响应的统计量会变得粗糙;RGB 图直接滤波会把三个通道的反应混在一起,导致纹理方向判断失真。下面三步是每次提取前的固定操作:
img = imread('fabric.png'); if size(img, 3) == 3 img = rgb2gray(img); end img = im2double(img); img = imresize(img, [256 256]);第一段把彩色图转到灰度域;im2double 把 uint8 的 0~255 映射到 0~1;imresize 把尺寸统一到 256×256。为什么尺寸必须统一:后面要比较不同图像的统计量,如果图像面积不同,能量项天然和面积成正比,纹理差异会被面积差异淹没。固定到 256×256 并不是必须,可以换成 512 或 128,但整个数据集要一致。
如果不想破坏宽高比,可以用以下写法:
scale = 256 / size(img, 1); img = imresize(img, scale);这种缩放按高度为基准,适合物体图像。对布料、瓷砖这类纹理区域纹理特征提取,固定 256×256 的强制缩放通常也够用,因为统计量对轻微拉伸不敏感。
3.2 卷积响应计算:imgaborfilt
生成滤波器组后用 imgaborfilt 一次算完所有尺度和方向的响应:
g = gabor([3 6 12 24], [0 45 90 135]); [mag, phase] = imgaborfilt(img, g);输入 img 是灰度 double 图像,g 是 Gabor 对象数组。返回的 mag 是 m×n×16 的三维数组,第三维索引对应当前滤波器在 g 数组中的位置;phase 同样是 m×n×16 的相位响应,范围在 -pi 到 pi 之间。纹理特征提取通常只用 mag,因为幅度响应代表该尺度方向上纹理“能量”的高低,相位则对像素位移非常敏感,直接做统计量意义不大。后面第 5.5 节会给出一种把相位用起来的方法。
imgaborfilt 的默认边界填充是 circular,也就是把图像左右边缘卷接起来。对纹理图来说,这种填充一般不会造成明显伪影,但波长很长时会产生跨边界的假纹理。显式改成 replicate(边缘复制)更稳:
[mag, ~] = imgaborfilt(img, g, 'Padding', 'replicate');replicate 的代价是图像四边各有一段无效邻域,滤波后会形成边框伪影。如果只靠整图统计量,这个边框占比很小;但要做像素级纹理分割,就需要单独处理边界区域。
3.3 统计量压缩:从响应图到特征向量
mag 是 256×256×16 的数组,不能直接当特征。一是维数超过一百万,分类器全部过拟合;二是图像平移几个像素,mag 里每个位置的数值都会变化,像素级特征完全失去平移不变性。把每个滤波器的响应图压缩成几个全局统计量,是最常用的做法:
function feat = extractGaborTextureStat(mag, g) % mag: imgaborfilt返回的三维数组, 第三维对应滤波器索引 % 输出: 长度为 numel(g)*3 的行向量 nFilt = numel(g); feat = zeros(1, nFilt * 3); for i = 1:nFilt m = mag(:, :, i); feat((i-1)*3 + 1) = mean(m(:)); % 平均响应强度 feat((i-1)*3 + 2) = std(m(:)); % 响应起伏程度 feat((i-1)*3 + 3) = mean(m(:).^2); % 能量 end end三个统计量各有分工。mean 反映该通道对图像整体的激发程度;std 描述响应在空间上的起伏,纹理对比度越高,std 越大;mean(m(:).^2) 是二阶矩,对少数强响应点更敏感,能凸显局部边缘密集的区域。三者在数值上互有重叠,但拼到一起后分类器的可分性通常优于只用其中一两个。
如果场景对纹理密度的分布很敏感,还可以加第 4 个统计量——信息熵。用entropy(m(:))就能补上,代价是特征维数从 48 升到 64。
3.4 主流程函数与响应可视化
把前面几段合成一个可复用函数:
function feat = gaborTextureFeatureFromImage(imgPath, wavelength, orientation) % 提取单张图像的多尺度Gabor纹理特征 % 输入: 图像路径, 波长数组(像素/周期), 方向数组(度) % 输出: 1 x (numel(wavelength)*numel(orientation)*3) 特征行向量 img = imread(imgPath); if size(img, 3) == 3 img = rgb2gray(img); end img = im2double(img); img = imresize(img, [256 256]); g = gabor(wavelength, orientation); [mag, ~] = imgaborfilt(img, g); feat = extractGaborTextureStat(mag, g); end调用示例:
feat = gaborTextureFeatureFromImage('fabric.png', [3 6 12 24], [0 45 90 135]); disp(size(feat)); % 1x48如果滤波器响应图需要可视化,可以直接用imshow(mag(:,:,i), [])查看某一通道的空间分布,方便判断纹理主要出现在图像哪些区域。
实操中有一个常见错误:把 16 张响应图的平均值当成特征。这样只剩 16 维,丢失了方差和能量信息,分类精度通常会掉 5 个百分点以上。另一个极端是把所有滤波器响应图全部拉平,这种做法只在做像素级分割时才有意义。
4. 特征归一化、PCA 降维与系统集成
4.1 特征归一化:不同量纲的特征不能直接比较
48 维特征里,mean 的量级通常在 0.1 左右,能量项平均在 0.01 以下。直接做欧氏距离或训练 SVM,低量纲的统计量会被完全淹没,滤波器的能量信息等于被丢弃。归一化最常用 Z-score:
featAll = ...; % 已经堆叠好的 样本数x48 矩阵 mu = mean(featAll, 1); sigma = std(featAll, 0, 1); featNorm = (featAll - mu) ./ (sigma + eps);std 的第二个参数 0 表示除以 N-1,样本标准差;加 eps 是为了防止某个滤波器在所有样本上响应完全相同,导致方差为 0 时除零。N-1 的细节不重要,重点是整个数据集统一用同一组 mu 和 sigma,不能每张图单独算。
4.2 批量提取数据集特征
实际项目里不是一张图一张图地试,而是批量扫描文件夹。这里给出一个最小批量脚本:
folder = 'data'; imgFiles = dir(fullfile(folder, '*.png')); wavelength = [3 6 12 24]; orientation = [0 45 90 135]; featAll = []; labels = {}; for i = 1:numel(imgFiles) path = fullfile(imgFiles(i).folder, imgFiles(i).name); featAll = [featAll; gaborTextureFeatureFromImage(path, wavelength, orientation)]; % 标签可以从文件名或子目录路径解析, 这里留占位 [~, name, ~] = fileparts(path); labels{end+1, 1} = name(1:2); % 按实际命名规则改 end循环里反复 imread 和卷积是耗时瓶颈,可以用 parfor 并行提取。wavelength、orientation 都是只读参数,gaborTextureFeatureFromImage 是纯函数,符合 parfor 的使用条件。要先把 featAll 预分配成numel(imgFiles)×48的零矩阵,否则在循环里动态增长数组会拖慢整体速度。
4.3 PCA 降维与保留成分数量的判断
Gabor 多尺度特征相邻尺度的相关性很高,48 维里实际有效自由度通常在 10~20 左右。PCA 可以把相关性打掉,同时保住 90% 以上的方差:
[coeff, score, ~, ~, explained] = pca(featNorm); cumExplained = cumsum(explained); k = find(cumExplained >= 90, 1); fprintf('保留前%d个主成分, 累计解释%.2f%%方差\n', k, cumExplained(k)); featReduced = score(:, 1:k);pca 默认对列做中心化,返回的 score 是样本在新坐标系下的坐标,coeff 是投影矩阵。没有 Statistics and Machine Learning Toolbox 时,用 SVD 手动实现同样的步骤:
centered = featNorm - mean(featNorm); [~, ~, V] = svd(centered, 'econ'); featReduced = centered * V(:, 1:k);两种写法结果一致,只是主成分符号方向可能相反。用 SVD 的好处是不依赖特定工具箱,在基础版的 MATLAB Online 上也能跑。
4.4 接入分类器与图像检索
降维后的特征可以直接喂给分类器。这里用 SVM 加 5 折交叉验证作为基线:
rng(2024); mdl = fitcecoc(featReduced, labels); cvmdl = crossval(mdl, 'KFold', 5); fprintf('5折错误率 = %.2f%%\n', kfoldLoss(cvmdl) * 100);如果是图像检索任务,查询图先走同样的特征提取和投影,再做欧氏距离最近邻:
queryFeat = gaborTextureFeatureFromImage('query.png', wavelength, orientation); queryNorm = (queryFeat - mu) ./ (sigma + eps); queryScore = queryNorm * coeff(:, 1:k); dist = pdist2(featReduced, queryScore); [~, idx] = sort(dist); disp({imgFiles(idx(1:3)).name});这里要提醒一个容易忽略的细节:mu、sigma、coeff 都在全部样本上计算,再做交叉验证,会形成轻度的数据泄漏。严谨的做法是在交叉验证里把数据集先切分,每次只用训练折计算 mu、sigma 和 PCA 投影矩阵,再变换测试折。快速验证时上面的代码够用,写论文或正式评估时必须改。
| 滤波器组配置 | 原始特征维数 | 适用场景 |
|---|---|---|
| [3 6 12 24] × [0 45 90 135] | 48 | 常规纹理分类与检索 |
| [2 4 8 16 32] × [0 30 60 90 120 150] | 90 | 纹理跨度大、类别多 |
| [4 8 16] × [0 45 90 135] | 36 | 样本少或要求实时推断 |
5. Gabor 纹理提取参数调优与四个常见坑
5.1 先确认该用几个尺度和方向
尺度数不是越多越好。尺度多了,相邻滤波器响应相关系数很容易超过 0.95,特征冗余,SVM 决策面被无关维度干扰。方向数越多,旋转敏感度越高,同一种纹理换个角度就可能被判成另一类。工程上我的起点固定是 4 尺度 4 方向,跑完基线再看结果。如果某类别分类错误率明显偏高,可视化平均幅值条形图,看哪些滤波器对该类别响应接近 0,再决定增加细尺度还是增加方向。
5.2 坑一:边界填充方式影响长波长响应
imgaborfilt 默认 circular 填充。波长 24 且图像 256×256 时,影响很小;但如果图像只有 64×64,循环填充会让图像左右边缘、上下边缘互相串扰,长波长滤波器的输出接近纯伪影。参数里显式指定即可:
[mag, ~] = imgaborfilt(img, g, 'Padding', 'replicate');5.3 坑二:光照不均导致低频通道饱和
光照渐变在频域里是低频分量,会被长波长滤波器当成纹理周期,把响应值整体抬高。对纹理分类来说,最简单的方法是先做高通滤波,把照明背景减掉:
img = img - imgaussfilt(img, 8); img = (img - min(img(:))) / (max(img(:)) - min(img(:)));imgaussfilt 的 8 是高斯核标准差,以像素为单位,至少比最长的波长大一倍,这样才不会把纹理成分一并滤掉。这一步对纺织图像的效果通常比直方图均衡好,直方图均衡会改变纹理对比度的相对关系。
5.4 坑三:分辨率改变后原波长参数失效
同一块材料,100 dpi 和 300 dpi 扫描出来的图,像素/周期差 3 倍。3 像素波长的滤波器在 300 dpi 下捕捉的物理纹理,相当于 100 dpi 下的 1 像素波长,已经逼近传感器噪声。换数据采集设备后必须重跑波长搜索,或者在代码里写一个参考分辨率常量,所有波长按比例换算。
5.5 提升精度的一个技巧:把相位信息用起来
幅值统计量对“有没有这种纹理”很敏感,但区分周期相同走向相同的两类纹理时,相位分布还有可用信息:
[~, phase] = imgaborfilt(img, g); phHist = zeros(1, 8 * numel(g)); for p = 1:numel(g) ph = phase(:, :, p); phHist((p-1)*8 + (1:8)) = histcounts(ph(:), 8); end这组直方图拼接在原先的特征之后,可以让特征维数从 48 增加到 112。由于相位直方图保留了周期结构的分布形态,对规则周期性纹理往往能带来 1~2 个百分点的提升。加不加这组特征,需要固定分类器为同一配置,只看交叉验证精度的差值,其他环节保持不变。
本文还有配套的精品资源,点击获取