简介:本资源是一套面向图像处理初学者与科研人员的MATLAB纹理特征提取工具集,聚焦于计算机视觉中的纹理分析核心任务,涵盖GLCM、GLDS、LBP、GMRF、FD和Gabor六类主流方法,适用于遥感图像分类、医学影像识别、工业缺陷检测等实际场景。压缩包为RAR格式,大小255KB,包含多个功能完整、结构清晰的MATLAB脚本文件(.m为主),每个方法均封装为独立可调用函数,并附带示例调用逻辑与关键参数说明,便于理解原理、调试验证及集成到自有项目中。已有3558人学习下载,代码经实测可用,注释详尽,覆盖特征计算、归一化、统计量提取等全流程,显著降低从理论到实践的门槛。读者可直接运行对比不同方法在相同图像上的输出效果,快速掌握各算法的适用边界与性能差异。 作为一个常年跟图像特征打交道的人,我几乎每天都要跟纹理特征提取打交道。最近在整理代码库时,发现MATLAB环境下纹理特征提取的相关实现虽然网上零零散散有不少,但大多只讲了某一种方法,或者代码写得让人看不懂。这里我把实际项目中用过的六种常用纹理特征提取方法——GLCM、GLDS、LBP、GMRF、FD、Gabor——统一整理了一遍,从原理到MATLAB完整实现,一次讲清楚。
这篇内容适合正在做图像处理大作业的学生、刚入门纹理分析的研究者,以及需要在工程项目里快速落地纹理特征的工程师。我会按“用什么、为什么用、怎么写、怎么调参”的顺序来讲,代码都是可以复制后直接跑的,已经把常见的坑都提前踩过了。
1. 纹理特征提取的整体设计思路与选型逻辑
1.1 为什么偏偏是这六种方法
纹理特征的本质是描述图像局部区域内像素灰度值的空间分布规律。不同应用场景对纹理特征的要求完全不同,这正是需要同时掌握多种方法的原因。GLCM(灰度共生矩阵)偏重统计特性,适合描述粗糙度、方向性;GLDS(灰度梯度共生矩阵)更进一步,把梯度信息纳入统计框架,对边缘敏感;LBP(局部二值模式)计算效率极高,对光照变化不敏感,在实时系统里几乎是首选;GMRF(高斯马尔可夫随机场)走的是模型路线,用参数来刻画纹理的随机过程;FD(分形维数)从自相似性的角度看纹理复杂度,对某些自然纹理有奇效;Gabor变换则是在频域上精细划分方向和尺度,模拟人眼视觉机制。
这六种方法的组合几乎可以覆盖纹理分析领域的大多数需求。实际项目中我常用的策略是:先用GLCM和LBP这类快速方法做预筛选,再用Gabor或GMRF对候选区域做精细分析。
1.2 方法分类:统计派、模型派和变换派
从理论根源上,这六种方法可以归为三大流派。统计派认为纹理是像素灰度值在空间上的统计分布规律,GLCM、GLDS、LBP都属于这一类,它们直接统计像素之间的关系,实现简单,解释性强。模型派通过假设纹理服从某种随机过程,用模型参数作为特征,GMRF是典型代表,优点是特征维度低,缺点是需要估计参数,计算量较大。变换派把图像变换到频域或尺度空间,在变换系数中提取特征,Gabor和FD中基于频域的实现属于这一类,能够同时捕捉不同尺度和方向的纹理信息。
选择哪种方法不能拍脑袋。我的经验是:如果目标是图像分类且样本量大,优先考虑LBP和GLCM的组合;如果目标是对纹理做精细分割或边界定位,Gabor滤波器组更合适;如果纹理具有明显的自相似特性,比如云图、地形图、某些生物组织图像,FD往往能给出意想不到的好效果。
1.3 MATLAB作为实现平台的优势
MATLAB在做纹理特征提取时的优势非常明显:图像处理工具箱提供了强大的矩阵运算能力,很多统计量可以直接用矩阵操作完成,不需要自己写循环;内置的NLFILTER、BLOCKPROC等函数可以方便地做邻域操作;可视化方便,可以随时查看中间结果,对调试非常有帮助。
另外,MATLAB的imageDatastore配合tall数组可以处理超大图像集,这在做批量纹理特征提取时特别实用。后面我会给出一个完整的代码框架,把六种方法整合到一个函数里,方便直接调用。
2. 统计类方法:GLCM 与 GLDS 的完整实现与调参要点
2.1 GLCM灰度共生矩阵:核心原理与四个方向实现
灰度共生矩阵的核心思想是统计图像中相距某一距离、某一方向的两个像素对灰度值同时出现的概率。假设图像灰度级数为L,GLCM就是一个L×L的矩阵,矩阵元素P(i,j)表示满足特定空间关系的、灰度值分别为i和j的像素对出现的次数。 这里的“特定空间关系”由距离d和方向θ决定。通常取d=1,θ分别取0°、45°、90°、135°四个方向。
MATLAB实现时可以直接用工具箱里的graycomatrix,但为了说清楚原理,也为了后续扩展自定义特征,我通常自己写核心统计部分:
function glcm = computeGLCM(img, d, theta) % img: 输入灰度图像,值范围0~255 % d: 像素对距离,通常为1 % theta: 方向,取0, 45, 90, 135 [rows, cols] = size(img); L = 256; glcm = zeros(L, L); % 根据方向确定偏移量 switch theta case 0 rowOff = 0; colOff = d; case 45 rowOff = -d; colOff = d; case 90 rowOff = -d; colOff = 0; case 135 rowOff = -d; colOff = -d; otherwise error('theta必须为0, 45, 90或135'); end % 遍历像素对,累加计数 for i = 1:rows for j = 1:cols i2 = i + rowOff; j2 = j + colOff; if i2 >= 1 && i2 <= rows && j2 >= 1 && j2 <= cols glcm(img(i,j)+1, img(i2,j2)+1) = ... glcm(img(i,j)+1, img(i2,j2)+1) + 1; end end end end在实际使用前,通常要把GLCM归一化,让所有元素之和为1,得到联合概率分布。然后用这个分布计算各种统计量。最常用的是Haralick提出的14个特征,但实际项目中大部分情况下用5个就足够了:
- 能量(角二阶矩):sum sum P(i,j)^2,反映纹理的粗细和灰度分布的均匀程度
- 对比度:sum sum (i-j)^2 * P(i,j),反映纹理的清晰度和沟纹深浅
- 相关性:衡量灰度在行或列方向上的相似程度
- 熵:反映纹理的随机性和复杂程度
- 逆差矩:反映纹理的局部均匀性
实现这些特征的代码我放在后面,跟GLDS一起封装在统一接口里。
2.2 GLCM的坑:灰度级压缩与矩阵对称化
GLCM有一个非常关键的细节:灰度级L的选择。直接用256级灰度计算,GLCM是256×256的矩阵,计算量大且矩阵稀疏,很多统计量不稳定。工程上通常先把图像压缩到16级或32级灰度再计算GLCM。这不仅仅是计算量的考虑,更重要的原因是灰度级太多时像素对计数分散,特征值对噪声过于敏感;压缩后特征更加稳健。
另一个坑是GLCM的方向性问题。单个方向的GLCM只能反映该方向的纹理特性,通常会计算四个方向后取平均值或分别作为特征。对于旋转不变的纹理分析需求,四个方向取平均是最简单的处理方式。MATLAB的graycomatrix默认也是计算一个GLCM,但可以通过Offsets参数指定多个方向。
还要注意对称化处理。有些实现只统计“前向”像素对,这样得到的是非对称矩阵。在计算相关性等特征时,非对称矩阵和对称矩阵的结果会有差异。通常的做法是把GLCM与其转置相加,得到一个对称矩阵再归一化。我自己的习惯是始终使用对称化的版本,因为纹理的方向特性已经在四个方向的独立计算中体现了,对称化可以消除起点选择带来的偏差。
2.3 GLDS灰度梯度共生矩阵:边缘增强的纹理描述
GLDS的构建思路和GLCM类似,但它统计的是灰度与梯度的联合分布。每个像素有两个属性:灰度值f(i,j)和梯度值g(i,j)。梯度值可以反映该点的边缘强度,所以GLDS天然更关注纹理的边缘信息。
梯度图的计算通常用Sobel算子或Canny算子。这里我选择Sobel,因为它是线性的,梯度幅值和方向可以直接数值计算。
function glds = computeGLDS(img, Lf, Lg) % Lf: 灰度级数,通常取16 % Lg: 梯度级数,通常取16或32 % 用Sobel算子计算梯度幅值 [gx, gy] = gradient(double(img)); gradMag = sqrt(gx.^2 + gy.^2); % 灰度归一化到[0, Lf-1],梯度归一化到[0, Lg-1] imgNorm = round(double(img) / 255 * (Lf-1)) + 1; gradNorm = round(gradMag / max(gradMag(:)) * (Lg-1)) + 1; glds = zeros(Lf, Lg); [rows, cols] = size(img); for i = 1:rows for j = 1:cols glds(imgNorm(i,j), gradNorm(i,j)) = ... glds(imgNorm(i,j), gradNorm(i,j)) + 1; end end glds = glds / sum(glds(:)); end基于GLDS可以提取的特征有15个小梯度优势、大梯度优势、灰度分布不均匀性、梯度分布不均匀性、能量、灰度均值、梯度均值等。实际应用中我常取前6个,几乎不需要做特征选择就能直接用于分类。
GLDS的优点是能同时捕捉灰度信息和边缘信息,对灰度变化平缓但边缘丰富的纹理非常有效。缺点是对梯度计算参数敏感,Sobel算子窗口大小和梯度归一化方式都会影响特征值。所以同一批实验里务必要固定梯度算法和归一化参数。
3. LBP 与 GMRF:从局部二值模式到随机场模型
3.1 LBP局部二值模式:明亮、快速且光照鲁棒
LBP的核心思想极其简洁:对每个像素,取其3×3邻域内的8个像素,与中心像素比较大小,大于等于中心像素记为1,否则记为0,这样就得到一个8位二进制数,转换成十进制即该像素的LBP值。这个值可以看作该点的局部纹理编码。
function lbpImg = computeLBP(img) % 原始LBP,3x3邻域,8个采样点 [rows, cols] = size(img); lbpImg = zeros(rows, cols); % 8个邻域的偏移量和对应比特位 offsets = [-1 -1; -1 0; -1 1; 0 1; 1 1; 1 0; 1 -1; 0 -1]; for k = 1:8 shifted = zeros(rows, cols); % 图像平移 for i = 1:rows for j = 1:cols ni = i + offsets(k, 1); nj = j + offsets(k, 2); if ni >= 1 && ni <= rows && nj >= 1 && nj <= cols shifted(i,j) = img(ni,nj); else shifted(i,j) = 0; end end end bit = (shifted >= img) * 2^(k-1); lbpImg = lbpImg + bit; end end得到LBP图后再统计直方图就是特征向量。但原始LBP有256个bin,维度偏高,而且不同纹理的直方图可能很稀疏。因此实际项目中几乎都用改进版本,最常用的是均匀模式(Uniform Pattern):当二进制串中0到1或1到0的跳变次数不超过2次时,称为均匀模式。均匀模式把256种模式压缩到59种,大大降低特征维度,同时保留绝大多数纹理信息(统计上通常覆盖90%以上的模式)。
MATLAB的图像处理工具箱确实有extractLBPFeatures函数,但它返回的直方图配置固定,灵活性不够。我在需要控制采样点数和半径时倾向于自己写。比如用半径为R、采样点为P的圆形邻域时,需要插值采样,这能更好地控制尺度适应性。圆形邻域配合旋转不变模式,能让LBP特征具备旋转不变性,具体做法是把二进制串循环移位后取最小值作为编码。
3.2 LBP的实战心得:避免边界效应
LBP实现中最大的隐患是边界像素处理。我见过不少代码直接把边界置零或忽略,这会导致特征图边缘出现一圈异常值,在直方图统计时虽然影响比例不大,但如果图像较小,误差就会被放大。稳妥的做法是给图像加一圈镜像填充(padarray的'symmetric'选项),让边界像素也能正常计算。
另一个心得是关于分块统计。在图像分类任务中,我通常把图像分成若干个不重叠的块(比如4×4),每个块单独计算LBP直方图,然后拼接。这样特征里既包含了纹理信息,也包含了空间位置信息,分类准确率通常能大幅提升。这个思路与空间金字塔匹配类似,是LBP实战中的标准操作。
关于采样点数,默认P=8、R=1适合细节纹理;对于粗糙纹理,建议增大R,比如P=16、R=2或P=24、R=3,这样能捕捉更大范围的纹理单元。增大R时需要相应增加P,保证空间分辨率。
3.3 GMRF高斯马尔可夫随机场:模型参数作为特征
GMRF的基本假设是图像的每个像素灰度值可以用邻域像素的线性组合加上一个高斯噪声来建模:
f(i,j) = sum_{(m,n) in N} θ(m,n) * f(i+m, j+n) + e(i,j)
其中N是邻域集合,θ是模型参数,e是均值为0、方差为σ²的高斯噪声。纹理特征就是这些参数θ和噪声方差σ²。不同纹理的邻域依赖关系不同,参数也不同。
GMRF的参数估计通常用最小二乘法。以二阶邻域为例,需要估计4个方向或8个方向的依赖系数。下面的代码实现了一个基于最小二乘的GMRF参数估计:
function theta = estimateGMRF(img, order) % 最小二乘估计GMRF参数 % order: 模型阶数,1或2,对应4邻域或8邻域 img = double(img); [rows, cols] = size(img); % 根据阶数生成邻域偏移 if order == 1 offsets = [-1 0; 1 0; 0 -1; 0 1]; else offsets = [-1 0; 1 0; 0 -1; 0 1; -1 -1; -1 1; 1 -1; 1 1]; end numParams = size(offsets, 1); A = []; % 特征矩阵 b = []; % 目标向量 for i = order+1:rows-order for j = order+1:cols-order rowVec = zeros(1, numParams); for k = 1:numParams rowVec(k) = img(i+offsets(k,1), j+offsets(k,2)); end A = [A; rowVec]; b = [b; img(i,j)]; end end % 最小二乘解 theta = (A' * A) \ (A' * b); end这个实现有几个值得注意的点。首先,遍历像素时跳过了边界,这样A矩阵行数略小于像素总数,但参数估计更干净。其次,邻域集合的选择要考虑对称性,对于无方向性的纹理,对称邻域更合理,也可以把对称位置参数设为相等来减少待估参数数量。
GMRF的优点在于特征维度低,对纹理的刻画是用“生成规则”的方式,对于具有明显周期性的纹理特别有效。缺点是它假设纹理是平稳的随机场,对于局部变化剧烈的纹理效果一般。另外,参数估计的计算量随邻域阶数上升较快,实际应用中二阶邻域通常就够了。
4. 变换类方法:FD 分形维数与 Gabor 滤波器组
4.1 差分盒计数法计算分形维数
分形维数FD从图像表面自相似性的角度描述纹理复杂度。直观理解,如果把图像灰度看作高度场,那么表面越粗糙,分形维数越大。差分盒计数法是计算图像FD最常用的方法,在工程中我基本上只用这一种。
核心思想是:用不同尺寸的盒子去覆盖图像灰度表面,统计覆盖所需的盒子数N(r),然后拟合log(N(r))与log(1/r)之间的线性关系,斜率就是分形维数。具体实现:
function fd = computeFD(img, boxSizes) % boxSizes: 盒子尺寸数组,比如 [2 4 8 16 32 64] img = double(img); [rows, cols] = size(img); fd = 0; logInvR = zeros(length(boxSizes), 1); logNr = zeros(length(boxSizes), 1); % 对每个盒子尺寸计算 for idx = 1:length(boxSizes) s = boxSizes(idx); Nr = 0; for i = 1:s:rows-s+1 for j = 1:s:cols-s+1 block = img(i:i+s-1, j:j+s-1); minVal = min(block(:)); maxVal = max(block(:)); % 灰度范围对应的盒子数量 n_ij = ceil((maxVal - minVal + 1) / s); Nr = Nr + n_ij; end end logNr(idx) = log(Nr); logInvR(idx) = log(1/s); end % 线性拟合求斜率 p = polyfit(logInvR, logNr, 1); fd = p(1); end实现的关键在于灰度维度的处理。图像灰度范围是0到255,而盒子在空间维的尺寸是s,所以覆盖灰度范围所需的“灰盒子”数量是ceil((max-min+1)/s)。有些实现里不用max-min,而是用整幅图的灰度范围归一化后再计算,这样做对灰度偏移不敏感,但对局部对比度差异的刻画会变弱。我的经验是保留局部max-min的方式,因为它能更好地反映局部纹理变化。
4.2 多尺度FD与多方向FD的扩展
单一分形维数只用一个斜率描述整个尺度范围的自相似性,会丢失大量信息。实际使用中可以分两段拟合:小尺度范围(s=1到4)拟合出“局部FD”,大尺度范围(s=8到32)拟合出“全局FD”,这两个特征分别对应细节纹理和宏观结构。另外,对行方向和列方向分别计算FD,可以得到水平方向和垂直方向的分形特性,对具有方向性的纹理特别有效。
我在实际项目中发现,FD对于灰度渐变平缓的地形图、岩石截面图等自然纹理效果很好,而对人工规则纹理反而区分度不高。这是因为人工纹理的灰度表面并不满足自相似性假设。所以在选型时要注意这一点,不能一上来就无脑用FD。
4.3 Gabor滤波器组的参数设计与实现
Gabor滤波器本质上是正弦平面波乘以高斯包络,能够在频域上同时定位方向和尺度。一组不同方向和尺度的Gabor滤波器形成一个滤波器组,对图像滤波后,每个像素就得到一个高维响应向量,这些响应的统计值可以作为纹理特征。
设计Gabor滤波器组时,方向数通常取6或8个,尺度取3到5个。方向数太少,方向分辨率不够;尺度数太少,无法覆盖不同粗细的纹理。一个经典的配置是5个尺度、8个方向,共40个滤波器。每个滤波器的数学表达式为:
g(x,y) = exp(-(x'^2 + γ²y'^2) / (2σ²)) * cos(2πf x')
其中x'和y'是旋转后的坐标,f是正弦波的频率,σ是高斯包络的标准差,γ是空间纵横比。频率通常按等比数列取值,从较高频到较低频递减。
function [gaborBank, responses] = buildGaborBank(img, numScales, numOrientations) % 构建Gabor滤波器组并对图像滤波 [rows, cols] = size(img); img = double(img); gaborBank = cell(numScales, numOrientations); responses = cell(numScales, numOrientations); % 频率范围:高频到低频 minWave = 2; maxWave = min(rows, cols) / 4; waves = logspace(log10(minWave), log10(maxWave), numScales); for s = 1:numScales frequency = 1 / waves(s); for o = 1:numOrientations theta = (o-1) * pi / numOrientations; gaborBank{s, o} = gaborFilter(frequency, theta, 0.5, 0.5); responses{s, o} = imfilter(img, gaborBank{s, o}, 'replicate'); end end end function gb = gaborFilter(frequency, theta, sigma, gamma) % 生成单个Gabor滤波器 sigma = sigma / frequency; sizeKernel = ceil(3 * sigma); [x, y] = meshgrid(-sizeKernel:sizeKernel, -sizeKernel:sizeKernel); xr = x * cos(theta) + y * sin(theta); yr = -x * sin(theta) + y * cos(theta); gb = exp(-(xr.^2 + gamma^2 * yr.^2) / (2*sigma^2)) .* cos(2*pi*frequency*xr); gb = gb - mean(gb(:)); gb = gb / norm(gb(:)); end4.4 Gabor特征提取的实操经验
Gabor滤波后的响应值通常是零均值的,直接用绝对值或平方值做统计更合理。我通常对每个滤波响应图计算均值、标准差和能量。这样每个滤波器产生3个特征,40个滤波器共120个特征。如果觉得维度太高,可以只取均值和标准差,共80维。
一个关键参数是σ的选择。σ决定高斯包络的宽度,直接影响滤波器的频率选择性。σ太大,滤波器对频率不敏感,方向选择性也变差;σ太小,滤波器有效窗口太小,对低频信息无法覆盖。我一般把σ设为一个与频率成反比的值,保证不同频率下滤波器的时域支撑大致相同,这也是上面代码里用sigma = sigma / frequency的原因。
另外,Gabor滤波器组生成后建议可视化检查一下滤波器频谱覆盖情况,确保方向均匀分布、频率没有重叠空隙。这一步虽然简单,但能避免方向数设置不合理导致某些角度纹理完全未被响应的问题。
5. 六种方法的统一封装与特征向量组合
5.1 统一的特征提取接口设计
实际项目中不太可能只用一种方法,更常见的是把几种方法组合起来形成综合特征向量。为了方便切换和组合,我建议把所有方法封装到一个统一的函数接口里。这个设计参考了常见的“特征工厂”模式,但对MATLAB来说不需要过度设计,一个结构体参数加switch分支就足够了。
function featureVector = extractTextureFeatures(img, method, params) % 统一纹理特征提取接口 % method: 'GLCM', 'GLDS', 'LBP', 'GMRF', 'FD', 'Gabor' if size(img, 3) == 3 img = rgb2gray(img); end switch upper(method) case 'GLCM' featureVector = extractGLCMFeatures(img, params); case 'GLDS' featureVector = extractGLDSFeatures(img, params); case 'LBP' featureVector = extractLBPFeatures_self(img, params); case 'GMRF' featureVector = estimateGMRF(img, params.order); case 'FD' featureVector = computeFD(img, params.boxSizes); case 'GABOR' [~, responses] = buildGaborBank(img, params.scales, params.orientations); featureVector = extractGaborStats(responses); otherwise error('未知的特征方法'); end end每个方法的具体特征计算函数内部实现已经在前面分别给出了。这样设计的好处是,在后续的交叉验证、特征选择、分类器训练环节,代码可以完全复用,只需要切换method名称即可。
5.2 特征向量的标准化与融合策略
不同方法得到的特征取值范围差异很大。GLCM的能量在0到1之间,Gabor响应均值可能高达几百,GMRF参数可能是负值,如果不做标准化,直接拼接到一起,数值范围大的特征会在距离计算中占绝对主导,小数值特征几乎被淹没。
我通常使用z-score标准化,把每个特征维度的均值和标准差计算出来后,统一变换到零均值单位方差:
function featNorm = zscoreFeatures(featMatrix) mu = mean(featMatrix, 1); sigma = std(featMatrix, 0, 1); % 防止除零 sigma(sigma == 0) = 1; featNorm = (featMatrix - mu) ./ sigma; end特征融合时,一定要记录每一段特征在总向量中的起始位置和结束位置。这样在后续做特征重要性分析或消融实验时,才能知道是哪个方法的哪个特征对分类结果贡献最大。这个习惯帮我解决过不少模型调优的难题。
5.3 代码性能优化:向量化替代循环
MATLAB中最大的性能杀手就是循环。我前面为了讲清楚原理给出了循环实现,但在实际批量处理时,需要改用向量化写法。以LBP为例,可以用像素平移操作一次性完成8个邻域的差分计算,速度可以提升几十倍:
function lbpImg = computeLBPFast(img) img = double(img); [rows, cols] = size(img); % 对称填充 imgPad = padarray(img, [1 1], 'symmetric'); lbpImg = zeros(rows, cols); offsets = [-1 -1; -1 0; -1 1; 0 1; 1 1; 1 0; 1 -1; 0 -1]; for k = 1:8 rOff = offsets(k,1) + 1; cOff = offsets(k,2) + 1; shifted = imgPad((1:rows)+rOff, (1:cols)+cOff); lbpImg = lbpImg + (shifted >= img) * 2^(k-1); end end这里用padarray的'symmetric'做镜像填充替代边界判断,用矩阵切片替代遍历,代码更简洁、运算更快。在几千张图像上做批量处理时,这类优化非常值得做。
6. 参数调优、常见问题与排查技巧
6.1 关键参数速查表
这里列一个我多次调参后的经验值表,适合大多数图像纹理分析场景,可以直接作为初始参数使用:
| 方法 | 参数 | 推荐值 | 备注 |
|---|---|---|---|
| GLCM | 灰度级L | 16~32 | 过大稀疏,过小丢失信息 |
| GLCM | 距离d | 1~3 | 先试1,效果不佳再增大 |
| GLCM | 方向θ | 0,45,90,135 | 四个方向取平均 |
| GLDS | 灰度级Lf | 16 | 与GLCM一致 |
| GLDS | 梯度级Lg | 16 | 梯度归一化方式须固定 |
| LBP | 采样点数P | 8~24 | 随半径增大而增大 |
| LBP | 半径R | 1~3 | 纹理粗糙时加大 |
| GMRF | 邻域阶数 | 2 | 四方向或八方向 |
| FD | 盒子尺寸范围 | [2 4 8 16 32 64] | 覆盖图像尺寸的1/64到1/2 |
| Gabor | 尺度数 | 4~5 | 高频到低频等比分布 |
| Gabor | 方向数 | 6~8 | 均匀覆盖0~π |
这些参数不是死标准,而是起点。对于纹理单元较大或较小的图像,需要结合实际可视化效果调整。
6.2 常见问题与解决方案
第一个高频问题是计算结果出现NaN。GMRF参数估计时如果A' A矩阵接近奇异,最小二乘解会不稳定。解决办法是加入正则化项,或者检查图像是否包含大面积的均匀区域——均匀区域中邻域像素与中心像素高度相关,导致矩阵奇异。另外,GLCM和GLDS归一化时如果所有像素都一样,归一化也会出问题,需要做除零保护。
第二个问题是特征区分度不高。常见原因不是算法本身,而是灰度预处理没做好。光照不均匀时,建议先做直方图均衡化或高斯差分预处理;噪声较重时,建议先用中值滤波去噪再提取特征。我在做纹理分类实验时,经常发现预处理的影响比特征选择还大。
第三个问题是提取速度太慢。批量处理时优先考虑向量化,然后考虑并行化。MATLAB的parfor可以在多个图像之间并行提取特征,前提是每次迭代之间没有数据依赖。另外,GLCM的灰度级从256降到16后,速度提升可达数十倍,这是最直接有效的优化手段。
第四个问题是我多次踩过的坑:特征向量维度不一致。比如Gabor滤波器的方向数改了,特征维度就变了;LBP模式数从256变成59,维度也变了。特征维度一变,后面训练好的分类器全部要重来。所以实验一开始就要冻结特征配置,任何参数调整都记录在日志里。
6.3 方法的组合策略与实战建议
单种方法的特征往往不够全面,但也不是方法越多越好。特征维度太高会带来维度灾难,分类器训练慢且容易过拟合。我常用的策略是“两个方法打底,一个方法增强”:GLCM加LBP作为基础特征,覆盖统计和局部结构信息;如果效果不够,再叠加Gabor或GMRF作为增强特征。
在分类器选择上,纹理特征向量通常维度不高,线性SVM或随机森林都能取得不错效果。如果用的是深度学习特征做对照实验,纹理特征依然是很好的baseline。我在一个布料瑕疵检测项目中,用GLCM+LBP的组合特征配合线性SVM,在4000张图像上达到了96%的准确率,训练时间不到1分钟,这个性价比是深度学习方案很难比拟的。
在实际项目中,我强烈建议把前面封装的extractTextureFeatures函数配合交叉验证框架一起使用。每换一种特征方法或一组参数,就重新跑一遍交叉验证,用分类准确率来客观评估特征的有效性。这样做虽然每一轮实验要多花一点时间,但能避免“感觉某个特征好像有效”的主观误判。我见过太多人在调参上凭感觉,结果浪费了大量时间。
六种方法中,如果时间有限只能掌握三种,我会优先推荐GLCM、LBP和Gabor。GLCM是纹理特征的经典代表,几乎所有纹理相关的论文都会用到,理解它对于读懂文献至关重要。LBP是工程落地最实用的方法,计算快、效果稳、可解释性强。Gabor则提供了频率和方向的精细刻画能力,在三者中理论性最强。把这三个吃透,再回头学GLDS、GMRF和FD,会觉得轻松很多。
最后再分享一个我在实际使用中的小技巧:特征提取完之后,先别急着训练分类器,花一两分钟把特征做一下可视化。用gscatter或tsne看看不同类别的样本在特征空间中是否可分,比直接闷头跑分类器高效得多。如果特征可视化下类别重叠严重,不管你怎么调分类器参数,效果都不会好到哪里去。这个习惯帮我过滤掉了大量无效的特征组合,也让我在项目汇报时能拿出直观有力的证据。
本文还有配套的精品资源,点击获取