
我家楼下的垃圾分类督导员阿姨盯了我半分钟就因为我举着一个沾了油污的外卖盒不知道该往哪个桶里扔。那一刻我意识到垃圾分类这事儿看着简单真到手里全是知识盲区。后来我干脆写了个基于 Django 图像识别的智能垃圾分类系统用手机拍张照片传到网页上后端模型判断出这是可回收物还是厨余垃圾连置信度和投放建议一起返回。这篇文章就把整个设计与实现过程拆开来讲技术选型的理由、模型怎么训练、Django 里怎么封装推理逻辑、ORM 怎么查询和删除记录再叠加部署排坑的实操经验。适合正在做 Django 项目实战的新手也适合想把深度学习模型接进 Web 系统、但还没找到完整路径的同学。1. 项目起点与选型思路这套系统解决了谁的痛点动手之前我认真想了一下这套系统到底要给谁用、解决什么问题、以及为什么要用 Django 而不是更轻的框架。很多毕设开始得很仓促上来就找数据集、跑模型Web 端随便选个框架写个上传页面最后再拼装起来。结果模型精度挺高接口却慢得没法看数据库表也拍脑袋设计演示的时候翻车。所以这个项目的第一步不是写代码而是把需求边界划清楚。1.1 需求本质不是“识别垃圾”而是“帮人做投放决策”垃圾分类的难点不在“认识物品”而在“归类到对应垃圾桶”。比如一个矿泉水瓶是可回收物但你要是往里面扔了半瓶剩奶茶严格说它属于污损塑料投放逻辑就变了。系统里我设计了两个层级图像识别模型先输出细分类比如“塑料瓶”“纸板箱”“香蕉皮”“废电池”等具体类别然后由一份映射表将细分类归入四分类体系可回收物、厨余垃圾、有害垃圾、其他垃圾。这套设计的价值在于前端展示时用户看到的是“这是塑料瓶可回收物置信度 92%”而不是冷冰冰的“Class 7”。细分类也方便以后扩展——比如统计哪种垃圾识别错误率最高再针对性补充训练数据。1.2 为什么选择 Django 而不是 Flask 或 FastAPI图像识别模型通常用 Python 生态训练Web 端也必须用 Python 才能把模型加载和业务逻辑放进同一个进程里这时候 Django 和 Flask 之间需要做个取舍。我最终选了 Django原因很实际自带 Admin 后台和 ORM能快速搭建记录查询、用户管理、数据统计页面不用重复造轮子模板系统加上 Django REST Framework 可以同时支持服务端渲染和纯 API 两种交互模式后期改成小程序端也方便项目结构强制按 app 划分模型推理、垃圾识别、用户台账可以拆得比较干净符合课程设计和工程化的评分要求生态成熟资料多遇到问题搜索“django 执行查询 删除对象”“django 创建 app”这类关键词能找到大量已踩坑记录。当然如果你的目标是极致的并发性能FastAPI 的异步特性确实更有优势但在这个场景里模型推理本身是 CPU 重负载任务Web 框架的差异远小于推理耗时的差异。为了让前后端职责清晰我可以用 Django 提供 API配合前端页面调用这也是后面会讲到的方案。1.3 图像识别方案自己训练 CNN 还是迁移学习图像识别这块新手常见的误区是一上来就搭建一个 10 层左右的 CNN用几百张图跑几十个 epoch然后发现准确率上不去。实际上在中小规模数据集上自己做训练很难赶上成熟的预训练模型。我采用迁移学习方案以 MobileNetV2 的预训练权重作为特征提取器替换最后的全连接分类层在垃圾分类数据集上做微调。MobileNetV2 的优势是模型体积小、推理速度快CPU 环境也能跑到几十毫秒到几百毫秒。在毕设演示场景里用户上传图片后等 2 秒是可以接受的但模型文件超过 500MB 就不太体面了。EfficientNet 在同样精度下也不差但转换和部署时对 tf 版本兼容性的要求更严格。为了稳我选了 MobileNetV2实测下来 Top-1 准确率在 90% 左右已经足够支撑演示和日常自测。1.4 整体模块划分系统整体分为三个大的功能域项目里对应三个 Django appgarbageImage或者叫common负责图像上传、预处理、调用模型、返回识别结果records负责识别历史记录的新增、查询、删除、导出对应 ORM 操作部分users负责用户登录注册和权限控制识别记录需要关联到当前用户避免互相看到对方数据。数据库层面识别记录表是核心。每个记录保存原始图片、模型输出的细分类标签、映射后的四分类类别、置信度、创建时间。这样一个表就把整个业务闭环串起来了用户上传图片系统返回结果结果写入数据库用户可以在历史记录页查询或删除。2. 图像识别模型训练数据集处理、迁移学习与导出的坑很多人做这类项目时间花在写 Web 页面上模型处只用了官方示例权重甚至随机初始化权重导致识别效果像抽盲盒。我来把模型训练这一环的关键操作拆开这部分是识别系统好用与否的地基。2.1 数据集怎么找、怎么清洗公开的垃圾分类数据集有不少我用的是一份覆盖几十个细分类的数据集图片数量在几万张量级。如果你没有现成数据集也可以找 TrashNet 这类公开垃圾分类数据集作为底座再补充一些手机拍摄的实物图。数据集处理需要注意三个点清除无效图片下载下来的压缩包里经常出现损坏的、尺寸为 0 的文件训练前统一检查按类别平衡样本量类别之间数量差距过大会导致模型对大类别有偏向建议每个类别至少保留 200 张以上不足的部分靠数据增强补足统一预处理逻辑训练时图片被缩放到224x224推理时也必须走完全一样的缩放和归一化流程前后不一致是“训练挺好、上线拉胯”的头号原因。2.2 迁移学习的具体做法模型结构我用的是 TensorFlow/Keras 的 MobileNetV2 预训练版本把include_topFalse去掉自带的分类层然后接一个GlobalAveragePooling2D再加一个Dropout最后接Dense分类层。这里为什么要用GlobalAveragePooling2D而不是直接Flatten因为 MobileNetV2 输出特征图是7x7x1280直接 Flatten 会产生 6 万多个参数不仅容易过拟合还让模型变大。而全局平均池化把每个通道压缩成一个数值参数量小得多效果反而更稳这也是迁移学习实践中的常见做法。代码大致是这样from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras import layers, models base_model MobileNetV2( weightsimagenet, include_topFalse, input_shape(224, 224, 3) ) base_model.trainable False model models.Sequential([ base_model, layers.GlobalAveragePooling2D(), layers.Dropout(0.3), layers.Dense(len(class_names), activationsoftmax) ]) model.compile( optimizeradam, losscategorical_crossentropy, metrics[accuracy] )第一步先冻结底层只训练新加的分类头等分类头收敛后再把base_model的后面十几层解冻用很小的学习率微调。这样既保留底层通用的纹理与形状特征又能让高层特征更贴合垃圾图片的细节差异。2.3 训练参数与精度实测训练脚本里我用ImageDataGenerator做数据增强包括随机旋转、翻转、缩放、亮度调整增强后的图片按8:2划分训练集和验证集。推荐参数如下参数设置说明输入尺寸224x224x3与预训练模型一致批量大小32显存不足时降到 16第一阶段学习率1e-3只训练分类头快点收敛第一阶段轮数30观察验证集是否过拟合第二阶段学习率1e-5微调预训练层避免破坏已有特征第二阶段轮数10精度提升主要看这一步我在自己机器上跑完验证集准确率大约在 92%单张图片 CPU 推理均值在 0.4 秒左右。如果硬件资源有限也可以减少微调轮数或把 Dropout 调高一些防止过拟合。2.4 模型导出的格式坑训练完成后保存模型可能遇到两类典型问题使用model.save(garbage_model.h5)保存的 H5 文件在后端load_model时提示找不到自定义层。解决办法是保存时把自定义层注册到custom_objects或者在保存前把模型结构全部转成原生 Keras 层序列化方式TensorFlow 版本和 NumPy 版本不兼容加载模型时直接报numpy.core.multiarray failed to import。这类问题优先检查requirements.txt里的版本组合比如 TensorFlow 2.10 搭配numpy1.24.3通常没问题但换上更高版本 NumPy 就会翻车。我在项目里最终导出的是 H5 格式因为后端做进度演示时最直观。如果后续有部署到手机端或浏览器端的想法可以再转成 TFLite那样量化后的模型更小、推理更快。3. Django 后端集成模型封装、ORM 查询删除与图片上传模型训练只是起点真正让系统完整的是 Django 后端。这一章我重点讲模型怎么封装进视图、接口怎么设计、以及热搜词里频繁出现的“django 执行查询-删除对象”到底在项目里怎么落地。3.1 为什么不能在视图里直接 load_model新手最常见的写法是from tensorflow.keras.models import load_model def classify(request): model load_model(garbage_model.h5) ...这个写法有两个问题。第一每次请求都会重新加载一次上百 MB 的模型文件用户多点两下服务器内存直接爆炸第二并发请求时可能同时触发多次加载造成重复占用资源。正确做法是让模型作为进程级单例存在在模块导入时加载一次所有请求共享同一个模型实例。Django 里实现单例最省事的方式是用functools.lru_cache或者全局变量。示例# garbage_prediction/service.py from functools import lru_cache from tensorflow.keras.models import load_model lru_cache(maxsize1) def get_model(): model load_model(models/garbage_model.h5) return model这样第一个请求进来时加载模型后续请求直接复用。如果你用的是 Gunicorn 多 worker 部署每个 worker 进程会各自加载一份模型所以内存消耗要按“worker 数 x 模型大小”提前估算。3.2 预测模块封装把预处理、推理、结果映射放进一个函数识别业务不能直接写在views.py里那样会让视图变得又长又难测试。我单独建了一个service.py负责图片预处理、模型推理和类别映射。import numpy as np from PIL import Image def preprocess_image(img): img img.resize((224, 224), Image.LANCZOS) arr np.array(img, dtypenp.float32) if arr.shape[-1] 4: arr arr[:, :, :3] arr arr / 255.0 arr np.expand_dims(arr, axis0) return arr def predict_batch_garbage(image): model get_model() arr preprocess_image(image) probs model.predict(arr, verbose0)[0] idx int(np.argmax(probs)) confidence float(probs[idx]) subclass_name index_to_subclass[idx] category subclass_to_category[subclass_name] return subclass_name, category, confidence这里有个细节如果上传的是 PNG 透明图PIL 读取后可能有 4 个通道必须在预处理里截断成 RGB否则模型输入尺寸对不上。另外Image.open拿到的是文件句柄对象读图片前要确认file.seek(0)否则可能出现“读出来的图一直是第一张”的诡异问题。3.3 数据库设计与“查询-删除对象”实操识别记录表的设计决定了查询和删除好不好写。我定义的模型如下from django.conf import settings from django.db import models class GarbageRecord(models.Model): user models.ForeignKey( settings.AUTH_USER_MODEL, nullTrue, blankTrue, on_deletemodels.SET_NULL ) image models.ImageField(upload_togarbage/%Y%m/) subclass models.CharField(max_length32) category models.CharField(max_length16) confidence models.FloatField() created_at models.DateTimeField(auto_now_addTrue)Django ORM 的查询和删除正是很多刚入门 Django 的人卡住的地方。这里给一份常用操作清单# 查询最近 10 条记录 GarbageRecord.objects.filter(userrequest.user).order_by(-created_at)[:10] # 条件查询只看置信度高于 0.9 的厨余垃圾记录 GarbageRecord.objects.filter( userrequest.user, category厨余垃圾, confidence__gte0.9 ) # 聚合统计按类别统计条数 from django.db.models import Count GarbageRecord.objects.values(category).annotate(totalCount(id)) # 删除单个对象 record GarbageRecord.objects.get(pk1) record.delete() # 按条件批量删除 GarbageRecord.objects.filter( userrequest.user, category可回收物, confidence__lt0.7 ).delete()特别注意on_delete的选取。用户被删时如果希望保留这条识别记录就选SET_NULL如果想同步清空用户产生的所有记录则用CASCADE。我在项目中选SET_NULL因为识别记录本身有分析价值用户解绑后数据仍应保留。另外一个容易被忽视的是“软删除”。首页的历史记录里我其实不建议直接物理删除而是加一个is_active布尔字段执行删除操作时改为is_activeFalse列表查询时统一过滤掉。这样用户误删后还能找回数据库里数据也不会因为频繁物理删除而产生大量碎片。3.4 图片上传接口的设计与安全校验接口采用POST /api/classify/前端用 FormData 上传图片字段image。Django 视图里有三步校验不能省import uuid from PIL import Image as PILImage from django.core.files.uploadedfile import InMemoryUploadedFile from rest_framework.decorators import api_view from rest_framework.response import Response api_view([POST]) def classify_image(request): upload request.FILES.get(image) if not upload: return Response({error: 缺少图片}, status400) if upload.size 5 * 1024 * 1024: return Response({error: 图片不能超过5MB}, status400) try: image PILImage.open(upload) image.verify() upload.seek(0) except Exception: return Response({error: 文件不是有效图片}, status400) subclass, category, confidence predict_batch_garbage(image) record GarbageRecord.objects.create( userrequest.user if request.user.is_authenticated else None, imagesave_upload(upload), subclasssubclass, categorycategory, confidenceconfidence ) return Response({ subclass: subclass, category: category, confidence: round(confidence, 4) })save_upload里我没有直接用原始文件名而是用uuid.uuid4().hex os.path.splitext(name)[1]重新生成文件名。中文文件名、路径穿越、文件名冲突全部靠这一行解决。3.5 Django 项目初始化的基础流程如果你对这个项目还不熟先按这个顺序把环境搭起来这个流程覆盖了热搜词里大部分内容# 1. 创建虚拟环境并安装依赖 python -m venv venv source venv/bin/activate pip install django4.2 tensorflow-cpu2.10.0 pillow10.1.0 # 2. 创建项目与 app django-admin startproject garbage_project . python manage.py startapp garbage_prediction python manage.py startapp records # 3. 在 settings.py 中注册 app、配置数据库连接和 MEDIA 路径 # 4. 生成迁移并创建管理员 python manage.py makemigrations python manage.py migrate python manage.py createsuperuser新手容易犯的错是忘记把garbage_prediction和records加进INSTALLED_APPS结果执行makemigrations时提示没有变化。还有Django 4.0 之后csrf_protect的逻辑没有变化但如果你用 DRF 的api_view它默认就走了 CSRF 豁免前端传X-CSRFToken的代码可能需要根据实际配置调整。4. 前端交互上传、拍照、Canvas 压缩与识别反馈闭环后端模型再准前端交互做得生硬演示效果也会打折扣。我这一版前端没有使用复杂的框架就是 Django 模板 原生 JavaScript。上传方式分两种相册选图和摄像头拍照。4.1 相册上传与 Ajax 提交页面里放一个隐藏的input typefile用户点上传按钮后触发文件选择。拿到文件后先做压缩再提交这一步是很多项目忽略的。手机拍出来的图动辄 3MB 以上如果不压缩直接传给后端不仅网络慢PIL 读取和模型 resize 也会拖慢响应。压缩代码我放在前端 Canvas 里async function compressImage(file, maxWidth 800, quality 0.8) { const img await createImageBitmap(file); const scale Math.min(1, maxWidth / img.width); const canvas document.createElement(canvas); canvas.width img.width * scale; canvas.height img.height * scale; const ctx canvas.getContext(2d); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); return new Promise((resolve) { canvas.toBlob(resolve, image/jpeg, quality); }); }压缩后的图片宽度控制在 800 像素以内识别精度基本不受影响但传输和推理时间大幅下降。然后在fetch中提交const formData new FormData(); formData.append(image, compressedBlob, capture.jpg); fetch(/api/classify/, { method: POST, headers: { X-CSRFToken: csrftoken }, body: formData }) .then(response response.json()) .then(data renderResult(data));记得在页面加载时从 cookie 里取csrftoken否则 Django 会回 403。如果你配置了 DRF也可以直接设置SessionAuthentication之外的认证方式但最简单可靠的还是把 token 加进请求头。4.2 摄像头拍照识别摄像头拍照在电脑上演示效果很好但有一个坑getUserMedia要求页面必须在localhost或 HTTPS 环境下才能调用。Django 开发服务器用http://localhost:8000没问题但如果你用 IP 地址在局域网里访问摄像头就会以权限不足为由拒绝打开。拍照流程是const video document.getElementById(cameraStream); const stream await navigator.mediaDevices.getUserMedia({ video: { facingMode: environment } }); video.srcObject stream; await video.play();用户点击“拍照”时用canvas.drawImage(video, 0, 0, width, height)截取当前画面然后同样走toBlob压缩流程。拍完别忘了stream.getTracks().forEach(track track.stop())不然摄像头指示灯会一直亮着用户体验很糟糕。4.3 识别结果展示与二次确认反馈结果展示区我用一张卡片展示三个信息细分类名称、四分类标签、置信度。置信度低于 0.6 时把结果用黄色框标出提示“该结果置信度较低请换一个角度重新拍摄或上传更清晰的图片”。这样即使用户拍到一张光线很差的图片被误判他也不会直接觉得系统是坏的而是会主动调整拍摄方式。同时我在结果卡片下方放了两个按钮“标记正确”和“标记错误”。用户点了标记后前端发一个POST /api/feedback/请求后端把这些反馈记录下来。这些数据回头可以导出作为下一轮模型迭代扩充训练集的重要来源。这个功能在毕设答辩时是加分项因为它说明你考虑到了模型持续优化而不是交一个静态演示。5. 部署上线与性能优化从“本机能跑”到“别人能用”开发阶段用python manage.py runserver没有任何问题但真正把系统给别人演示时必须面对并发、静态文件处理、数据库连接等一系列问题。这一章节把从零部署到服务器上能稳定访问的全过程梳理一遍。5.1 依赖版本与部署前检查清单先看依赖组合。我的requirements.txt如下django4.2.7 djangorestframework3.14.0 tensorflow-cpu2.10.1 Pillow10.1.0 numpy1.24.3 gunicorn21.2.0 whitenoise6.6.0TensorFlow 2.10 在 Python 3.11 上的兼容性并不理想建议使用 Python 3.8 或 3.10 环境。如果你的 TensorFlow 版本升级到 2.13 以上NumPy 版本限制可以放宽具体以导入时是否报错为准。部署前检查清单settings.py中DEBUG FalseALLOWED_HOSTS里写入服务器 IP 或域名DATABASES如果继续用 SQLite只适合低并发演示如果想更稳一点换成 PostgreSQL静态文件收集python manage.py collectstatic图片上传目录MEDIA_ROOT要有写入权限并且不能用rbind这种容易被忽略的权限问题拦住。5.2 Gunicorn 与 WhiteNoise 静态文件方案服务器上我选择了 Gunicorn 作为 WSGI 服务器配合 WhiteNoise 处理静态文件Nginx 只做反向代理和 HTTPS 终结。好处是项目结构简单单个 Django 进程就能服务静态资源不需要单独配一套复杂的 Nginx 静态文件路径。启动命令gunicorn garbage_project.wsgi:application \ --workers 3 \ --threads 2 \ --bind 0.0.0.0:8000 \ --timeout 60注意--timeout 60是必须的。模型推理在 CPU 上最坏情况可能超过 30 秒Gunicorn 默认 30 秒超时会直接杀掉 worker用户看到的就是 500 错误。我这个配置里3 个 worker 意味着内存峰值约等于 3 份模型大小加 Django 基线内存服务器内存最好预留 3GB 以上。5.3 推理性能实测与常见坑我在 2C4G 的轻量服务器上实测单次识别请求的总耗时分布环节耗时图片上传与反序列化约 80ms图片校验与预处理约 20msMobileNetV2 推理约 400ms结果写入数据库约 20ms响应返回约 10ms总耗时在 0.5 秒左右作为单用户演示完全没问题。但如果短时间内有几十个人同时上传三个 worker 会很快被推理任务占满其他请求只能排队。这时候的优化思路有两种把高并发的图片接收与低并发的模型推理拆开用消息队列如 Redis RQ异步处理识别任务前端轮询获取结果不要用 SQLite 存高速写入的识别记录换成 PostgreSQL 或者先写内存表再定期落库。第二种方案涉及异步流程代码复杂度会上升不建议在基础版本里做。我更推荐的做法是把识别历史记录和画像统计分开识别时只写一条轻量记录不需要在请求里做复杂事务。5.4 我踩过的几个坑汇总给你这些坑我都实际踩过按影响程度排序问题现象解决办法TensorFlow 与 NumPy 版本冲突导入 tf 时报 numpy 初始化错误锁定numpy1.24.3或用更高版本 tf 搭配新版 numpy上传超大图片网页请求发送很久没响应前端 Canvas 压缩到 800px 内后端限制 5MB中文文件名上传到对象存储乱码后端用 UUID 重命名原文件名写入字段DEBUGFalse 后样式丢了页面光秃秃一片安装 whitenoise 并执行 collectstatic摄像头黑屏getUserMedia 报权限错误必须用 localhost 或 HTTPS 访问不能用局域网 IPSQLite 被并发写锁多用户同时识别时偶发 “database is locked”生产环境切 PostgreSQL5.5 前端加一层轮询的轻量方案如果你想在不上消息队列的前提下缓解等待体验可以在前端识别时先拿到一个 task 号后端把图片存下来立刻返回“识别中”然后再由前端每隔 0.5 秒请求一次/api/classify/result/获取结果。这样即使推理时间超过 30 秒用户也不会看到一个一直转圈的请求超时页面。后端实现上任务结果可以先存在内存字典或缓存里键是 task 号值是识别结果。识别完成后由后台线程把结果写入GarbageRecord同时更新缓存。前端轮询拿到结果后再渲染页面。这个方案只增加少量代码就能让体验提升一个档次。写在最后这套系统还能怎么迭代做完这个项目我最大的体会是图像识别只占系统复杂度的一小部分真正花时间的是把模型、Web、数据库、交互这四层拧在一起。最难调试的往往不是模型精度而是文件上传类型判断、CSRF token 配置、静态文件收集这些看似不起眼的小细节。如果后续你想继续扩展可以在三个方向上迭代一是接入语音播报识别结束后直接告诉用户应该投哪个桶这对老人和小孩更友好二是按地理位置记录投放点结合历史识别数据生成小区垃圾分类热力图这会让系统从“工具”变成“数据产品”三是把模型换成 TFLite 并部署到手机端配合前端摄像头实现完全离线的垃圾分类助手不需要服务器也能工作。每个方向单独拆出来都是一篇完整的实战文章但骨架始终是这个项目搭起来的东西。对新手来说先把这篇文章里的链路完整走通一遍后面再谈优化也不迟。