
第16篇两种模式下的点集获取策略 — original vs new一、第一性原理点集获取的本质是什么1.1 为什么需要点集在金属材料晶粒分析中我们的核心目标是测量晶粒的尺寸。但直接对整个图像进行像素级遍历计算不仅效率低下而且容易受到噪声干扰。材料学界经过长期实践总结出了一种经典方法——截线法Intercept Method通过在图像上绘制若干条等距的水平检测线统计这些线与晶界的交点通过相邻交点的间距来估算晶粒尺寸。这种方法的理论基础来自于立体金相学Stereology对于随机分布的晶粒结构截线法测得的平均截距与晶粒的真实平均尺寸之间存在确定的数学关系。因此点集交点坐标集合就是连接图像数据与晶粒尺寸统计的桥梁。1.2 点集获取的两条路径在实际的材料分析工作流中存在两种典型场景场景一首次分析original模式输入原始显微图像输出晶粒尺寸统计结果 交点坐标特点需要完整的AI模型推理和图像处理流程场景二人工修正后重计算new模式输入人工修正后的图像 已保存的交点坐标输出更新后的晶粒尺寸统计结果特点跳过模型推理直接利用已有交点进行重计算这两种模式对应了GrainServer系统的双服务器架构PORT 5000处理original模式PORT 5001处理new模式。1.3 为什么需要保存交点到INI文件一个自然的问题是为什么不直接保存统计结果而是要保存交点坐标答案在于灵活性和可追溯性人工修正需求AI分割结果可能不完美用户需要在修正图上调整晶界然后重新计算参数调整如果想改变统计参数如最小间距阈值可以直接从交点重算无需重新跑模型可追溯性交点是原始测量数据统计结果是派生数据保存原始数据便于审计和复现计算效率模型推理是最耗时的步骤跳过推理可以将重计算时间从秒级降到毫秒级二、original模式完整的点集提取流程2.1 流程总览original模式下点集获取需要经过以下完整链路原始图像 → 模型预测语义分割 → 骨架化 → 绘制检测线 → 交点提取 → 近点过滤 → 写入INI每一步都有其明确的物理意义和工程考量。让我们逐一拆解。2.2 模型预测与骨架化模型预测的输出是一张二值分割图其中晶界被标记为特定颜色。但直接在分割图上找交点会遇到一个问题晶界是有宽度的区域而不是理想的细线。为了解决这个问题需要进行**骨架化Skeletonization**处理。骨架化是形态学中的经典操作它将一个区域瘦身到只剩其中心轴线就像把一根粗棍子削成细针。这样处理后晶界就变成了单像素宽度的骨架线便于后续精确计算交点。让我们看看项目中的骨架化实现位于ImgUtils.py的selektoned()方法defselektoned(self):forfilenameinos.listdir(self.predict_floder):image_pathos.path.join(self.predict_floder,filename)imgcv2.imread(image_path,cv2.IMREAD_GRAYSCALE)# 二值化反向白色变黑色黑色变白色_,binarycv2.threshold(img,128,255,cv2.THRESH_BINARY_INV)# 骨架化将前景简化为细线状骨架skeletonskeletonize(binary//255)skeleton(skeleton*255).astype(np.uint8)# 连通组件分析过滤小面积噪声num_labels,labels,stats,centroidscv2.connectedComponentsWithStats(skeleton,connectivity8)output_imagenp.zeros(binary.shape,np.uint8)foriinrange(1,num_labels):ifstats[i,cv2.CC_STAT_AREA]100:output_image[labelsi]255output_imagecv2.bitwise_not(output_image)cv2.imwrite(os.path.join(self.seleton_floder,filename),output_image)这段代码有几个值得注意的设计二值化反转第40行cv2.THRESH_BINARY_INV将黑白反转因为骨架化算法通常处理白色前景。面积过滤第52行面积小于100像素的连通区域被丢弃这是一种有效的噪声抑制手段。再次反转第54行处理完后再反转回来保持与原始图像一致的视觉习惯。2.3 绘制检测线骨架化完成后下一步是在图像上绘制水平检测线。GrainServer采用10条等距检测线策略2条边界线 8条中间线。检测线的绘制在ImgUtils.py的draw_lines()方法中实现defdraw_lines(self,pred_img,request_type):height,widthpred_img.shape[:2]# 定义感兴趣区域ROI范围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,(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)# 计算线间距绘制中间8条线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这里有两个重要的工程设计ROI感兴趣区域裁剪水平方向左右各裁剪70像素x_start 70,x_end width - 70垂直方向上下各裁剪54像素y_start 54,y_end height - 54为什么要裁剪因为图像边缘区域的晶粒往往是不完整的如果纳入统计会引入系统误差。裁剪掉边缘不完整的晶粒只统计中心区域的完整晶粒可以提高测量准确性。双图策略pred_img用于可视化展示上面绘制绿色检测线line_img用于实际计算的二值图不包含绘制的线这种计算图与展示图分离的设计避免了检测线本身干扰交点检测。2.4 交点提取算法有了骨架图和检测线坐标接下来就是提取每条检测线与骨架的交点。这个功能由DataProcessor.py中的find_line_segments_in_roi()方法实现deffind_line_segments_in_roi(self,line_img,y):segments[]in_segmentFalsesegment_start0# 取相连点中点为交点forxinrange(self.imgutils.x_start,self.imgutils.x_end):ifline_img[y,x]0:# 黑色像素点晶界ifnotin_segment:in_segmentTruesegment_startxelse:continueelse:ifin_segment:segment_center(segment_startx-1)//2segments.append((segment_center,y))in_segmentFalseifin_segment:segment_center(segment_startself.imgutils.x_end-1)//2segments.append((segment_center,y))returnsegments这个算法的设计非常巧妙让我们逐行理解算法原理由于晶界骨架虽然经过细化但仍然可能有1-2个像素的宽度。直接找单个像素作为交点不稳定。因此采用**“连通段中点法”**沿x轴方向逐像素扫描第y行遇到黑色像素晶界时记录段的起点遇到白色像素背景时计算这段黑色像素的中点作为交点坐标用中点代表整个晶界段提高了交点定位的稳定性边界处理第72-74行处理了一个边界情况如果扫描到ROI右边界时还处于一段晶界内部in_segment为True就用右边界作为段的终点来计算中点。这保证了边缘处的晶界也能被正确计数。2.5 近点过滤提取到初始交点后还需要一步重要处理过滤距离过近的点。为什么需要这一步因为晶界骨架可能有分叉或毛刺导致同一条晶界产生多个交点图像噪声可能产生假交点不同金属的晶粒大小不同需要不同的过滤阈值过滤算法在DataProcessor.py的remove_close_points()方法中defremove_close_points(self,points,min_distance):ifnotpoints:return[]# 对点坐标进行排序默认保存第一个点pointssorted(points)filtered_points[points[0]]# 计算点与点之间的距离forpointinpoints[1:]:# 两点之间的距离大于最小距离则保留这个点ifabs(point[0]-filtered_points[-1][0])min_distance:filtered_points.append(point)returnfiltered_points这是一个经典的贪心过滤算法先将所有点按x坐标排序保留第一个点依次检查后续每个点如果它与上一个保留点的距离≥min_distance就保留它否则就丢弃认为是同一个晶界产生的重复交点不同金属的min_distance值在Utils/utils.py中配置self.min_distance{Ta:35,Ti:28,Al:30,Al-Cu:30,Al-Si:30,Al-Si-Cu:30,Cu:65,W:40}可以看到铜Cu的阈值最大65像素因为铜的晶粒通常较大晶界也相对更粗。2.6 写入INI文件提取并过滤完所有交点后需要将这些坐标保存到INI文件中供后续new模式使用。写入功能在FileHandle.py的write_points_to_ini()方法defwrite_points_to_ini(self,all_segments,output_path):withopen(output_path,w)asf:f.write([Points]\n)fori,(x,y)inenumerate(all_segments):f.write(fPoint_{i1}_X{x}\n)f.write(fPoint_{i1}_Y{y}\n)生成的INI文件格式如下[Points] Point_1_X125 Point_1_Y54 Point_2_X256 Point_2_Y54 Point_3_X389 Point_3_Y54 ...为什么选用INI格式而不是JSON、CSV或其他格式有几个考量可读性INI格式键值对清晰人类易于阅读和编辑解析方便Python标准库configparser原生支持顺序性点按编号顺序存储语义明确传统兼容在工业软件领域INI格式有着悠久的使用历史三、new模式基于已有交点的重计算3.1 new模式的核心价值new模式是GrainServer架构中的一个精妙设计。当用户在前端对AI分割结果进行人工修正后我们需要重新统计晶粒尺寸。但是如果重新跑一遍模型推理既浪费时间也不合理——因为修正后的图已经是人工确认的结果了。new模式的核心思路是复用original模式提取的交点只在修正图上重新绘制检测线进行展示然后直接用已有交点计算统计量。3.2 读取INI文件new模式首先要做的就是从INI文件中读取之前保存的交点坐标。这个功能在FileHandle.py的read_list_from_ini()方法中defread_list_from_ini(self,output_path):# 创建一个ConfigParser对象configConfigParser()# 读取.ini文件config.read(output_path)# 初始化一个空列表来存储元组newpoint_list[]# 获取节名称 PointssectionPoints# 读取节中的所有键值对keysconfig[section]# 将X和Y值配对并转换为元组foriinrange(1,len(keys)//21):x_valueconfig.getint(section,fPoint_{i}_X)y_valueconfig.getint(section,fPoint_{i}_Y)newpoint_list.append((x_value,y_value))returnnewpoint_list这里使用了Python标准库的ConfigParser来解析INI文件。有几个细节值得注意len(keys) // 2因为每个点有X和Y两个键所以点的总数是键数的一半。config.getint()直接读取并转换为整数省去了手动类型转换的麻烦。元组列表返回格式是[(x1, y1), (x2, y2), ...]与original模式中all_segments的格式完全一致保证了后续处理逻辑的统一。3.3 在修正图上绘制检测线读取交点后还需要在人工修正后的图像上绘制检测线用于可视化展示。注意这里绘制的线只用于展示不用于计算——计算用的交点已经从INI文件中读出来了。这也是为什么在draw_lines()方法中有if request_type original:的条件判断。在new模式下line_img返回None因为不需要用它来检测交点。四、核心方法精讲get_all_segments()4.1 方法总览get_all_segments()是整个点集获取模块的核心入口方法它统一了original和new两种模式的调用接口。让我们来看完整代码位于DataProcessor.pydefget_all_segments(self,request_type,min_distance,output_path):all_segments[]h_lines[]ifrequest_typeoriginal:pred_imgcv2.imread(self.input_path)ifpred_imgisNone:self.logger.error(fFailed to read image{self.input_path})h_lines,line_imgself.imgutils.draw_lines(pred_img,request_type)foryinh_lines:segmentsself.find_line_segments_in_roi(line_img,y)segmentsself.remove_close_points(segments,min_distance)all_segments.extend(segments)self.FileHandle.write_points_to_ini(all_segments,output_path)elifrequest_typenew:ori_imgcv2.imread(self.ori_input_path)ifori_imgisNone:self.logger.error(fFailed to read image{self.ori_input_path})h_lines,line_imgself.imgutils.draw_lines(ori_img,request_type)all_segmentsself.FileHandle.read_list_from_ini(output_path)returnall_segments,h_lines4.2 设计模式分析这个方法体现了一个重要的设计原则统一接口内部差异化实现。对外无论是original还是new模式调用方都只需要调用同一个方法传入不同的request_type参数返回相同格式的结果all_segments和h_lines。这种设计的好处是上层逻辑简洁调用方不需要关心两种模式的差异易于扩展如果以后增加第三种模式只需在方法内增加一个分支代码复用后续的统计计算逻辑完全复用不需要写两套4.3 两种模式的路径对比让我们用表格来清晰对比两种模式的执行路径步骤original模式new模式输入图像模型预测分割图self.input_path人工修正图self.ori_input_path绘制检测线是生成line_img用于计算是仅用于可视化展示交点提取通过find_line_segments_in_roi()逐行扫描从INI文件读取近点过滤通过remove_close_points()过滤跳过已在original模式过滤过写入INI调用write_points_to_ini()保存不写入读取INI不读取调用read_list_from_ini()读取返回值all_segments, h_linesall_segments, h_lines4.4 在完整流程中的位置get_all_segments()被processed_data()方法调用是数据处理流水线的第一步defprocessed_data(self,min_distance,filename,request_type):# ... 路径初始化 ...try:all_segments,h_linesself.get_all_segments(request_type,min_distance,self.point_path)lengthsself.calculate_segment_stats(all_segments,h_lines,self.pix_to_num)interval_percentagesself.calculate_length_distribution(lengths)# ... 日志输出 ...exceptExceptionase:self.logger.error(f处理图像时发生错误{e})raise可以看到整个数据处理流水线分为三步点集获取get_all_segments拿到所有交点坐标统计计算calculate_segment_stats计算平均/最大/最小/标准差分布统计calculate_length_distribution计算各粒径区间的分布五、理论到实践的闭环5.1 截线法的理论基础截线法Heyn法的理论基础是立体金相学中的基本公式平均截距长度 L 1 / N_L其中 N_L 是单位长度截线上的晶界交点数。对于等轴晶各向同性的晶粒结构平均晶粒尺寸 G 与平均截距长度 L 之间存在线性关系。GrainServer采用的10条等距水平线策略本质上是对截线法的工程实现多条检测线提高了统计的代表性ROI裁剪排除了边缘不完整晶粒的干扰近点过滤提高了交点计数的准确性5.2 为什么重计算可以跳过模型推理这是一个很有价值的问题。理解了它就理解了整个双模式架构的精髓。答案在于在重计算场景中交点坐标是真理来源Source of Truth而不是图像本身。让我们思考一下当用户在前端修正图像时他们实际上在做什么他们在调整晶界的位置。但是如果晶界位置变了交点坐标也应该跟着变啊这里有一个关键的架构设计选择GrainServer的new模式假设人工修正不改变晶界的水平位置只改变视觉呈现。或者说交点坐标是在original模式下精确计算的是经过AI模型推理得到的测量值人工修正只是确认和微调不需要重新计算交点。这种设计在实际工程中是合理的因为模型分割的精度通常已经足够高人工修正主要是处理一些极端情况完全重新计算的成本很高需要重新跑模型对于大多数场景保存交点并复用已经足够满足需求5.3 INI文件作为数据交换格式的意义INI文件在这里扮演了数据契约的角色连接了original模式和new模式两个阶段original模式是生产者负责生成并写入交点数据new模式是消费者负责读取并使用这些数据这种通过文件进行数据交换的方式有几个优点解耦两个模式不需要在内存中共享状态通过文件系统解耦持久化数据持久化到磁盘可以随时查看和调试可追溯每个任务的交点数据都有记录便于审计容错如果某个步骤失败可以从最近的文件恢复不需要从头开始六、总结通过本文的分析我们深入理解了GrainServer中两种模式下的点集获取策略original模式走完整的模型预测→骨架化→检测线→交点提取→近点过滤→写入INI流程是系统的主要计算路径。new模式通过读取INI文件中的已有交点跳过模型推理和交点提取直接进入后续统计计算大幅提升了重计算效率。核心方法get_all_segments()用统一的接口封装了两种模式的差异体现了良好的设计思想。INI文件作为两种模式之间的数据交换媒介实现了计算结果的持久化和可追溯。这种设计不仅在工程上高效优雅也完全符合材料分析的实际工作流——AI首次分析给出结果人工确认和微调后快速重算。下一篇文章中我们将继续深入探讨这些交点数据如何被用来计算晶粒尺寸的统计量。