
简介基于深度学习的矿物显微图像智能分类项目提供完整源码与说明文档面向计算机相关专业学生、毕业设计或课程设计开发者。项目采用迁移学习思路包含数据爬取、数据集划分、模型训练、评估与单张图像预测等完整流程并实现将模型转换为TensorFlow.js格式配合HTML/JS前端页面完成浏览器端实时演示。整个压缩包共15个文件涵盖7个Python脚本、2个Jupyter Notebook、2个HTML页面及配套JS、README说明和网络结构示意图大小仅1.52MB结构清晰。Python脚本覆盖数据爬取、数据划分、训练、评估、预测等环节Notebook适合逐段运行调试前端与模型转换代码则展示了从离线训练到Web部署的完整链路。目前已有79人学习下载既可作为机器学习入门实战也可直接作为课程设计、项目立项演示或毕业设计的基础框架具有一定的工程参考价值和可扩展性。1. 矿物显微图像分类为什么值得用深度学习做矿物显微图像分类是把偏光显微镜或扫描电镜下的矿物薄片图像自动识别为石英、长石、方解石、辉石等类别。传统做法是地质工程师用肉眼观察颜色、解理、干涉色等特征经验门槛高、主观性强。深度学习通过卷积神经网络直接学习图像纹理和结构特征在多个公开矿物数据集上的准确率能超过90%且推理速度远快于人工。这套源码包正好把完整链路都给了出来从数据采集的spider_data.py到训练脚本train.py再到能转成TensorFlow.js部署到浏览器的convert_model_to_javascript.py适合做课程设计、毕业设计也适合想实际跑通一个图像分类项目的开发者。2. 数据准备与预处理spider_data.py 与 split_data.py 的工程化拆解深度学习项目的成败很大程度取决于数据质量。矿物图像分类的数据集不像ImageNet那样随手可下载通常需要自己采集和整理。这个项目里用spider_data.py来做数据抓取再用split_data.py划分数据集实现了从原始图像到训练集的完整流程。2.1 用spider_data.py爬取矿物图像数据spider_data.py的核心作用是从矿物图片网站或搜索引擎结果中批量抓取图像。常见的做法是用requests库请求图片列表页解析出图片直链再通过concurrent.futures多线程下载。下面的代码示意了这类脚本的骨架import requests import os from concurrent.futures import ThreadPoolExecutor def download_image(url, save_path): headers {User-Agent: Mozilla/5.0} try: r requests.get(url, headersheaders, timeout10) if r.status_code 200 and len(r.content) 2000: with open(save_path, wb) as f: f.write(r.content) print(f下载成功: {save_path}) else: print(f跳过无效图片: {url}) except Exception as e: print(f下载失败 {url}: {e}) def spider(mineral_name, urls, save_dir): os.makedirs(save_dir, exist_okTrue) with ThreadPoolExecutor(max_workers8) as executor: for i, url in enumerate(urls): save_path os.path.join(save_dir, f{mineral_name}_{i}.jpg) executor.submit(download_image, url, save_path)这段代码的逻辑是先用os.makedirs建好类别目录再把图片URL列表映射成本地文件路径借助ThreadPoolExecutor把下载任务丢进8个线程并行执行。下载时判断响应码和文件大小是为了过滤掉404页面和过小的占位图避免把无效图片送进训练集。参数上timeout10是连接超时防止某个URL长时间挂起。max_workers8控制并发数如果目标网站有反爬建议降到2或3并加上time.sleep(random.uniform(0.5, 1.5))。len(r.content) 2000这个阈值可以根据图像尺寸调整通常显微镜照片都在几十KB以上设置更保守的20KB也可以。2.2 数据清洗与标签整理爬下来的图片经常混有网页logo、重复截图、非目标矿物所以需要清洗。项目里没有单独的清洗脚本但spider_data.py在保存时按矿物名建目录这本身就承担了给数据打标签的功能。目录结构如下data/ ../../../calcite/ ../../../feldspar/ ../../../quartz/ ../../../garnet/这种按类别分目录的组织方式是Keras的ImageDataGenerator和flow_from_directory直接支持的目录名就是标签。清洗时可以直接人工浏览每个目录把明显不对的图片删除也可以用简单脚本计算所有图片的哈希值去重import hashlib from pathlib import Path from collections import defaultdict def find_duplicates(root_dir): hashes defaultdict(list) for path in Path(root_dir).rglob(*.jpg): with open(path, rb) as f: digest hashlib.md5(f.read()).hexdigest() hashes[digest].append(path) return {digit: paths for digit, paths in hashes.items() if len(paths) 1}这个函数遍历所有.jpg文件计算MD5摘要把相同摘要的路径归到同一组。返回的字典里value长度大于1的组就是重复图片组删除多余副本即可。注意MD5只对完全相同的文件有效如果图片是同一张但被缩放或重压缩过需要改用感知哈希比如imagehash库的phash方法。2.3 split_data.py划分训练集、验证集、测试集一个容易踩的坑是随意划分数据集导致类别分布不一致。split_data.py的作用是按固定比例把每个类别的图片拆成三份。常见参数是训练集70%、验证集15%、测试集15%并且设置随机种子保证可复现。核心逻辑如下import random import shutil from pathlib import Path def split_data(src_dir, train_dir, val_dir, test_dir, train_ratio0.7, val_ratio0.15, seed42): random.seed(seed) for class_dir in Path(src_dir).iterdir(): if not class_dir.is_dir(): continue images list(class_dir.glob(*.jpg)) list(class_dir.glob(*.png)) random.shuffle(images) n_train int(len(images) * train_ratio) n_val int(len(images) * val_ratio) assign {train: images[:n_train], val: images[n_train:n_train n_val], test: images[n_train n_val:]} for split_name, file_list in assign.items(): target Path(train_dir if split_name train else val_dir if split_name val else test_dir) / class_dir.name target.mkdir(parentsTrue, exist_okTrue) for img in file_list: shutil.copy(img, target / img.name) if __name__ __main__: split_data(data, train_data, val_data, test_data)这段代码先对每个类别的图片列表做随机打乱再按比例切片。shutil.copy而不是move保留了原始数据方便后续补充。随机种子seed42很关键否则每次运行划分结果都不同模型复现和对比实验就无从谈起。需要说明的是划分前应当先确认每类图片数量足够。如果某个矿物图像只有30张那么15%的验证集只有4张评估结果方差会很大。项目里如果遇到这种情况可以改用分层抽样或者直接在训练时用validation_split从训练集内部再分保证每个batch都覆盖所有类别。下表总结了常用参数参考值参数建议值说明train_ratio0.7训练集比例图像少于100张/类时可提到0.8val_ratio0.15验证集比例用于早停和模型选择seed42随机种子保证划分可复现max_workers8下载线程数反爬严格时降为2最小文件阈值2000字节过滤无效响应的阈值可调大3. 模型训练与评估从train.ipynb到evaluate_model.py的完整闭环数据准备好之后进入模型训练环节。项目里同时提供train.py和train.ipynb前者适合命令行批量运行后者适合在Jupyter里边改边看曲线。模型基于预训练的卷积神经网络做迁移学习默认的InceptionV3或ResNet50都能在矿物图像上取得不错效果。inceptionV3_model.png展示了模型结构训练和评估脚本则是完整的可执行闭环。3.1 用ImageDataGenerator做数据增强矿物显微图像在拍摄时存在光照不均、旋转角度随机、尺度变化等特点但类别本身对旋转不敏感所以很适合做旋转、翻转、缩放类的数据增强。train.py里典型做法是用tf.keras.preprocessing.image.ImageDataGeneratorfrom tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1./255, rotation_range20, width_shift_range0.1, height_shift_range0.1, shear_range0.1, zoom_range0.1, horizontal_flipTrue, vertical_flipTrue, fill_modereflect ) val_datagen ImageDataGenerator(rescale1./255) train_generator train_datagen.flow_from_directory( train_data, target_size(224, 224), batch_size32, class_modecategorical ) val_generator val_datagen.flow_from_directory( val_data, target_size(224, 224), batch_size32, class_modecategorical )rescale1./255把像素值从0-255归一化到0-1是模型输入的必要步骤。rotation_range20表示随机旋转不超过20度width_shift_range和height_shift_range是平移比例shear_range是错切变换zoom_range是缩放范围。水平翻转和垂直翻转对于矿物图像都是安全的因为显微镜下没有“上下”的概念。fill_modereflect处理旋转后产生的空白区域用镜像填充比填0更自然。需要注意验证集和测试集都不能做数据增强只能做归一化。否则验证集的分布会偏离真实数据评估结果不可信。flow_from_directory会自动读取每个子目录名作为类别并按字母顺序排序这一点要和训练时保持一致。3.2 迁移学习与冻结参数矿物图像数据集通常不大从头训练一个深度网络很容易过拟合。项目里采用迁移学习加载在ImageNet上预训练的InceptionV3模型把顶部分类器替换成自定义全连接层。常见做法是冻结卷积基只训练新加的分类层from tensorflow.keras.applications import InceptionV3 from tensorflow.keras.models import Model from tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout base_model InceptionV3(weightsimagenet, include_topFalse, input_shape(224, 224, 3)) base_model.trainable False x base_model.output x GlobalAveragePooling2D()(x) x Dense(256, activationrelu)(x) x Dropout(0.5)(x) predictions Dense(num_classes, activationsoftmax)(x) model Model(inputsbase_model.input, outputspredictions) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy])include_topFalse扔掉了预训练模型自带的1000类分类器只保留卷积基。trainable False让卷积层参数固定不变训练时只更新后面新加的层。GlobalAveragePooling2D把最后一层特征图压缩成向量比直接Flatten参数量少、更抗过拟合。Dropout(0.5)随机丢弃一半神经元是控制过拟合的常规操作。softmax输出每个矿物类别的概率。如果训练精度上不去可以解冻部分卷积层做微调。做法是把base_model.trainable设成True然后用更小的学习率比如1e-5重新训练。微调时学习率必须比普通训练低否则会破坏预训练学到的特征。3.3 train.py的训练循环与回调train.py里的训练逻辑通常包含ModelCheckpoint、EarlyStopping和ReduceLROnPlateau三个回调from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau callbacks [ EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue), ModelCheckpoint(best_model.h5, monitorval_accuracy, save_best_onlyTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience3, min_lr1e-6) ] history model.fit( train_generator, steps_per_epochtrain_generator.samples // train_generator.batch_size, validation_dataval_generator, validation_stepsval_generator.samples // val_generator.batch_size, epochs50, callbackscallbacks )EarlyStopping监控验证集损失连续10个epoch不下降就停止训练并自动恢复到最佳权重。ModelCheckpoint只保存验证集准确率最好的模型避免最后一轮过拟合模型覆盖最优结果。ReduceLROnPlateau在验证损失连续3个epoch不下降时把学习率乘0.5最低降到1e-6帮助损失跨过局部极小点。steps_per_epoch和validation_steps需要设置为样本数除以batch_size。如果不设置model.fit会用完整epoch的长度在小数据集上问题不大但在数据不整除时会漏掉尾部样本。train.ipynb则更适合直接查看训练曲线通过plot history的损失和准确率判断模型是否收敛。3.4 evaluate_model.py评估模型性能训练完成后需要独立评估而不是只看训练时的历史曲线。evaluate_model.py做的事情是用测试集计算损失、准确率、精确率、召回率、F1-score并生成混淆矩阵。混淆矩阵对于矿物分类尤其重要因为有些矿物在镜下外观很接近比如石英和长石类矿物模型可能系统性地混淆。代码示意from tensorflow.keras.models import load_model from sklearn.metrics import classification_report, confusion_matrix import numpy as np model load_model(best_model.h5) y_true, y_pred [], [] for images, labels in test_generator: probs model.predict(images) y_pred.extend(np.argmax(probs, axis1)) y_true.extend(np.argmax(labels, axis1)) if len(y_true) test_generator.samples: break print(classification_report(y_true, y_pred, target_nameslist(test_generator.class_indices.keys()))) print(confusion_matrix(y_true, y_pred))classification_report会输出每一类的精确率、召回率和F1这三项指标在类别不平衡时比整体准确率更有参考价值。confusion_matrix是方阵行是真实类别列是预测类别对角线越高说明分类越可靠。如果对角线上某类明显偏低需要返回去检查这个类别的图片质量或数量。evaluate_model.ipynb是对应的笔记本版本方便在Jupyter里可视化混淆矩阵的热力图。建议在训练过程中定期跑一次评估而不是等到训练彻底结束这样能及早发现数据或标签问题。4. 模型部署到浏览器convert_model_to_javascript.py与tfjs实战这是项目里比较有亮点的部分。很多图像分类项目止步于训练脚本但这里提供了把模型转成TensorFlow.js并部署到浏览器的完整链路。这意味着地质工作人员不需要安装Python环境打开网页就能上传矿物照片识别类别。4.1 把Keras模型转换为TensorFlow.js格式convert_model_to_javascript.py使用的是TensorFlow.js转换器。转换前需要确保本机安装了tensorflowjs包然后执行加载模型并保存为web格式import tensorflowjs as tfjs tfjs.converters.save_keras_model( best_model.h5, tfjs_model )这段代码会把best_model.h5转换成tfjs_model目录下的model.json和一组分片权重文件。model.json描述了模型结构和权重分片索引浏览器通过fetch这个文件就能加载模型。保存时需要确保模型是完整的Keras .h5格式如果的是SavedModel格式的目录需要使用tfjs.converters.convert_tf_saved_model。转换过程中最容易遇到的坑是自定义层或自定义损失函数。如果训练时用了自定义层比如带权重的注意力层转换器会报错。一般解决办法是在转换前重建一个纯Keras模型只用标准层训练和推理都走同样的结构。这个项目里的模型是InceptionV3加全连接层所有层都是标准操作转换通常一次通过。4.2 main.js加载模型并进行预测转换完成后前端通过main.html展示界面把部分逻辑封装在main.js里。加载模型和预处理图像的代码大致是这样let model; async function loadModel() { model await tf.loadLayersModel(tfjs_model/model.json); console.log(模型加载完成); } function preprocessImage(imageElement) { const tensor tf.browser.fromPixels(imageElement) .resizeBilinear([224, 224]) .toFloat() .div(tf.scalar(255.0)) .expandDims(0); return tensor; } async function predictImage() { const tensor preprocessImage(document.getElementById(inputImage)); const probs await model.predict(tensor).data(); const classes [calcite, feldspar, quartz, garnet]; const argMax probs.indexOf(Math.max(...probs)); document.getElementById(result).innerText 预测类别: classes[argMax] 置信度: probs[argMax].toFixed(4); tensor.dispose(); }tf.loadLayersModel从model.json加载模型返回的是可调用的模型对象。tf.browser.fromPixels把HTML的ImgElement或Canvas变成张量。resizeBilinear把图像缩放到训练时的224x224尺寸注意这里用的是双线性插值和Keras的target_size默认缩放方式一致。归一化除以255和训练时的rescale1./255完全对应。expandDims(0)增加batch维度因为模型期望输入是[batch, height, width, channels]。model.predict返回的是概率数组Math.max(...probs)找到最大概率的索引再从类别列表里取出名称。这里有一个容易漏掉的细节类别列表的顺序必须和训练时flow_from_directory里的class_indices顺序一致否则预测结果会张冠李戴。训练时可以用train_generator.class_indices打印对应的映射表。tensor.dispose()是TensorFlow.js中释放显存/内存的重要步骤。浏览器环境下内存泄漏往往来自张量没有清理长时间运行页面会卡死。每次预测完都要手动dispose中间产生的resize结果如果不复用也要一并释放。4.3 main.html与linear.html的工程取舍main.html是主要的交互页面一般包含一个文件上传控件和一个canvas用来预览选中的图像。上传后用URL.createObjectURL生成临时链接并赋值给img图片加载完成后调用predictImage。这里需要注意模型的加载是异步的用户首次点击预测时模型可能还没有加载完毕所以要做一个await loadModel()的初始化流程。linear.html则是一个更轻量级的演示页它可能只包含一个线性层的简化模型或者直接展示模型运行的日志。两个html分开的意义在于main.html适合完整演示给非技术人员看linear.html适合调试模型输出时使用把每个步骤的结果打印到页面上方便确认预处理链路是否正确。对于熟悉前端调试的人来说线性版本反而更容易定位是前端张量处理的问题还是模型本身的问题。5. 进阶技巧与常见踩坑最后一章写几个在实践中真正会用到的问题和对应处理方式。5.1 类别不平衡时改用加权损失如果某类矿物图像数量特别少整体准确率会虚高。常见做法是在Model.compile时给每个类别一个权重让少数类的loss被放大。使用class_weight参数可以做到class_weights {0: 1.0, 1: 2.0, 2: 1.5, 3: 3.0} model.fit(..., class_weightclass_weights)权重设置一般按每类样本数的反比来算比如样本数最小的类别权重设为3最大的类别设为1。但要注意权重过大会导致模型对少数类过拟合所以需要观察验证集F1而不是单纯看准确率。如果数据集里某类只有不到20张图光靠权重还不够建议增加这类图像的数量或者用更重的数据增强只针对该类别。5.2 检查测试集和训练集的图像尺寸分布显微镜图像的分辨率可能不一致有些是1024x1024有些是512x512。训练时统一resize到224x224会把小图放大导致细节模糊。建议在split_data后先统计所有图像的分辨率把过小的图过滤掉或单独处理。如果出现某类图像全是小分辨率模型可能学到的是图像的模糊程度而不是矿物特征这类伪特征在真实场景一测就露馅。统计尺寸可以直接用PILfrom PIL import Image from pathlib import Path sizes {} for path in Path(train_data).rglob(*.jpg): with Image.open(path) as img: sizes[path.name] img.size print(set(sizes.values()))如果发现尺寸分布差异大可以在训练时启用target_size之外的随机缩放或者先用resize把所有图片统一成更大尺寸再训练比如先处理到512x512再喂给模型。当然这样会增加显存消耗。对于矿物图像至少应保证最小边不小于224否则关键解理纹理会被插值算法抹掉。5.3 TF.js转换时遇到Unsupported Op怎么办转换InceptionV3的卷积基一般不会出问题但如果你后续加了实例归一化、自定义激活函数转换器可能报Unsupported Ops。一个临时方案是改用TensorFlow.js 4.x的dynamic loading模式把部分操作放到内联函数里实现。更推荐的做法是回到Keras模型结构检查一下是否有用Lambda层直接写了原生代码的算子有就换成Keras标准层。另一个坑是模型权重文件过大。InceptionV3的模型转换后有几十MB浏览器首次加载会比较慢。可以在转换时加权重压缩参数tfjs.converters.save_keras_model( model, tfjs_model_quantized, quantization_dtype_map{uint8: 1} )把权重量化到uint8体积能减少约四分之一精度损失通常在1%以内。注意某些浏览器对fetch大文件有并发限制分片文件会被卡住用tf.loadLayersModel的onProgress回调显示加载进度条会友好很多。最后验证整个链路是否一致的一个简单技巧用训练集里一张图片先通过test_model.py得到Python环境下的预测类别再用同一张图在浏览器里跑一遍。如果两个结果不一致说明前后端预处理差异还没有对齐需要逐项检查resize方式、归一化顺序和通道顺序。通过这个对照两分钟就能确定问题出在哪一侧。本文还有配套的精品资源点击获取