
简介面向混凝土骨料形态定量分析需求一套Matlab工具包可帮助从图像中提取图形边缘轮廓将轮廓按指定数量n等分进而计算n边凹多边形的内角并依据骨料计算公式求取骨料棱角指数AI。适用于材料科学、混凝土骨料级配与棱角性评估等研究场景也可作为Matlab图像处理学习者掌握边缘提取、等分插值与几何计算的参考实例。压缩包共11个文件以6个m脚本为核心分别承担轮廓提取、等分插值、内角求解等功能辅以3张PNG示例图用于直观对照处理效果2个TXT文本提供运行说明整包仅70KB轻量易用。目前已有648人学习下载结合源码注释与示例脚本可快速重现骨料轮廓等分和内角计算流程直接输出轮廓等分点、内角数据及AI指数为骨料形貌特征与棱角性分析提供量化基础。1. 轮廓到手之后等分和测角才是真正的坑做图像测量和形状分析时很多人都有过这样的经历从图里把目标边缘轮廓抠出来并不难难的是后续怎么用这条轮廓。比如要分析一个不规则零件的边缘磨损、给一个花瓣形截面做等分采样、或者把叶片轮廓近似成多边形去算角度第一步都是“获取图形边缘轮廓将轮廓图形n等分并求出n边凹多边形内角”。这条链路在Matlab里看起来就是几个函数调用但真正跑起来会发现三个痛点轮廓点的顺序和方向一致性、等分是“按顶点数分”还是“按弧长分”、凹多边形内角的定义和计算方式。这篇文章就把这条链路从头到尾拆开讲每个环节给可直接复制的代码和参数说明最后单独列一节实操中我反复踩过的坑。适合正在做轮廓分析、形状描述、零件检测和几何参数提取的从业者新手可以按步骤走通熟手可以对照检查自己的边界处理。2. 从图像到轮廓点序列bwboundaries与边缘跟踪的原理和选型2.1 为什么轮廓提取首选bwboundaries而不是edge很多人在这一步习惯性地用edge函数提取边缘然后拿着边缘图像去上一步的算法。但edge返回的是二值边缘图它只标记哪些像素属于边缘并没有把这些像素组织成一条或多条闭合曲线。后续要等分、要算角度必须拿到按顺序排列的点序列不然“等分”就是一句空话。bwboundaries函数的作用是输入二值图像输出每个连通区域的边界点坐标而且边界点是沿着轮廓方向有序排列的。它内部用的是摩尔邻域跟踪算法也就是沿着边界像素一步一步走把整圈走完形成一个闭合点序列。这一步省去了自己写边缘跟踪的麻烦而且它对孔洞的处理很自然——外层边界和内部孔洞会分开返回。另一个常见选项是boundary函数它基于Delaunay三角剖分适合点集外包络的提取但输入是散点坐标而不是图像本身计算复杂度也更高。对于“从图像里取轮廓”这个需求bwboundaries是更直接、更稳的选择。实际项目里只有当轮廓已经是一组散点而不是图像时我才会考虑boundary。2.2 最小可复现的轮廓提取代码下面这段代码演示最基础的轮廓提取流程。假设我们有一张灰度图目标是提取图中最亮区域的轮廓。% 读图并转二值 img imread(object.png); gray rgb2gray(img); % 如果是彩色图先转灰度 bw gray 128; % 简单阈值分割阈值按实际图像调整 % 提取边界返回边界点坐标和区域编号 B bwboundaries(bw, noholes); % noholes表示忽略孔洞只取外层轮廓 boundary B{1}; % 取第一个区域的边界点序列 x boundary(:, 2); % 注意bwboundaries返回的坐标是[y, x] y boundary(:, 1); % 可视化确认 figure; imshow(bw); hold on; plot(x, y, r-, LineWidth, 1.5); title(提取到的轮廓);这段代码里有几个需要解释的点。bwboundaries返回的坐标第一列是行索引对应y轴第二列是列索引对应x轴和第几内习惯的(x, y)顺序相反所以上面对调了一下。noholes选项适合只关心外轮廓的场景如果图形内部有孔洞、也要参与后续分析就把它去掉B里会包含多个轮廓用cellfun(length, B)或者画图检查每个轮廓的长度确定哪个是目标轮廓。阈值128是临时值。真实图像的分割质量直接决定轮廓质量这一步建议配合imbinarize和自适应阈值或者用手动ROI来排除干扰。轮廓提取本身不复杂但分割没做好后面所有结果都是错的。2.3 轮廓点序列的方向与坐标陷阱拿到的轮廓点序列方向可能是顺时针也可能是逆时针取决于图像中目标的分布和bwboundaries内部算法。这本身不影响后面的等分但会影响角度计算时的符号判断。标准做法是先归一化方向算一下轮廓的有向面积根据面积正负决定是否反转序列。% 计算有向面积判断方向 n length(boundary); area 0; for i 1:n j mod(i, n) 1; % 下一个点索引形成闭合环 area area x(i)*y(j) - x(j)*y(i); end area area / 2; if area 0 boundary flipud(boundary); % 顺时针转为逆时针 x boundary(:, 2); y boundary(:, 1); end这段代码的数学基础是鞋带公式。面积为正对应逆时针方向为负对应顺时针。为什么要统一方向因为后续求内角时我要用叉积的正负来判断多边形顶点是凸还是凹方向不统一同一个顶点的凹凸性判断结果是反的角度就会算成补角。这事我在项目里翻车过当时轮廓方向反了所有凹角被算成小于180度的凸角角度和完全对不上排查了很久才意识到是坐标方向的问题。另一个坐标陷阱是像素坐标与物理坐标的区别。如果后面要算实际角度或弧长需要知道每个像素对应的物理尺寸。等分点在像素坐标下做算出来的角度是像素几何的角度但如果图像有畸变或非均匀缩放角度结果会有偏差。对于精度要求不高的分析像素坐标够用做计量类项目先做相机标定再谈角度。3. 把轮廓n等分顶点索引等分与弧长等分的本质区别3.1 顶点索引等分为什么不行拿到轮廓点序列后最直觉的做法是均匀取索引也就是每隔floor(n/N)个点取一个作为等分点。这个做法在轮廓点密度均匀时效果尚可实际图像里这是很少见的情况——边缘跟踪产生的点在曲率大的地方密、在平直段稀尤其是有斜边时栅格化导致阶梯效应点的疏密差距更大。直接用索引等分会导致等分点在直边段过密、在圆弧段过稀最终连出来的多边形严重偏离原始轮廓。这个问题在凹多边形上尤其明显因为凹进去的线段往往伴随高曲率那里本该有更多等分点。解决办法是放弃索引等分改用弧长参数化把轮廓看作一条参数曲线按照累计弧长来均匀取样。3.2 弧长参数化的等分算法弧长参数化的核心思想是先计算每个轮廓点相对起点的累计长度得到“弧长-索引”对应关系再在目标弧长位置做插值取到等分点坐标。function [px, py] resampleContourByArcLength(x, y, n) % 输入轮廓点x坐标、y坐标闭合曲线等分数n % 输出n个等分点的px、py坐标 % 计算相邻点之间的距离并累积 dx diff(x); dy diff(y); segLen sqrt(dx.^2 dy.^2); cumLen [0; cumsum(segLen)]; % 每个点处的累计弧长 totalLen cumLen(end); % 轮廓总周长 % 按n等分计算目标弧长位置 targetLen linspace(0, totalLen - totalLen/n, n); % 对每个目标弧长位置做线性插值 px interp1(cumLen, x, targetLen, linear); py interp1(cumLen, y, targetLen, linear); % 闭合让首尾重合或首尾相连 px(end1) px(1); py(end1) py(1); end这个函数的逻辑分三步走。第一步用diff和cumsum构建累计弧长数组注意cumLen的长度是N1比原始点序列多一个0对应起点处弧长为0。第二步用linspace生成n个目标弧长值totalLen - totalLen/n这个写法是为了避免最后一个等分点与第一个点重合因为轮廓是闭合曲线起点即终点。第三步用interp1做线性插值这是整段代码的核心它保证等分点落在轮廓线上而不是简单取邻近像素点。调用方式很简单n 12; % 等分成12份 [px, py] resampleContourByArcLength(x, y, n); figure; imshow(bw); hold on; plot(px, py, go-, MarkerSize, 6, LineWidth, 1.2); plot(px, py, b-); % 蓝色连线显示等分后的多边形参数说明n是等分数也就是目标多边形的边数。对圆或近似圆形的轮廓n取8到16就能看出明显趋势对复杂凹形建议n不要小于12否则会丢失很多细节但对含噪声的轮廓n过大反而会让角度计算对微小锯齿敏感后面会细说。插值方式选linear还是spline也有讲究。linear忠实于原始轮廓点走不产生原始轮廓之外的点但等分点之间的连线会有折角——这其实正是我们想要的因为我们本来就是在用一个n边形去逼近原始轮廓。spline会让等分点落在平滑曲线上但它的插值结果可能超出原始轮廓范围对于测量类应用不推荐。3.3 等分数n怎么选等分数n不是越大越好。它本质上是“用n边形去逼近原始轮廓”这个行为的分辨率参数。n太小凹细节被抹平内角结果整体偏差大n太大轮廓上像素级噪声会被保留下来反映在角度上就是相邻等分点之间方向剧烈变化算出来的内角忽大忽小没有统计意义。我常用的策略是先用n36跑一遍画出等分多边形叠加原轮廓目视检查贴合度。如果多边形在凹弧段明显“切进去了”或“飘出去了”说明n不够如果多边形边界在平直段出现抖动说明n过大。另一个经验法则是参考轮廓总周长与平均像素间隙的比例让每条边的长度大约在5到10个像素之间太短就说明等分过密。如果需要自动选n可以写一个简单的迭代从n等于轮廓点数除以10开始逐步增加每次计算等分多边形与原轮廓的Hausdorff距离选取距离下降趋于平缓的最小n。这个方法计算量不大实际应用中效果不错。但大多数场景下目视检查配合一次参数微调就够了不必做复杂优化。4. 求n边凹多边形内角方向向量、凹凸性与角度修正4.1 内角的数学定义与向量求角拿到n个等分点后问题变成“求这个n边多边形的内角”。先明确一下定义对于按顺序排列的多边形顶点P1, P2, ..., Pn内角是顶点Pi处两条边Pi-1→Pi和Pi→Pi1之间形成的夹角但一定要区分凸角和凹角。凸多边形时所有内角都小于180度直接算夹角就行。凹多边形里至少有一个内角大于180度——这就是凹进去的顶点。用两个向量的点积求夹角得到的永远是0到180度之间的较小角这对凹角是不对的。凹角的实际角度应该大于180度。所以求内角必须做方向判定。4.2 凹角是大于180度的atan2不会直接告诉你常见的错误是用acos(dot(a,b)/(norm(a)*norm(b)))来算夹角然后当作内角。这个做法把所有角都夹到[0, pi]区间里凹角全部算错。正确做法是用atan2求方向角之差然后归一化到0到2pi。设顶点为P2前一个点为P1后一个点为P3。向量v1 P2 - P1进入方向v2 P3 - P2离开方向。内角是v1逆时针旋转到v2所扫过的角度这个角度可能大于180度。用atan2计算方式如下ang1 atan2(v1(2), v1(1)); % 进入方向向量v1与x轴夹角 ang2 atan2(v2(2), v2(1)); % 离开方向向量v2与x轴夹角 diffAng ang2 - ang1; diffAng mod(diffAng, 2*pi); % 归一化到[0, 2pi)这里的关键是最后一步mod(diffAng, 2*pi)。如果直接判断diffAng 0就加2pi会遇到一个边界问题——当diffAng正好等于-pi或接近-pi时会因为浮点误差给出错误的补角。mod函数在Matlab里对负数的处理是返回非负结果所以mod(-pi/2, 2*pi)会得到3pi/2这正好是我们想要的凹角。例如一个凹口的角度是270度用mod归一化后得到的就是270度而不是90度。4.3 完整的内角计算函数把上面的逻辑整理成一个完整函数输入是等分后的多边形顶点坐标输出是每个顶点的内角弧度制。function angles polygonInteriorAngles(px, py) % 输入多边形顶点坐标px、py按顺序排列首尾不重复 % 输出每个顶点对应的内角单位弧度 n length(px); angles zeros(n, 1); for i 1:n % 取前一个、当前、后一个顶点注意边界回绕 iPrev mod(i-2, n) 1; iNext mod(i, n) 1; pPrev [px(iPrev), py(iPrev)]; pCur [px(i), py(i)]; pNext [px(iNext), py(iNext)]; % 构造进入方向和离开方向向量 v1 pCur - pPrev; % 从上一个顶点指向当前顶点 v2 pNext - pCur; % 从当前顶点指向下一个顶点 % 用atan2求两个向量的方向角再求差值 ang1 atan2(v1(2), v1(1)); ang2 atan2(v2(2), v2(1)); diffAng ang2 - ang1; % 归一化到[0, 2*pi)保证凹角正确表示 angles(i) mod(diffAng, 2*pi); end % 转成角度制方便阅读 angles angles * 180 / pi; end这段代码有几个细节值得说明。iPrev和iNext的回绕方式用的是mod(i-2, n)1和mod(i, n)1。这里要注意Matlab索引从1开始所以公式比Python等语言多一个1偏移。比如i1时iPrev mod(-1, n)1 n指向最后一个顶点iNext mod(1, n)1 2正确。v1 pCur - pPrev与v2 pNext - pCur都要写成向量形式Matlab允许点坐标直接相减得到向量。方向角差值的范围是[-2pi, 2pi]但mod处理后会落在[0,2pi)这样当多边形是凸的所有角度都小于pi转成角度就是0到180凹的地方会得出大于180的值。一个自检方法是验证角度和是否等于(n-2)*180。n边形内角和定理对凹多边形同样适用比如五边形不管是凸是凹内角和都是540度。如果算出来的和不满足这个约束九成是方向判定或顶点顺序出了问题。调用方式和可视化angles polygonInteriorAngles(px, py); disp(angles); % 可视化角度在每个顶点处标注内角度数 figure; imshow(bw); hold on; plot(px, py, b-, LineWidth, 1.2); for i 1:length(px)-1 text(px(i)5, py(i)5, sprintf(%.1f°, angles(i)), ... FontSize, 8, Color, r); end可视化这一步非常关键只看数字很难发现哪个顶点算错了。把角度值直接标注在对应顶点旁边立刻能看出凹角和凸角的分布是否符合直觉。5. 五个避坑记录轮廓、等分、角度三个环节各自的坑5.1 轮廓点顺序颠倒所有内角变成补角现象算出来的角度普遍在0到180度之间没有大于180度的凹角而且角度和不是(n-2)*180。原因轮廓点序列是顺时针方向而我的算法假设逆时针方向。方向反过来时每个顶点处“进入方向”和“离开方向”互换实际角度变成了360度减去正确内角。解决在提取轮廓后、等分之前加一步有向面积检查。面积小于0就把序列flipud一下。具体代码在2.3节已给出这一步一定要放在所有处理的最前面。5.2 等分点扎堆在凹弧段多边形严重变形现象等分后的多边形在某个凹口处连续出现多个顶点而在直边段几乎没有顶点多边形和原始轮廓明显不贴合。原因使用了基于索引的等分而不是基于弧长的等分。凹弧段曲率大轮廓跟踪产生的像素点多索引等分就在这里扎堆。解决改用3.2节的弧长参数化等分函数。如果已经用了弧长等分仍然出现扎堆检查轮廓是否本身存在重复点或回环——用unique去掉重复点再跑。5.3 孔洞被当成轮廓参与等分现象等分多边形出现很短的边三角形化明显内角出现大量接近0度或360度的值。原因bwboundaries默认把外边界和内部孔洞都返回如果取B{1}只取了外层还好但有些情况下目标对象内部有异色区域比如文字、污点第二个轮廓的长度占比不小干扰了可视化判断。解决在提取时明确指定noholes只保留最外层轮廓。如果孔洞是分析对象本身的一部分则需要对不同轮廓分别做等分和测角然后汇总结果不要混在一起。5.4 凹角算出负值或超360度现象角度输出里出现负值或大于360度的值角度和严重偏离理论值。原因没有做mod归一化或者mod使用不当。直接用atan2的差值可能出现负角度如果用rem而不是mod负值会保留因为rem的结果符号跟随被除数。解决统一使用mod(diffAng, 2*pi)不要用rem。这二者的区别是rem(-pi/2, 2*pi)返回-pi/2而mod(-pi/2, 2*pi)返回3*pi/2。在角度归一化场景里我们要的是后者。5.5 等分数n大于轮廓点数插值失败或点重合现象调用interp1时报警提示采样点不足或者输出等分点中有大量坐标相同的重复点。原因轮廓很短但n设得很大。比如一个只有40个像素点的轮廓要等分50份interp1在目标位置多数落在原始点之间的空隙线性插值本身没问题但等分点之间的弧长小于像素间距相邻等分点几乎重合角度计算就失去意义。解决先检查轮廓长度设定n不超过轮廓点数的三分之一。用plot画出等分结果如果相邻点过密目视可辨就降低n。另外interp1要求cumLen严格递增如果原始轮廓有重复坐标点先去掉再算。6. 进阶验证用角度和与凸包交叉检验你的凹多边形等分和测角都跑通之后还有一个习惯值得养成每次算完角度不要急着看个别值先做两个整体校验。第一个是内角和校验n边形内角和恒为(n-2)*180度凹多边形也一样。如果偏差超过1度说明顶点顺序、回绕或mod归一化某一步出了问题。第二个是用凸包做交叉验证——把等分点做凸包凸包上的顶点内角必定小于180度而且它们的位置应该对应原始轮廓上的“凸出”区域如果某个凸包顶点在原始轮廓上对应的位置凹陷明显说明等分点选取或方向判断有误。% 内角和校验 sumAngles sum(angles); expected (length(px)-1 - 2) * 180; % 注意px因为闭合多了一个重复点 fprintf(实际内角和: %.2f, 理论值: %.2f\n, sumAngles, expected); % 凸包交叉验证 k convhull(px, py); plot(px(k), py(k), m--, LineWidth, 1.5);这个校验技巧能帮你把算法错误和图像噪声区分开。算法错误通常表现为角度和偏差大噪声导致的误差通常表现为角度和接近理论值但个别角度明显失真。前者回头查代码后者考虑增加等分数或先对轮廓做平滑滤波。关于角度归一化我还有一个比较顽固的习惯无论中间计算用弧度还是角度最终输出前都会统一把角度映射到(0, 360)区间并且在旁路记录每个角度的“凹凸标志”也就是判断角度是否大于180。这个标志在后续做凹多边形分类、凹点检测、形状匹配时经常会用到提前存下来比每次重新算一遍好得多。做这类轮廓分析项目我的经验是先在一个已知形状上验证整个流程比如用polyshape手工构造一个五角星走一遍提取、等分、测角的标准流程确认角度输出与几何直觉一致再换到真实图像上调试阈值和等分数参数。这个习惯帮我避开了很多“看起来结果合理但实际算法有误”的坑。希望这个工具链和这些避坑记录能帮到你尤其是那个用mod处理凹角的细节值得存进自己的工具箱。本文还有配套的精品资源点击获取