灰度共生矩阵(GLCM)原理与Python实现:从纹理分析到特征提取

📅 2026/7/29 16:41:59
灰度共生矩阵(GLCM)原理与Python实现:从纹理分析到特征提取
1. 从“看山是山”到“看山是纹理”为什么需要灰度共生矩阵我们每天都在处理图像无论是手机拍照、刷短视频还是做设计、搞科研。很多时候我们评价一张图片会说“这张图很清晰”、“那张图很模糊”或者“这个材质看起来很有质感”。这里的“质感”在图像处理领域有一个更专业的词叫做纹理。纹理是什么它不是颜色也不是亮度而是像素之间的一种空间排列规律。想象一下一块光滑的大理石板和一块粗糙的砂纸即使它们的平均亮度相同我们也能一眼分辨出来靠的就是纹理。再比如一片森林的卫星图像和一片沙漠的颜色可能相近但森林的像素点分布杂乱、有规律沙漠则相对均匀这种差异也是纹理。那么计算机如何“看见”并量化这种纹理呢这就是灰度共生矩阵要解决的问题。GLCM全称Gray-Level Co-occurrence Matrix是上世纪70年代由Haralick等人提出的一种经典纹理分析方法。它的核心思想非常直观不孤立地看单个像素的灰度值而是看一对像素共生对的灰度值组合在特定空间关系下出现的频率。这就像我们看一幅点彩画。如果只看一个色点你只能知道它是红色还是蓝色。但如果你统计“在它右边1厘米处出现另一个红色点”这种情况在整个画布上出现了多少次你就能量化出这幅画在水平方向上的“红色连续性”或“红色聚集程度”。GLCM做的就是这个统计工作只不过把“色点”换成了“灰度级”把“1厘米”换成了特定的距离和方向。为什么这个方法历经半个世纪依然经典因为它将抽象的“纹理感觉”转化为了一个具体的、可计算的矩阵。从这个矩阵中我们可以进一步提取出诸如“对比度”、“能量”、“熵”、“同质性”等十几个量化特征。这些特征就是让计算机理解图像纹理的“语言”。在遥感领域GLCM特征用于区分林地、农田、水体在医学影像中它帮助鉴别肿瘤组织的纹理异质性在工业检测里它用来发现产品表面的划痕、瑕疵甚至在艺术分析中也能辅助鉴定画作的笔触风格。可以说只要你的问题涉及到“模式”、“结构”、“粗糙度”GLCM很可能就是一个有力的工具。很多人初次接触GLCM会觉得公式复杂、概念抽象从而望而却步。其实它的内核非常朴素。接下来我将抛开复杂的数学外衣用最直白的思路和可运行的Python代码带你从零构建GLCM并亲手计算出那些强大的纹理特征。你会发现它不过是一个“数数”的游戏但数出来的结果却能揭示图像的深层秘密。2. 拆解GLCM一个“数对子”的统计游戏理解GLCM最关键的一步是忘掉矩阵先理解“共生”这个概念。我们通过一个极度简化的例子来建立直觉。假设我们有一张4x4的微型图像其灰度值已经被量化为只有0, 1, 2三个级别0最黑2最白。图像数据如下1, 1, 0, 0 1, 1, 0, 0 2, 2, 1, 1 2, 2, 1, 1现在我们定义一种空间关系一个像素和它正右侧的像素。用参数表示就是距离d1方向θ0°水平向右。我们要为这种关系建立一个GLCM。怎么建就是遍历图像中的每一个像素作为“参考像素”找到它右边距离为1的那个像素作为“邻居像素”记录下这一对像素的灰度值(i, j)其中i是参考像素的灰度值j是邻居像素的灰度值。然后在整个图像上统计每一对(i, j)出现的次数。让我们手动数一下从左上角(0,0)的像素1开始它右边的像素是1所以我们记录到一对(1, 1)。接着是(0,1)的像素1右边是0记录(1, 0)。(0,2)的像素0右边是0记录(0, 0)。第一行最后一个像素(0,3)的0右边没有像素了跳过。第二行同理会得到(1,1),(1,0),(0,0)。第三行(2,2)的2右边是2记录(2,2)2右边是1记录(2,1)1右边是1记录(1,1)。第四行同理得到(2,2),(2,1),(1,1)。现在我们来统计所有可能的(i, j)对出现的次数。因为灰度级有0,1,2所以我们的矩阵是3x3的。统计结果如下(0,0)出现了 2次来自第一、二行的第三个位置(0,1)出现了 0次(0,2)出现了 0次(1,0)出现了 2次来自第一、二行的第二个位置(1,1)出现了 4次来自第一、二行的第一个位置第三、四行的第三个位置(1,2)出现了 0次(2,0)出现了 0次(2,1)出现了 2次来自第三、四行的第二个位置(2,2)出现了 2次来自第三、四行的第一个位置把这个统计结果填入矩阵其中行索引i代表参考像素灰度值列索引j代表邻居像素灰度值就得到了我们的GLCMi\j012020012402022这个矩阵就是GLCM。它的大小由图像的最大灰度级决定。矩阵中的每个元素P(i, j)表示在定义的方位关系下灰度值为i和j的像素对出现的次数。注意在实际应用中我们通常会将这个“次数矩阵”转换为“概率矩阵”即每个元素除以所有元素之和使得矩阵所有元素之和为1。这消除了图像大小对统计量的影响便于不同图像间的比较。上面的矩阵总和是12所以概率矩阵中P(1,1)4/12≈0.333。空间关系的多样性我们刚才只用了(d1, θ0°)这一种关系。实际上d和θ是可以变化的。常见的θ有0°, 45°, 90°, 135°四个方向。对于每个方向我们都可以计算出一个GLCM。因此描述一个图像的纹理我们往往会得到一组GLCM例如4个方向各一个。矩阵的对称性在上面的统计中我们只考虑了“参考像素→邻居像素”这一个顺序。但有时我们也会考虑“邻居像素→参考像素”即(j, i)对。更常见的做法是在统计时同时考虑这两种顺序即无方向性这样得到的GLCM是对称矩阵。具体实现时可以在统计完(i, j)后也累加一次(j, i)到矩阵中或者直接使用P(i, j) P(j, i)。很多库的默认设置就是生成对称的GLCM。理解了这个“数对子”的过程GLCM就不再神秘。它本质上是一个条件概率的统计表反映了在某种空间构型下某种灰度值组合出现的可能性。图像纹理越粗糙、对比越强烈GLCM的数值就越容易集中在主对角线两侧图像纹理越均匀、平滑GLCM的数值就越容易集中在主对角线上。3. 从矩阵到特征Haralick特征的物理意义与计算得到了GLCM概率矩阵P(i, j)之后我们面对的是一个Ng x Ng的矩阵Ng是灰度级数。直接使用这个矩阵作为特征维度太高且不直观。因此Haralick等人定义了14个从GLCM中推导出的标量特征用来量化纹理的不同属性。这里我们重点讲解最常用、物理意义最明确的5个。在计算之前有一个至关重要的预处理步骤灰度级压缩。如果原图是8位灰度图灰度级有256级那么GLCM就是256x256的矩阵不仅计算量大而且矩阵会非常稀疏很多位置是0统计不稳定。通常我们会通过线性或非线性变换将灰度级压缩到更少的级别例如8级、16级或32级。这是GLCM应用中非常关键且容易被忽略的一步直接影响到特征的稳定性和区分度。假设我们已经得到了一个归一化的元素之和为1、灰度级为Ng8的GLCM概率矩阵P。下面我们来计算特征我会给出数学公式和对应的Python实现思路。3.1 对比度公式Contrast Σ Σ |i-j|² * P(i, j)物理意义衡量图像的清晰度和纹理的沟壑深浅。对比度值大说明纹理反差大图像局部变化剧烈视觉效果清晰、棱角分明。想象一下锐利的岩石边缘与平滑的水面岩石的对比度特征值会远大于水面。计算解读|i-j|是像素对的灰度差。这个公式对灰度差大的像素对赋予了更大的权重平方。因此如果图像中有很多亮度差异很大的像素紧挨着比如黑白棋盘格那么对比度值就会非常高。Python计算核心contrast 0.0 for i in range(Ng): for j in range(Ng): contrast ((i - j) ** 2) * P[i, j]3.2 能量公式Energy Σ Σ P(i, j)²物理意义也称为“角二阶矩”。它反映了图像纹理的均匀程度和粗糙度。能量值高表明GLCM中的元素分布非常集中可能大量集中于主对角线附近意味着图像灰度分布均匀、纹理粗糙、变化缓慢。一块纯色布料的能量值会接近1。计算解读由于P(i, j)是概率值平方之后会更小。只有当某些P(i, j)值本身很大时其平方和才会大。所以能量是“主导元素”强度的度量。Python计算核心energy 0.0 for i in range(Ng): for j in range(Ng): energy P[i, j] ** 23.3 熵公式Entropy - Σ Σ P(i, j) * log(P(i, j))(约定0*log(0)0)物理意义衡量图像纹理的随机性或混乱程度。熵值越大表示GLCM中元素分布越均匀图像纹理越复杂、越无序。一片杂乱无章的灌木丛的熵值会很高。它与能量特征通常呈负相关。计算解读信息论中的概念。概率分布越平均信息熵越大。如果GLCM中只有一个元素为1其余为0极度均匀的图像熵为0如果所有元素都相等极度混乱的图像熵最大。Python计算核心entropy 0.0 for i in range(Ng): for j in range(Ng): if P[i, j] 0: # 避免log(0) entropy - P[i, j] * np.log(P[i, j])3.4 同质性公式Homogeneity Σ Σ P(i, j) / (1 |i-j|)物理意义也称为“逆差矩”。衡量图像纹理的局部均匀性。同质性值高说明纹理局部变化小GLCM中的元素主要集中在对角线附近|i-j|小。光滑的表面同质性高。计算解读分母中的(1|i-j|)使得灰度差越大的像素对其贡献权重越小。因此这个特征对主对角线附近的元素非常敏感。Python计算核心homogeneity 0.0 for i in range(Ng): for j in range(Ng): homogeneity P[i, j] / (1 abs(i - j))3.5 相关性公式Correlation Σ Σ [ (i - μ_i)(j - μ_j) * P(i, j) ] / (σ_i * σ_j)其中μ_i Σ i * P(i, j),μ_j Σ j * P(i, j),σ_i² Σ (i - μ_i)² * P(i, j),σ_j² Σ (j - μ_j)² * P(i, j)物理意义衡量GLCM中行元素和列元素之间的线性依赖关系。可以理解为图像在指定方向上像素灰度值的线性关联程度。如果图像在该方向上有明显的线性结构如条纹相关性会较高。计算解读这是概率论中标准相关系数公式在二维联合概率分布上的应用。计算稍复杂需要先求出行和列的边际分布及它们的均值和标准差。Python计算核心# 计算边际概率和均值 px np.sum(P, axis1) # 行和 P(i) py np.sum(P, axis0) # 列和 P(j) ux np.sum(np.arange(Ng) * px) uy np.sum(np.arange(Ng) * py) # 计算标准差 sigmax np.sqrt(np.sum((np.arange(Ng) - ux) ** 2 * px)) sigmay np.sqrt(np.sum((np.arange(Ng) - uy) ** 2 * py)) # 计算相关性 correlation 0.0 if sigmax * sigmay 0: # 避免除零 for i in range(Ng): for j in range(Ng): correlation ((i - ux) * (j - uy) * P[i, j]) / (sigmax * sigmay) else: correlation 1 # 如果标准差为0说明灰度恒定定义为完全相关这五个特征是最常被使用的它们从不同侧面刻画了纹理。在实际项目中我们通常会计算多个方向如0°, 45°, 90°, 135°的GLCM然后对每个特征取各个方向上的均值或最大值作为最终的特征值以得到旋转不变的纹理描述。4. 手把手实现从图片到特征向量的完整Python代码理论说得再多不如亲手跑一遍代码来得实在。这一部分我们将抛开任何高级图像处理库中可能封装好的GLCM函数从最底层开始用NumPy和PIL或OpenCV实现整个流程。我会详细解释每一步的意图和注意事项。4.1 环境准备与图像读取首先确保你的环境安装了必要的库。我们使用Pillow读取图片NumPy进行矩阵运算。pip install Pillow numpy然后我们写一个函数来读取图像并转换为灰度图。这是所有计算的基础。import numpy as np from PIL import Image def load_and_grayscale(image_path): 加载图像并转换为灰度图。 参数: image_path: 图像文件路径。 返回: gray_img: 二维NumPy数组代表灰度图像值范围[0, 255]。 img Image.open(image_path).convert(L) # L模式表示8位灰度图 gray_img np.array(img) return gray_img4.2 核心GLCM计算函数这是整个教程最核心的部分。我们将实现一个函数根据指定的距离、方向和灰度级计算GLCM。def compute_glcm(image, d1, theta0, gray_levels16, symmetricTrue, normalizedTrue): 计算灰度共生矩阵。 参数: image: 二维NumPy数组输入灰度图像。 d: 整数像素对之间的距离像素单位。 theta: 浮点数方向角度度。0右45右上90上135左上。 gray_levels: 整数将灰度压缩到的级别数通常为8, 16, 32。 symmetric: 布尔值是否生成对称GLCM。 normalized: 布尔值是否将GLCM归一化为概率矩阵。 返回: glcm: 二维NumPy数组形状为(gray_levels, gray_levels)的GLCM。 # 1. 灰度级压缩 # 将[0, 255]的灰度值线性映射到[0, gray_levels-1] max_val image.max() # 避免除零同时进行线性缩放和取整 compressed ((image.astype(np.float32) / max_val) * (gray_levels - 1)).astype(np.int32) # 确保值在有效范围内 compressed np.clip(compressed, 0, gray_levels - 1) # 2. 根据角度计算偏移量(dx, dy) # 注意图像坐标系原点在左上角y轴向下x轴向右。 # 因此角度需要对应转换。 theta_rad np.deg2rad(theta) dx int(round(d * np.cos(theta_rad))) # x方向偏移 dy int(round(d * np.sin(theta_rad))) # y方向偏移 # 由于y轴向下我们通常用 -dy但这里根据常见定义我们保持dy为正表示向下。 # 在图像处理中0度通常指向右90度指向下正y方向。 # 为了符合习惯0度右90度下我们直接使用计算出的dx, dy。 # 3. 初始化GLCM矩阵 glcm np.zeros((gray_levels, gray_levels), dtypenp.float64) # 4. 获取图像尺寸并计算有效的像素对区域 rows, cols compressed.shape # 参考像素的有效范围要保证邻居像素仍在图像内 start_row max(0, -dy) if dy 0 else 0 end_row rows - dy if dy 0 else rows start_col max(0, -dx) if dx 0 else 0 end_col cols - dx if dx 0 else cols # 5. 遍历有效区域统计共生对 for i in range(start_row, end_row): for j in range(start_col, end_col): ref_val compressed[i, j] neighbor_val compressed[i dy, j dx] glcm[ref_val, neighbor_val] 1 # 6. 如果要求对称则加上转置 if symmetric: glcm glcm glcm.T # 7. 如果要求归一化则转换为概率 if normalized: glcm_sum glcm.sum() if glcm_sum 0: glcm glcm / glcm_sum else: # 如果矩阵全零理论上不会发生则返回零矩阵 pass return glcm关键点解析与避坑指南灰度级压缩(image / max_val) * (gray_levels - 1)这个线性映射是常用方法。但这里有个坑如果图像对比度很低max_val可能很小导致压缩后动态范围依然很窄。更稳健的做法是使用np.percentile或直接设定固定的缩放区间如2%和98%分位数之间的值映射到[0, gray_levels-1]或者使用直方图均衡化后再压缩。这里为了简单采用了线性映射。坐标偏移计算角度到(dx, dy)的转换需要小心图像坐标系y轴向下。我们使用标准数学坐标系x向右y向上计算然后应用到图像上。对于theta90°(dx0, dy1)表示参考像素和它正下方的像素组成一对这符合“90度方向”的常见定义。遍历边界处理start_row, end_row等变量的计算是为了确保(idy, jdx)这个索引不会超出图像边界。这是实现中容易出错导致索引越界的地方。对称化处理glcm glcm glcm.T这一步将矩阵与其转置相加使得P(i, j) P(j, i)。这相当于在统计时同时考虑了(i, j)和(j, i)这对顺序使得矩阵对称得到的纹理特征具有方向无关性对于某些特征。这是很多应用中的默认做法。4.3 特征提取函数基于第3章讲解的公式我们实现特征提取函数。这里我们计算对比度、能量、熵、同质性四个最常用的特征。def glcm_features(glcm): 从GLCM中提取Haralick纹理特征。 参数: glcm: 归一化的灰度共生矩阵。 返回: features: 字典包含计算出的特征值。 # 检查glcm是否全零理论上归一化后不会但安全起见 if np.sum(glcm) 0: return {contrast: 0, energy: 0, entropy: 0, homogeneity: 0} Ng glcm.shape[0] # 灰度级数 i, j np.ogrid[:Ng, :Ng] # 创建网格索引用于向量化计算 # 注意i和j是二维数组i代表行索引参考像素灰度j代表列索引邻居像素灰度 # 1. 对比度 contrast np.sum((i - j) ** 2 * glcm) # 2. 能量 / 角二阶矩 energy np.sum(glcm ** 2) # 3. 熵 # 避免log(0)只对非零元素计算 entropy_elements glcm * np.log(glcm (glcm 0)) # glcm0的位置log参数为1结果为0 entropy -np.sum(entropy_elements) # 4. 同质性 / 逆差矩 homogeneity np.sum(glcm / (1 np.abs(i - j))) features { contrast: contrast, energy: energy, entropy: entropy, homogeneity: homogeneity } return features代码优化技巧使用np.ogrid创建网格索引i和j然后利用NumPy的广播机制进行向量化运算这比用双重for循环快几个数量级。熵的计算中glcm (glcm 0)是一个小技巧。(glcm 0)得到一个布尔矩阵在数值运算中True为1False为0。这样当glcm为0时log的参数变为1log(1)0整个项为0避免了log(0)的警告或错误。4.4 整合与实战分析一张纹理图片现在我们把所有函数整合起来用一张真实的图片做测试。我们使用一张布纹图片。def analyze_texture(image_path, distances[1], angles[0, 45, 90, 135], gray_levels16): 分析图像纹理的主函数。 参数: image_path: 图片路径。 distances: 距离列表。 angles: 角度列表度。 gray_levels: 灰度级数。 返回: all_features: 列表包含每个(d, angle)组合下的特征字典。 avg_features: 字典所有方向和距离下各特征的平均值。 # 1. 加载图像 img_gray load_and_grayscale(image_path) print(f图像加载成功尺寸: {img_gray.shape} 灰度范围: [{img_gray.min()}, {img_gray.max()}]) all_features [] feature_sums {contrast: 0.0, energy: 0.0, entropy: 0.0, homogeneity: 0.0} count 0 # 2. 遍历所有距离和角度组合 for d in distances: for angle in angles: # 计算GLCM glcm compute_glcm(img_gray, dd, thetaangle, gray_levelsgray_levels) # 提取特征 features glcm_features(glcm) features[distance] d features[angle] angle all_features.append(features) # 累加特征值用于求平均 for key in feature_sums.keys(): feature_sums[key] features[key] count 1 # 3. 计算平均特征 avg_features {key: feature_sums[key] / count for key in feature_sums} return all_features, avg_features # 使用示例 if __name__ __main__: # 替换为你的图片路径 image_path fabric_texture.jpg # 示例一张布纹图片 try: detailed_features, avg_result analyze_texture(image_path) print(\n--- 详细特征每个方向 ---) for feat in detailed_features: print(f距离 {feat[distance]}, 角度 {feat[angle]}°: f对比度{feat[contrast]:.4f}, f能量{feat[energy]:.4f}, f熵{feat[entropy]:.4f}, f同质性{feat[homogeneity]:.4f}) print(\n--- 平均纹理特征 ---) for key, value in avg_result.items(): print(f{key}: {value:.4f}) except FileNotFoundError: print(f错误找不到图片文件 {image_path}请检查路径。) except Exception as e: print(f处理过程中发生错误: {e})运行这段代码你会得到四个方向上的纹理特征值以及它们的平均值。对于纹理明显的图片如木材、织物、云层你会看到不同方向的特征值可能有差异各向异性而平均特征则给出了一个整体的纹理描述。5. 参数调优与实战经验让GLCM真正为你所用代码跑通了只是第一步。要让GLCM特征在实际项目如分类、分割、检索中发挥最大效用参数的选择和预处理至关重要。这部分是我在实际项目中踩过坑后总结的经验。5.1 关键参数深度解析灰度级数这是最重要的参数之一。值太小如4会丢失大量灰度细节导致纹理信息过度简化不同纹理可能被映射到相同的GLCM区分度下降。值太大如64或256GLCM矩阵会变得非常大且稀疏计算量激增并且由于统计样本相对不足图像像素数有限矩阵中会出现很多零值使得特征估计不稳定噪声敏感。经验值对于大多数自然图像16级是一个很好的起点它在计算复杂度和信息保留之间取得了平衡。对于非常平滑或纹理简单的图像可以尝试8级对于高分辨率、纹理极其丰富的图像如显微图像可以考虑32级。务必在你的数据集上进行交叉验证选择表现最好的级别。距离d1考察的是最相邻像素之间的关系描述的是微观纹理、细粒度结构。d增大考察的是更大尺度上的像素关系描述的是宏观纹理、粗粒度结构。例如对于周期性纹理如格子布当d等于纹理周期时GLCM会表现出特定的模式。如何选择没有固定答案。通常的做法是尝试一组距离如[1, 2, 3, 4]然后计算每个距离下的特征将所有特征拼接成一个长特征向量。这叫做多尺度纹理分析能同时捕捉不同尺度的纹理信息。方向默认计算[0°, 45°, 90°, 135°]四个方向然后对每个特征取平均值。这是为了获得旋转不变性即无论图像如何旋转纹理特征值大致不变。在某些应用中纹理的方向性本身就是重要信息如检测木材纹理走向。这时可以保留各个方向的特征或者取最大值/最小值来表征最强的方向性响应。图像区域GLCM统计的是全局信息。如果图像包含多种纹理如一幅风景画中有天空、树木、地面全局GLCM会混合所有信息失去区分力。标准做法是使用滑动窗口将图像划分为许多重叠或非重叠的小块如64x64像素对每个小块计算GLCM特征。这样每个小块得到一个特征向量整张图就变成了一个特征图或一组特征向量可用于像素级或区域级的纹理分析。5.2 预处理比想象中更重要灰度标准化/均衡化直接使用原始灰度值计算GLCM其值受光照影响极大。同一纹理在亮光和暗光下GLCM会完全不同。因此必须先进行光照归一化。常用方法有直方图均衡化增强对比度使灰度分布更均匀对光照变化有一定鲁棒性。自适应直方图均衡化效果更好能增强局部对比度。更简单的方法对每个图像块进行零均值单位方差标准化即(patch - mean(patch)) / std(patch)。这能有效消除整体亮度和对比度的影响。滤波去噪图像噪声尤其是椒盐噪声、高斯噪声会严重污染GLCM因为噪声点会创造出原本不存在的灰度值组合。在计算GLCM前进行适度的平滑滤波如高斯滤波、中值滤波是很有必要的。但滤波强度不宜过大以免模糊掉真正的纹理边缘。5.3 一个完整的、鲁棒的纹理分析流程示例结合以上经验一个更健壮的纹理特征提取流程如下def robust_texture_feature_extraction(image_path, window_size64, step32, gray_levels16, distances[1, 3]): 鲁棒的纹理特征提取流程滑动窗口 预处理 多尺度。 参数: image_path: 图片路径。 window_size: 滑动窗口大小。 step: 滑动步长。 gray_levels: 灰度级。 distances: 多尺度距离列表。 返回: feature_matrix: 二维数组每行是一个窗口的特征向量。 window_locations: 每个窗口的左上角坐标列表。 from scipy.ndimage import gaussian_filter from skimage.exposure import equalize_hist # 可选需要scikit-image img_gray load_and_grayscale(image_path) rows, cols img_gray.shape # 1. 全局预处理轻度高斯滤波去噪 img_filtered gaussian_filter(img_gray.astype(np.float32), sigma0.5) features_list [] locations [] angles [0, 45, 90, 135] # 2. 滑动窗口遍历 for y in range(0, rows - window_size 1, step): for x in range(0, cols - window_size 1, step): window img_filtered[y:ywindow_size, x:xwindow_size] # 3. 窗口内预处理局部对比度归一化 (Local Contrast Normalization) window_mean np.mean(window) window_std np.std(window) if window_std 1e-6: # 避免除零 window_normalized (window - window_mean) / window_std else: window_normalized window * 0.0 # 4. 将归一化后的窗口值线性映射到[0, gray_levels-1] # 由于经过了零均值单位方差值域大致在[-3,3]我们将其缩放到[0, gray_levels-1] win_min, win_max window_normalized.min(), window_normalized.max() if (win_max - win_min) 1e-6: window_scaled ((window_normalized - win_min) / (win_max - win_min) * (gray_levels - 1)).astype(np.int32) else: window_scaled np.zeros_like(window_normalized, dtypenp.int32) window_scaled np.clip(window_scaled, 0, gray_levels - 1) window_feature_vector [] # 5. 多尺度、多方向GLCM特征计算 for d in distances: for angle in angles: glcm compute_glcm(window_scaled, dd, thetaangle, gray_levelsgray_levels) feats glcm_features(glcm) # 将四个特征值按顺序加入向量 window_feature_vector.extend([feats[contrast], feats[energy], feats[entropy], feats[homogeneity]]) # 特征向量长度 len(distances) * len(angles) * 4 features_list.append(window_feature_vector) locations.append((y, x)) return np.array(features_list), locations这个流程提取出的feature_matrix每一行代表图像中一个局部区域的纹理特征。这个矩阵可以直接用于训练分类器如SVM、随机森林进行纹理分类或者通过聚类来分析图像中不同的纹理区域。5.4 常见问题与排错特征值全是0或NaN检查你的图像数据。可能是图像加载失败全黑或全白或者灰度压缩后所有像素值相同导致GLCM除主对角线外全为0。添加数据有效性检查。计算速度慢对于大图像或小窗口、多尺度多方向的情况循环计算GLCM会非常慢。解决方案使用scikit-image库的greycomatrix和greycoprops函数它们是高度优化的C扩展速度极快。如果必须自己实现确保使用NumPy向量化操作避免Python层级的循环。考虑减少灰度级数、距离和方向的数量。特征区分度不高可能是参数不适合你的数据。尝试调整灰度级、距离并务必进行局部归一化。也可以考虑使用其他纹理特征描述子如LBP、Gabor滤波器作为补充或替代。GLCM是一个强大的工具但它不是万能的。它对于具有明显周期性或结构性纹理的图像效果很好但对于完全随机或语义性很强的“纹理”比如人脸效果可能有限。理解其原理和局限合理地选择参数和预处理方法才能让它在你具体的项目里真正发光发热。