ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于CNN与Django的糖尿病视网膜病变分级识别系统实现

2026/9/17 7:36:59 拓冰建站 浏览量
基于CNN与Django的糖尿病视网膜病变分级识别系统实现 简介一套基于PythonDjango与卷积神经网络CNN的糖尿病视网膜病变识别系统毕业设计项目面向软件工程、人工智能、电子信息等计算机相关专业的在校学生、教师或初级开发者解决医学图像分类任务从模型训练到Web端部署的完整落地问题。压缩包共含2000个文件大小约28.1MB其中1416个py文件覆盖CNN模型搭建、数据预处理与Django后端逻辑156个html、271个js及102个css等构成前端交互界面45个txt及模型、说明文档提供使用指引与训练成果整体目录层次清晰。目前已有152人学习/浏览该项目。源码经运行验证且获导师指导认可答辩评审分达95分配套说明文档与数据资料可帮助理解糖尿病视网膜病变识别的实现思路也便于在此基础上扩展功能、替换数据集或调整网络结构能直接支撑毕业设计、课程设计及项目初期演示等场景。1. 从一张眼底照片到 DR 分级这套系统替你做了什么糖尿病视网膜病变DR是全球范围内导致成年人失明的主要眼病之一。临床上医生通过眼底彩照观察微血管瘤、出血点、渗出物等病灶再按照国际分级标准把它分成 0 到 4 级。这个流程高度依赖阅片经验和耐心基层眼科门诊一天几十张片子看下来漏诊率并不低。这套毕业设计资源把整个流程做成了三个可以交付的产物一份基于 Python 的 CNN 分类模型、一个基于 Django 的 Web 识别系统以及从数据处理到部署说明的完整文档。换言之你拿到的不只是能跑的代码而是一条从上传眼底图到输出 DR 等级与置信度的完整链路。它适合三类人做图像分类毕业设计的学生、想学 Django 如何承载机器学习模型的开发者以及想快速改造成其他医学图像分类任务的人。2. CNN 模型与眼底图像的特征提取链路2.1 为什么 CNN 能识别视网膜病灶眼底图像里的病灶是典型的局部纹理差异微血管瘤表现为暗红小点出血区是形态不规则的暗色团块硬性渗出是黄白色亮斑。传统方案用形态学算子提取这些特征再交给 SVM特征工程成本高换一台相机拍出来的图分布一变准确率立刻下滑。CNN 的优势在于把特征设计这件事交给卷积核浅层卷积核响应边缘、色块、圆斑深层卷积核把这些局部模式组合成更抽象的病征组合池化又让网络对病灶位置不敏感适应病灶可能出现在眼底任何象限的实际情况。这一类任务里常用的基础结构是先堆叠卷积块每个卷积块包含卷积、批归一化和池化最后用全局平均池化接全连接层输出分类概率。批归一化在这里效果明显眼底图像受拍摄设备影响亮度与色差变化大BN 能压低层间分布偏移让训练收敛速度明显加快。2.2 图像归一化与数据增强2.2.1 尺寸与通道处理模型输入固定为 224×224×3但原始眼底照片通常接近 2000×3000 甚至更大直接缩放会丢失细小的微血管瘤特征所以缩放策略比普通分类任务更讲究。合理的做法是先用 OpenCV 读图裁剪掉四周黑色边框保留视盘和血管弓区域再缩放到模型输入尺寸。import cv2 import numpy as np def preprocess(path, target_size(224, 224)): # 读入 BGR 图像后转为 RGB保证通道顺序与训练一致 img cv2.imread(path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 去掉眼底照片常见的黑边避免黑色区域干扰归一化统计 gray cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) _, mask cv2.threshold(gray, 5, 255, cv2.THRESH_BINARY) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: x, y, w, h cv2.boundingRect(contours[0]) img img[y:y h, x:x w] # INTER_AREA 对缩小图像更平滑能保留更多病灶边缘信息 img cv2.resize(img, target_size, interpolationcv2.INTER_AREA) img img.astype(np.float32) / 255.0 # ImageNet 统计均值与标准差配合预训练权重使用时不要改 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) img (img - mean) / std return img几个需要留意的参数。target_size决定模型输入分辨率224 是速度和精度的折中想把微血管瘤看得更清楚可以提到 299但显存占用和推理耗时都会上升。cv2.threshold的阈值 5 是为了去掉视野外的纯黑像素而不是去掉低照度的病灶区域。缩放插值选INTER_AREA因为它对缩小图像做像素区域平均比INTER_LINEAR更不容易产生高频伪影。最后一步按 ImageNet 统计做标准化如果后续要加载预训练权重这一步的均值方差必须保持一致否则第一层卷积的输入分布就是错的。2.2.2 与图像增强配合的 CLAHE 对比度增强DR 数据集的对比度差异主要来自采集设备同一个病变在不同品牌相机下拍出来的亮度与对比度可能差很远。图像增强 CNN 算法里常用 CLAHE 来压平这种差异它的特点是只增强局部区域的对比度不会像全局直方图均衡那样把噪声一起放大。def clahe_pipeline(img): # 输入为 RGB、float32、0~1 范围的图像 img (img * 255.0).astype(np.uint8) lab cv2.cvtColor(img, cv2.COLOR_RGB2LAB) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) # 只对 L 亮度通道做均衡保留 a/b 颜色通道的原始色偏 lab[:, :, 0] clahe.apply(lab[:, :, 0]) img cv2.cvtColor(lab, cv2.COLOR_LAB2RGB) return img.astype(np.float32) / 255.0clipLimit2.0控制对比度放大的上限值调大画面会更锐利但也会让噪声更明显眼底出血点和渗出区域本身偏暗或偏亮噪声被放大后容易掩盖小型微血管瘤。tileGridSize(8, 8)表示把图像切成 8×8 个小块分别做直方图均衡块越小局部增强越强也越容易出现块状伪影。这里常见的误用是对整张图做cv2.equalizeHist眼底暗区会立刻过曝病灶细节反而丢失。2.3 模型结构与训练配置这个资源里的基础模型是一个自建的轻量 CNN没有直接套 VGG16 或 ResNet50好处是参数少CPU 上也能完成推理适合毕业设计的演示环境。结构上采用卷积块叠加的方式核心配置如下from tensorflow.keras import layers, models def build_cnn(input_shape(224, 224, 3), num_classes5): model models.Sequential([ # 第一层需要显式传入输入形状后续层自动推导 layers.Conv2D(32, (3, 3), activationrelu, paddingsame, input_shapeinput_shape), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activationrelu, paddingsame), layers.BatchNormalization(), layers.GlobalAveragePooling2D(), layers.Dropout(0.5), layers.Dense(num_classes, activationsoftmax) ]) return modelpaddingsame保证卷积不改变特征图尺寸让池化层按 2 的倍数降采样。GlobalAveragePooling2D替代 Flatten 加全连接直接把每个通道压成一个值参数数量大幅下降同时让网络对输入尺寸有更好的适应能力。Dropout(0.5)放在分类头前防止全连接层把训练集的阅片习惯背下来。训练参数是这类任务里最值得花时间调的环节下面这份配置在多数 DR 数据集上都能直接落地配置项推荐值说明优化器Adam初始学习率 1e-3收敛速度比 SGD 明显更快学习率调度ReduceLROnPlateaupatience3验证损失不降时学习率乘 0.5损失函数categorical_crossentropyDR 五分类任务默认选择类别权重class_weight0 级样本通常占一半以上按类别频率反比配置批量大小32显存不够时降到 16但注意 BN 在小 batch 下统计有偏差Epochs30配合早停不设死上限2.4 损失函数与评估指标严格说DR 识别在竞赛里的标准评估指标是 Quadratic Weighted KappaQWK它惩罚离真实等级差太多的预测。比如把 0 级判成 1 级扣分远小于把 0 级判成 4 级。这个指标很贴合医学场景漏掉一个早期病变和把一个正常眼判成重度代价完全不同。Keras 内置没有 QWK需要自己实现。常见的写法是from sklearn.metrics import cohen_kappa_score def qwk(y_true, y_pred): # y_pred 是模型输出的 argmax 结果类别取 0~4 return cohen_kappa_score(y_true, y_pred, weightsquadratic)cohen_kappa_score的weightsquadratic就是竞赛评分标准取值范围从 -1 到 1。DR 数据集中 QWK 超过 0.8 已经算很强的模型。训练时可以用 accuracy 观察收敛但最终答辩和对比实验要优先以 QWK 为准。3. Django 把训练好的 CNN 模型搬进浏览器3.1 MVT 请求链路与模型加载策略Django 的项目壳在这里只承担一件事接收上传的眼底照片把图片喂给 CNN 模型再把预测结果渲染到页面。整个链路是浏览器 POST 表单到urls.py路由路由分发给 views 里的视图函数视图操作 ORM 模型存储记录最后把上下文交给模板渲染。模型加载策略是这块最需要关注的点。如果把load_model写在视图函数内部每来一个请求都会把整个权重从头读一遍一张眼底图预测要等几秒钟页面长时间白屏。合理的做法是模块级单例加载进程启动时只加载一次# model_loader.py from tensorflow.keras.models import load_model as keras_load from django.conf import settings _model None def get_model(): global _model if _model is None: _model keras_load(settings.MODEL_PATH) return _model_model是模块级全局变量第一次调用时加载权重之后复用同一份模型对象。注意 Django 开发服务器默认开启 autoreload代码修改后会自动重启进程模型会被重新加载一次。h5 文件比较大时每次保存代码后等几秒属于正常现象不必担心。3.2 上传接口与预测视图核心视图代码一般长这样import os from django.shortcuts import render from django.conf import settings from .models import PredictionRecord from .ml.preprocess import preprocess from .ml.model_loader import get_model def predict_view(request): if request.method POST: image_file request.FILES.get(image) if not image_file: return render(request, predict/error.html, {msg: 请上传眼底照片}) # 用 chunks() 逐块写盘避免大图一次性读入内存 save_path os.path.join(settings.MEDIA_ROOT, image_file.name) with open(save_path, wb) as f: for chunk in image_file.chunks(): f.write(chunk) model get_model() img preprocess(save_path) probs model.predict(img[None, ...])[0] pred_label int(probs.argmax()) confidence float(probs[pred_label]) record PredictionRecord.objects.create( imageimage_file.name, labelpred_label, confidenceconfidence ) return render(request, predict/result.html, {record: record}) return render(request, predict/upload.html)几个容易被忽略的参数细节。image_file.chunks()是 Django 对上传文件的迭代器默认每块 64KB逐块写盘比直接read()整个文件更省内存眼底原图动辄 10MB这一步不能省。model.predict(img[None, ...])是在输入张量上加一维 batch 维度CNN 要求输入是(batch, height, width, channels)漏掉会直接抛维度错误。probs.argmax()拿到 0 到 4 的类别索引float(probs[pred_label])转成置信度模板里可以直接渲染成百分比。PredictionRecord表是识别记录的落库位置字段设计如下字段类型用途imageCharField保存上传眼底照片的文件名或路径labelIntegerField0 到 4 的 DR 分级结果confidenceFloatField对应类别的 softmax 概率created_atDateTimeField自动记录识别时间3.3 路由、模板与静态资源组织urls.py 里只需要一条路由from django.urls import path from . import views urlpatterns [ path(, views.predict_view, namepredict), ]模板里最关键的坑在表单的编码类型。上传文件必须显式声明enctypemultipart/form-data否则request.FILES永远是空的form methodpost enctypemultipart/form-data {% csrf_token %} input typefile nameimage acceptimage/* / button typesubmit开始识别/button /formacceptimage/*只是浏览器端的筛选后端仍然要判断文件类型和大小因为可以直接用工具构造请求绕过前端限制。常见做法是在视图里检查content_type并限制image_file.size不超过 20MB。这个资源的后端还带了一套后台管理界面里面引用了table.css、date-picker.css、transfer.css等组件样式。这类样式表通常是从通用后台模板里整体复制进来的实际用到的可能只有其中几类改项目时可以把没引用的 CSS 删掉减少页面加载时间。3.4 依赖清单与启动命令项目的依赖集中在requirements.txt从用途上可以拆成 Web 框架、深度学习框架和图像处理三部分Django4.2 tensorflow2.10 opencv-python4.8 numpy pillow启动流程如下pip install -r requirements.txt python manage.py makemigrations predict python manage.py migrate python manage.py runserver 0.0.0.0:8000makemigrations和migrate是为了把PredictionRecord表同步到 SQLite 数据库。第一次跑项目的人经常卡在pip install tensorflow下载困难的问题上可以先确认 Python 版本再选对应 wheel。python manage.py runserver默认只监听本机 127.0.0.1局域网里其他机器要访问时0.0.0.0:8000这个参数不能漏。4. 数据准备、模型训练与一次完整记录4.1 数据集目录与类别分布这个资源的全部数据资料里眼底图像数据预先按照 DR 分级分好了目录典型的组织方式如下data/ train/ 0_no_dr/ 1_mild/ 2_moderate/ 3_severe/ 4_proliferative/ val/ 0_no_dr/ ... test/ 0_no_dr/ ...训练集和验证集按患者维度切分而不是按图片维度切分。这一点在 DR 场景里非常关键同一个病人的左右眼图像高度相似如果一张进训练集、另一张进验证集验证指标会虚高真到新患者身上准确率明显下滑。切分时优先按病人的文件夹 ID 做分组随机而不是直接对图片路径做train_test_split。4.2 训练脚本的数据流数据流上常见的实现是ImageDataGenerator配合flow_from_directory一行代码自动完成标签读取、数据增强、批量打包from tensorflow.keras.preprocessing.image import ImageDataGenerator train_gen ImageDataGenerator( rotation_range15, width_shift_range0.1, height_shift_range0.1, zoom_range0.1, horizontal_flipTrue, brightness_range(0.8, 1.2), preprocessing_functionclahe_pipeline ) train_flow train_gen.flow_from_directory( data/train, target_size(224, 224), batch_size32, class_modecategorical, shuffleTrue )rotation_range15表示在正负 15 度范围内随机旋转。眼底图像不像自然图像有严格的上下概念旋转增强不会产生语义错误。width_shift_range和height_shift_range负责平移模拟视盘不在正中心的情况brightness_range(0.8, 1.2)把亮度随机乘 0.8 到 1.2 倍对应不同相机设备的曝光差异。preprocessing_functionclahe_pipeline是把 CLAHE 增强挂到数据流里训练集和验证集都需要走同一套预处理但验证集不应加随机旋转和翻转否则指标不稳定。验证集使用单独的ImageDataGenerator(rescale1.0/255)保证每个 epoch 看到的验证样本完全一致loss 曲线才能真实反映模型状态。4.3 训练参数与验证指标训练过程中的回调配置决定了模型能不能在最佳点停下来from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau callbacks [ EarlyStopping(monitorval_loss, patience8, restore_best_weightsTrue), ModelCheckpoint(best_model.h5, monitorval_qwk, modemax, save_best_onlyTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience3, min_lr1e-6) ]EarlyStopping的patience8意味着连续 8 个 epoch 验证损失不下降就停止restore_best_weightsTrue保证恢复到验证集最优的那一轮权重。ModelCheckpoint按val_qwk保存最佳模型这里要配合 2.4 节自定义的 QWK 指标注册到 Keras 里。ReduceLROnPlateau在验证损失停滞 3 轮后把学习率减半让训练后期可以更精细地逼近最优解。评估阶段除了 accuracy还要看每个类别的 precision 和 recall。DR 数据集有个典型现象0 级样本占比超过 60%模型全预测成 0 级也能拿到接近 0.6 的准确率但这个模型没有任何临床价值。下面这个表是这个任务里常用的指标关注点指标关注原因达到什么值算可用QWK与阅片医生分级的一致性高于 0.750 级召回率避免正常眼被误判为病变高于 0.904 级召回率增殖期病变漏诊后果严重高于 0.80平均准确率整体判断能力参照高于 0.784.4 训练日志分析与一次完整记录实际训练时输出会逐 epoch 滚动下面是一段典型的收尾日志Epoch 12/30 170/170 [] - 38s 224ms/step - loss: 0.4319 - acc: 0.8420 - val_loss: 0.5201 - val_acc: 0.8010 Epoch 13/30 170/170 [] - 38s 224ms/step - loss: 0.4251 - acc: 0.8460 - val_loss: 0.5588 - val_acc: 0.7950第 12 轮之后训练损失还在下降但验证损失从 0.52 涨到 0.55这是典型的过拟合信号说明模型开始记住训练集中特定眼底照片的拍摄噪声。此时ReduceLROnPlateau会把学习率从 1e-3 降到 5e-4如果接下来几个 epoch 验证损失仍然回升EarlyStopping在 patience 期满后自动停在最佳权重。另外一个容易误导人的细节是训练日志里acc偏高、val_acc偏低。DR 五分类中相邻类别的图像差异很小模型预测成相邻等级也属于可接受错误accuracy 把所有错误一视同仁所以会感觉比人工阅片的准确率低很多。这也是为什么最终对比实验一律以 QWK 为准而不是拿 accuracy 去和医生的阅片准确率直接比较。5. 让识别系统更像一个可交付的产品5.1 从开发服务器到生产部署runserver只适合开发调试它单进程且没有并发优化。更实用的方案是 gunicorn 作为 WSGI 进程管理器pip install gunicorn gunicorn project_name.wsgi:application -b 0.0.0.0:8000 --workers 3 --timeout 120--workers 3的取值一般按 CPU 核心数加 1 来配CNN 推理是 CPU 密集型任务worker 超过核心数反而会因为频繁切换上下文而变慢。--timeout 120要调大因为模型首次加载和一张大图的预处理可能超过默认的 30 秒。如果前面挂了 nginx注意把上传文件大小限制同步调大nginx 默认client_max_body_size 1m眼底原图经常超过这个值直接导致上传失败。用宝塔面板部署 Django 的底层逻辑也是配置 Python 虚拟环境和这条 gunicorn 启动命令只是把操作挪到了网页上。5.2 推理延迟测量与模型压缩模型能跑只是第一步答辩和演示时最常见的问题是页面转圈太久。建议在预测视图里包一层计时逻辑import time start time.time() probs model.predict(img[None, ...])[0] elapsed time.time() - start print(fsingle inference: {elapsed:.3f}s)单张推理超过 1 秒就需要考虑压缩。优先检查是否真的需要 224 以上的输入尺寸如果训练时用的是 224推理时强行改成 299 反而会因为尺寸分布不一致导致准确率下降。更实用的方向是替换网络主干把自建 CNN 换成 MobileNetV3 或 EfficientNet-Lite同一批数据下推理时间通常能缩一半以上准确率损失在 2 个百分点以内。这个资源里自带的模型在 CPU 上推理一张图大约在 0.3 到 0.6 秒之间属于可接受的演示范围。5.3 数据泄露防范与可视化验证如果要在这个项目基础上做优化实验最值得补的一课是划分数据集时按患者分组。用 scikit-learn 的GroupShuffleSplit可以一次完成from sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) for train_idx, val_idx in gss.split(images, labels, groupspatient_ids): train_images, val_images images[train_idx], images[val_idx]groupspatient_ids会保证同一个病人的所有图像只出现在训练集或只出现在验证集避免左右眼图像跨集合泄露。做完这一步再对比原来的切分方式通常会发现 QWK 下降 0.05 到 0.1这才是模型在新患者身上的真实水平。最后答辩展示时可以结合 Grad-CAM 热力图看模型关注了哪些区域如果热力图集中在黄斑或视盘位置而不是出血斑块说明模型学到的是拍摄设备的光线模式需要回到数据层做更彻底的颜色归一化。本文还有配套的精品资源点击获取