第14篇:检测线算法设计 — 十条水平线测量晶粒尺寸
第14篇检测线算法设计 — 十条水平线测量晶粒尺寸一、截线法Heyn法原理1.1 为什么用截线法在材料科学中测量晶粒尺寸有很多种方法如面积法、截线法、比较法等。GrainServer采用的是截线法Intercept Method也叫Heyn法。截线法的基本思想很简单在显微图像上画一条或多条直线统计直线与晶粒边界的交点数用交点数除以直线长度得到单位长度内的晶界交点数从而推算出平均晶粒尺寸。截线法之所以被广泛采用是因为它原理简单容易理解和实现统计稳定只要线足够长、足够多结果就有统计意义效率高不需要对每个晶粒进行完整的轮廓提取抗干扰强个别边界的不完整对整体结果影响小1.2 截线法的数学基础截线法的计算公式平均晶粒尺寸 1 / (单位长度内的交点数 / 2) 2 / 单位长度内的交点数 总线段长度 / (交点数 / 2) 2 × 总线段长度 / 交点数为什么要除以2因为一条直线穿过一个晶粒会和晶界有两个交点进入一个出去一个所以晶粒数等于交点数的一半。或者更直观地说相邻两个交点之间的距离就是一个晶粒的尺寸。所以平均晶粒尺寸 所有相邻交点间距的平均值这正是GrainServer采用的计算方式——直接计算所有相邻交点之间的距离然后取平均。1.3 为什么是水平线截线法可以用任意方向的直线GrainServer选择了水平线。原因是实现简单水平线的y坐标固定扫描和计算都很方便无方向性偏差如果材料是各向同性的水平、垂直、斜线的结果应该一致与图像坐标系一致图像的行扫描本身就是水平的水平线的处理效率更高当然对于有各向异性的材料可能需要多方向截线。但对于大多数金属材料的等轴晶来说水平线足够了。二、为什么选择10条线2.1 统计意义与计算效率的平衡检测线的数量是一个重要的设计参数线太少线太多统计样本不足结果波动大计算量大效率低偶然因素影响大边际效益递减增加线带来的精度提升有限GrainServer选择了10条线上下边界 8条中间线这是统计意义和计算效率的一个平衡点。2.2 10条线的构成10条线 2条边界线 8条等分线上下边界线位于ROI区域的上下边缘8条中间线在上下边界之间均匀分布为什么要有上下边界线因为充分利用ROI区域扩大测量范围边界线也是有效的测量线增加统计样本上下边界定义了整个测量区域的范围2.3 num_lines 8 的含义代码中有一个变量self.num_lines 8self.num_lines8代码位置ImgProcessor/ImgUtils.py:25注意这个8是中间线的数量不包括上下两条边界线。总线条数是 8 2 10 条。为什么用中间线数量来计数而不是总线数因为上下边界线是由ROI区域决定的是固定的中间线是可以调整数量的是可变的用可变部分作为参数更灵活三、ROI感兴趣区域设计3.1 什么是ROIROIRegion of Interest即感兴趣区域是指图像中我们真正关心、用于计算的那部分区域。为什么不直接用整张图因为边缘效应图像边缘的晶粒往往是不完整的直接测量会有偏差边界失真模型在图像边缘的预测效果可能不如中心区域标注需求绿色检测线需要画在图像内部不能超出边界3.2 ROI参数配置GrainServer的ROI配置如下self.x_start70# 左边界左边裁掉70像素self.x_endwidth-70# 右边界右边裁掉70像素y_start54# 上边界上边裁掉54像素y_endheight-54# 下边界下边裁掉54像素代码位置ImgProcessor/ImgUtils.py:96-99直观地说水平方向左右各裁剪70像素垂直方向上下各裁剪54像素┌────────────────────────────┐ │ 上裁54px │ │ ┌───────────────────────┐ │ │ │ │ │ │左│ ROI区域 │右 │ │70│ │70 │ │p │ │p │ │x │ │x │ │ └───────────────────────┘ │ │ 下裁54px │ └────────────────────────────┘3.3 为什么是70和5470像素水平和54像素垂直是经验值它们的设定基于图像分辨率对于2000像素级别的图像几十像素的裁剪比例很小约2-3%模型边缘效应深度学习模型在图像边缘的感受野不完整预测质量略差标注线可见性检测线需要有一定的边距才能在可视化时清晰展示安全性留出一定余量避免边界问题影响统计这两个值不是绝对的可以根据实际情况调整。但在GrainServer的使用场景下它们是合理的默认值。3.4 ROI对检测线的约束检测线只在ROI区域内有效检测线的x范围从x_start到x_end检测线的y位置从y_start到y_end交点检测也只在ROI范围内进行超出ROI的交点不算数。这确保了所有测量都在有效区域内。四、线间距计算4.1 线间距公式8条中间线在上下边界之间均匀分布线间距的计算公式是line_spacing(y_end-y_start)//(self.num_lines1)代码位置ImgProcessor/ImgUtils.py:114为什么分母是num_lines 1因为8条线把空间分成了9段y_start ──────────────────── 第1条边界线 │ ← line_spacing ──────────────────── 第2条线 │ ← line_spacing ──────────────────── 第3条线 ... │ ← line_spacing y_end ──────────────────── 第10条边界线n条线在两个边界之间均匀分布会产生 n1 个间隔。所以8条中间线 2条边界线 10条线共有9个间隔。4.2 整数除法的使用代码中使用的是整数除法//line_spacing(y_end-y_start)//(self.num_lines1)为什么不用浮点数除法因为像素坐标必须是整数整数除法直接得到整数结果不需要额外转换微小的间距误差最多1像素对统计结果影响可以忽略整数除法会导致最后一段的间距可能比其他段大几个像素但这在统计上是无关紧要的。4.3 计算所有检测线的y坐标检测线的y坐标通过循环计算h_lines[y_start,y_end]# 先放上下边界foriinrange(1,self.num_lines1):yy_starti*line_spacing h_lines.append(y)代码位置ImgProcessor/ImgUtils.py:115-122这个循环从1到8因为self.num_lines 8计算每条中间线的y坐标然后添加到列表中。最终h_lines列表包含10个y坐标[y_start, y_end, y_start1*line_spacing, y_start2*line_spacing, ..., y_start8*line_spacing]注意列表的顺序是先上下边界然后中间线。所以不是按y坐标排序的。后续使用时如果需要顺序会再排序。五、绿色标注线绘制5.1 为什么要画检测线检测线不仅是计算用的也是可视化用的。在图像上画出绿色的检测线有几个作用直观展示用户可以看到测量位置心里有数质量检查可以直观检查检测线位置是否合理结果验证配合交点显示可以验证检测是否正确报告输出标注后的图像可以直接用于报告5.2 绘制代码解析绘制检测线的核心代码在draw_lines()函数中ifrequest_typeoriginal:# 创建一个单独的图像用于检测线段line_imgcv2.cvtColor(pred_img,cv2.COLOR_BGR2GRAY)_,line_imgcv2.threshold(line_img,128,255,cv2.THRESH_BINARY)# 绘制上下两条直线cv2.line(pred_img,(self.x_start,y_start),(self.x_end,y_start),(0,255,0),2)cv2.line(pred_img,(self.x_start,y_end),(self.x_end,y_end),(0,255,0),2)# 绘制中间直线foriinrange(1,self.num_lines1):yy_starti*line_spacingifrequest_typeoriginal:cv2.line(pred_img,(self.x_start,y),(self.x_end,y),(0,255,0),2)h_lines.append(y)代码位置ImgProcessor/ImgUtils.py:102-1225.3 关键参数说明cv2.line()函数的参数cv2.line(image,start_point,end_point,color,thickness)参数值说明imagepred_img要画线的图像start_point(self.x_start, y)起点坐标x, yend_point(self.x_end, y)终点坐标x, ycolor(0, 255, 0)颜色BGR格式绿色thickness2线宽2像素为什么是绿色绿色在大多数图像背景上都很醒目绿色与常见的金相图像色调灰色、棕色等对比度高绿色是常见的标注色符合用户习惯为什么线宽是2像素1像素太细在高分辨率图像上不明显2像素足够醒目又不会太粗遮挡细节对于2000像素级别的图像2像素的线宽比例合适5.4 两种模式的绘制差异注意draw_lines()函数对request_type有判断ifrequest_typeoriginal:cv2.line(pred_img,...)# 画线代码位置ImgProcessor/ImgUtils.py:108, 110, 120在original模式下才画线new模式下不画线不对让我们再仔细看。实际上new模式下也会调用draw_lines()但传入的是原始图像elifrequest_typenew:ori_imgcv2.imread(self.ori_input_path)h_lines,line_imgself.imgutils.draw_lines(ori_img,request_type)代码位置ImgProcessor/DataProcessor.py:154-157但在draw_lines()函数中只有request_type original时才画线。那new模式下岂不是没有检测线其实不是的。让我们再仔细看draw_lines()的完整逻辑defdraw_lines(self,pred_img,request_type):height,widthpred_img.shape[:2]self.x_start70self.x_endwidth-70y_start54y_endheight-54line_imgNoneifrequest_typeoriginal:line_imgcv2.cvtColor(pred_img,cv2.COLOR_BGR2GRAY)_,line_imgcv2.threshold(line_img,128,255,cv2.THRESH_BINARY)# 画上下两条线cv2.line(pred_img,...)cv2.line(pred_img,...)# 画中间线line_spacing(y_end-y_start)//(self.num_lines1)h_lines[y_start,y_end]foriinrange(1,self.num_lines1):yy_starti*line_spacingifrequest_typeoriginal:cv2.line(pred_img,(self.x_start,y),(self.x_end,y),(0,255,0),2)h_lines.append(y)returnh_lines,line_img代码位置ImgProcessor/ImgUtils.py:91-124所以两种模式的差异是操作original模式new模式创建line_img用于检测是否返回None在图像上画绿色检测线是否计算检测线y坐标是是返回h_lines列表是是也就是说new模式下draw_lines()只负责计算检测线的位置不负责画线也不创建检测用的二值图。因为重计算模式下交点是从INI文件读取的不需要重新检测。六、draw_lines() 函数代码精讲让我们系统地解析draw_lines()函数的完整实现。6.1 函数签名与返回值defdraw_lines(self,pred_img,request_type):# ...returnh_lines,line_img代码位置ImgProcessor/ImgUtils.py:91, 124参数pred_img输入图像预测结果图或原始图像request_type请求类型original或new返回值h_lines检测线的y坐标列表10个值line_img用于交点检测的二值图像仅original模式有new模式为None6.2 函数执行流程1. 获取图像尺寸height, width 2. 设置ROI边界x_start, x_end, y_start, y_end 3. 如果是original模式 3.1 转换为灰度图 3.2 二值化 3.3 绘制上下边界线 4. 计算线间距line_spacing 5. 初始化h_lines列表先放y_start, y_end 6. 循环计算中间线位置 6.1 如果是original模式画这条线 6.2 将y坐标加入h_lines 7. 返回h_lines和line_img6.3 两个副作用draw_lines()函数有两个副作用——修改了对象的属性self.x_start70self.x_endwidth-70代码位置ImgProcessor/ImgUtils.py:96-97这两个值被保存为实例属性后面的find_line_segments_in_roi()函数会用到forxinrange(self.imgutils.x_start,self.imgutils.x_end):代码位置ImgProcessor/DataProcessor.py:59这种设计有优点也有缺点优点不用传递参数代码简洁ROI配置集中在一个地方缺点有隐式依赖不看代码不知道find_line_segments_in_roi依赖draw_lines先执行有状态多次调用可能互相影响在GrainServer的使用场景下这种设计是可以接受的因为调用顺序是固定的。6.4 line_img 的作用在original模式下函数会创建一个line_imgline_imgcv2.cvtColor(pred_img,cv2.COLOR_BGR2GRAY)_,line_imgcv2.threshold(line_img,128,255,cv2.THRESH_BINARY)代码位置ImgProcessor/ImgUtils.py:104-105注意这里用的是THRESH_BINARY正向二值化而不是骨架化时用的THRESH_BINARY_INV反向二值化。这个line_img是专门用于交点检测的二值图像。后续的find_line_segments_in_roi()函数就是在这张图上逐行扫描寻找黑色像素骨架。等等如果是正向二值化那么骨架是黑色的吗让我们回顾一下在骨架化函数selektoned()的最后输出图像被反相了output_imagecv2.bitwise_not(output_image)所以保存到img_predict_out目录的图像是骨架黑色0背景白色255然后在draw_lines()中对这张图做正向二值化THRESH_BINARY大于128的白色背景→ 255白小于等于128的黑色骨架→ 0黑所以line_img的效果和原图一样骨架是黑色背景是白色。后续交点检测时判断条件是line_img[y, x] 0也就是找黑色像素即骨架像素。七、检测线算法的设计思想7.1 统计采样的思想10条检测线本质上是一种统计采样不可能也不需要测量图像中每一个晶粒选取有代表性的样本通过统计推断整体样本越多统计结果越可靠10条线在统计上已经足够给出稳定的结果。7.2 均匀分布的原则检测线均匀分布是为了避免局部偏差如果线都集中在某个区域结果只反映那个区域的情况保证代表性均匀分布能更好地代表整张图像的平均水平简单易实现等间距分布计算最简单7.3 边界安全余量ROI区域的设计体现了安全余量的工程思想不冒险使用边缘数据宁可牺牲一点测量面积也要保证数据质量边缘的几个晶粒对整体统计影响很小不值得冒险这种留余量的思想在工程设计中非常常见。八、总结检测线算法是截线法的具体实现它将抽象的测量原理转化为可执行的计算步骤。GrainServer采用10条水平检测线上下边界 8条等分线在ROI感兴趣区域内左右各裁70px上下各裁54px均匀分布。线间距通过整数除法计算确保均匀分布。绿色检测线不仅用于计算也用于可视化展示让用户直观看到测量位置。draw_lines()函数负责计算检测线位置、创建检测用二值图、绘制绿色标注线三项工作。检测线的设计体现了统计采样、均匀分布、安全余量等工程思想在保证准确性的同时兼顾了效率和可解释性。在下一篇文章中我们将深入探讨交点检测与距离过滤——看看系统是如何精确提取晶粒边界点的。