去年我在做社区环保主题的技术方案时,接触到一个特别实际的需求:很多人扔垃圾时,站在垃圾桶前犹豫半天,不知道手里的东西到底算什么垃圾。与其做一个只能查资料的知识库,不如直接拍照识别。于是我基于Django框架,把图像识别模型和Web业务系统串在一起,做了一整套“拍照—识别—分类—投放建议”的智能垃圾分类系统。这篇博文就来说说这个项目的完整设计思路、核心代码实现,以及我在实操中踩过的坑,给正在做Django项目实战或者想用Python做图像识别Web应用的朋友一份能直接参考的作业。
先说结论:这个系统没有想象中那么难,真正的功夫花在数据集整理、模型调优和Web集成这三个环节。Django本身很成熟,负责用户上传、数据存储、后台管理等业务部分;图像识别用迁移学习的方式训练一个轻量级分类模型,部署成本远低于从零训练。整套项目做完,你既能把Django的MVT流程、ORM、Admin后台完整过一遍,又能实打实接触深度学习模型的训练、导出和推理链路,练手价值很高。
1. 项目整体设计与技术选型
1.1 需求拆解:这个系统到底要做什么
先别急着写代码。智能垃圾分类系统的核心需求,可以拆成四件事:用户上传垃圾图片、系统识别垃圾类别、返回投放建议、沉淀识别记录。听起来简单,但每一环都有细节。
比如“类别”怎么定义。国内大多数城市采用的是可回收、有害、厨余、其他四分类体系,但图像识别模型更适合输出比较具体的垃圾材料类别,比如塑料瓶、易拉罐、玻璃瓶、纸板箱。所以我的方案是:模型做细粒度识别,输出具体垃圾标签;业务层再建立标签到四分类的映射关系。这样做的好处是用户看到的反馈更“聪明”——不仅能告诉你“这是可回收垃圾”,还能告诉你“这是塑料瓶,请清洗后压扁投放”。纯四分类模型通常只能输出一个大类,用户体验会差很多。
再比如“识别记录”。每次识别都存一条记录,图片路径、预测标签、置信度、时间都记下来。这不仅是业务需要,还能作为后续模型迭代的数据来源。用户在真实场景里拍到的大量图片,经过人工修正后,比网上找的数据集更贴近实际使用环境。这个点很多教程不会提,但我觉得是系统设计里最有价值的部分之一。
1.2 技术选型:为什么是Django + Python图像识别
图像识别Web项目,后端框架可选Django、Flask或FastAPI。我最终选了Django,原因很实在:项目里既有图片上传、分类记录这类典型CRUD操作,又有后台管理知识库的需求。Django自带ORM、Admin后台和模板系统,一次搞定,不需要再额外拼接一堆组件。
对比一下:Flask很灵活,适合极简API服务,但用户管理、后台维护、表单处理都得自己搭或者引第三方库;FastAPI性能好,和异步生态配合强,但生态相对年轻,ORM、Admin这类配套不如Django成熟。我的项目里需要一个让管理员能随时维护垃圾知识库的界面,Django Admin打开即用,几分钟就能把增删改查做完,这对于快速交付一个完整系统来说太重要了。
模型层面,图像识别我用的是TensorFlow/Keras。选择一个框架不能只看名气,还要看你团队熟悉什么。如果换PyTorch也能做,只是我把训练、导出、加载的链路在Keras里跑得更顺,就没有引入第二个框架。Python 3.8以上版本、Django 4.x、TensorFlow 2.x,这套组合目前兼容性很稳定,网上资料也多,遇到问题基本都能搜到解决方案。
1.3 识别方案选型:从零训练还是迁移学习
垃圾分类图片识别,本质上是一个图像分类问题。如果你问我要不要从零训练一个卷积神经网络,我的答案是:除非你有几十万张高质量标注图,否则别碰这条路。从零训练CNN对数据量要求极高,而且训练周期长,分类效果还不一定比迁移学习好。
迁移学习的思路是:用别人在大规模数据集上预训练好的模型作为特征提取器,替换掉最后的分类层,用自己的数据微调。我选的是MobileNetV2,核心优势是模型体积小、推理快,部署在CPU环境也能在几百毫秒内出结果。对一个Web应用来说,没必要上ResNet这种大模型,毕竟识别一张图让用户等两秒,体验就很差了。
如果你有更好的硬件资源,还可以试试EfficientNet系列,准确率更高但模型更大。我的建议是:先跑通,再优化,别一上来就追高精度,哪怕一开始准确率只有80%,先把“上传—识别—展示”的完整链路跑通,后面再迭代模型不迟。整个项目做下来,你会发现最难调试的往往不是模型的几个百分点的准确率,而是图片上传、路径处理、数据迁移这种“琐碎但致命”的问题。
2. 数据、模型与Django的数据建模
2.1 数据集准备与类别体系设计
数据集是做这个项目最花时间、也最容易翻车的环节。公开数据集里比较常用的是TrashNet,包含glass、paper、cardboard、plastic、metal、trash六大类,图片比较干净,适合做基线实验。但TrashNet没有厨余垃圾和有害垃圾这两类,所以我另外补充了自建数据:用手机拍日常产生的垃圾,再加上一些公开来源的厨余垃圾图片,凑成完整的训练集。
这里有个经验可以分享:数据不需要一上来就追求海量,每个类别先保证200到300张图片,把流程跑通。之后再通过数据增强扩充,比如随机旋转、翻转、亮度调整、缩放裁剪,让模型见过更多“变形”的输入,泛化能力会明显提升。我自己用Keras的ImageDataGenerator做在线增强,边训练边生成新样本,比手动离线扩图省事得多。
类别体系设计上,我的标签和四分类映射关系如下:
| 模型输出标签 | 映射到四分类 | 投放建议参考 |
|---|---|---|
| plastic | 可回收垃圾 | 清洗后压扁投放 |
| glass | 可回收垃圾 | 小心轻放,破碎的需包裹 |
| paper | 可回收垃圾 | 保持干燥,叠放整齐 |
| cardboard | 可回收垃圾 | 拆开压平后投放 |
| metal | 可回收垃圾 | 瓶罐类请清空内容物 |
| trash | 其他垃圾 | 直接投入其他垃圾桶 |
| kitchen | 厨余垃圾 | 沥干水分后投放 |
| battery | 有害垃圾 | 投入有害垃圾收集容器 |
这套映射逻辑放在一个配置文件里,模型只负责输出标签,业务层负责把它翻译成用户能懂的信息。解耦之后,系统扩展新类别只需要加映射,不用改模型推理逻辑。
2.2 Django模型设计:把垃圾分类知识结构化
Django的ORM非常适合这类业务系统。我先设计了三个核心模型:垃圾类别表、垃圾物品表、识别记录表。
# models.py from django.db import models class GarbageCategory(models.Model): name = models.CharField(max_length=50, unique=True, verbose_name="分类名称") color = models.CharField(max_length=20, blank=True, verbose_name="分类标志色") description = models.TextField(blank=True, verbose_name="分类说明") class Meta: verbose_name = "垃圾类别" verbose_name_plural = "垃圾类别" def __str__(self): return self.name class GarbageItem(models.Model): name = models.CharField(max_length=100, verbose_name="垃圾名称") category = models.ForeignKey( GarbageCategory, on_delete=models.CASCADE, related_name="items", verbose_name="所属分类" ) image = models.ImageField(upload_to="garbage_items/", verbose_name="示例图片") suggestion = models.TextField(verbose_name="投放建议") created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = "垃圾物品" verbose_name_plural = "垃圾物品" def __str__(self): return self.name class RecognitionRecord(models.Model): image = models.ImageField(upload_to="recognition/", verbose_name="上传图片") predicted_category = models.ForeignKey( GarbageCategory, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="预测分类" ) predicted_label = models.CharField(max_length=100, blank=True, verbose_name="识别标签") confidence = models.FloatField(default=0.0, verbose_name="置信度") created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = "识别记录" ordering = ["-created_at"] def __str__(self): return f"{self.predicted_label} - {self.confidence:.2%}"三个模型的职责边界很清晰:GarbageCategory存“可回收垃圾、厨余垃圾”这种大类的元信息;GarbageItem存具体物品的知识库,比如“塑料瓶”对应的分类和投放建议;RecognitionRecord记录每一次识别行为。特别说一下外键的on_delete设置,GarbageItem删除时级联删除它关联的物品,用CASCADE;但RecognitionRecord删除时会连累一条识别记录消失,这不好,所以预测分类的外键用SET_NULL,保留历史记录,只是分类信息置空。
2.3 图像识别模型与Django的两种集成方式
图像识别模型训练好之后,怎么和Django程序结合,有两种主流方案。
第一种是直接把模型文件放在Django项目里,视图函数收到图片后,用TensorFlow加载模型,执行推理,返回结果。优点是架构简单,部署时只跑一个Web服务就行;缺点是Django和模型推理耦合在一起,模型一旦比较大,每个worker都要占额外内存,并发能力受影响。
第二种是把模型推理封装成独立的服务,Django通过HTTP接口调用。可以用TensorFlow Serving或者FastAPI起一个专门推理接口,Django只负责业务逻辑。优点是模型更新不需要重启Web服务,推理压力可以单独扩容;缺点是架构多了一个节点,部署和运维成本上升。
我做项目时先用第一种方案跑通,后面发现模型预热加载、并发推理这些问题,确实需要更干净的解耦方式。为了博文结构清晰,我下面把两种方式的取舍点说透:如果你的系统是课程设计、毕业设计或者小规模部署,第一种方案完全够用;如果是面向公众的正式产品,优先考虑第二种方案。
3. 完整实操:从初始化项目到跑通识别流程
3.1 环境准备和项目初始化
先把环境搭好。我建议用虚拟环境隔离依赖,避免系统里多个Python项目互相干扰。创建虚拟环境后,安装核心依赖:
pip install django tensorflow pillow numpy这里要注意TensorFlow版本和Python版本的匹配,我用的是Python 3.10和TensorFlow 2.10,稳定没有踩到兼容问题。装完Django后,创建项目和应用:
django-admin startproject smart_garbage cd smart_garbage python manage.py startapp recognition创建完app后,需要手动把recognition加进INSTALLED_APPS,然后再配置媒体文件相关的设置。因为图片上传是项目核心功能,所以媒体路径必须提前规划好:
# settings.py INSTALLED_APPS = [ # ... 'recognition', ] MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'MEDIA_ROOT是图片物理存储的根目录,MEDIA_URL是访问这些图片的URL前缀。数据库我直接用Django默认的SQLite,开发阶段足够,后续要换MySQL也只需改DATABASES配置。初始化数据库的命令:
python manage.py makemigrations recognition python manage.py migrate python manage.py createsuperusercreatesuperuser这个步骤千万别跳过,后面用Admin维护垃圾知识库全靠它。
3.2 训练一个分类模型并导出
模型训练脚本我用的是TensorFlow的迁移学习方案。以MobileNetV2作为基座模型,去掉原来的全连接分类层,加上全局平均池化和Dropout,再训练一个分类层:
# train.py import tensorflow as tf from tensorflow.keras import layers, models from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen = ImageDataGenerator( rescale=1./255, rotation_range=20, width_shift_range=0.2, height_shift_range=0.2, brightness_range=[0.8, 1.2], horizontal_flip=True, validation_split=0.2 ) train_generator = train_datagen.flow_from_directory( 'dataset/train', target_size=(224, 224), batch_size=32, class_mode='categorical', subset='training' ) base_model = MobileNetV2(weights='imagenet', include_top=False, input_shape=(224, 224, 3)) base_model.trainable = False model = models.Sequential([ base_model, layers.GlobalAveragePooling2D(), layers.Dropout(0.3), layers.Dense(7, activation='softmax') ]) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) model.fit( train_generator, steps_per_epoch=train_generator.samples // 32, epochs=20 ) model.save('garbage_model.h5')有几个关键点要说清楚。MobileNetV2的输入尺寸是224x224,所有训练图片统一resize到这个尺寸;rescale=1./255是像素值归一化到0到1区间,训练集和预测集必须保持一致;基座模型冻结权重,只训练最后新增的全连接层。先把这些跑通,后续再解冻部分层做微调,准确率还能再往上提。
我自己的训练结果是:20轮跑完后验证准确率在85%左右,对这个轻量级项目来说已经够用。训练时建议把GPU打开,没有GPU的话CPU也能跑,就是慢一些,别被漫长的训练劝退,可以先跑5轮验证链路是否正常。
3.3 实现图片上传与识别接口
Django这边,我先封装了一个识别服务类,负责加载模型和推理。模型的加载是一个耗时的操作,所以要设计成单例模式,只加载一次,避免每来一个请求就重新加载一次模型:
# services.py import numpy as np from PIL import Image from tensorflow.keras.models import load_model from tensorflow.keras.applications.mobilenet_v2 import preprocess_input class GarbageClassifier: model = None label_names = ['battery', 'cardboard', 'glass', 'kitchen', 'metal', 'paper', 'plastic', 'trash'] @classmethod def get_model(cls): if cls.model is None: cls.model = load_model('garbage_model.h5') return cls.model @classmethod def predict(cls, image_path): img = Image.open(image_path).convert('RGB').resize((224, 224)) img_array = np.expand_dims(preprocess_input(np.array(img, dtype=np.float32)), axis=0) preds = cls.get_model().predict(img_array)[0] idx = int(np.argmax(preds)) return cls.label_names[idx], float(preds[idx])注意Image.open方法读进来的图片可能带有Alpha通道,所以先转成RGB三通道;preprocess_input是MobileNetV2配套的预处理函数,它会做特定的均值归一化,和训练脚本里的rescale不是一回事,很多人容易在这里翻车,导致预测结果奇怪。
接下来是视图函数。用户提交图片后,先保存图片生成记录,再进行推理,最后把结果写回记录并展示:
# views.py from django.shortcuts import render from django.views.decorators.http import require_POST from .models import RecognitionRecord from .services import GarbageClassifier CATEGORY_MAPPING = { 'battery': '有害垃圾', 'kitchen': '厨余垃圾', 'plastic': '可回收垃圾', 'glass': '可回收垃圾', 'paper': '可回收垃圾', 'cardboard': '可回收垃圾', 'metal': '可回收垃圾', 'trash': '其他垃圾', } def upload_page(request): return render(request, 'recognition/upload.html') @require_POST def upload_and_recognize(request): image_file = request.FILES.get('image') if not image_file: return render(request, 'recognition/upload.html', {'error': '请选择图片'}) record = RecognitionRecord.objects.create(image=image_file) label, confidence = GarbageClassifier.predict(record.image.path) category_name = CATEGORY_MAPPING.get(label, '其他垃圾') record.predicted_label = label record.confidence = confidence record.predicted_category = None record.save() suggestion = get_suggestion_by_category(category_name) return render(request, 'recognition/result.html', { 'record': record, 'category_name': category_name, 'suggestion': suggestion, 'confidence': confidence, })get_suggestion_by_category这个函数可以从GarbageCategory表里查对应分类的投放建议,这样管理员在Admin后台修改知识库,前端展示就会跟着变,不需要改代码。这里也是体现Django ORM价值的地方:业务查询用链式filter和get,代码简洁,可读性高。
3.4 前端模板与结果展示
前端我用一个简单的HTML表单接收图片,加上Bootstrap让页面不那么难看。上传部分的关键是form必须带enctype="multipart/form-data",否则Django的request.FILES永远是空的,这个坑我见不少人踩过:
<!-- upload.html --> <form method="post" enctype="multipart/form-data" action="{% url 'upload_and_recognize' %}"> {% csrf_token %} <input type="file" name="image" accept="image/*" required> <button type="submit">开始识别</button> </form>识别结果页展示用户上传的图片、预测分类、置信度和投放建议。置信度要转成百分比展示,低于某个阈值时提示用户“识别置信度较低,仅供参考”。这个细节很影响实际体验,因为系统总有判断不准的时候,提前给用户一个心理预期,比硬着头皮说“这就是可回收垃圾”要诚实得多。
如果不想做整页刷新,也可以用fetch做异步上传,后端接口返回JSON,前端动态更新结果。我后来就改成了这种方式,用户体验更接近现代Web应用:
const formData = new FormData(); formData.append('image', fileInput.files[0]); fetch('/recognize/', { method: 'POST', body: formData, headers: { 'X-CSRFToken': getCookie('csrftoken') } }) .then(response => response.json()) .then(data => { document.getElementById('result').textContent = data.category_name; });异步方案在调试时要额外注意CSRF token的传递,这是Django特有的安全机制,必须在请求头上带上。
3.5 部署上线前要处理的细节
本地跑通和部署上线是两个世界。生产环境我建议用gunicorn + nginx的组合,Django自带的开发服务器绝对不适合直接对外服务。静态文件和媒体文件的处理也要调整,开发时Django能自己服务媒体文件,生产环境这些都要交给nginx或者单独的文件服务去处理。
还有模型文件的问题。h5模型文件可能有几十兆,每次发布代码都要重新上传,比较麻烦。更合理的做法是把模型放在和代码独立的目录,通过环境变量指定路径,这样模型更新不需要重新部署代码。另外,多个gunicorn worker会各自加载一份模型,内存占用成倍增长,解决办法是控制worker数量,或者用前面提到的独立推理服务。
还有一点容易被忽略:图片上传的安全限制。恶意用户可能上传超大图片或者非图片文件,导致磁盘写满或者后端处理报错。我加了文件类型校验和大小限制,只允许jpeg、png、webp格式,大小控制在10MB以内。
4. 常见问题与排查经验实录
4.1 图片上传失败的常见原因
图片上传是新手最容易卡住的地方,而且报错花样很多。第一个常见问题是表单没有写enctype="multipart/form-data",导致Django这边request.FILES为空,页面不报错但就是没有文件。第二个问题是MEDIA_ROOT路径没配置对,图片“保存成功”了但找不到文件,检查一下settings里的BASE_DIR拼接是否准确。第三个问题是nginx层默认会限制请求体大小,上传大图返回413,要在nginx配置里加client_max_body_size 10m这样的设置。
排查这类问题有个通用思路:先看Django日志,再看nginx日志,最后看浏览器Network面板。请求能到达Django但没有FILES,就是表单或请求头问题;请求根本没到Django,就是nginx或网络层问题。按照这个顺序排查,基本能快速定位。
4.2 识别准确率上不去的调优思路
很多朋友跑完基线模型发现准确率只有60%出头,就开始怀疑代码写错了。准确率上不去的常见原因有三个:数据量太少、类别不均衡、预处理不一致。我建议按照这个顺序去排查优化。
第一,给每个类别补数据,至少保证每个类别300张以上,实在不够就用数据增强硬撑。第二,检查训练集里哪类图片最少,比如有害垃圾图片可能只有几十张,模型在训练时会严重偏向图片多的类别,解决方法是使用class_weight给少数类别更高的损失权重,或者对少数类别做更多的增强。第三,排查预测阶段的预处理是不是和训练阶段完全一致,尤其注意resize尺寸、归一化方式、是否转为RGB这些细节。如果这些都调过还不行,再考虑解冻基座模型的后几层做微调,把学习率调小,比如1e-5级别,配合EarlyStopping防止过拟合。
4.3 Django查询和删除对象时容易踩的坑
这个项目里多次用到Django查询和删除,有几个坑值得单独提一下。QuerySet是惰性的,如果你写obj = Model.objects.filter(name='x'),这个查询并不会立即执行,只有真正访问结果时才触达数据库。所以想要拿单个对象,应该用get而不是filter,避免后续代码拿到一个QuerySet后调用属性报错。
删除对象也有讲究。单个对象删除用obj.delete(),它会返回一个元组,包含删掉的记录总数和各类型详细数量。批量删除用QuerySet.delete(),比如RecognitionRecord.objects.filter(confidence__lt=0.5).delete()可以清理低置信度的历史记录。但要特别小心:在循环里一边遍历QuerySet一边删除记录,会导致部分对象被跳过,正确做法是先转成list再循环,或者直接使用批量删除。外键级联删除也要留意,on_delete=models.CASCADE删除父对象时会连坐所有子对象,用好了很省事,用错了可能一夜之间删掉一大堆关联数据,所以设计外键时就要想清楚业务上它该不该级联。
4.4 性能优化:让识别接口响应更快
跑通之后,我花了不少时间优化性能。识别接口的耗时主要集中在三块:图片读取和预处理、模型推理、数据库写入。图片预处理用PIL重新resize和转换会花几十毫秒,想提速可以限制上传图片的原始尺寸,前端先压缩再提交。模型推理部分在CPU上跑MobileNetV2,单张图大概是200到400毫秒,如果对这个速度还不满意,可以转成TensorFlow Lite格式,推理速度能提升一倍以上。
数据库写入倒不是瓶颈,但要注意不要在循环里逐条save,批量操作要用bulk_create或者直接一次创建。还有一个容易被忽略的点:如果模型分类VGG、ResNet这种百兆级大模型,首次推理会因为权重加载而非常慢,所以必须在服务启动时做预热,也就是在启动脚本里先调用一次predict,把权重加载进内存,这样用户的第一请求才不会等半天。
最后一个性能建议:知识库的查询结果也是热点数据,不会频繁变动,可以用缓存存起来。Django自带的cache框架配置Memcached或Redis,把分类建议和垃圾物品列表缓存几个小时,数据库压力会小很多。
最后说点实际的经验
做完这个项目,我最大的体会是,Django这类“全家桶”框架做AI Web应用确实省心,但技术核心其实在模型和数据的配合上。我的建议是:先保证完整链路跑通,再谈准确率和性能优化。一开始识别不准没关系,把上传、识别、展示、记录这条流水线走顺,你会对整个系统有更真实的感知,后续不管是换更好的模型,还是加新功能,心里都有底。
再分享一个小技巧:如果识别置信度比较低,千万不要硬给结论。我后来加了一个逻辑,置信度低于0.6的时候,页面不直接展示“这是某类垃圾”,而是提示“识别不确定,建议去知识库手动查询”,然后附上搜索入口。这个改法看似保守,实则让系统可信度明显提升,用户不会因为一次错误判断就彻底不信任你。智能垃圾分类系统本身是一个很有实用价值的落地场景,后续还能扩展语音识别查询、积分激励、社区投放点地图功能,只要骨架搭得好,往上盖楼就很顺手了。