图像表示与描述:从像素到可识别对象的特征提取实战
1. 从“像素堆”到“可理解对象”表示与描述到底在解决什么问题做图像处理的人迟早会撞上一堵墙前面几章把图像增强、滤波、边缘检测玩得再溜到了真正要让机器“看懂”图像里有什么的时候还是会发现手里只有一堆孤零零的像素点。边缘检测给你一堆亮线阈值分割给你一堆白色区域但这些区域是什么形状、有多大、朝哪个方向、跟旁边的区域是什么关系一概不知道。第十一章“表示和描述”就是来解决这个问题的——它负责把分割后的像素集合转化成计算机能进一步分析、比较、识别的结构化信息。打个比方前面几章像是把一块石头从山上敲下来、洗干净、切成块而表示和描述则是把这些石块打磨成有规格的砖头让后面的“建筑工人”分类器、识别算法能直接拿去盖房子。没有这一步再好的分割结果也只是一盘散沙。这一章的核心链条其实很清晰分割 → 表示 → 描述 → 识别。表示解决的是“用什么形式来刻画这个区域”描述解决的是“用什么特征来区分这个区域和其他区域”。两者一前一后缺一不可。表示选得不好描述再精细也是白搭描述选得不对表示再优雅也区分不开目标。适合谁来啃这一章如果你正在做缺陷检测、医学影像分析、遥感图像分类、工业零件计数、OCR预处理或者任何需要从图像中提取目标并做判断的项目这一章就是你的必修课。哪怕你只是用现成的深度学习模型理解表示和描述的原理也能帮你在数据预处理、后处理、结果解释上少踩很多坑。我见过太多项目分割做得漂漂亮亮结果卡在“怎么把区域变成特征向量”这一步最后硬生生用像素均值凑数效果自然惨不忍睹。这一章的内容就是帮你把这块短板补上。2. 表示把区域“说清楚”的几种主流方式2.1 外部特征与内部特征先搞清楚你要描述的是什么表示一个区域首先要明确你关心的是它的“外形”还是“内涵”。外部特征关注的是区域的边界比如边界的长短、弯曲程度、凹凸性内部特征关注的是区域覆盖的像素集合比如面积、灰度分布、纹理。这个区分非常关键因为它直接决定了后续描述子的选择。举个例子你要识别一批零件有没有缺角那外部特征就是重点边界描述子、傅里叶描绘子会更管用你要区分不同材质的区域那内部特征更靠谱灰度共生矩阵、区域灰度直方图才是正解。注意很多初学者一上来就堆特征外部内部混在一起结果特征维度爆炸分类器反而学不动。先想清楚任务需要什么再决定表示方式这是省时间的做法。2.2 边界追踪把散落的边缘点串成有序曲线分割后的边界往往是一堆无序的像素点直接拿来算特征很麻烦。边界追踪要做的就是把这些点按顺时针或逆时针串成一条闭合曲线。最常用的方法是摩尔邻域追踪法核心思路是从边界上某个已知点出发每次在8邻域里找下一个边界点保证始终“左手扶墙”或“右手扶墙”走。具体操作时起始点的选择会影响追踪结果。通常选最左上角的边界点作为起点然后按固定方向扫描。追踪过程中要记录每一步的方向码最终得到一条链码。链码的好处是它把二维的边界压缩成了一维的序列后续做旋转、缩放、匹配都方便很多。我实测下来摩尔追踪在二值图上非常稳但有两个坑一是噪声导致的孤立边界点会让追踪断掉所以追踪前最好先做一次小核的形态学闭运算二是如果区域有孔洞外边界和内边界要分开追踪别混在一起。2.3 链码用方向序列压缩边界信息链码的本质是把边界上相邻像素的方向用数字编码。4方向链码用0、1、2、3表示右、下、左、上8方向链码用0到7表示八个方向。显然8方向链码更精细实际项目里用得更多。链码有个很实用的性质起点归一化。同一条边界从不同起点开始追踪链码序列会循环移位。做匹配时可以把链码循环移位到某个规范位置比如最小数值序列这样就能消除起点的影响。另外旋转归一化可以通过对链码做差分来实现——一阶差分反映的是方向的变化而不是绝对方向所以旋转后差分序列基本不变。# 8方向链码的一阶差分示例 chain_code [0, 1, 2, 3, 4, 5, 6, 7, 0] diff [(chain_code[(i1) % len(chain_code)] - chain_code[i]) % 8 for i in range(len(chain_code))] # diff 就是旋转不变的一阶差分链码实操心得链码对噪声比较敏感边界上多一个毛刺链码就会多出一段。所以追踪前做一次边界平滑比如用高斯平滑后再二值化能显著提升链码质量。2.4 多边形逼近用最少的点抓住主要形状链码虽然紧凑但点数还是太多。多边形逼近的目标是用尽可能少的线段来近似边界同时保证误差在可接受范围内。最经典的是Douglas-Peucker算法先连接边界首尾找到离这条线最远的点如果距离超过阈值就保留这个点然后递归处理两段子边界。这个算法的效果高度依赖阈值的选择。阈值太小逼近结果跟原边界差不多没起到压缩作用阈值太大形状细节全丢了。我的经验是阈值取边界周长的1%到2%作为起点然后根据实际效果微调。对于规则零件1%通常够用对于形状复杂的生物组织可能要降到0.5%。多边形逼近之后边界的表示就从几百个点变成了十几个顶点后续算角度、边长、凸性都方便多了。2.5 标记与骨架另一种思路除了边界表示还有两类表示方式值得了解。标记是把区域内部用一组代表点来表示比如质心、中轴点。骨架则是把区域细化成一条单像素宽的线保留拓扑结构的同时去掉冗余的宽度信息。骨架提取常用中轴变换但原始中轴变换对边界噪声极其敏感一个小的凸起就能长出一条多余的骨架分支。实际项目中通常用形态学细化算法迭代地剥掉边界像素直到只剩单像素宽。骨架在字符识别、血管分析里用得很多因为字符和血管的本质结构就是线状的。3. 描述子让区域变得“可比较”的关键特征3.1 简单描述子面积、周长、圆形度、偏心率这些描述子计算简单但在很多任务里已经够用。面积就是区域像素数周长是边界像素数8连通和4连通的计算方式略有差异。圆形度定义为 (4\pi \times 面积 / 周长^2)完美圆形为1越不规则越接近0。偏心率则通过区域的惯性主轴来计算反映区域的狭长程度。这些描述子的组合能区分很多基本形状。比如一个圆形零件和一个方形零件圆形度差异很明显一个细长裂纹和一个圆形气孔偏心率差异很大。但要注意面积和周长对尺度敏感如果图像有缩放这两个特征会变需要做归一化。3.2 拓扑描述子孔洞数、连通分量数拓扑特征的好处是对形变不敏感。一个区域不管怎么拉伸、旋转只要不撕裂不粘连孔洞数欧拉数就不变。欧拉数 连通分量数 - 孔洞数这个特征在缺陷检测里特别有用——比如检测铸件气孔孔洞数直接就是关键指标。计算孔洞数的常用方法是先对区域做填充得到无孔洞的实心区域然后用实心区域减去原区域剩下的连通分量数就是孔洞数。这个方法实现简单但要注意填充算法要能正确处理复杂边界。3.3 纹理描述子灰度共生矩阵与能量、熵纹理是区域内部灰度空间分布的规律性。灰度共生矩阵GLCM的思路是统计一对像素在特定方向和距离上同时出现的频率。比如取距离为1、方向为0度统计所有“左边像素灰度i、右边像素灰度j”的组合次数形成一个矩阵。从GLCM可以导出几个关键特征能量矩阵元素平方和反映纹理均匀程度熵反映纹理复杂度对比度反映局部灰度变化剧烈程度相关性反映灰度线性关系。这些特征组合起来能有效区分粗糙表面和光滑表面、规则纹理和随机纹理。注意GLCM的维度是灰度级数的平方。如果原图是256级灰度矩阵就是256×256计算量和存储量都不小。实际项目中通常先把灰度量化到16级或32级再算GLCM效果损失不大但效率提升明显。3.4 不变矩旋转、缩放、平移都不怕矩描述子是我个人最推荐的一类区域描述方法。基本思想是用区域的灰度分布或二值分布计算各阶矩然后构造出对平移、缩放、旋转不变的组合量。Hu在1962年提出的七个不变矩是最经典的它们由二阶和三阶中心矩归一化组合而成。这七个矩的物理意义不太直观但用起来很省心不管目标怎么转、怎么缩放这七个值基本不变。在字符识别、零件分类里直接用这七个值作为特征向量喂给分类器往往就能得到不错的效果。import cv2 import numpy as np # 计算Hu不变矩 img cv2.imread(region.png, 0) _, binary cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) moments cv2.moments(binary) hu_moments cv2.HuMoments(moments).flatten() # 取对数压缩动态范围 hu_log -np.sign(hu_moments) * np.log10(np.abs(hu_moments) 1e-10)实操心得Hu矩的数值动态范围很大直接用作特征时通常要取对数。另外高阶矩对噪声敏感如果区域边界毛糙七阶矩里的高阶项会波动很大建议先做边界平滑。3.5 傅里叶描绘子频域里的形状特征把边界表示成复数序列x jy然后做傅里叶变换得到的系数就是傅里叶描绘子。低频系数描述整体形状高频系数描述细节。只保留前几十个低频系数就能在压缩数据的同时保留主要形状信息。傅里叶描绘子的一个优雅之处是旋转对应频域相位变化缩放对应幅度缩放起点变化对应线性相位。这意味着可以通过简单的归一化操作实现旋转、缩放、起点不变性。在形状检索任务里傅里叶描绘子是经典且有效的选择。4. 从理论到代码一个完整的区域分析实操流程4.1 场景设定与数据准备假设我们要做一个工业零件分类任务传送带上随机摆放几种不同形状的金属零件需要根据形状把它们分到不同料框。图像已经采集好背景比较干净零件和背景对比明显。这个场景的典型流程是预处理 → 分割 → 形态学清理 → 区域标记 → 表示与描述 → 分类。我们重点看后三步。4.2 分割与区域标记先用Otsu阈值分割把零件从背景里分出来然后做一次开运算去掉小噪点一次闭运算填补内部小孔。接着用连通分量分析给每个零件打上独立标记。import cv2 import numpy as np img cv2.imread(parts.png, 0) _, binary cv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) kernel np.ones((5,5), np.uint8) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) binary cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) num_labels, labels, stats, centroids cv2.connectedComponentsWithStats(binary) # num_labels包含背景实际零件数要减14.3 对每个区域提取描述子对每个连通区域我们提取一组组合特征面积、周长、圆形度、偏心率、Hu矩前四个、孔洞数。这些特征覆盖了尺寸、形状复杂度、拓扑结构三个维度。features [] for i in range(1, num_labels): mask (labels i).astype(np.uint8) * 255 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) cnt contours[0] area cv2.contourArea(cnt) perimeter cv2.arcLength(cnt, True) circularity 4 * np.pi * area / (perimeter ** 2) if perimeter 0 else 0 moments cv2.moments(cnt) hu cv2.HuMoments(moments).flatten()[:4] # 孔洞数填充后减去原区域 filled mask.copy() contours_h, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cv2.drawContours(filled, contours_h, -1, 255, -1) holes cv2.connectedComponents(filled - mask)[0] - 1 features.append([area, perimeter, circularity] list(hu) [holes])4.4 特征归一化与分类不同特征的量纲差异很大面积可能是几千圆形度是0到1之间。直接送进分类器会被大面积特征主导。所以要做标准化通常用Z-score归一化减去均值除以标准差。from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC X np.array(features) scaler StandardScaler() X_norm scaler.fit_transform(X) # 假设有标注好的训练数据 clf SVC(kernelrbf, C10, gammascale) clf.fit(X_train_norm, y_train)这套流程在零件种类不多、形状差异明显的情况下准确率通常能到90%以上。如果零件形状很接近就需要加入纹理特征或者用更精细的边界描述子。5. 踩坑实录表示与描述环节的常见问题与排查5.1 边界追踪断链怎么办最常见的原因是二值图上有孤立的边界像素或者细小的桥接。排查方法是先可视化边界追踪结果看断在哪里。解决手段包括追踪前做一次3×3的闭运算或者用形态学细化后再追踪。如果区域本身有多个连通分量要确保只对主区域追踪。5.2 不变矩数值不稳定Hu矩的高阶项在区域接近对称时会趋近于零取对数后会出现很大的负值。解决办法是加一个小的正则项如1e-10或者只用前四个矩。另外如果区域面积太小比如小于50像素矩的计算本身就不稳定建议先过滤掉过小的区域。5.3 纹理特征区分度不够GLCM特征对方向和距离参数很敏感。如果纹理有明确方向性要选对应的方向角如果方向不确定可以取多个方向的平均值。另外灰度量化级数太少会丢失纹理细节太多则计算量大16到32级是常用折中。5.4 特征维度爆炸一开始容易贪多把能算的特征全算上结果维度上百样本才几十个分类器直接过拟合。我的建议是先用少量核心特征面积、圆形度、偏心率、前四个Hu矩跑一版看混淆矩阵再针对容易混淆的类别补充特征。特征选择比特征堆砌重要得多。问题现象可能原因排查手段解决方案边界追踪中断噪声孤立点、细桥接可视化追踪路径闭运算、细化预处理不变矩波动大区域过小、边界毛糙检查区域面积和边界平滑度过滤小区域、边界平滑纹理特征区分差方向/距离参数不当尝试不同参数组合多方向平均、调整量化级数分类过拟合特征过多、样本过少看训练/验证准确率差距特征选择、降维、增样本旋转后匹配失败描述子不具备旋转不变性旋转测试图验证改用Hu矩或傅里叶描绘子最后分享一个小技巧在做形状匹配时如果发现Hu矩效果不理想可以试试把边界归一化到固定周长后再算傅里叶描绘子两者结合往往比单用其中一个更稳。我在几个零件分类项目里都是这么干的实测比纯Hu矩的误判率低不少。这个方向后续还可以往形状上下文、骨架图匹配等更结构化的描述方法扩展尤其是当目标形状复杂、部件之间有明确拓扑关系时这些方法比全局描述子更有优势。