ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

实战:用PaddleDetection与Flask构建猪只计数系统

实战:用PaddleDetection与Flask构建猪只计数系统 简介本资源是一套面向农业智能化场景的猪只目标检测与自动计数系统实现方案适用于具备Python基础及一定深度学习认知的开发者、农林信息化研究者与边缘AI部署实践者。项目基于PaddlePaddle训练YOLO系列模型PP-YOLO结合Flask构建轻量级Web服务接口并通过Docker容器化封装实现跨环境部署切实解决养殖场人工清点效率低、误差大等实际问题。压缩包共29个文件含10张标注测试图像jpg、5个核心Python脚本app.py、infer.py等支撑推理与服务逻辑、1个Dockerfile、2个模型文件.pdmodel/.pdiparams、1个配置文件infer_cfg.yml及README说明文档整体大小34.17MB。已有64人下载学习资源提供完整训练→推理→部署闭环代码、Postman调用示例、容器路径映射规范及性能优化提示目录结构清晰便于快速复现与二次开发。 前几个月接了一个智慧养殖方向的小项目——猪场盘点计数。需求很简单猪舍监控拍一张照片自动告诉养殖户这一栏有多少头猪。听起来不就是目标检测加个计数么真做起来才发现坑不少——从数据标注到模型训练再到web接口封装每一步都有讲究。这篇文章把我整个实操过程记录下来包括基于PaddlePaddle训练PP-YOLO检测模型的完整流程以及用Flask搭建识别计数服务的接口设计思路还有源码和模型导出的细节希望能给正在做类似视觉计数项目的朋友一些参考。先说结论项目最终用PaddleDetection自带的PP-YOLO tiny作为检测模型在自建的猪舍数据集上训练测试集mAP能到92%以上然后通过PaddleInference导出静态模型用Flask封装成HTTP接口上传一张图片返回猪只数量和带检测框的标注图。整个链路跑通之后单张图片在GPU上的推理耗时约25msCPU上约350ms完全能满足猪场日常盘点的使用频率。1. 整体设计与技术选型思路1.1 场景痛点与需求拆解猪只计数这个需求表面看是“识别统计”但实际部署时会遇到几个非常实际的约束。首先是猪舍环境复杂栏位之间有大片阴影、料槽、栏杆遮挡猪又是群居动物经常出现互相叠压和挤在一起的情况这对小目标的检测精度要求很高。其次是拍摄角度不统一有些监控是俯拍有些是平视同一头猪在不同角度下形态差异很大。第三是现场没有太强的算力设备养殖户大概率用一台普通台式机跑服务所以模型不能太大推理速度要跟上。在做需求拆解的时候我先把“猪只智能识别计数”拆成了三个子问题一是目标检测把图像里所有的猪框出来二是目标过滤把置信度过低的检测框和重复检测的框去掉避免重复计数三是数量统计统计最终保留下来的检测框数量。如果后续还要做“按栏统计”可以再加一个区域划分逻辑通过预设的多边形区域来判断每个检测框的中心点落在哪个栏位。不过第一版先不做这个只实现整图计数。1.2 为什么选Paddle这套方案说实话最开始我也纠结过到底用YOLOv5还是PaddleDetection。后来选Paddle的原因有三个。第一PaddleDetection对新手友好配置化训练不需要自己写太多模型代码PP-YOLO系列在精度和速度之间平衡得不错尤其是PP-YOLO tiny这种轻量级模型适合部署在普通CPU机器上。第二飞桨的PaddleInference在x86 CPU上有专门的优化配合mkldnn加速实测推理速度比直接用PyTorch的onnxruntime还快一些。第三PaddleDetection团队持续更新文档和issue都比较全碰到问题基本能搜到解决方案。不过也要说清楚Paddle并不是没有坑。动态图训练的模型要导出成inference模型才能用PaddleInference加速这里涉及一个模型转换的过程另外Paddle的安装包比较大环境依赖容易冲突建议用conda单独建环境。如果你们团队对PyTorch更熟那用YOLOv5也完全可以只是后面部署链路的差异要自己评估。1.3 Web框架选定Flask的理由Web框架这块我几乎没有犹豫就选了Flask而不是FastAPI或者Django。原因是这个项目本质上是一个轻量级的模型推理服务不需要数据库、不需要后台管理、不需要异步任务队列核心就是“接收图片-跑模型-返回结果”。Flask足够简单一个文件就能把服务和推理逻辑串起来。FastAPI虽然性能更好、参数校验更方便但考虑到调试成本Flask的生态和示例更多尤其是模型推理这类同步阻塞型任务两者差别不大。至于Django对这个场景来说太重了杀鸡不用牛刀。2. 数据准备与训练环境搭建2.1 环境依赖与安装训练和部署建议分成两个环境。训练环境用GPU机器部署环境是用户现场的CPU机器两者的Python依赖可以保持一致减少后期排障成本。我用的是conda管理的Python 3.8环境GPU机器上安装的是CUDA 11.2 cuDNN 8.2PaddlePaddle版本是2.4.2PaddleDetection是release/2.5分支Flask版本是2.2.2。安装命令记录一下方便后面复现# 创建虚拟环境 conda create -n pig_count python3.8 -y conda activate pig_count # 安装PaddlePaddle GPU版CUDA 11.2 python -m pip install paddlepaddle-gpu2.4.2.post112 -f https://www.paddlepaddle.org.cn/packages/stable/cu112/ # 克隆PaddleDetection仓库2.5版本稳定 git clone https://github.com/PaddlePaddle/PaddleDetection.git -b release/2.5 cd PaddleDetection pip install -r requirements.txt # 安装Flask pip install flask2.2.2这里有个容易踩的坑PaddleDetection的requirements里会装一些跟训练相关的库比如pycocotools、shapely、opencv-python这些在部署阶段并不需要。如果部署机上网络条件不好可以先在训练机上通过pip download把依赖包下好再拷贝到部署机离线安装能省掉很多麻烦。2.2 采集图片与标注规范数据是这次项目里最花时间的部分。猪舍监控视频有现成的我按每3秒截一帧的方式抽了大概2600张图片再手工剔除掉模糊帧和完全无猪的帧最终留下1800张有效图片。如果是从不同猪舍、不同光线条件下采集的图建议尽量让数据来源分散一些否则训练出来的模型换个场地就“失灵”。标注工具选的是LabelImg标注格式导出为Pascal VOC格式也就是每张图片对应一个同名xml文件。类别只有一个pig。标注的时候有几个细节需要注意。第一被遮挡的猪也要标全框。哪怕只能看到猪头或者半个身体只要人能判断出这是一头猪就按完整身体的大致范围标注。这样模型能学到遮挡情况下的特征。第二不要标得太保守。有些人会把框刚好卡在猪身体边缘这样模型收敛会很慢建议框外扩1%-2%的像素给模型一点上下文信息。第三边界上的猪也要标不要因为只露出一半身体就不标否则模型会把这种“半猪”当成误检。2.3 数据集划分与质量检查1800张图片按8:1:1划分成训练集、验证集和测试集。划分的时候一定要先打乱再按比例切避免同一个视频片段里截出来的连续帧同时出现在训练集和测试集中造成“假高分”。我写了一个简单脚本按文件名哈希值做分层抽样确保同一时刻附近的帧不会被分到两个集合里。划分完之后建议统计一下每张图的标注框数量分布。如果大多数图片只有1-3个标注框而少数图片有10个以上模型对密集场景会学不好最好把密集图片的数量补一补。另一个检查项是用脚本渲染出部分标注结果可视化看一眼标注框和猪只是否对齐这一步虽然土但能揪出很多标注规范不一致的问题。3. PaddleDetection模型训练与导出3.1 配置文件选择与关键参数PaddleDetection官方仓库提供了一批现成的配置文件在configs/ppyolo/目录下。我选的是ppyolo_tiny_650e_coco.yml这套配置考虑到部署机器性能有限tiny版本参数量小、推理快。但这套配置默认是COCO数据集80类用在单类别检测上需要改动不少参数。按我的经验核心要改的参数有这几项。第一个是数据集路径改成自己数据集的coco或voc格式路径。这里我说一下我直接用了PaddleDetection自带的voc格式支持把train和eval的配置指到标注文件所在目录。第二个是num_classes改成1。第三个是学习率和batch size的适配。PP-YOLO tiny的默认batch size是96用4卡训练我的GPU显存只有12G只能把batch size调到每卡8学习率按比例从0.005降到0.0005左右。第四个是snapshot_epoch我这边300轮出一个快照避免每轮都存模型占用磁盘。# 关键配置节选 epoch: 650 LearningRate: base_lr: 0.0005 schedulers: - !PiecewiseDecay gamma: 0.1 milestones: [400, 550] OptimizerBuilder: optimizer: momentum: 0.9 regularizer: factor: 0.0001 type: L2 architecture: PPYOLO use_gpu: true TrainReader: batch_size: 8 inputs_def: image_shape: [3, 416, 416]其实epoch 650不是必须的我自己训练到300轮左右loss就已经稳定了后面靠早停机制避免过拟合。如果你有自己的预估时间预算可以考虑把epoch降到300-400观察评估指标不再上升就提前结束。3.2 训练过程与指标解读配置改好后训练命令比较简单export CUDA_VISIBLE_DEVICES0 python tools/train.py -c configs/ppyolo/ppyolo_tiny_650e_coco.yml日志里会输出每个iter的loss、learning rate、耗时以及周期性在验证集上的评估结果。我自己重点关注的指标有三个loss收敛曲线、mAP、以及FPS。训练初期loss下降很快100个epoch之后loss基本在0.3-0.5之间波动mAP则一路爬到0.85以上。这里要提醒一句mAP的绝对值跟数据集的难度强相关单类别、目标面积大的场景mAP很容易上0.9但目标小、遮挡多的场景可能0.8就算不错了不要盲目追求数字。训练过程中如果发现loss在后期出现回弹很可能是学习率没降下来或者数据增强太强导致训练不收敛。PP-YOLO默认带了MixUp、CutMix等多类增强在小数据集上有时反而起反效果。我用的时候把train reader里的MixUp开关注掉了从配置文件的enable_mixup: true改成falseloss稳定了不少。断点续训也是一个高频操作。训练中断后直接加-r output/ppyolo_tiny_650e_coco/100参数会加载第100轮的权重继续跑。这个功能很实用尤其是训练集比较大的时候中断重启不用从头开始。3.3 模型评估与验证训练完成后在测试集上做一次完整的评估python tools/eval.py -c configs/ppyolo/ppyolo_tiny_650e_coco.yml -o weightsoutput/ppyolo_tiny_650e_coco/best_model.pdparams评估结果会输出每个类别的AP、AR以及整体mAP。我这边最终单类AP是0.921这个成绩在部署场景里已经能用了。不过评估指标只是参考真正要验证的是“实际效果”建议从测试集中随机抽50张图片跑一次推理并可视化输出人为检查误检和漏检情况。这一步别省很多时候mAP高不代表漏检少特别是密集猪舍场景检测框可能把几头猪框成一个大框mAP会惩罚这种样本但人类视觉上还是能看出来不对。3.4 导出PaddleInference模型训练得到的是动态图权重.pdparams部署阶段用PaddleInference加载时需要先导出成静态图模型python tools/export_model.py -c configs/ppyolo/ppyolo_tiny_650e_coco.yml -o weightsoutput/ppyolo_tiny_650e_coco/best_model.pdparams导出后在output_inference目录下会生成三个文件model、__params__和infer_cfg.yml。这三个文件构成了一个完整的静态推理模型可以直接被PaddleInference加载。这里有一个容易踩的坑export_model.py导出后的模型输入格式是NCHW的float32张量输入尺寸固定为训练时的尺寸我这里用的是416x416所以推理前必须把图片resize到416x416否则会报维度不匹配的错误。另外导出时加一个--save_txttrue参数可以顺便把每个类别的标签名写到infer_cfg.yml里后面Flask推理时读取类别名就方便了不用硬编码。4. Flask服务端实现与推理接口4.1 项目目录结构与依赖部署项目我用了很简洁的结构看起来一目了然pig_count_service/ ├── app.py ├── predictor.py ├── inference_model/ │ ├── __model__ │ ├── __params__ │ └── infer_cfg.yml ├── templates/ │ └── index.html ├── static/ │ └── uploads/ └── requirements.txtrequirements.txt里只需要列出推理时实际用到的库paddlepaddle或paddlepaddle-gpu、flask、opencv-python、numpy、Pillow。不要直接把PaddleDetection整个装到部署机器上那是训练环境的依赖部署没必要。4.2 推理类的封装与模型加载部署时最大的一个原则是模型在服务启动时加载一次放进一个全局单例里不要在每次请求时都重新加载模型。PaddleInference模型加载耗时大概几百毫秒到几秒不等如果每个请求都加载服务基本没法用。我写了一个predictor.py核心代码如下import numpy as np import paddle.inference as paddle_infer class PigDetector: def __init__(self, model_dir): config paddle_infer.Config( str(Path(model_dir) / __model__), str(Path(model_dir) / __params__) ) config.enable_memory_optim() config.enable_use_gpu(512, 0) # 有GPU时启用 config.switch_ir_optim(True) self.predictor paddle_infer.create_predictor(config) self.input_names self.predictor.get_input_names() self.output_names self.predictor.get_output_names() self.input_handle self.predictor.get_input_handle(self.input_names[0]) self.output_handle self.predictor.get_output_handle(self.output_names[0]) def predict(self, image): # image 为BGR numpy数组shape(H,W,3) resized cv2.resize(image, (416, 416)) # 归一化到[0,1]并转为CHW input_data resized.astype(np.float32) / 255.0 input_data input_data.transpose(2, 0, 1)[None, ...] self.input_handle.copy_from_cpu(input_data) self.predictor.run() outputs [self.output_handle.copy_to_cpu()] return outputs如果部署机器是纯CPU环境把config.enable_use_gpu那行改成config.enable_mkldnn() config.set_cpu_math_library_num_threads(8)MKLDNN对CNN推理的加速效果很显著实测在酷睿i5上能把推理时间从700ms压到300ms左右。4.3 后处理与计数逻辑模型输出的结果形式跟训练时的后处理配置有关。PP-YOLO导出后的输出是一个shape为[N, D]的数组其中N是候选框数量D是6如果是单类分别对应[x1, y1, x2, y2, score, class_id]如果是多类D会变成6 num_classes。拿到的其实是经过NMS后的最终检测框不需要再做NMS但为了稳妥还是可以再过滤一遍低置信度框。def postprocess(prediction, confidence_threshold0.5): boxes prediction[0] if boxes.ndim 1: boxes boxes[None, :] keep [] for box in boxes: x1, y1, x2, y2, score, cls_id box if score confidence_threshold: continue keep.append({ class_id: int(cls_id), score: float(score), bbox: [float(x1), float(y1), float(x2 - x1), float(y2 - y1)] }) return keep计数的逻辑就更简单了——检测框的数量就是猪的数量。这里有一个容易忽略的点如果图片中有多个栏位这种全局计数的方式会把好几个栏位的猪加在一起得到的是“全图猪总数”而不是“某一栏的猪数量”。要实现分栏计数可以把每个栏位定义成多边形区域然后判断检测框中心点是否落在区域内。4.4 Flask接口设计Flask部分我只写了两个接口。一个是首页上传页面方便调试和给客户演示另一个是Post接口接收图片返回JSON格式的识别结果和统计信息。from flask import Flask, request, jsonify, render_template import cv2 import os from predictor import PigDetector app Flask(__name__) detector PigDetector(inference_model) app.route(/) def index(): return render_template(index.html) app.route(/api/pig_count, methods[POST]) def pig_count(): file request.files.get(image) if file is None: return jsonify({error: no image uploaded}), 400 img_bytes file.read() img_array np.frombuffer(img_bytes, np.uint8) image cv2.imdecode(img_array, cv2.IMREAD_COLOR) if image is None: return jsonify({error: image decode failed}), 400 prediction detector.predict(image) detections postprocess(prediction) result_image draw_detections(image, detections) _, encoded cv2.imencode(.jpg, result_image, [cv2.IMWRITE_JPEG_QUALITY, 85]) img_base64 base64.b64encode(encoded.tobytes()).decode(utf-8) return jsonify({ count: len(detections), detections: detections, image_base64: img_base64 }) if __name__ __main__: app.run(host0.0.0.0, port5000, threadedTrue)这里有几个设计细节值得说说。第一图片用二进制流上传不要用base64字符串方式传流量消耗大而且Flask处理multipart/form-data的效率和兼容性都更好。第二结果里我返回了base64编码的标注图方便前端直接展示识别效果不需要把图片保存到服务器磁盘上减少文件管理的负担。第三接口要设置超时默认的Flask同步模式在模型推理耗时较长时会阻塞worker线程所以在部署时我把应用跑在gunicorn里配置了多个worker。不过要注意gunicorn多worker模式下每个进程都会各自加载一份模型内存开销会成倍增加。4.5 线程安全与并发处理Flask自带的开发服务器虽然是多线程的但Python的GIL决定了CPU密集型任务无法真正并行。PaddleInference的run方法内部是否释放GIL不同版本行为不一致实测在PaddlePaddle 2.4版本下多线程并发推理时基本没有性能提升反而因为资源竞争导致速度下降。所以如果这道服务将来并发量高了最直接的办法是把模型推理放到独立进程中用消息队列异步处理。但就猪场盘点这个场景来说一天可能就用十几回完全没必要上那么复杂的架构。我个人建议先用Flask默认的threadedTrue模式跑着瓶颈出现了再加gunicorn多worker再不够再考虑异步化一步步来别上来就上RocketMQ、Celery那一套那是在给自己找活干。5. 常见问题与排查技巧实录5.1 训练阶段显存不足12G显存跑PP-YOLO tinybatch size设8虽然能跑但偶尔出现OOM。后来排查发现是数据加载阶段缓存了过多图片。解法是把use_shared_memory改成False同时把batch size降到4勉强稳定。其实还有个更省显存的办法开AMP混合精度训练PaddleDetection的配置里加上AMP: true就能启用显存占用几乎减半训练速度还能提升30%左右代价是精度略微下降对这类单类别检测来说可接受。5.2 推理结果出现很多重复框如果同一头猪被检测出多个高置信度框首先要怀疑的是NMS的IoU阈值设置得太宽松。PP-YOLO的NMS参数在配置文件里的NMS:部分默认IoU threshold是0.45如果重复框多可以调到0.35。另一个原因可能是数据标注时把同一头猪标重了有两帧图片里猪的位置贴得太近导致模型在某个位置倾向于输出两个框。排查方法很简单把检测结果可视化出来看框之间的重叠程度。5.3 CPU机器推理慢到无法接受部署机如果是老旧台式机CPU推理速度可能超过600ms。我试过的优化手段包括开启MKLDNN、设置线程数为8、把输入分辨率从416降到320。最后一个办法能显著提速但对小目标检测精度影响比较大需要自己权衡。另外在部署机上安装paddlepaddle时如果CPU指令集支持AVX512建议编译安装支持AVX512的版本推理速度能再涨一截不过这种优化普通人用不上直接用官网提供的CPU wheel包就够了。5.4 OpenCV中文路径读取失败Windows部署机上有一个很经典的问题如果图片路径带中文cv2.imread会返回None并打印一堆警告导致推理报错。解决办法是不要用cv2.imread直接读路径而是用np.fromfile读字节流再交给cv2.imdecode解码后端接口里也是同样的操作。这个坑在中文Windows环境下必踩提前规避能省很多时间。5.5 常见问题速查表现象可能原因解决办法训练loss不下降学习率过大或数据增强过强调低base_lr关闭MixUp模型导出后推理结果全为0忘了归一化或输入channels顺序不对检查预处理是否转CHW除以255.0CPU推理特别慢没开MKLDNN或线程数少开启config.enable_mkldnn()和set_cpu_math_library_num_threadsFlask接口第一次请求很慢模型懒加载或初始化耗时在服务启动时预热一次推理调用detect空图图片带中文路径报错cv2.imread不支持中文改用np.fromfile cv2.imdecode多worker内存爆炸gunicorn多个进程各自加载模型减少worker数量或用单worker多线程5.6 部署过程中的一些经验感悟做这个项目最深的感受是目标检测模型本身不是难点难的是把模型变成一条稳定的服务链路。数据质量直接决定了模型上限而工程细节决定了系统能不能真正用起来。比如图片传输的格式、接口的异常处理、模型的预热加载这些看起来不起眼的点在实际部署时往往会花掉一半以上的调试时间。再分享一个小技巧给Flask接口加一个简单的预热接口服务启动后自动用一张测试图跑一次推理把所有变量初始化和内存分配都完成这样真实请求进来时响应时间会稳定很多。具体就是在app.run之前调用一次detector.predict(np.zeros((416,416,3), dtypenp.uint8))反正就一次几毫秒的事能省掉用户感知到的“第一次请求特别慢”的尴尬。这个项目的源码和训练代码我都整理到本地仓库里了包括数据预处理脚本、PaddleDetection配置、Flask服务端、以及模型导出的说明文档。后续如果有时间我打算把这个服务扩展成支持视频文件的猪只统计顺便加入分栏区域计数的功能让养殖户对着监控画面就能算出每个栏位的存栏数。这类项目做下来最大的成就感不在于模型多少分而在于真的能帮用户解决一个具体问题。本文还有配套的精品资源点击获取
返回列表