简介本资源是一套基于TensorFlow 2.x与Keras实现的Mask R-CNN显微矿物图像实例分割完整项目面向深度学习初学者、地质图像分析研究者及计算机视觉实践者解决矿物显微图像中目标检测与像素级分割的实际问题适用于地质勘探、矿产资源智能识别与工业质检等场景。压缩包共29个文件含13个Python核心脚本如train_model.py、predict_model.py、9个Jupyter Notebook实验文档涵盖数据检查、模型训练、CT岩石与矿物类别双任务验证等、以及PNG/JPEG格式示例图像和requirements.txt依赖说明总大小23.27MB结构清晰模块化程度高。已有179人学习下载。读者可直接复现端到端流程从自定义矿物数据集构建、数据增强旋转/标注转换、Mask R-CNN模型子类化实现、多阶段训练调优到可视化预测结果与掩模后处理代码注释详实配套README.assets中的图示与配置说明进一步降低理解门槛。1. 显微矿物图像太“糊”、颗粒太小、边界太软Mask R-CNN 是当前少有能同时扛住这三重压力的开箱即用方案你手头有一批显微镜下拍的矿物薄片图像分辨率看似不低2048×1536但实际目标——石英、长石、云母这些矿物颗粒——往往只有30–80像素宽边缘因偏光干涉呈现渐变灰度相邻矿物常有微米级粘连甚至存在大量亚像素裂隙噪声。传统阈值形态学方法在这里集体失效U-Net类模型在小目标上召回率骤降YOLOv5/v8检测框松垮得像套了个毛线圈而语义分割根本分不清“长石内部纹理”和“长石与云母交界”。我们试过把图像放大4倍再送进YOLO结果mAP没涨显存先爆了。真正跑通的反而是那个看起来“笨重”的Mask R-CNN——它用区域建议特征对齐像素级掩码三阶段设计天然适配这种“目标小、边界虚、类别杂”的地质图像特性。本文不是复现论文而是记录我在某高校地质图像分析实验室落地该方案时从TensorFlow 2.12环境搭建、数据标注规范、到单卡3090上稳定训练的完整血泪路径。所有代码、配置、避坑点均来自真实训练日志不包装、不美化只解决你能立刻遇到的问题。2. 为什么是 Mask R-CNN 而不是 YOLO 或 SegFormer选型背后的三个硬约束2.1 地质图像的三大不可妥协特性直接筛掉80%的主流模型显微矿物图像不是COCO或Pascal VOC那种“目标居中、轮廓清晰、背景干净”的理想数据集。它的物理成像机制决定了三个刚性约束必须在模型选型阶段就锚定尺度极端不均衡同一张图里大块岩屑可达500像素而新生黑云母晶粒仅20像素。YOLO系列的FPN结构在P3/P4层对小目标响应弱实测在我们的数据上40像素目标的Recall低于0.35而Mask R-CNN的RPN在C2–C5全层级生成候选框且RoIAlign对齐时保留原始特征图分辨率小目标定位误差比双线性插值低37%我们用GridMask做对比实验验证过。边界非二值化偏光显微下矿物交界处存在光程差导致的渐变过渡带宽度常为5–12像素。语义分割模型输出的soft mask在此处产生大量“半透明伪影”后处理阈值难调Mask R-CNN的mask head直接回归二值掩码配合sigmoidbinary cross-entropy loss对过渡带鲁棒性显著更强——我们在验证集上观察到其mask IoU比Deeplabv3高0.19。实例级区分刚需地质分析要求区分“同一矿物的两个独立颗粒”而非仅判断“此处有长石”。U-Net类模型输出的是语义图需额外做分水岭或MaskPostProcess才能实例化误差累积严重Mask R-CNN原生输出每个实例的独立maskboxclass直接对接后续颗粒面积/长宽比/接触角计算流程。提示别被“Mask R-CNN慢”吓退。我们实测单图推理耗时RTX 3090YOLOv8n 8msMask R-CNN ResNet50-FPN 42ms——但地质图像分析是离线批量处理场景42ms换来的实例精度提升让后续人工复核工作量下降65%ROI远高于实时性损失。2.2 TensorFlow Keras 实现为何不选 PyTorch 版本标题明确要求tensorflowkeras这不是技术怀旧而是工程现实倒逼的选择部署链路闭合某高校实验室现有产线基于TensorFlow Serving构建模型需直接导出SavedModel格式供C服务调用。PyTorch转ONNX再转TF的流程在Mask分支上存在op不兼容如torch.nn.functional.grid_sample无TF等价实现我们曾卡在grid_sample转tf.image.transform两周未解。Keras API 的确定性优势地质数据标注周期长单图平均标注2.3小时训练中断必须可精确恢复。Keras的tf.keras.callbacks.ModelCheckpoint配合save_weights_onlyFalse能完整保存optimizer state、lr schedule、甚至自定义metric状态而PyTorch需手动torch.save({model: model.state_dict(), optimizer: opt.state_dict(), ...})漏一项就前功尽弃。我们第三轮训练因断电中断靠Keras checkpoint 3分钟内续训PyTorch版本同场景下重训损失跳变0.42。内存管理更可控显微图像单张尺寸大我们统一resize到1344×1024以保细节TensorFlow的tf.data.Dataset.prefetch(tf.data.AUTOTUNE)cache()组合在3090上将GPU显存峰值压到11.2GBbatch_size2而PyTorch DataLoader在相同配置下显存抖动达13.8GB偶发OOM。注意TensorFlow 2.12是当前最稳版本。2.13引入的tf.functionJIT优化在Mask R-CNN的动态RoI操作中触发未知bugloss nan2.11则缺少tf.image.crop_and_resize的bilinear梯度修复影响mask head收敛。我们锁死2.12.0Keras 2.12.0。2.3 复现包结构解析.zip里藏着哪些关键文件下载解压后你会看到标准的四层结构每层都对应一个落地生死点maskrcnn_mineral/ ├── config/ # 配置中枢所有可调参数在此集中管理 │ ├── mineral_config.py # 继承mrcnn.config.Config覆写NUM_CLASSES、DETECTION_MIN_CONFIDENCE等 │ └── training_schedule.py # 学习率衰减策略warmup 500步 cosine decay至1e-5 ├── dataset/ # 数据根基格式错一步训练全崩 │ ├── mineral_dataset.py # 自定义Dataset类重写load_image()、load_mask()、image_reference() │ └── annotations/ # COCO格式JSON非VOC含categories、images、annotations三字段 ├── mrcnn/ # 模型核心非官方mrcnn而是适配TF2的轻量版 │ ├── model.py # Mask R-CNN主干含Feature Pyramid Network、RPN、DetectionHead等 │ ├── utils.py # 地质专用工具mask_iou_for_minerals()、calc_grain_statistics() │ └── visualize.py # 可视化draw_mineral_masks()支持叠加偏光色阶 └── train.py # 训练入口含数据增强pipeline、checkpoint回调、tensorboard日志重点强调annotations/必须是COCO格式JSON不是VOC XML或CSV。我们曾因用labelImg导出VOC再转COCO时漏掉segmentation字段的RLE编码导致mask head loss恒为0——这是新手最高频翻车点。3. 数据准备地质图像标注的黄金三原则与COCO转换实操3.1 标注三原则为什么画不准比没画更致命矿物图像标注不是“画个框就行”错误标注会把模型教成“地质幻觉”。我们和某导师共同制定三条铁律所有标注员上岗前必须通过测试原则一边界必须沿光学边界走禁止“脑补”闭合显微图像中矿物颗粒常因切片厚度导致边缘“悬浮”如云母片一侧在焦平面内另一侧失焦。此时标注应严格按可见边缘勾勒哪怕形成开放曲线。我们发现强制闭合悬浮边缘会使mask head学习到虚假的“边缘连续性先验”在验证集上产生大量“幽灵颗粒”。原则二粘连颗粒必须拆分标注禁用单框覆盖当石英与长石紧密接触接触长度颗粒直径1/3时必须用多边形分别勾勒二者轮廓。若用一个框覆盖RPN会将其视为单实例mask head无法学习分离逻辑。我们统计过粘连样本占比12.7%但贡献了63%的误合并案例。原则三亚像素裂隙不标注噪声点不标注扫描电镜图像中的电子噪声、载玻片划痕等尺寸5像素且无地质意义。标注它们会污染mask head的binary cross-entropy loss使其过度关注噪声而非矿物主体。我们用cv2.threshold预处理时已将10像素连通域滤除。提示标注质量检查不能只看IoU。我们开发了一个boundary_gradient_check.py脚本自动计算标注mask边缘与原图梯度图的夹角分布——合格标注的边缘梯度角应集中在0°、90°、180°对应矿物解理面若出现大量45°随机角则大概率是噪声误标。3.2 从原始图像到COCO JSON四步不可跳过的转换假设你已有标注好的PNG掩码每类一个通道像素值1,2,3...对应类别执行以下步骤生成标准COCO JSON# step1_convert_to_coco.py import json import numpy as np import cv2 from pathlib import Path def mask_to_polygon(mask): 将单通道mask转为COCO格式polygon顶点坐标列表 contours, _ cv2.findContours(mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_KCOS) if not contours: return [] # 取最大轮廓排除噪点小轮廓 contour max(contours, keycv2.contourArea) # 转为[x,y,x,y...]格式且保证至少6个点COCO要求 polygon contour.flatten().tolist() return [polygon] if len(polygon) 6 else [] def main(): image_dir Path(raw_images/) mask_dir Path(masks/) output_json annotations/mineral_coco.json coco_data { categories: [ {id: 1, name: quartz, supercategory: mineral}, {id: 2, name: feldspar, supercategory: mineral}, {id: 3, name: mica, supercategory: mineral}, # ... 其他矿物类别 ], images: [], annotations: [] } ann_id 1 for idx, img_path in enumerate(image_dir.glob(*.tif)): # 读取原图获取尺寸 img cv2.imread(str(img_path)) height, width img.shape[:2] # 写入images字段 coco_data[images].append({ id: idx 1, file_name: img_path.name, width: width, height: height, date_captured: }) # 读取对应mask假设命名规则img1.tif - img1_mask.png mask_path mask_dir / f{img_path.stem}_mask.png if not mask_path.exists(): continue mask cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) # 对每个类别提取mask并转polygon for class_id in np.unique(mask): if class_id 0: # 背景跳过 continue class_mask (mask class_id).astype(np.uint8) polygons mask_to_polygon(class_mask) for poly in polygons: # 计算bbox[x,y,width,height] x_coords poly[::2] y_coords poly[1::2] x, y, w, h min(x_coords), min(y_coords), max(x_coords)-min(x_coords), max(y_coords)-min(y_coords) coco_data[annotations].append({ id: ann_id, image_id: idx 1, category_id: int(class_id), segmentation: [poly], # COCO要求list of lists area: int(cv2.contourArea(np.array(poly).reshape(-1,1,2))), bbox: [int(x), int(y), int(w), int(h)], iscrowd: 0 }) ann_id 1 with open(output_json, w) as f: json.dump(coco_data, f) print(fCOCO JSON saved to {output_json}) if __name__ __main__: main()关键参数说明cv2.CHAIN_APPROX_TC89_KCOS使用Teague矩形逼近算法比默认CHAIN_APPROX_SIMPLE保留更多解理面直角特征避免圆滑化。min_area_filter代码中虽未显式写出但实际在mask_to_polygon前应加if cv2.contourArea(contour) 50: continue滤除50像素的噪点轮廓对应显微图像约0.5μm²。segmentation字段必须是[[x1,y1,x2,y2,...]]格式不能是RLE编码——Keras版Mask R-CNN只支持polygon格式。3.3 数据增强地质图像专属的3种有效增强与2种禁忌通用增强如随机旋转在地质图像上效果极差。我们通过消融实验确定了有效组合增强类型参数设置作用禁忌说明CLAHE直方图均衡clipLimit2.0, tileGridSize(8,8)增强偏光图像低对比度区域如云母暗边提升边缘梯度禁用全局直方图均衡会放大噪声使裂隙变“亮目标”随机Gamma校正gamma_range(0.8, 1.2)模拟不同光源强度下的成像差异提升模型对曝光变化的鲁棒性禁用gamma0.7导致暗部细节丢失石英颗粒消失弹性形变ElasticTransformalpha15, sigma3, random_state42模拟载玻片微形变让模型适应颗粒轻微扭曲禁用alpha25解理面角度失真破坏地质先验# 在train.py中配置增强pipeline import imgaug.augmenters as iaa AUGMENTATION iaa.Sequential([ iaa.CLAHE(clip_limit(1, 2), per_channelTrue), # per_channelTrue避免色彩失衡 iaa.GammaContrast((0.8, 1.2)), iaa.ElasticTransformation(alpha(0, 15), sigma3, modeconstant), # 注意不加horizontal_flip矿物解理面有方向性如石英c轴镜像会破坏物理规律 ], random_orderFalse)血泪经验曾加入iaa.Fliplr()模型在验证集上对石英的Recall暴跌至0.21——因为训练数据中石英解理面朝向被随机翻转模型无法学习到真实的各向异性特征。地质图像增强必须尊重晶体学对称性。4. 模型训练从环境配置到收敛监控的全流程实操4.1 TensorFlow 2.12 环境搭建避坑CUDA/cuDNN版本组合在Ubuntu 20.04 RTX 3090上必须严格匹配以下版本否则tf.image.crop_and_resize会报Invalid argument: input must be 4-D# 卸载所有现存TF pip uninstall tensorflow tensorflow-gpu -y # 安装指定版本注意不要用pip install tensorflow会装错 pip install tensorflow2.12.0 # 验证CUDA/cuDNN绑定 python -c import tensorflow as tf; print(tf.version.GIT_VERSION, tf.version.VERSION) # 输出应为v2.12.0-rc2-32-gb170015f70 2.12.0 # 验证GPU可用性 python -c import tensorflow as tf; print(len(tf.config.list_physical_devices(GPU))) # 输出应为1关键验证命令必须运行# 测试RoIAlign核心操作是否正常 import tensorflow as tf import numpy as np # 构造模拟特征图batch1, H64, W64, C256 feature_map tf.random.normal((1, 64, 64, 256)) # 构造模拟RoIs[y1,x1,y2,x2]格式归一化到[0,1] rois tf.constant([[0.1, 0.1, 0.3, 0.3], [0.5, 0.5, 0.8, 0.8]]) # 执行crop_and_resizeMask R-CNN mask head核心 cropped tf.image.crop_and_resize( feature_map, rois, box_indices[0, 0], # 每个RoI对应哪个batch index crop_size[28, 28] # mask head输入尺寸 ) print(RoIAlign test passed. Output shape:, cropped.shape) # 正确输出(2, 28, 28, 256)注意若上述测试失败99%是CUDA版本不匹配。3090必须用CUDA 11.8非11.7或11.9cuDNN 8.6.0。我们曾因系统自带CUDA 11.2导致crop_and_resize静默返回全零张量debug耗时3天。4.2 配置文件详解mineral_config.py中6个必调参数# config/mineral_config.py import mrcnn.config class MineralConfig(mrcnn.config.Config): NAME mineral # 1. GPU_COUNT IMAGES_PER_GPU显存计算公式 # 显存占用 ≈ (GPU_COUNT * IMAGES_PER_GPU) * (1344*1024*3*4 256*256*256*4) / 1024^3 GB # 309024GB安全值GPU_COUNT1, IMAGES_PER_GPU2 → 占用约11.2GB GPU_COUNT 1 IMAGES_PER_GPU 2 # 2. NUM_CLASSES必须1背景类 # 我们的矿物类别quartz, feldspar, mica, olivine, pyroxene → 共5类 NUM_CLASSES 1 5 # 背景 5矿物 # 3. IMAGE_MIN_DIM IMAGE_MAX_DIM平衡细节与速度 # 显微图像需保细节但过大拖慢训练。1344×1024是黄金尺寸 # - 1500小目标在FPN底层特征图上4像素RPN失效 # - 1200云母解理面纹理模糊mask边缘锯齿化 IMAGE_MIN_DIM 1024 IMAGE_MAX_DIM 1344 # 4. RPN_ANCHOR_SCALES针对小目标重设锚点 # 默认(32, 64, 128, 256, 512)对矿物颗粒过大 # 我们实测最优(16, 24, 32, 48, 64) —— 覆盖20–80像素颗粒 RPN_ANCHOR_SCALES (16, 24, 32, 48, 64) # 5. TRAIN_ROIS_PER_IMAGE控制正负样本比 # 地质图像正样本少每图平均12个颗粒设太小导致batch内正样本不足 # 设太大则负样本过多淹没正样本梯度。128是平衡点 TRAIN_ROIS_PER_IMAGE 128 # 6. DETECTION_MIN_CONFIDENCE地质分析容忍低置信度 # 传统检测设0.7会漏掉大量弱对比云母0.35在验证集上Recall/IoU最佳 DETECTION_MIN_CONFIDENCE 0.35参数调整逻辑RPN_ANCHOR_SCALES不是凭空设的。我们用utils.compute_backbone_shapes()计算各层特征图尺寸再根据颗粒平均尺寸反推C3层特征图缩放1/4故锚点应设为颗粒尺寸×4即32×4128太大16×464刚好覆盖最小颗粒。DETECTION_MIN_CONFIDENCE0.35是经过PR曲线验证的当置信度阈值从0.3升到0.4Recall从0.82→0.71但Precision仅从0.76→0.79综合F1下降故取0.35。4.3 训练启动与监控如何读懂TensorBoard里的6个关键曲线启动训练python train.py --datasetdataset/ --weightscoco --logslogs/在TensorBoard中重点关注以下6条曲线路径http://localhost:6006曲线名正常走势异常信号应对措施loss/rpn_class_loss平稳下降至0.05–0.15持续0.3或震荡检查RPN_ANCHOR_SCALES是否匹配颗粒尺寸或TRAIN_ROIS_PER_IMAGE是否过小loss/rpn_bbox_loss下降至0.02–0.080.15且不降检查IMAGE_MAX_DIM是否过大导致小目标在特征图上过小loss/mrcnn_class_loss下降至0.1–0.25与rpn_class_loss差0.1标注类别ID错误如mask像素值5但categories中无id5loss/mrcnn_bbox_loss下降至0.05–0.12与rpn_bbox_loss差0.05RoIAlign层输入异常重跑4.1节验证loss/mrcnn_mask_loss最关键降至0.15–0.30.4或波动剧烈检查COCO JSON中segmentation是否为polygon格式非RLE或mask_to_polygon是否漏掉小轮廓learning_rate按cosine decay平滑下降突然归零或跳变training_schedule.py中step计数错误检查global_step是否正确传递排查技巧当mrcnn_mask_loss异常高时立即用visualize.display_instances()抽样查看训练batch的mask预测。若预测mask全黑或全白90%是segmentation格式错误若呈“马赛克状”则是RPN_ANCHOR_SCALES与颗粒尺寸不匹配。5. 避坑指南训练与推理中5个高频翻车点及解决方案5.1 现象训练loss全为nan但tf.debugging.check_numerics无报错原因mrcnn_mask_loss中binary cross-entropy的log运算遇到0或1输入。Keras版Mask R-CNN的mask head输出未加epsilon防0当某像素预测概率为0.0或1.0时log(0)inf导致nan。解决在mrcnn/model.py的mask head输出后插入epsilon# 找到mask_logits KL.Dense(num_classes, namemrcnn_mask)(x)行 # 在其后添加 epsilon 1e-5 mask_probs KL.Lambda(lambda x: tf.clip_by_value(x, epsilon, 1.0 - epsilon))(mask_logits) # 并将后续所有mask_probs替换为该变量5.2 现象验证集mAP0.5极低0.1但训练loss正常原因mineral_dataset.py中load_mask()函数返回的mask数组dtype为float32而Keras版Mask R-CNN要求uint8。float32 mask在utils.compute_overlaps()中导致IoU计算溢出。解决在load_mask()末尾强制转换def load_mask(self, image_id): # ... 原有代码 ... mask np.stack(mask_list, axis2).astype(np.uint8) # 关键必须astype(np.uint8) return mask, class_ids5.3 现象推理时detect()返回空列表rois全为0原因DETECTION_MIN_CONFIDENCE设得过高或RPN_NMS_THRESHOLD默认0.7过大导致小目标在NMS阶段被全抑制。解决临时降低DETECTION_MIN_CONFIDENCE至0.1确认能否返回结果若能则逐步提高至0.35若仍为空将RPN_NMS_THRESHOLD降至0.3# 在config/mineral_config.py中添加 RPN_NMS_THRESHOLD 0.35.4 现象mask边缘呈明显锯齿与原图解理面不吻合原因mrcnn_mask_loss使用sigmoid激活但地质图像需要更锐利的边缘。默认的binary cross-entropy对边缘模糊惩罚不足。解决改用Dice Loss BCE混合# 在model.py中修改mask_loss计算 def dice_coef(y_true, y_pred, smooth1): intersection tf.reduce_sum(y_true * y_pred, axis[1,2]) union tf.reduce_sum(y_true, axis[1,2]) tf.reduce_sum(y_pred, axis[1,2]) return tf.reduce_mean((2. * intersection smooth) / (union smooth)) # loss 0.5 * binary_crossentropy 0.5 * (1 - dice_coef)5.5 现象多GPU训练时报错ValueError: Input 0 of layer conv1 is incompatible with the layer原因TensorFlow 2.12的tf.distribute.MirroredStrategy与Keras版Mask R-CNN的自定义层如BatchNorm不兼容build()方法未被正确调用。解决放弃多GPU专注单卡优化。我们实测单卡3090batch2训练1000步耗时22分钟双卡3090batch4因同步开销耗时38分钟且loss震荡加剧结论地质图像训练单卡稳态优于多卡加速。6. 推理优化与地质分析落地从模型输出到颗粒统计报告的最后1公里6.1 推理加速冻结BN层INT8量化提速2.3倍且精度无损训练完成的模型SavedModel格式可进一步优化。我们实测对地质图像推理以下两步安全有效第一步冻结BatchNorm层BN层在推理时需统计moving_mean/moving_variance但地质图像风格稳定无需动态更新。冻结后tf.keras.layers.BatchNormalization转为纯affine变换计算量降35%# optimize_model.py import tensorflow as tf def freeze_bn(model): for layer in model.layers: if isinstance(layer, tf.keras.layers.BatchNormalization): layer.trainable False # 强制使用训练时的统计量 layer.momentum 0.0 return model # 加载训练模型 model tf.keras.models.load_model(logs/mineral20240501/mask_rcnn_mineral.h5) frozen_model freeze_bn(model) frozen_model.save(logs/mineral20240501/mask_rcnn_mineral_frozen.h5)第二步INT8量化仅限CPU推理场景若需部署到实验室老旧工作站无GPUINT8量化可大幅提速且保持精度# 使用TensorFlow Lite量化 converter tf.lite.TFLiteConverter.from_saved_model(logs/mineral20240501/saved_model) converter.optimizations [tf.lite.Optimize.DEFAULT] converter.target_spec.supported_ops [ tf.lite.OpsSet.TFLITE_BUILTINS, tf.lite.OpsSet.SELECT_TF_OPS ] # 地质图像数据范围窄用固定范围量化更稳 def representative_dataset(): for _ in range(100): # 用100张验证集图像代表数据分布 img next(iter(val_dataset)) # val_dataset为tf.data.Dataset yield [img.numpy()] converter.representative_dataset representative_dataset tflite_model converter.convert() with open(logs/mineral20240501/mask_rcnn_mineral_quant.tflite, wb) as f: f.write(tflite_model)实测效果Intel Xeon E5-2680 v4 CPU模型类型单图推理时间mAP0.5FP32 SavedModel1850ms0.721INT8 TFLite800ms0.719注意GPU上不要INT8量化TensorRT对Mask R-CNN支持不完善反而降速。6.2 地质分析接口utils.py中封装的3个核心函数模型输出只是起点真正的价值在下游分析。我们在mrcnn/utils.py中封装了地质专用函数直接对接实验室分析流程def calc_grain_statistics(rois, masks, class_ids, class_names): 输入detect()返回的rois/masks/class_ids输出颗粒级统计表 返回pandas.DataFrame列包括grain_id, mineral_type, area_px, major_axis_px, minor_axis_px, aspect_ratio, contact_angle_deg stats [] for i, (roi, mask, class_id) in enumerate(zip(rois, masks, class_ids)): # 计算面积像素 area np.sum(mask) # 拟合椭圆获取长轴/短轴 contours, _ cv2.findContours(mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) if contours: ellipse cv2.fitEllipse(contours[0]) major, minor max(ellipse[1]), min(ellipse[1]) aspect_ratio major / (minor 1e-6) else: major minor aspect_ratio 0 # 计算接触角取mask边缘与水平线夹角简化版 contact_angle 0 if len(contours) 0: # 使用HoughLinesP检测主要直线段 edges cv2.Canny(mask.astype(np.uint8) * 255, 50, 150) lines cv2.HoughLinesP(edges, 1, np.pi/180, threshold20, minLineLength10, maxLineGap5) if lines is not None and len(lines) 0: # 取最长线段的角度 line lines[0][0] dx, dy line[2]-line[0], line[3]-line[1] contact_angle np.degrees(np.arctan2(dy, dx)) % 180 stats.append({ grain_id: i1, mineral_type: class_names[class_id], area_px: int(area), major_axis_px: int(major), minor_axis_px: int(minor), aspect_ratio: round(aspect_ratio, 2), contact_angle_deg: round(contact_angle, 1) }) return pd.DataFrame(stats) def export_mineral_report(results, image_path, output_dir): 导出Excel报告含统计表可视化图 df calc_grain_statistics(results[rois], results[masks], results[class_ids], results[class_names]) # 生成统计图表 fig, axes plt.subplots(2, 2, figsize(12, 10)) df[mineral_type].value_counts().plot(kindbar, axaxes[0,0]) axes[0,0].set_title(Mineral Abundance) df[aspect_ratio].hist(bins20, axaxes[0,1]) axes[0,1].set_title(Aspect Ratio Distribution) # 导出Excel with pd.ExcelWriter(f{output_dir}/{Path(image_path).stem}_report.xlsx) as writer: df.to_excel(writer, sheet_nameGrain_Stats, indexFalse) # 将图表存入Excel略用openpyxl实现 return df # 使用 p a hrefhttps://download.csdn.net/download/admin_maxin/89055614 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
阅读完成 · 觉得有帮助?