摘要数电镜照片里的纳米颗粒是表征岗最枯燥的活——手动数几百个颗粒要一下午还会引入主观偏倚。本文用 scikit-image 写一个批量颗粒统计管线读图→预处理→分水岭分割→像素-纳米换算→粒径分布拟合并讲清过分割、边缘颗粒、尺度标定三个最容易翻车的环节。依赖scikit-image、numpy、matplotlib。一、为什么必须自动计数TEM/cryo-EM 照片的粒径统计审稿人和 CMC 审评都认数出来的分布number-weighted因为它直接反映颗粒真实尺寸——不像 DLS 是强度加权少量大颗粒能主导信号。但手动计数有三个系统性问题取样偏倚人会不自觉多数好看的、孤立的颗粒重叠和粘连的跳过——恰好把分布尾部切掉了样本量不足一张照片通常只有 50-150 个颗粒要 300 才能让分布的 D50 稳定手动数意味着 3-5 张照片;不可重复两个人数同一张照片结果可以差 10%。自动计数的意义就是把这件事从一下午变成一条命令同时让取样规则显式化、可审计。二、完整管线代码2.1 读图与预处理电镜照片常见问题背景灰度不均照明场漂移、噪声颗粒感、对比度低。预处理的目标是让阈值分割变稳importnumpyasnpimportmatplotlib.pyplotaspltfromskimageimportio,filters,morphology,measure,segmentation,featurefromscipyimportndimageasndi imgio.imread(particles.tif,as_grayTrue)# 1) 背景校正大半径开运算估计背景再相减照明不均时必需backgroundmorphology.opening(img,morphology.disk(25))corrimg-background# 2) 去噪 增强fromskimageimportexposure corrfilters.median(corr,morphology.disk(2))correxposure.equalize_adapthist(corr.astype(float)/corr.max())两个注意点disk(25)的半径要大于最大颗粒半径否则等于把颗粒本身也开掉adaptive histogram equalization 只是给阈值算法铺路不要拿增强后的图算灰度统计。2.2 阈值 分水岭处理粘连颗粒单阈值 (threshold_otsu) 对分散好的颗粒够用但电镜视野里颗粒经常粘连甚至重叠直接阈值会把两个颗粒连成一个。标准解法是距离变换 分水岭threshfilters.threshold_otsu(corr)binarycorrthresh# 距离变换每个前景像素到最近背景的距离distndi.distance_transform_edt(binary)coordsfeature.peak_local_max(dist,min_distance10,labelsbinary)markers,_ndi.label(ndi.maximum_filter(dist,size3)dist)# 简化标记markersnp.zeros(dist.shape,dtypeint)markers[tuple(coords.T)]np.arange(1,len(coords)1)labelssegmentation.watershed(-dist,markers,maskbinary)print(检出颗粒数:,labels.max())原理一句话距离变换在颗粒中心形成山峰两颗粒粘连处是山脊分水岭沿山脊切开。min_distance是最关键的参数——设小了过分割一个颗粒碎成两三个设大了粘连切不开。经验起点min_distance ≈ 预期粒径像素的一半然后对结果做 sanity check。2.3 过滤与测量分水岭输出的是带标签矩阵接下来剔除垃圾区域再测量propsmeasure.regionprops(labels)min_area,max_area50,5000# 像素²按标尺换算后设置areas_pxnp.array([p.areaforpinprops])keep(areas_pxmin_area)(areas_pxmax_area)# 边界颗粒剔除接触图像边缘的区域不完整必须排除on_edgeset(np.unique(np.concatenate([labels[0,:],labels[-1,:],labels[:,0],labels[:,-1]])))keepnp.array([p.labelnotinon_edgeforpinprops])三个必须过滤的对象太小噪声/碎屑、太大聚集体块或污物——除非你研究的就是聚集、触边不完整轮廓会低估粒径。2.4 尺度标定从像素到纳米# 方式A照片自带比例尺推荐# 手动量比例尺像素长度例如 100 nm 的标尺横跨 137 像素nm_per_px100/137# 方式B由放大倍数推# pixel_size detector_pixel_um / magnification需查相机手册diameters_nmnp.array([p.equivalent_diameterforpinnp.array(props,dtypeobject)[keep]])*nm_per_px标定是整个分析的头号误差源。同一台电镜不同放大倍数的照片绝不能混在一起统计除非各自标定后合并比例尺尽量在原始 TIFF 上量JPEG 压缩会挪动边缘。2.5 分布统计与拟合纳米颗粒粒径通常近似对数正态分布fromscipyimportstats ln_dnp.log(diameters_nm)mu,sigmaln_d.mean(),ln_d.std(ddof1)d_geonp.exp(mu)# 几何平均 中位数gsdnp.exp(sigma)# 几何标准差print(fN {len(diameters_nm)}, D50 {d_geo:.1f}nm, GSD {gsd:.2f})# 正态性检查对数空间print(log-normal 检验 p ,stats.shapiro(ln_d).pvalue)xsnp.linspace(diameters_nm.min(),diameters_nm.max(),200)plt.hist(diameters_nm,bins25,densityTrue,alpha0.6,label实测)plt.plot(xs,stats.lognorm.pdf(xs,sigma,scaled_geo),r-,labellog-normal 拟合)plt.xlabel(粒径 (nm));plt.ylabel(概率密度);plt.legend()plt.show()报告口径建议N颗粒总数、D50、D10/D90、GSD外加 KS 或 Shapiro 检验的 p 值。N 200 的分布别报百分位抽样误差比分辨率还大。三、三个高频翻车点翻车 1过分割分水岭对噪声极敏感——距离变换上的小毛刺就会被当成山峰一个大颗粒被切成三四块分布整体偏小。诊断方法把 labels 叠加回原图肉眼抽检 20 个颗粒修复方法先形态学开运算抹掉毛刺或调大min_distance或对 dist 先做一次高斯平滑再找峰。翻车 2把对比度当尺寸负染 TEM 里颗粒边缘有晕圈染色剂堆积阈值取得低则粒径系统性偏大。对同一批照片做过两种阈值otsu vs 手动 ±10%的敏感性分析D50 漂移超过 5% 说明分割对阈值敏感需要回头改预处理。翻车 3视野取样偏差自动数得快人就会偷懒只数最清楚的视野。正确做法是预先定取样规则每张碳膜网格随机取 N 个视野、焦距统一、不挑图——规则写进方法学比结果本身更重要。四、与其他表征交叉验证电镜的 number-weighted 分布和 DLS 的 intensity-weighted 分布可以直接换算对账Mie 理论假设球形换算后 DLS 预测峰应落在电镜分布乘 d⁶ 权重后的位置。两个常见对账结果DLS 峰明显偏大 → 样品里有少量大颗粒/聚集体电镜视野里容易漏因为大颗粒沉降或被稀释出去电镜分布偏小 → 负染干燥收缩或 Cryo-TEM 与 dry-TEM 的差异。NTA 给的是 number-weighted 但受检测下限影响50 nm 散射太弱。三种方法各报各的权重口径别混着比。五、小结电镜颗粒统计自动化的价值不在快在于取样规则显式化阈值、min_distance、过滤条件全部可审计可重复分水岭的 min_distance 从预期粒径一半起步结果必须叠图抽检尺度标定是最大误差源不同放大倍数照片不能混统分布报 N D50 GSD 检验 p 值N 200 不报百分位。参考scikit-image 官方文档watershed、regionprops、threshold 章节含可运行示例Meyer F. Topographic distance and watershed lines. Signal Processing, 1994分水岭算法原始文献负染/冷冻电镜样品制备对手动计数偏差的影响相关方法学讨论ISO 13322-1 粒度分析—图像分析法动态/静态图像粒度标准口径