python的先进制造技术工业场景模拟第三十篇:导入3D打印零件孔隙检测数据,统计孔隙率,筛选孔隙超标试样。
发布时间:2026/10/2 6:02:46来源:尧图网络
周二下午增材制造实验室。这批钛合金试样SLM 打印完做了 CT 扫描材料工程师小陈把 U 盘插上电脑孔隙率是关键指标要求 ≤0.5%。CT 出来一堆切片数据每个试样几十层每层几百个孔隙手动数根本不现实。我点开导出的 CSV。这表里有什么小陈问。每条记录一行有试样编号、层号、孔隙面积、等效直径、坐标 X/Y、灰度值我指着数据问题是散的——一个试样可能跨 80 层每层十几个孔隙要汇总到试样级算总孔隙率再跟阈值 0.5% 比筛出超标的。我就想干一件事小陈说把每个试样的孔隙汇总算孔隙率标出超标的再看看超标试样的孔隙分布特征——是大孔多还是小孔多、集中在哪几层。如果是同一层集中出现可能是铺粉没铺好如果是随机分布可能是粉末本身有夹杂。比如试样 S-07总体积 1200 mm³孔隙总体积 8.2 mm³孔隙率 0.68%超了我接话但光看一个数不够——这 8.2 mm³ 是 3 个大孔还是 200 个小孔大孔是工艺缺陷小孔是粉末未熔合根因完全不同。还有孔隙是不是集中在第 30~35 层如果是那几层激光功率可能掉了。对小陈点头还有我想看所有试样的孔隙率分布是不是正态分布均值离 0.5% 多远有没有明显的离群批次。最好还能用聚类把孔隙特征相似的试样归一组看看是不是某台设备、某天打的。用 pandas 按试样聚合numpy 算孔隙率matplotlib 画分布直方图层热力图scipy 做正态性检验和批次 t 检验scikit-learn 聚类分群networkx 建试样-层-缺陷类型关系网我开工程数据自包含合成一批孔隙检测数据下载就能跑。敲了行原型pore_rate df.groupby(sample_id).apply(lambda g: g[pore_volume].sum() / g[part_volume].iloc[0])reject pore_rate[pore_rate 0.005].index.tolist()完整版用 OOP 封好我说一个类管数据加载一个类算孔隙率一个类筛超标试样一个类做统计检验一个类聚类分群一个类建缺陷关系网一个类出图。输出孔隙率排名、超标清单、分布检验、聚类结果存 results/。小陈凑近看那以后看报告20 个试样均值 0.32%3 个超标S-07/S-12/S-19S-07 的孔隙集中在 30~35 层大孔为主等效直径 150~300μm正态性检验 p0.12 接近正态但 S-07 那一簇明显偏右。聚类分了 3 组正常致密组、均匀微孔组、局部大孔组。局部大孔组全是周二下午那批查了是当时保护气流量不稳。对我接话孔隙不是一个数是形貌分布定位。筛超标只是第一步定位根因才是增材制造工艺迭代的关键。数字孪生里要建打印质量预测模型孔隙分布特征就是训练数据的标签。一、实际应用场景真实痛点场景设定金属 3D 打印SLM车间每批次试样经显微 CT 或光学扫描后输出孔隙检测数据。数据粒度细到单层单孔但质量部门需要试样级孔隙率判定≤0.5% 合格并进一步区分缺陷类型未熔合/球化/气孔以追溯工艺参数。现场原话叙事化不是我们打不好小陈说是孔隙这东西太狡猾。有时候孔隙率 0.3% 看着合格但全是 200μm 以上的大孔力学性能照样崩。有时候孔隙率 0.6% 超了但全是 10μm 以下微孔拉伸强度其实没问题。我就想先看总数再看分布再看形貌分类三层筛下来才知道到底是哪出了问题。还有批次问题小陈补充上周五打的 10 个试样有 4 个超标这周一打的 10 个全合格。同一台设备、同一批粉末、同一个人操作。区别就是周五下午保护气瓶快空了没换。这种批次效应靠肉眼看 CT 图像根本发现不了得统计。核心矛盾单层单孔级检测流水 与 试样级孔隙率判定 缺陷形貌分类 层分布定位 批次效应检测 超标根因追溯 之间的断层。二、痛点分析映射到滨州职业学院《先进制造技术》课程模型《先进制造技术》模块 本篇痛点对应增材制造(3D打印)技术工艺缺陷机理、孔隙率控制 孔隙检测数据聚合与超标判定先进制造技术基础质量检测、统计过程控制(SPC) 孔隙率分布统计与正态性检验智能制造与数字孪生打印质量预测模型数据底座 缺陷特征工程与标签化FMS与先进生产管理批次追溯、工艺参数反馈 批次效应检测与根因定位先进制造新模式数据驱动工艺优化 聚类分群识别缺陷模式一句话总结我们需要一个3D 打印零件孔隙检测数据分析与超标筛选程序用pandas 聚合试样级孔隙率numpy 算体积分数scipy 做正态性检验和批次对比scikit-learn 聚类缺陷模式networkx 建试样-层-缺陷类型关系网实现从CT 切片散点到孔隙率判定 形貌分类 层定位 批次追溯。三、核心逻辑讲解大白话3.1 问题本质把孔隙想成面包里的洞把 3D 打印零件想成一片吐司面包* 孔隙 面包里的气泡有的小如针尖有的大如樱桃* 孔隙率 气泡总体积 ÷ 面包总体积* 超标 气泡太多太大面包一捏就塌* 层分布 气泡集中在哪几片面包上如果全在一片那片面团有问题* 形貌分类 气泡是圆的气体还是长的未熔合还是球形的球化* 批次效应 同一天烤的面包都有大气泡那天烤箱温度不对3.2 业务逻辑 → 代码映射导入孔隙检测数据│▼ PoreLoader (pandas)读取 CSVsample_id, layer, pore_area, equivalent_diameter,x_coord, y_coord, gray_value, defect_type,part_volume校验必要列 过滤无效值│▼ PoreRateCalculator (pandas/numpy)试样级聚合总孔隙体积 Σ(孔隙面积×层厚) 或 直接用孔隙体积列孔隙率 总孔隙体积 / 零件体积超标标记 (阈值 0.5%)│▼ PoreStatistics (scipy)统计检验Shapiro-Wilk 正态性检验(孔隙率是否正态)批次间 t 检验(周五 vs 周一)│▼ PoreDefectClassifier (scikit-learn)缺陷形貌分类特征[等效直径, 圆度, 灰度]KMeans 聚类 → 微孔/中孔/大孔/异常孔│▼ PoreGraph (networkx)试样-层-缺陷类型关系网节点试样/层/缺陷类型边存在关系, 边权该层该类缺陷数量│▼ PoreVisualizer (matplotlib)可视化1. 试样孔隙率条形图(红超标)2. 孔隙率分布直方图正态拟合3. 超标试样层分布热力图4. 缺陷聚类散点图(直径 vs 灰度)5. 试样-层-缺陷关系网│▼ SyntheticPoreGenerator (numpy)合成数据20试样×多批次正常孔隙率~0.3%含3个超标试样(局部大孔)缺陷类型: 未熔合/球化/气孔3.3 为什么不能只看均值视角 问题孔隙率均值 0.3% 可能掩盖个别试样 2%只看最大孔径 忽略大量微孔累积效应三层分析 率分布形貌缺一不可3.4 分析前后对比维度 CT 原始数据 本程序孔隙率 手动算 自动聚合超标判定 凭经验 阈值统计检验缺陷分类 靠人眼 聚类自动分层定位 翻切片 热力图一目了然批次追溯 无 t 检验分组对比四、OOP 代码实现4.1 项目结构slm_pore_analysis/├── slm_pore_analysis/│ ├── __init__.py│ ├── pore_loader.py # 孔隙数据加载│ ├── pore_rate_calculator.py # 孔隙率计算│ ├── pore_statistics.py # 统计检验(scipy)│ ├── pore_defect_classifier.py # 缺陷聚类(sklearn)│ ├── pore_graph.py # 试样-层关系网(networkx)│ ├── visualizer.py # 可视化│ └── synthetic_data.py # 合成数据├── tests/│ ├── __init__.py│ └── test_pore_analysis.py├── results/│ ├── pore_rate_bar.png│ ├── pore_dist_hist.png│ ├── layer_heatmap.png│ ├── defect_cluster.png│ ├── pore_graph.png│ ├── pore_rates.csv│ ├── reject_list.csv│ ├── defect_clusters.csv│ └── pore_report.txt└── run_pore_analysis.py4.2 核心源码detailssummary/summary3D打印孔隙检测数据加载器import pandas as pdfrom pathlib import Pathfrom typing import Optionalclass PoreLoader:加载CT/光学孔隙检测数据def __init__(self, filepath: str pore_data.csv,encoding: str utf-8):self.filepath Path(filepath)self.encoding encodingself._raw: Optional[pd.DataFrame] Nonedef load(self) - pd.DataFrame:if not self.filepath.exists():raise FileNotFoundError(f文件不存在: {self.filepath})self._raw pd.read_csv(self.filepath, encodingself.encoding)rename {}for tgt, al in {sample_id: [sample_id, 试样编号, sample],layer: [layer, 层号, layer_no],pore_area: [pore_area, 孔隙面积, area],equivalent_diameter: [equivalent_diameter, 等效直径, eq_diam],x_coord: [x_coord, X坐标, x],y_coord: [y_coord, Y坐标, y],gray_value: [gray_value, 灰度, gray],defect_type: [defect_type, 缺陷类型, defect],part_volume: [part_volume, 零件体积, volume],pore_volume: [pore_volume, 孔隙体积, pore_vol],batch: [batch, 批次, batch_id],}.items():if tgt not in self._raw.columns:for a in al:if a in self._raw.columns:rename[a] tgtbreakself._raw self._raw.rename(columnsrename)req [sample_id, layer]miss [c for c in req if c not in self._raw.columns]if miss:raise ValueError(f缺少必要列: {miss})# 数值列处理num_cols [pore_area, equivalent_diameter, pore_volume,part_volume, gray_value, x_coord, y_coord]for c in num_cols:if c in self._raw.columns:self._raw[c] pd.to_numeric(self._raw[c], errorscoerce)# 体积计算: 如果有 pore_volume 用 pore_volume, 否则用 pore_area × 层厚(默认0.03mm)if pore_volume not in self._raw.columns:layer_thickness 0.03 # mmself._raw[pore_volume] (self._raw.get(pore_area, 0) * layer_thickness)self._raw self._raw.dropna(subset[sample_id, layer])self._raw[defect_type] self._raw.get(defect_type, 未知).fillna(未知).str.strip()self._raw[batch] self._raw.get(batch, B-001).fillna(B-001).str.strip()return self._raw.reset_index(dropTrue)/detailsdetailssummary/summary孔隙率计算与超标筛选import numpy as npimport pandas as pdfrom typing import Optionalclass PoreRateCalculator:试样级孔隙率计算def __init__(self, threshold: float 0.005):self.threshold threshold # 默认0.5%def calculate(self, df: pd.DataFrame) - pd.DataFrame:按试样聚合计算孔隙率if pore_volume not in df.columns:raise ValueError(缺少 pore_volume 列)agg df.groupby(sample_id).apply(lambda g: pd.Series({total_pore_volume: g[pore_volume].sum(),part_volume: g[part_volume].iloc[0] if part_volume in g.columnselse g[pore_volume].sum() / 0.005, # 反推pore_count: len(g),max_diameter: g.get(equivalent_diameter, pd.Series([0])).max(),mean_diameter: g.get(equivalent_diameter, pd.Series([0])).mean(),layers_affected: g[layer].nunique(),batch: g[batch].iloc[0] if batch in g.columns else B-001,})).reset_index()agg[pore_rate] (agg[total_pore_volume] / agg[part_volume]).round(6)agg[is_reject] agg[pore_rate] self.thresholdagg[pore_rate_pct] (agg[pore_rate] * 100).round(3)return agg.sort_values(pore_rate, ascendingFalse).reset_index(dropTrue)def reject_list(self, rate_df: pd.DataFrame) - pd.DataFrame:超标试样清单return rate_df[rate_df[is_reject]].copy().reset_index(dropTrue)def layer_concentration(self, df: pd.DataFrame,sample_id: str) - pd.DataFrame:指定试样的层分布sub df[df[sample_id] sample_id].copy()if sub.empty:return pd.DataFrame()layer_stat sub.groupby(layer).agg(pore_count(pore_area, count) if pore_area in sub.columnselse (equivalent_diameter, count),total_volume(pore_volume, sum),).reset_index()layer_stat[total_volume] layer_stat[total_volume].round(4)return layer_stat.sort_values(total_volume, ascendingFalse).reset_index(dropTrue)/detailsdetailssummary/summary孔隙率统计检验 (scipy)import numpy as npimport pandas as pdfrom scipy import statsfrom typing import Dict, Optionalclass PoreStatistics:正态性检验 批次对比def __init__(self, alpha: float 0.05):self.alpha alphadef normality_test(self, pore_rates: np.ndarray) - Dict:Shapiro-Wilk 正态性检验if len(pore_rates) 3:return {statistic: np.nan, p_value: np.nan, is_normal: False}stat, p stats.shapiro(pore_rates)return {statistic: round(float(stat), 4),p_value: round(float(p), 4),is_normal: bool(p self.alpha),}def batch_ttest(self, df: pd.DataFrame,batch_a: str, batch_b: str) - Dict:两批次孔隙率 t 检验a_rates df[df[batch] batch_a][pore_rate].valuesb_rates df[df[batch] batch_b][pore_rate].valuesif len(a_rates) 2 or len(b_rates) 2:return {statistic: np.nan, p_value: np.nan}stat, p stats.ttest_ind(a_rates, b_rates, equal_varFalse)return {batch_a: batch_a, batch_b: batch_b,mean_a: round(float(a_rates.mean()), 6),mean_b: round(float(b_rates.mean()), 6),statistic: round(float(stat), 4),p_value: round(float(p), 4),significant: bool(p self.alpha),}def summary_stats(self, rate_df: pd.DataFrame) - Dict:汇总统计rates rate_df[pore_rate].valuesreturn {count: len(rates),mean: round(float(np.mean(rates)), 6),std: round(float(np.std(rates, ddof1)), 6),min: round(float(np.min(rates)), 6),max: round(float(np.max(rates)), 6),median: round(float(np.median(rates)), 6),reject_count: int(rate_df[is_reject].sum()),reject_ratio: round(float(rate_df[is_reject].mean()), 4),}/detailsdetailssummary/summary缺陷形貌聚类 (scikit-learn)import numpy as npimport pandas as pdfrom sklearn.cluster import KMeansfrom sklearn.preprocessing import StandardScalerfrom typing import Optionalclass PoreDefectClassifier:基于孔隙形貌特征的 KMeans 聚类def __init__(self, n_clusters: int 3, random_state: int 42):self.n_clusters n_clustersself.random_state random_statedef classify(self, df: pd.DataFrame) - pd.DataFrame:聚类并标注缺陷模式out df.copy()features []for c in [equivalent_diameter, gray_value]:if c in out.columns:features.append(c)if not features:out[defect_cluster] 0out[defect_pattern] 未知return outX out[features].fillna(0).valuesscaler StandardScaler()Xs scaler.fit_transform(X)km KMeans(n_clustersself.n_clusters, random_stateself.random_state)out[defect_cluster] km.fit_predict(Xs)# 按聚类中心标注模式centers scaler.inverse_transform(km.cluster_centers_)diam_idx features.index(equivalent_diameter) if equivalent_diameter in features else -1pattern_map {}for i, center in enumerate(centers):if diam_idx 0:d center[diam_idx]if d 50:pattern_map[i] 微孔组(50μm)elif d 150:pattern_map[i] 中孔组(50-150μm)else:pattern_map[i] 大孔组(150μm)else:pattern_map[i] f簇{i}out[defect_pattern] out[defect_cluster].map(pattern_map)return outdef cluster_summary(self, df: pd.DataFrame) - pd.DataFrame:聚类汇总if defect_pattern not in df.columns:return pd.DataFrame()rows []for pat, g in df.groupby(defect_pattern):rows.append({pattern: pat,count: len(g),avg_diameter: round(g.get(equivalent_diameter, 0).mean(), 2),avg_gray: round(g.get(gray_value, 0).mean(), 2),samples_affected: g[sample_id].nunique(),})return pd.DataFrame(rows).sort_values(avg_diameter).reset_index(dropTrue)/detailsdetailssummary/summary试样-层-缺陷关系网 (networkx)import networkx as nximport pandas as pdfrom typing import Optionalclass PoreGraph:建试样-层-缺陷类型三层关系网def __init__(self):self.G nx.Graph()def build(self, df: pd.DataFrame) - nx.Graph:self.G.clear()for _, r in df.iterrows():sid fS:{r[sample_id]}lid fL:{r[layer]}dt fD:{r.get(defect_type, 未知)}self.G.add_node(sid, ntypesample)self.G.add_node(lid, ntypelayer)self.G.add_node(dt, ntypedefect)w float(r.get(pore_volume, r.get(pore_area, 1)))self._add_edge(sid, lid, w)self._add_edge(lid, dt, w)return self.Gdef _add_edge(self, a, b, w):if self.G.has_edge(a, b):self.G[a][b][weight] welse:self.G.add_edge(a, b, weightw)def defect_hotspots(self) - pd.DataFrame:按缺陷类型节点加权度排序rows []for n, d in self.G.nodes(dataTrue):if d.get(ntype) defect:ws [self.G[n][nb][weight] for nb in self.G.neighbors(n)]rows.append({defect_type: n.split(:)[-1],total_weight: round(sum(ws), 4),connected_layers: len([nb for nb in self.G.neighbors(n)if self.G.nodes[nb].get(ntype) layer]),})df pd.DataFrame(rows)if df.empty:return dfreturn df.sort_values(total_weight, ascendingFalse).reset_index(dropTrue)/detailsdetailssummary/summary可视化 (matplotlib)import numpy as npimport pandas as pdimport matplotlib.pyplot as pltfrom pathlib import Pathplt.rcParams[font.sans-serif] [SimHei, DejaVu Sans]plt.rcParams[axes.unicode_minus] Falseclass PoreVisualizer:def __init__(self, results_dir: str results):self.results_dir Path(results_dir)self.results_dir.mkdir(exist_okTrue)def pore_rate_bar(self, rate_df):试样孔隙率条形图(超标红色)fig, ax plt.subplots(figsize(14, 6))colors [#E74C3C if r else #3498DBfor r in rate_df[is_reject]]y_pos range(len(rate_df))ax.barh(y_pos, rate_df[pore_rate_pct], colorcolors, alpha0.85)ax.set_yticks(list(y_pos))ax.set_yticklabels(rate_df[sample_id], fontsize9)ax.set_xlabel(孔隙率 (%))ax.set_title(试样孔隙率排名(红色超标0.5%),fontsize14, fontweightbold)ax.axvline(x0.5, color#C0392B, linestyle--,linewidth2, label阈值0.5%)ax.legend()ax.grid(axisx, alpha0.3)plt.tight_layout()plt.savefig(self.results_dir / pore_rate_bar.png,dpi150, bbox_inchestight)plt.close()def distribution_hist(self, rate_df):孔隙率分布直方图fig, ax plt.subplots(figsize(10, 6))ax.hist(rate_df[pore_rate_pct], bins15, color#3498DB,edgecolorwhite, alpha0.85)ax.axvline(xrate_df[pore_rate_pct].mean(), color#E74C3C,linestyle-, linewidth2, label均值)ax.axvline(x0.5, color#C0392B, linestyle--,linewidth2, label阈值0.5%)ax.set_xlabel(孔隙率 (%))ax.set_ylabel(试样数)ax.set_title(孔隙率分布直方图, fontsize14, fontweightbold)ax.legend()ax.grid(axisy, alpha0.3)plt.tight_layout()plt.savefig(self.results_dir / pore_dist_hist.png,dpi150, bbox_inchestight)plt.close()def layer_heatmap(self, df, sample_id):指定试样层分布热力图sub df[df[sample_id] sample_id]if sub.empty or layer not in sub.columns:returnlayer_stat sub.groupby(layer)[pore_volume].sum().reset_index()layers sorted(sub[layer].unique())volumes [layer_stat[layer_stat[layer] l][pore_volume].sum()for l in layers]fig, ax plt.subplots(figsize(12, 4))colors plt.cm.Reds(np.array(volumes) / max(volumes [1]))ax.bar(range(len(layers)), volumes, colorcolors, edgecolorwhite)ax.set_xticks(range(len(layers)))ax.set_xticklabels(layers, rotation90, fontsize8)ax.set_xlabel(层号)ax.set_ylabel(孔隙体积 (mm³))ax.set_title(f试样 {sample_id} 层分布热力图,fontsize14, fontweightbold)plt.tight_layout()plt.savefig(self.results_dir / flayer_heatmap.png,dpi150, bbox_inchestight)plt.close()def defect_cluster_scatter(self, df):缺陷聚类散点图if defect_pattern not in df.columns:returnfig, ax plt.subplots(figsize(10, 7))patterns df[defect_pattern].unique()cmap plt.cm.Set1(np.linspace(0, 1, len(patterns)))for i, pat in enumerate(patterns):sub df[df[defect_pattern] pat]ax.scatter(sub.get(equivalent_diameter, 0),sub.get(gray_value, 0),c[cmap[i]], labelpat, s30, alpha0.7,edgecolorsblack, linewidths0.5)ax.set_xlabel(等效直径 (μm))ax.set_ylabel(灰度值)ax.set_title(缺陷形貌聚类散点图, fontsize14, fontweightbold)ax.legend(fontsize9)ax.grid(alpha0.3)plt.tight_layout()plt.savefig(self.results_dir / defect_cluster.png,dpi150, bbox_inchestight)plt.close()def graph_plot(self, G):关系网图fig, ax plt.subplots(figsize(14, 10))pos nx.spring_layout(G, seed42, k0.8)ncolor []for n, d in G.nodes(dataTrue):t d.get(ntype)ncolor.append({sample: #3498DB, layer: #27AE60,defect: #E74C3C}.get(t, #999))sizes [300 G.degree(n) * 40 for n in G.nodes()]nx.draw_networkx_nodes(G, pos, node_colorncolor,node_sizesizes, axax, alpha0.9)edges list(G.edges(dataTrue))ws 利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛
网站建设高端定制企业官网