ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ResNet50+Django实战:猪脸识别APP设计与落地全流程解析

ResNet50+Django实战:猪脸识别APP设计与落地全流程解析 简介一份面向深度学习与APP开发学习者的学术文献聚焦基于ResNet50模型的猪脸识别APP设计与实现。内容涵盖猪脸数据采集与预处理含OpenCV分帧、labelImg标注、ResNet50网络结构优化细节、不同学习率对验证集准确率的影响对比以及基于MUI与Django框架的APP四大功能模块设计实验表明优化后模型对猪脸照片识别准确率可达92%左右。该资料适合计算机视觉、智慧农业及移动应用开发方向的学生、工程师与科研人员参考既能理解卷积神经网络的工程化落地流程也可借鉴论文中数据增强、模型调参和实验分析的方法。包体为1个PDF文件大小约2.03MB图文清晰便于阅读与打印已有308人学习浏览具备较好的参考价值。资源内完整呈现了从数据准备到模型训练再到APP设计的闭环可帮助读者快速复现同类猪只识别任务提升养殖与保险场景的管理效率。1. 猪脸识别不是玄学一份 ResNet50 Django 的落地参考不带耳标、不打耳洞靠摄像头拍几张吃饭的照片就能识别出每头猪准确率还能到 92%——这套基于 ResNet50 的猪脸识别 APP 设计恰好把「深度学习 移动端落地」串成了一条完整链路。传统 RFID 方案耗时长、耳标易丢、打耳洞对猪和操作员都不友好而这份资料用 Keras 训练 ResNet50后端 Django 出接口前端 MUI 做混合 APP从数据采集、标注、训练到接口联调都有对应方案。对做农业 AI 项目、毕业设计或者想往边缘设备上搬视觉模型的从业者来说它最大的价值其实不是那个 92% 的准确率而是里面可以直接照抄的实验参数3:1:1 数据集划分、1e-4 学习率、Dropout 微调结构、base64 传图——这些才是能让你少走弯路的部分。2. 从监控视频到干净数据集分帧、标注、裁剪与增强的完整链路2.1 摄像头安装与视频采集为什么装在食槽上方这篇文章里猪脸数据的采集方式很有代表性猪圈分栏管理摄像头装在食槽上方调整安装角度后一个摄像头能拍到五头猪吃饭时的脸部。这个位置选得很巧妙——猪进食时头会自然朝向食槽面部正对摄像头能拍到清晰的眼部、口鼻和耳朵轮廓。如果装在侧面或过道拍到的多半是躯干和歪着的侧脸识别难度直接翻倍。拍摄周期是三天只在上午和下午的适宜时间打开摄像头。这个细节很多人会忽略猪圈不是完全室内正午强光下猪脸会过曝面部纹理细节直接丢失模型训练时能用的特征就少了一大截。我一般会建议把每天的拍摄时段固定住比如上午 8 点到 10 点、下午 15 点到 17 点这样既避开正午直射又能保证连续三天光线条件相对一致后期分帧出来的图片不会出现「一半像白天一半像黄昏」的亮度断层。还有一个容易被低估的点视频清晰度直接决定标注和训练的成本。食槽上方的摄像头建议至少用 720p 以上、25fps 左右的设备距离猪脸两到三米时眼睛和口鼻区域仍然要能看到足够纹理。拍得太糊后面 labelImg 标注时你连脸都框不准更别说让 ResNet50 学到细节特征。原始视频拿到后通常的做法是先用 OpenCV 做分帧把视频流变成一帧帧的 JPEG 图片。这一步在 Python 里几行就能搞定核心是控制抽帧频率——不需要每秒所有帧都保留相邻帧内容太像训练时冗余度高反而容易过拟合。常见做法是每秒抽一帧三天下来一个栏位大概能攒一万到两万张原始帧再人工筛掉模糊和猪脸遮挡严重的剩下用于标注的量就足够了。import os import cv2 video_path farm_1_bar_3.mp4 # 原始监控视频 frame_dir frames os.makedirs(frame_dir, exist_okTrue) cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) # 获取视频的帧率单位 fps frame_id 0 saved_count 0 while True: ret, frame cap.read() if not ret: break # 按帧率间隔抽帧frame_id % round(fps) 0 表示每隔 1 秒取一帧 if frame_id % round(fps) 0: cv2.imwrite(os.path.join(frame_dir, fframe_{frame_id:06d}.jpg), frame) saved_count 1 frame_id 1 cap.release() print(saved frames:, saved_count)这段代码的逻辑很直白用 VideoCapture 打开视频逐帧读入在编号整除帧率时把当前帧写盘相当于每秒保留一张。之所以不把全部帧都导出来是为了避免连续几十帧猪的动作几乎没变化导致同一头猪的照片几十张都高度相似训练集“看起来很大、实际信息量很少”。另外抽帧保存的文件名最好带上帧号后续如果发现某段视频画面异常比如猪群打架、摄像头被拱歪能快速回去定位对应时间段。2.2 labelImg 标注与裁剪从一张五头猪到单头猪脸分帧得到的每张图片里通常有好几头猪不能直接拿去喂模型。原文用 labelImg 工具把图片里较清晰的猪脸框出来每张图的标签信息保存成同名的 xml 文件这样的 PascalVOC 格式很容易读。标注这一步是整个流程里最费人力的环节也是数据质量的生命线。框太松会把食槽、水泥墙和饲料带进训练样本框太紧又可能切到猪耳朵丢失关键的面部特征。我一般会提醒自己一句话只要是肉眼觉得模糊的帧直接放弃标注别硬框。labelImg 里画矩形框时类别名务必填猪只编号比如 pig_01、pig_02而不是都填 pig。训练时每个编号就是一个类别标签一旦混了模型学到的就是错的特征对应关系。同一个目标在同一帧里如果出现两次比如猪侧了一下头又转回来只保留最正、最清晰的那个框避免同一张图里同一编号出现多个不一致的裁剪结果。标注完成之后用 Python 解析 xml 文件把每个 bndbox 框住的区域从原图里裁剪出来按猪只编号存入对应文件夹。这一步本身不复杂但有几个细节直接决定后面训练效果一是裁剪出来的图片尺寸不统一后续要统一 resize 到 224×224二是裁的时候不要完全贴着 bndbox 边缘可以在四周留出 2~5 像素的余量防止 labelImg 的框稍微偏了一点就把猪脸边缘切掉。import os import xml.etree.ElementTree as ET import cv2 xml_dir annotations # labelImg 输出的 xml 文件夹 frame_dir frames # 原始帧图片文件夹 save_dir pigs # 裁剪出的单头猪脸图片根目录 os.makedirs(save_dir, exist_okTrue) for xml_name in sorted(os.listdir(xml_dir)): if not xml_name.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_name)) root tree.getroot() image_name root.find(filename).text img_path os.path.join(frame_dir, image_name) img cv2.imread(img_path) if img is None: continue for obj in root.findall(object): pig_id obj.find(name).text # 例如 pig_01 bbox obj.find(bndbox) xmin int(bbox.find(xmin).text) ymin int(bbox.find(ymin).text) xmax int(bbox.find(xmax).text) ymax int(bbox.find(ymax).text) # 四周留 3 像素余量避免边缘被切到同时做边界限制 margin 3 xmin max(0, xmin - margin) ymin max(0, ymin - margin) xmax min(img.shape[1], xmax margin) ymax min(img.shape[0], ymax margin) crop img[ymin:ymax, xmin:xmax] out_dir os.path.join(save_dir, pig_id) os.makedirs(out_dir, exist_okTrue) out_path os.path.join(out_dir, f{image_name.rsplit(., 1)[0]}_{xmin}_{ymin}.jpg) cv2.imwrite(out_path, crop) print(crop done)这段代码读取每个 xml 文件遍历里面所有 object 标注框按 name 创建对应编号的文件夹并保存裁剪图。参数上最需要注意的是 margin 和边界限制labelImg 的框如果恰好落在图像边缘直接裁剪会越界报错或者裁出全黑区域留余量的另一层意义是防止猪耳朵、下颌轮廓被框线截断这些边缘纹理对识别同样有价值。标注时如果某一栏里猪只编号很多建议每个编号的文件夹里先检查一遍裁剪结果图片里是不是只有猪脸有没有把相邻猪的前腿也框进来有没有因为阴影导致半张脸过黑这些在训练前发现越早后面返工成本越低。2.3 数据划分与增强3:1:1 分组与 Keras 数据生成器数据整理完之后原文按 3:1:1 的比例把每个编号的数据集划分为训练集、验证集、测试集也就是训练 60%、验证 20%、测试 20%。这个划分有一个非常容易踩的坑不能把所有图片打乱后全局随机切分必须按猪只编号分组切。原因是同一个编号在连续视频帧里的照片很相似如果同一头猪的照片同时出现在训练集和测试集模型其实是在「记住」这头猪的样子而不是「识别」它测试准确率会虚高。import os import random from collections import defaultdict random.seed(42) # 固定随机种子保证复现结果一致 data_root pigs splits defaultdict(list) # {train: [(pig_01, img1), ...], ...} for pig_id in sorted(os.listdir(data_root)): pig_dir os.path.join(data_root, pig_id) if not os.path.isdir(pig_dir): continue imgs os.listdir(pig_dir) random.shuffle(imgs) # 同一编号内部打乱再按比例切 n len(imgs) n_train int(n * 0.6) n_val int(n * 0.2) train_imgs imgs[:n_train] val_imgs imgs[n_train:n_train n_val] test_imgs imgs[n_train n_val:] for img in train_imgs: splits[train].append((pig_id, img)) for img in val_imgs: splits[val].append((pig_id, img)) for img in test_imgs: splits[test].append((pig_id, img)) print({k: len(v) for k, v in splits.items()})这个划分逻辑的关键点有两个第一random.seed(42) 放在最前面所有随机操作都基于同一随机源下次运行结果完全一样训练出来的模型才能稳定对比第二先对每个编号单独 shuffle 再按比例切而不是把所有图片混在一起切能保证每个编号都均匀分布到三个集合里。如果不做这一步训练集可能集中在某几个编号测试集又集中在另几个编号模型训练和评估都失真。数据增强这一步原文用的是 Keras 里的 ImageDataGenerator对原始数据集做旋转等操作。它的核心思想是猪脸照片数量有限但通过小角度旋转、平移、水平翻转可以把一张图变成多张语义相同、像素不同的样本相当于免费扩了一倍多的数据量。from tensorflow.keras.preprocessing.image import ImageDataGenerator # 验证数据只看原始图不增强 train_datagen ImageDataGenerator( rotation_range15, # 旋转范围 ±15 度 width_shift_range0.1, # 水平方向随机平移 10% height_shift_range0.1, # 垂直方向随机平移 10% horizontal_flipTrue, # 随机水平翻转 fill_modenearest, # 平移/旋转后空白区域用最近像素填充 rescale1.0 / 255.0 # 像素值归一化到 [0, 1] ) val_datagen ImageDataGenerator(rescale1.0 / 255.0) train_generator train_datagen.flow_from_directory( train_dir, target_size(224, 224), # ResNet50 标准输入尺寸 batch_size32, class_modecategorical )ImageDataGenerator 的几个参数里rotation_range15 是原文「旋转」增强的具体化选择为什么不设成 30 或 45因为猪脸在自然进食场景下不会出现大幅度歪头旋转角度过大反而会产生现实中不存在的奇怪姿态让模型去学没有意义的分布width_shift_range 和 height_shift_range 模拟的是猪站在食槽前轻微左右移动、摄像头角度略有偏移的情况。fill_modenearest 表示平移后产生的空白区域用相邻像素填充比 fill_modeconstant 填充黑色更自然不会在图像边缘制造突变。flow_from_directory 会自动按文件夹名生成类别标签文件夹名叫 pig_01 则类别标签就对应 pig_01这就是前面强调标注时类别名必须统一的原因。到这一步一个干净、标注规范、带划分和增强的猪脸数据集就准备好了可以进入模型训练。3. ResNet50 与学习率调参把验证集准确率稳定在 92% 附近的实操记录3.1 残差网络选型为什么选 ResNet50结构如何微调图像分类里网络层数越深理论上能学到的特征越抽象但深层网络在反向传播时梯度连乘容易出现梯度消失或梯度爆炸——前面的层基本学不到东西。ResNet 引入 shortcut connections让每一层的输出可以跳过本层直接向后传递使得梯度有了一条“高速通道”回到前层这就是残差结构解决深度网络退化问题的核心逻辑。原文选择 ResNet50一个比较直观的原因是它比 VGG 系列参数少、计算量小又比 AlexNet 这类浅层模型特征表达能力更强在算力有限的服务器上也能跑。ResNet50 标准结构包含 49 个卷积层和 1 个全连接层原文在最后的全连接层后又接了一个最大池化层加 Dropout再接 Flatten 和全连接层形成一个针对猪脸识别微调过的分类头。这个结构改动跟官方默认的全局平均池化尾部不同最大池化保留的是区域里响应最强的特征对猪脸局部纹理差异更敏感。from tensorflow.keras.applications import ResNet50 from tensorflow.keras.layers import Dense, Flatten, MaxPooling2D, Dropout from tensorflow.keras.models import Model num_classes 20 # 猪只编号数量按实际标注的编号数修改 base_model ResNet50( include_topFalse, # 去掉官方自带的全连接分类头 weightsimagenet, # 使用 ImageNet 预训练权重做迁移学习 input_shape(224, 224, 3) ) x base_model.output x MaxPooling2D(pool_size(2, 2))(x) # 最大池化取 2x2 窗口内最大响应 x Dropout(0.25)(x) # 以 25% 概率随机丢弃特征防止过拟合 x Flatten()(x) # 把多维特征压成一维向量 x Dense(num_classes, activationsoftmax)(x) model Model(inputsbase_model.input, outputsx) model.summary()这段代码里include_topFalse 非常关键官方 ResNet50 的顶层是一个针对 ImageNet 1000 类设计的全连接层不拿掉的话最后一层输出的维数对不上你的猪只数量weightsimagenet 则直接加载预训练权重让网络一开始就具备识别边缘、纹理、形状等通用视觉能力训练时只需要在猪脸数据上做微调收敛速度比从零训练快得多。MaxPooling2D 和 Dropout 的组合对应原文的优化结构池化层压缩特征图尺寸减少参数量Dropout 在每次前向传播时随机丢掉四分之一的特征迫使网络不依赖某一小部分神经元泛化能力更好。需要提醒的是如果你的服务器显存比较紧张input_shape 可以改成 192×192 甚至 160×160识别准确率会略有下降但训练速度明显提升如果数据集的猪只编号少于 5 个迁移学习的效果反而比少量样本从零训练更稳因为预训练权重已经能把大部分通用特征提取出来分类头只需要做一个小范围的映射。3.2 学习率三组对比实验80% 震荡、72% 欠收敛与 92% 稳定学习率是训练过程中最值得较真的超参数之一甚至可以说有点「玄学」——同一个模型学习率差一个数量级结果可能差出 20 个百分点的准确率。原文在 ResNet50 上分别拿 1e-3、1e-4、1e-5 三个学习率做了实验结果具有非常典型的参考意义。学习率损失函数收敛情况验证集准确率1e-3损失难以下降曲线剧烈波动80% 附近大幅震荡1e-440 轮左右 loss 降到 0.5 附近约 92%稳定1e-5收敛速度过慢40 轮 loss 仍在 1.0 以上约 72%1e-3 学习率过大的表现是损失函数在最优点附近来回弹跳每一次梯度更新步子迈得太大直接跨过了谷底模型始终落不到一个稳定的最小值区域验证准确率自然跟着剧烈起伏。1e-5 则是另一个极端每一步更新都太小40 轮训练结束时损失还没降到 1.0 以下模型根本没学到足够特征准确率只有 72% 左右。1e-4 处在平衡区间前 10 轮就能看到 loss 明显下降到第 40 轮左右降到 0.5验证准确率稳定在 92% 附近。这个实验给后面复现的人一个很重要的经验不要一上来就迷信某个「推荐学习率」果断把 1e-3、1e-4、1e-5 都跑一组短实验每组长个 10 轮看 loss 趋势再决定正式训练的数值。看损失函数的形状比看准确率更可靠因为准确率在小数据集上波动大。训练时的优化器和回调配置可以参考下面这段。注意用了 ModelCheckpoint 保存最佳权重这个习惯非常关键——训练中后段模型可能过拟合导致验证准确率下滑保存最佳那一轮能让你拿到全训练过程里最理想的模型。from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping model.compile( optimizerAdam(learning_rate1e-4), # 学习率按实验结果选 1e-4 losscategorical_crossentropy, metrics[accuracy] ) callbacks [ ModelCheckpoint( pig_face_best.h5, monitorval_accuracy, save_best_onlyTrue, modemax, verbose1 ), EarlyStopping( monitorval_loss, patience8, # 连续 8 轮验证损失不改善就提前停止 restore_best_weightsTrue ) ] history model.fit( train_generator, steps_per_epochtrain_generator.samples // 32, epochs40, validation_dataval_generator, validation_stepsval_generator.samples // 32, callbackscallbacks )Adam(learning_rate1e-4) 是核心参数。EarlyStopping 在这里起到平衡轮数的作用如果 40 轮之前模型已经收敛并开始过拟合它会在验证损失连续 8 轮不改善时自动止损并恢复到最佳权重。steps_per_epoch 用样本数除以 batch_size 得到每个 epoch 的批次数确保训练时每个 epoch 都能完整看过一遍训练集。如果你换了更大的数据集还有余力可以把 epochs 从 40 提高到 60配合 EarlyStopping 不会浪费算力。3.3 模型保存与推理只用 .h5 就能完成预测训练完成之后模型被保存成一个 .h5 文件这也是后面 Django 接口里真正要用的东西。但 .h5 文件里保存的内容是有讲究的Keras 默认的 model.save() 会把网络结构和权重一起存进去推理时直接 load_model 一步到位如果只保存权重 model.save_weights()则必须先构建相同结构的模型对象再 load_weights否则报错。从部署角度来说一次性保存完整模型最简单不容易出结构对不上的问题。from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing.image import load_img, img_to_array import numpy as np # 加载训练好的完整模型推理阶段只需要这一行 model load_model(pig_face_best.h5) # 读取一张测试图并预处理到训练时一致的尺寸 img load_img(test_pig_01.jpg, target_size(224, 224)) arr img_to_array(img) / 255.0 # 归一化到 [0, 1] arr np.expand_dims(arr, axis0) # 增加 batch 维度从 (224,224,3) 变成 (1,224,224,3) pred model.predict(arr)[0] # 输出形状为 (num_classes,) label_idx int(np.argmax(pred)) # 概率最大的类别下标 confidence float(pred[label_idx]) # 对应置信度 print(label index:, label_idx, confidence:, round(confidence, 4))这里 label_idx 是类别数组的下标不是猪只编号本身。训练时 flow_from_directory 会按文件夹名的字母顺序生成类别索引你需要把「下标 → 猪只编号」的映射表保存下来比如映射成 Python 字典推理时用字典把 label_idx 转换回 pig_01 这样的实际编号。这一步如果遗漏接口返回给 APP 的就会是 0、1、2 这种用户看不懂的数字。还有一个容易被忽略的细节load_model 时如果遇到版本兼容问题最常见的是老版本 Keras 保存的模型里包含自定义层新版本加载时会报 Unknown layer。解决办法是加载时传入 custom_objects或者干脆在部署服务器上固定与训练时一致的 Keras 和 TensorFlow 版本。原文里用 Keras 平台训练这个约束同样适用于复现。推理速度上单张 224×224 的图片在 CPU 上大概需要 100~300 毫秒在 GPU 上几十毫秒就能出来对 APP 场景完全够用。4. MUI Django 混合 APP 架构四模块拆分与前后端接口设计4.1 四模块功能拆解登录注册、天气气温、信息修改与照片识别APP 的功能被拆成四个模块这个拆法是比较典型的农业管理类应用结构。登录注册模块承担身份验证保证不同养殖场之间的数据不串门天气气温模块解决的是猪只养殖里的一个实际问题——气温对猪的生长发育和繁殖力都有影响养殖户需要根据外部温度调整圈舍的通风和室温信息修改模块用来更新猪只成长过程中的体重、健康状况等数据照片识别模块则是核心入口用户拍一张猪脸照片APP 上传到服务器服务器返回猪只编号和详细信息。这四个模块在前后端职责划分上有一个值得借鉴的设计原则模型训练和识别部署在服务器端手机端只承担拍照、上传和展示。这样做的原因是 ResNet50 的推理虽然单张图只要几百毫秒但模型文件本身有一两百 MB直接塞进手机 App 既不现实也没必要还会让安装包体积爆炸。放在服务器上训练和预测都在同一个环境里完成模型迭代时手机端完全不用重新发版这个「高内聚低耦合」的思路对类似的 AI 应用都适用。4.2 Django 后端搭建与 MySQL 配置MTV 架构下的工程初始化后端选择 Django 有一个很实际的理由前端模型训练用的 Python后端也用 Python整个服务器端技术栈统一不用像某些团队那样训练用 Python、Web 用 Java部署时还要维护两套环境。Django 自带 MTV 架构、数据库访问组件和 admin 后台一个项目的初始化非常快。# 创建 Django 项目和应用 django-admin startproject pigfarm_server cd pigfarm_server python manage.py startapp recognition # 启动开发服务器默认监听 8000 端口 python manage.py runserver 0.0.0.0:8000项目与应用分离是 Django 的默认组织方式pigfarm_server 是整个工程的配置入口recognition 应用里放模型接口和业务逻辑。开发阶段直接 runserver 就能接口联调部署到生产环境后再换成 uwsgi 或者 gunicorn nginx这一步先不用管。连接 MySQL 需要在 settings.py 里配置数据库信息。这里有一个非常常见的报错点ENGINE 用了 django.db.backends.mysql 之后如果环境里没装 mysqlclient 或者 PyMySQL启动时立刻会报 Error loading MySQLdb module这个错本质上是缺少数据库驱动不是代码问题。# pigfarm_server/settings.py 数据库配置片段 DATABASES { default: { ENGINE: django.db.backends.mysql, # 指定 MySQL 引擎 NAME: pigfarm, # 数据库名需先在 MySQL 创建 USER: root, PASSWORD: your_password, # 按实际环境修改 HOST: 127.0.0.1, PORT: 3306, } }配置完数据库之后用 python manage.py makemigrations 和 python manage.py migrate 建表。猪只信息表可以直接定义成 Django 模型字段按原文的信息修改模块需求来猪只编号唯一标识体重和体温用于日常监测备注字段用来存疫苗接种或者特殊情况。# recognition/models.py 中定义猪只信息表 from django.db import models class Pig(models.Model): pig_id models.CharField(max_length32, uniqueTrue) # 猪只编号比如 pig_01 weight models.FloatField(nullTrue, blankTrue) # 体重单位 kg允许为空 temperature models.FloatField(nullTrue, blankTrue) # 体温单位 ℃ remark models.TextField(blankTrue) # 备注疫苗、健康情况等 def __str__(self): return self.pig_idPig 模型里 uniqueTrue 保证了数据库层面不会出现两个相同编号的猪只nullTrue 和 blankTrue 配合表示这个字段在创建时可以没有值。定义好模型后还需要在 admin.py 里注册才能使用 Django 自带的 admin 后台。管理员在网页端就能直接对数据库进行增删改查不需要写额外的后台管理界面。4.3 图片识别接口实现base64 解码、ResNet50 推理与 JSON 返回APP 上传照片时不能直接把二进制图片塞进 JSON因为 JSON 本身是纯文本协议。原文的做法是把图片转成 base64 编码后再放进请求体同时提到了这个编码方式对图片数据有一种「不可直接读」的效果相当于顺带做了加密。这个说法在安全语义上不严谨——base64 只是编码不是加密但用在传输过程中确实能让接收方避免处理二进制流的各种边界问题所以实践中依旧非常普遍。import base64 # 读取本地图片并转成 base64 字符串 with open(pig_photo.jpg, rb) as f: encoded base64.b64encode(f.read()).decode(utf-8)转换完成后字符串形如 /9j/4AAQSkZJRgABAQAAAQABAAD/...长度约为原图的三分之四。这也是一个实际优化点手机端拍照上传前最好先用压缩库把图片压缩到 1MB 以下否则一张 3MB 的照片转成 base64 后接近 4MB在 4G/5G 网络下传输延迟非常明显而 ResNet50 的输入只需要 224×224 的尺寸原始高清图里大量信息在推理时根本用不上。Django 端对应的识别接口可以这样组织。为了简洁这里用原生 JsonResponse 而不是 Django REST framework——接口就一个照片识别加几个普通查询引 DRF 反而要把序列化器、路由、视图集全都配一遍性价比不高。import json import base64 import io import numpy as np from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from tensorflow.keras.models import load_model from PIL import Image # 全局只加载一次模型避免每个请求都重新读 h5 文件 model load_model(pig_face_best.h5) label_map {0: pig_01, 1: pig_02, 2: pig_03} # 和训练时的类别索引保持一致 csrf_exempt def predict_pig(request): if request.method ! POST: return JsonResponse({error: only POST allowed}, status405) try: data json.loads(request.body) b64_str data[image] # 前端传过来的 base64 图片字符串 # data URI 格式是 data:image/jpeg;base64,xxxx按逗号切出纯 base64 部分 pure_b64 b64_str.split(,)[-1] img_bytes base64.b64decode(pure_b64) # 解码成 PIL Image并缩放到训练时一致尺寸 img Image.open(io.BytesIO(img_bytes)).convert(RGB).resize((224, 224)) arr np.array(img) / 255.0 arr np.expand_dims(arr, axis0) # 增加 batch 维度 pred model.predict(arr)[0] label_idx int(np.argmax(pred)) confidence float(pred[label_idx]) pig_id label_map[label_idx] # 把下标还原成猪只编号 # 从数据库查询详细信息返回给 APP 展示 pig Pig.objects.get(pig_idpig_id) return JsonResponse({ pig_id: pig.pig_id, weight: pig.weight, temperature: pig.temperature, confidence: confidence, }, json_dumps_params{ensure_ascii: False}) except Exception as e: return JsonResponse({error: str(e)}, status500)这段代码里有两个文件值得单拎出来说。第一个是 b64_str.split(,)[-1]前端如果传的是完整 data URI前面会有 data:image/jpeg;base64, 这一段前缀不切掉的话 base64 解码会直接抛 binascii.Error如果前端只传纯 base64split(,) 之后取最后一段也不受任何影响所以这个写法两种场景都兼容。第二个是 model load_model 放在视图函数外面、模块加载时执行一次否则每次请求都重新读一次一两百 MB 的 h5 文件服务器接口响应时间会从几十毫秒退化到几十秒。MUI 前端这边发送请求和展示结果相对简单。MUI 封装了 mui.ajax底层走的是 HTML5 的原生网络请求兼容性比纯 Web 的 XMLHttpRequest 更好。// MUI 框架中发送 base64 图片到识别接口 mui.ajax({ url: http://服务器IP:8000/recognition/predict_pig/, method: POST, data: JSON.stringify({ image: base64Str // 拍照后转成的 base64 字符串 }), dataType: json, success: function(res) { // res.pig_id 是猪只编号res.confidence 是置信度 mui.toast(识别结果: res.pig_id 置信度 res.confidence); // 根据 pig_id 从数据库查询更多信息并渲染到页面 }, error: function(xhr, type, errorThrown) { mui.toast(识别失败请检查网络); } });联调阶段要注意一个问题MUI 打包的 APP 运行在手机 WebView 里请求 http://服务器IP 时Django 所在服务器的 IP 必须是手机能直接访问的内网或公网地址不能是 localhost。如果用 127.0.0.1 访问手机找的是它自己的本机地址必然超时。开发时可以让手机和电脑连同一个 WiFi用电脑的局域网 IP 调试这样最快。天气气温模块和登录注册模块在技术上比识别接口简单得多天气模块直接调用第三方天气 API按城市名拉取实时温度登录注册用 Django 自带的 User 模型做身份校验即可数据库里存用户和密码哈希。整个 APP 的信息流里识别链路是核心其他模块更多是围着养殖场景做配套。5. 复现避坑清单数据质量、过拟合与接口联调的五个坑5.1 训练阶段学习率震荡与过拟合坑一学习率设成 1e-3验证准确率在 80% 附近剧烈波动。 现象训练跑到第 15 轮左右准确率曲线像锯齿一样上下跳动loss 完全不往下降。 原因梯度更新步长过大参数在损失函数低谷附近来回跨越始终收敛不到稳定区域。ResNet50 的残差结构能缓解梯度消失但无法抵消学习率过高带来的发散问题。 解决把 Adam 的 learning_rate 改成 1e-4继续观察 10 轮loss 会开始平稳下降。如果你用的不是 Adam 而是 SGD需要适当降低到 1e-3 以下并配合动量参数。参考原文结论1e-4 是这一任务下的稳定点。坑二训练集准确率接近 99%验证集只有 80% 出头。 现象模型在训练集上几乎完美换到没见过的验证照片上就出错典型的过拟合特征。 原因猪脸数据集规模本身不大每个编号的照片可能只有几百张模型把光照、圈舍背景这些与猪身份无关的信息也当成了判别特征。 解决优先做数据增强旋转、平移、水平翻转都是低成本手段其次检查 Dropout 是否生效确认模型结构里 Dropout(0.25) 真实存在于 Flatten 之前如果增强后验证准确率提升不明显把训练轮数降到 30 轮左右并用 EarlyStopping 在过拟合出现前自动截断。5.2 数据阶段标注框与划分泄漏坑三裁剪出来的猪脸图片里夹带了食槽和围栏背景模型对背景敏感。 现象训练时 loss 下降很快但实际拍一张新照片送到 APP 里识别结果乱跳看模型预测错误的图片发现它总是关注猪脸周围的区域。 原因labelImg 标注时框选太松把猪脸以外的背景大量带进训练样本。ResNet50 的特征提取层会把背景纹理也编码进特征图背景成了隐形的分类线索。 解决重新检查标注框尽量贴紧脸部轮廓裁剪代码里把 margin 从 3 像素改成 1 像素甚至 0别贪这几像素的余量同时把裁剪后的图统一等比缩放再居中裁剪成 224×224避免直接拉伸导致面部比例失真。坑四测试集准确率很高但 APP 上线后识别新猪只效果差。 现象测试集评估时准确率 92%实际对着另一栏猪拍照准确率掉到七成。 原因划分数据时没有按猪只编号分组同一头猪的相邻帧被同时切到训练集和测试集形成数据泄漏模型相当于「见过」测试照片的相似帧评估结果虚高。 解决严格按编号文件夹做 train/val/test 划分确保任何一头猪的照片只落在一个集合里标准做法是先用 3:1:1 把每个编号内部切好再汇总拼接成整体训练文件列表而不是把整个数据集一次性打乱再切。5.3 部署阶段模型加载与接口联调坑五Django 接口部署时加载 .h5 文件特别慢甚至直接卡死或报 Unknown layer 错误。 现象服务器响应前几个请求时耗时几十秒查看日志发现卡在 load_model 那一步或者服务启动后第一个请求直接返回 500。 原因h5 文件里包含了完整的网络结构和训练时的优化器状态Keras 版本不一致时老版本保存的结构里某些层名在新版本里无法识别服务器内存或 CPU 算力不足时加载大模型也会显著阻塞。 解决模型只加载一次放到模块顶部或单独初始化函数里不要放进视图函数部署时先确认训练和推理环境的 TensorFlow/Keras 版本一致生产环境建议用 model.to_json() 单独保存结构、save_weights() 保存权重加载时先通过 json 重建结构再 load_weights避免版本兼容问题如果还嫌慢可以在服务器上把 .h5 转成 tf.saved_model 格式推理速度会有明显提升。这些坑基本都是我实际拆这种「论文 代码复现」类项目时反复遇到的。绝大多数问题的根因不是模型结构不够好而是数据没整理干净、学习率没调到位、接口加载逻辑写得粗糙——这三件事优先级高于一切调参技巧。6. 扩展性验证换一种动物如何复用这套识别管线6.1 三步改造换数据集、改分类数、复用训练与接口代码原文最后提到在有合适的权重配置文件时这套系统可以直接迁移应用于其他动物识别。实际操作起来改造路径非常清楚。第一步把猪脸数据集替换成牛脸、羊脸或者猫狗的数据集数据采集方式可以沿用食槽上方摄像头方案也可以换成圈舍通道、投喂点的定点抓拍第二步把模型结构里最后一层 Dense 的 num_classes 改成新动物的个体数量其他层全部保留第三步重新跑第 3 章的训练代码生成新的 .h5 文件Django 接口和 MUI APP 完全不用动——接口只认图片内容和分类映射表换个 label_map 就能返回新的动物编号。整个管线里需要重做的主要是标注环节模型、后端、前端这三层几乎原样复用。6.2 验证方法准确率只是起点还要看置信度分布和线上表现换动物训练完模型之后怎么判断它真的能用我一般会强制走完三件事。第一件在测试集上算整体准确率同时画出混淆矩阵看哪些个体容易被互相混淆——如果猪_03 和猪_07 经常被搞混回去检查这两个编号的照片是不是标注时头型特征本身就很难区分必要时补充这两个体的更多角度照片。第二件统计所有测试样本的置信度分布正确预测的样本置信度和错误预测的样本置信度之间是否有明显分界如果错误样本也给出 0.95 以上的高置信度说明模型的概率校准有问题可以考虑在接口里加一个阈值置信度低于 0.85 时返回「不确定请重新拍摄」这比硬报一个结果更符合实际使用场景。第三件把模型接到 APP 上跑一天真实数据收集用户拍照后返回的结果定期把那些置信度高但确实是误判的样本拿出来加入训练集做增量微调——这相当于给模型装了个持续优化的闭环。从那以后我每次换物种做识别都强制走一遍这三件事先看测试集混淆矩阵再打置信度分布最后拿线上数据回喂训练集。这套思路比追求一次性的九成五准确率实际得多毕竟养殖场里没有人会拿着准确率报表来夸奖你他们只关心拍一张照片弹出来的编号到底对不对。希望这份拆解能帮你把猪脸识别的完整链路跑通少踩几个我当年踩过的坑。本文还有配套的精品资源点击获取
返回列表