首页 / 资讯中心 / 文章详情

图像表示方法详解:边界跟踪、链代码与PCA主成分描述

图像表示方法详解:边界跟踪、链代码与PCA主成分描述 ★ FEATURED ARTICLE
做图像处理项目时最容易被忽略、却最容易在后期拖后腿的一步往往是把目标“描述清楚”。我之前做一个零件分拣的模拟项目拿到二值图像后要提取轮廓、量化形状、跟标准件比对。那时就发现边界跟踪、链代码、主成分描述这些图像表示方法不是教科书里躺着的概念而是真正决定算法能不能稳定跑起来的底层能力。本文就把这几个模块串起来讲透边界跟踪怎么把轮廓变成有序点列链代码怎么用一串0到7的数字紧凑表示轮廓主成分描述又怎么从一堆边界坐标里提炼出朝向、长轴、短轴这类骨架级参数并且附上可直接运行的Matlab代码。无论你是正在做数字图像处理课设还是搞机器视觉特征工程这套内容都能直接抄作业。1. 图像表示的整体思路从像素到特征的一条流水线1.1 为什么要把图像“表示”出来图像本身只是一堆像素算法没法直接拿着像素矩阵做分类、匹配、测量。比如你想比较两个工件的形状是否一致直接比较二值图矩阵是不行的目标位置稍微偏一点、旋转几度矩阵就完全不同。所以图像处理里专门有一类工作叫“图像表示”目标是把目标区域的几何信息转化成一个紧凑、稳定、便于计算的数据结构。常见表示手段包括边界表示、区域表示、骨架表示、矩描述、傅里叶描述子等。本文聊的边界跟踪、链代码和主成分描述分别覆盖了“如何提取边界”“如何编码边界”“如何用统计方式描述边界/区域”三个层次。三者可以串成一条流水线二值图 → 边界点序列 → 链码 → PCA特征向量。1.2 三个模块如何协作我做这个模拟项目时的分工是这样的边界跟踪负责把目标的外轮廓变成一组有序的坐标点。只有拿到有序点列才能进行后续的曲线编码如果只给你一堆无序的边界像素点告诉你“这些点构成一个轮廓”算法根本没法继续。链代码负责把有序点列压缩成一串方向码。8方向链码每个方向用0到7表示一个轮廓变成一串很短的整数序列既省存储又方便做形状分析。再加上差分链码还能在一定程度上抵抗旋转带来的编码漂移。主成分描述负责从坐标或特征矩阵中提取统计主方向。对边界点坐标做PCA可以直接得到目标的主轴方向、长短轴长度这些参数对姿态估计、形状分类非常有用。1.3 适合谁来参考这篇文章适合三类人一是正在学数字图像处理的学生需要把教材里的边界跟踪和链码概念落到实际代码二是做机器视觉项目、需要快速实现轮廓特征提取的工程师三是想理解PCA在图像描述中如何应用的入门者。我会尽量把每个关键步骤的原理讲透再给出能直接跑的Matlab代码并标注容易踩坑的细节。2. 边界跟踪原理拆解把轮廓变成有序点列2.1 先搞清楚什么是边界数字图像里边界通常定义为“目标区域与背景区域相邻的像素集合”。对于二值图像一个目标像素如果它的4邻域或8邻域中至少有一个背景像素那它就是边界像素。边界跟踪的任务不是简单地把这些像素找出来而是要按照一定的绕行方向把边界点排成一个首尾相连的有序序列。这里有个很容易忽略的点二值图像目标的边界可能有多个闭合环比如一个内部有孔洞的形状。简单场景下只有一个外边界复杂场景需要先确定你是想跟踪外边界还是所有边界。本文代码以最典型的外边界提取为例如果你要处理孔洞可以在这个基础上扩展对每个连通区域分别跟踪。2.2 Moore邻域追踪思路经典的边界跟踪算法之一是Moore邻域追踪。它的核心思想很简单从一个已知的边界点出发沿着当前边界点的邻域搜索下一个边界点不断前进直到回到起点。具体逻辑从图像左上角开始逐行扫描找到第一个目标像素作为起始边界点b0。把b0左侧的背景像素作为初始搜索起点c0。在当前边界点b周围按固定方向顺序比如顺时针从c0开始检索8个邻域。第一个遇到的目标像素就是下一个边界点b_next。更新当前边界点为b_next更新搜索起点为b_next的某个邻域背景点通常是进入b_next方向的反方向邻点继续循环。当b_next回到起始边界点b0且下一步又回到第二步的初始点跟踪结束。这个算法听起来简单实际代码里最麻烦的是“搜索起点更新”。如果处理不当会漏掉边界点或者陷入死循环。我在项目里更常用的做法是先调用Matlab图像处理工具箱的bwboundaries直接获得有序边界点序列它内部实现了成熟的轮廓跟踪返回结果是一个元胞数组每个元胞对应一个连通区域的边界坐标。想理解底层原理可以手写一个简化版本但工程上没必要重复造轮子。下面给一版简化手写函数的思路适合学习和没有工具箱时的应急场景。function B simpleTrace(bw) % 简化版边界跟踪仅适用单连通区域返回边界点坐标 [row, col] [H, W] size(bw); % 找到第一个目标点 idx find(bw, 1, first); [sy, sx] ind2sub([H, W], idx); % 8邻域方向按顺时针东、东南、南、西南、西、西北、北、东北 dirs [0 1; 1 1; 1 0; 1 -1; 0 -1; -1 -1; -1 0; -1 1]; B [sy sx]; cur [sy sx]; prev [-1 -1]; maxIter H * W * 4; iter 0; while iter maxIter iter iter 1; found false; for k 1:8 nxt cur dirs(k, :); % 确保不越界 if nxt(1) 1 nxt(1) H nxt(2) 1 nxt(2) W if bw(nxt(1), nxt(2)) ~(nxt(1) prev(1) nxt(2) prev(2)) B(end1, :) nxt; prev cur; cur nxt; found true; break; end end end if ~found break; end % 回到起点且下一步与初始状态相同结束 if cur(1) sy cur(2) sx size(B, 1) 1 break; end end end这个手写版本在我的测试里能处理常见规则图形但它存在一个已知问题当边界有分叉或细小的凸起时可能提前终止或者绕到内部。所以主流程里我还是推荐用bwboundaries。手写函数的作用是帮你理解Moore追踪的每一步而不是替代成熟方案。2.3 为什么起始点会影响结果边界跟踪的起始点不同得到的有序点列起始位置就不同。这对后续链码影响很大同一个轮廓起点不同生成的链码序列完全不同。所以所有基于链码的匹配算法要么归一化起点例如找到链码序列的最小字典序旋转要么用差分链码并额外对齐。3. 链代码生成与旋转不变的差分链码3.1 8方向链码的编码规则链码也叫Freeman链码是一种用方向序列表示边界的经典方法。8方向链码中每个方向用一个0到7的数字表示。我这里统一采用逆时针编号0代表向右1代表右上2代表向上3代表左上4代表向左5代表左下6代表向下7代表右下。这里要特别注意图像坐标方向在Matlab矩阵中第一维是行对应y轴第二维是列对应x轴且y轴方向向下。因此“右上”对应行坐标减1、列坐标加1“右下”对应行坐标加1、列坐标加1。编码时必须搞清楚坐标顺序否则方向全反。3.2 从边界点列生成链码有了有序边界点序列生成链码就是逐点计算相邻点的方向差。假设边界点序列是B [p1; p2; ...; pn]对每对相邻点(dp p(k1) - p(k)其中行变化是dy列变化是dx)按照方向映射表得到0-7的编码。下面是Matlab实现function [cc, dcc] chainCodeFromBoundary(B) % B: N x 2 边界点序列每行为[row, col] % cc: 8方向链码长度为N-1 % dcc: 差分链码长度为N-2 n size(B, 1); cc zeros(n-1, 1); delta diff(B); % 每行是 [dRow, dCol] for k 1:n-1 dr delta(k, 1); % 行变化对应y dc delta(k, 2); % 列变化对应x % 8方向映射 if dc 1 dr 0 cc(k) 0; % 东 elseif dc 1 dr -1 cc(k) 1; % 东北 elseif dc 0 dr -1 cc(k) 2; % 北 elseif dc -1 dr -1 cc(k) 3; % 西北 elseif dc -1 dr 0 cc(k) 4; % 西 elseif dc -1 dr 1 cc(k) 5; % 西南 elseif dc 0 dr 1 cc(k) 6; % 南 elseif dc 1 dr 1 cc(k) 7; % 东南 else cc(k) -1; % 非8邻域跳变说明轮廓存在缝隙 end end dcc mod(diff(cc), 8); end这个实现里有一个隐藏细节如果边界跟踪结果中存在斜向移动没问题但如果出现“跳点”相邻两点不是8邻域关系链码会出现-1。我在实际项目里遇到过一次原因是bwboundaries返回的边界点序列里首尾相接处可能出现相同的点或者轮廓过于复杂导致中间点缺失。所以生成链码前最好检查一下delta的每步长度是否都落在8邻域内。3.3 差分链码为什么能抗旋转直接使用链码的缺点是同一个形状旋转后链码序列整体偏移。比如一个正方形旋转90度后链码中所有数值都加了2模8。解决思路有两个一是对链码做归一化把最小字典序旋转作为标准二是使用差分链码。差分链码的定义是相邻两个链码值的差值再取模8。它的好处是旋转一个角度相当于给链码整体加上一个常数偏移而差分以后这个常数被消掉了。所以差分链码具有天然的旋转不变性。当然它没有完全解决起点依赖问题但对平移、旋转的鲁棒性比原始链码好很多。我用一个直观例子说明假设轮廓某段依次是“右→右上→上”原始链码是0,1,2。如果轮廓旋转45度这段变成“右上→上→左上”原始链码是1,2,3。原始链码整体变了但差分链码都是(1-0) mod 8 1(2-1) mod 8 1没有变化。这就是差分链码的价值。3.4 链码的统计特征链码本身还可以继续加工成统计特征。最常用的是链码直方图统计0到7每个方向出现的次数形成8维向量。这个向量能粗略描述轮廓的方向分布计算廉价适合做初步筛选。进一步还可以把相邻两个方向的共现频率做成8×8矩阵但特征维度高一些一般配合PCA降维使用。4. 主成分描述用PCA提取形状的骨架参数4.1 从坐标到协方差矩阵主成分描述本质上是对一组描述图像的变量做PCA从而提取主要变化方向或压缩特征维度。在形状描述场景里最常见的做法是把边界点坐标当成一组二维样本计算协方差矩阵再求特征值和特征向量。假设边界点序列B有N个点每个点是(row, col)。把B看成一个N×2矩阵每一行是一个样本两个维度分别是行坐标和列坐标。计算均值向量mu和协方差矩阵C。协方差矩阵C是2×2矩阵它的两个特征值分别表示数据在两个正交方向上的方差大小对应的特征向量就是这两个方向。第一主成分方向是所有边界点投影后方差最大的方向它往往对应物体的长轴方向第二主成分方向与之正交对应短轴方向。这就是为什么PCA能给出形状骨架它用均值表示中心用一个主轴方向表示朝向用特征值表示长宽比例。4.2 Matlab实现PCA描述function [coeff, latent, angle, axisLen] pcaBoundary(B) % B: N x 2 边界点序列每行为 [row, col] % 返回PCA的主成分方向、特征值、主轴角度和轴长 X B; mu mean(X, 1); Xc X - mu; % 计算协方差矩阵 C (Xc * Xc) / (size(Xc, 1) - 1); % 特征值分解 [V, D] eig(C); [latent, idx] sort(diag(D), descend); coeff V(:, idx); % 每列是一个主成分方向 % 主轴与x轴夹角注意coeff(:,1)的分量顺序是[row, col] % 因此y方向分量是coeff(1,1)x方向分量是coeff(2,1) angle atan2(coeff(1,1), coeff(2,1)); % 主轴长度取2倍标准差覆盖约95%的边界点投影范围 axisLen 2 * sqrt(latent); end这段代码有几个细节需要解释一是为什么用axisLen 2 * sqrt(latent)。特征值是方差标准差是平方根。如果边界点近似服从二维高斯分布沿主轴方向约95%的点会落在均值±2个标准差的范围内。画主轴时用这个长度比较直观不会超出边界点范围太多。如果你想画严格覆盖全部边界点的线可以用边界点在该方向投影的最大最小值之差但我测试下来2倍标准差视觉上更稳定不容易被离群点带偏。二是角度计算的坐标顺序。因为Matlab中B的第一列是rowy第二列是colx所以计算与x轴夹角时要用y分量除以x分量求atan2即atan2(coeff(1,1), coeff(2,1))。我最初写反过一次画出来的主轴方向正好差90度排查了很久才发现是坐标顺序问题。4.3 用PCA结果构建形状指标拿到特征值和主轴方向后可以构造几个非常实用的形状描述参数朝向角即主轴与x轴的夹角angle。用于判断目标在图像中的旋转姿态。长轴长度axisLen(1)反映物体在主轴方向上的尺寸。短轴长度axisLen(2)反映物体在短轴方向上的尺寸。细长度axisLen(1) / axisLen(2)也叫长宽比。圆形区域接近1细长物体远大于1是很有判别力的形状特征。紧凑度可以用面积与周长的比值也可以结合PCA特征值定义。我在零件分拣项目里就是靠“长宽比主轴角度”这两个量快速筛掉一批角度不对的零件然后再用完整轮廓匹配做精判。PCA虽然只用了二维坐标但提供的几何参数已经足够解决很多工程问题了。4.4 链码直方图与PCA降维除了直接对坐标做PCA还有一种常见用法把多个形状的链码直方图或其他特征拼成特征矩阵然后对特征矩阵做PCA降维得到低维描述向量用于分类。这种做法本质上是特征压缩和“主成分描述”的教材含义略有不同但在实际工程中非常常用。我记得有一次做形状相似度检索原始特征有几十维直接算距离受噪声影响很大。用PCA把特征投影到前3维主成分后检索准确率反而提升了因为噪声往往集中在方差较小的维度上。如果你想把链码直方图用于PCA降维可以这样构造特征矩阵每一行是一个样本形状的8维链码直方图所有样本拼成一个N×8矩阵然后对矩阵做PCA。5. 完整实验流程从二值图到可比较的特征向量5.1 构造测试图像为了让你能直接跑通整套流程我构造一个简单的二值图一个矩形主体加一个凸起。这个形状不规则有足够的斜向边界适合观察链码和PCA效果。% 生成测试二值图 bw zeros(240, 320); bw(80:160, 100:140) 1; % 主体矩形 bw(100:120, 140:180) 1; % 右侧凸起 bw logical(bw);实际使用中你可以把这部分替换成imread加二值化的结果I imread(your_image.png); bw im2bw(I, 0.5);注意im2bw会把亮部设为1如果你的目标和背景明暗关系相反需要先取反再处理。5.2 串联整条流水线下面是一段完整的主脚本直接复制运行即可。%% 图像表示边界跟踪 链代码 主成分描述 clear; close all; clc; % 1. 构造或读取二值图 bw zeros(240, 320); bw(80:160, 100:140) 1; bw(100:120, 140:180) 1; bw logical(bw); % 2. 边界跟踪使用工具箱函数获取有序边界序列 if exist(bwboundaries, file) boundaries bwboundaries(bw); B boundaries{1}; % N x 2每行[row, col] else B simpleTrace(bw); % 自定义简化版 end % 3. 链代码生成 [cc, dcc] chainCodeFromBoundary(B); % 4. 主成分描述 [coeff, latent, angle, axisLen] pcaBoundary(B); fprintf(边界点数: %d\n, size(B, 1)); fprintf(链码长度: %d\n, length(cc)); fprintf(差分链码长度: %d\n, length(dcc)); fprintf(主轴角度(弧度): %.3f\n, angle); fprintf(长轴长度: %.2f\n, axisLen(1)); fprintf(短轴长度: %.2f\n, axisLen(2)); fprintf(长宽比: %.2f\n, axisLen(1) / axisLen(2)); % 5. 可视化 figure; imshow(bw); hold on; % 绘制边界 plot(B(:,2), B(:,1), r-, LineWidth, 1.5); % 绘制中心点 mu mean(B, 1); plot(mu(2), mu(1), go, MarkerSize, 8, MarkerFaceColor, g); % 绘制主轴 vec coeff(:, 1); % 第一主成分[row分量; col分量] len 0.8 * axisLen(1); p1 mu len * vec; p2 mu - len * vec; plot([p2(2), p1(2)], [p2(1), p1(1)], y-, LineWidth, 2); title(边界跟踪与PCA主轴可视化);5.3 运行结果怎么看运行这段脚本你会得到几个输出边界点数、链码长度、差分链码长度、主轴角度、长短轴长度和长宽比。可视化图上红色线是跟踪到的轮廓绿色点是形心黄色线是第一主成分方向。我自己跑出来的结果里长宽比大约在2左右这符合测试形状“横向长、纵向窄”的直观感受。主轴角度接近0弧度说明物体基本水平摆放。如果你把整个形状旋转30度再运行一遍会发现原始链码变了但差分链码的部分子序列、以及PCA主轴角度会跟着变化而长宽比基本不变。这是一个很好的验证实验建议你亲手试一下。6. 常见问题与排查技巧实录6.1 链码方向反了怎么办如果你生成链码后画出来发现绕行方向是顺时针而不是逆时针说明边界点的顺序是反的。bwboundaries返回的边界顺序有时受连通方式影响并不是固定方向。解决方法是检查边界多边形面积符号如果面积是负的就把边界点序列反转。面积可以用多边形叉积公式计算signedArea sum(B(1:end-1,1) .* B(2:end,2) - B(2:end,1) .* B(1:end-1,2)); if signedArea 0 B flipud(B); end统一方向的好处是让链码的统计特征具有一致性。如果你只是做差分链码方向反了会导致差分结果取反再取模也不方便。6.2 边界跟踪起点如何影响PCAPCA对起点不敏感因为所有边界点作为一个整体参与统计点的顺序不影响协方差矩阵。这是PCA优于链码的地方。所以建议这样组合使用用链码做细节形状编码用PCA做全局几何描述。两者互补链码对起点敏感但对局部形变敏感PCA对起点不敏感但只能提供全局统计信息。6.3 PCA主轴方向存在180度歧义PCA特征向量方向不是唯一的eig输出的特征向量可能指向正方向或反方向。这会导致主轴角度差180度也就是主轴画出来指向反方向。对于轴本身来说正反无所谓但如果你需要稳定的主轴角度用于姿态对齐最好加一个约束让特征向量的x分量恒为正。代码里加一行判断if coeff(2,1) 0 coeff(:,1) -coeff(:,1); end6.4 没有图像处理工具箱怎么处理如果bwboundaries不可用可以先用自定义simpleTrace。不过simpleTrace有个明显问题是它可能遗漏孔洞边界。更稳妥的替代方案是用图像形态学方法提取边界后再手动排序。具体做法用bw - imerode(bw, ones(3,3))提取外边界掩膜然后从掩膜中的任意边界点出发按8邻域跟随。这套逻辑在纯Matlab基础环境下也能实现只是效率偏低。6.5 链码长度不一致如何比较形状两个形状的周长不同链码长度自然不同。直接比较链码序列长度都不一致没法算距离。常见处理方式有三种一是统一重采样边界点到固定数量例如256点再生成链码二是用链码直方图归一化后作为特征三是用差分链码的统计分布代替原始序列。工程上我优先推荐重采样因为它保留了空间形态信息后续还能继续提取其他特征。6.6 边界点中存在重复点导致链码异常bwboundaries偶尔会在轮廓闭合处返回重复点也就是最后一个点和第一个点相同。此时生成链码时会出现一个从最后到第一的额外方向这个方向通常不是8邻域方向导致异常值。处理办法在生成链码前检查首尾是否相同如果相同就去掉最后一个点。if isequal(B(1,:), B(end,:)) B B(1:end-1, :); end我在实际代码里总是提前加上这段省了很多调试时间。我个人的一点实操体会做完这个模拟项目我最大的感受是图像表示方法之间不是互斥的而是层层递进的。边界跟踪解决“有了”链代码解决“省”PCA解决“懂”。真正工程里不要只依赖某一种描述子。我做分拣时先用PCA快速判断整体朝向和细长度再用差分链码做局部轮廓匹配准确率和速度都兼顾了。另外Matlab里的bwboundaries确实省事但建议你还是手写一遍Moore跟踪流程只有亲手把方向表映射、起点更新、停止条件这些细节敲一遍才能在看源码时一眼看出边界情况也才能在遇到“轮廓多了一个内孔”这种问题时快速定位原因。最后再分享一个小技巧无论是链码还是PCA特征拿到之后先画出来看一眼再谈后续匹配。特征可视化的习惯能帮你省掉一半以上的排查时间。
阅读完成 · 觉得有帮助?
咨询建站