实战:用PaddleDetection与Flask构建猪只计数系统
2026/8/30 8:26:48 网站建设 项目流程

简介:本资源是一套面向农业智能化场景的猪只目标检测与自动计数系统实现方案,适用于具备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上的推理耗时约25ms,CPU上约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.2,PaddlePaddle版本是2.4.2,PaddleDetection是release/2.5分支,Flask版本是2.2.2。

安装命令记录一下,方便后面复现:

# 创建虚拟环境 conda create -n pig_count python=3.8 -y conda activate pig_count # 安装PaddlePaddle GPU版(CUDA 11.2) python -m pip install paddlepaddle-gpu==2.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 flask==2.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_DEVICES=0 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改成false,loss稳定了不少。

断点续训也是一个高频操作。训练中断后,直接加-r output/ppyolo_tiny_650e_coco/100参数,会加载第100轮的权重继续跑。这个功能很实用,尤其是训练集比较大的时候,中断重启不用从头开始。

3.3 模型评估与验证

训练完成后,在测试集上做一次完整的评估:

python tools/eval.py -c configs/ppyolo/ppyolo_tiny_650e_coco.yml -o weights=output/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 weights=output/ppyolo_tiny_650e_coco/best_model.pdparams

导出后在output_inference目录下会生成三个文件:model、__params__和infer_cfg.yml。这三个文件构成了一个完整的静态推理模型,可以直接被PaddleInference加载。这里有一个容易踩的坑,export_model.py导出后的模型,输入格式是NCHW的float32张量,输入尺寸固定为训练时的尺寸(我这里用的是416x416),所以推理前必须把图片resize到416x416,否则会报维度不匹配的错误。

另外,导出时加一个--save_txt=true参数,可以顺便把每个类别的标签名写到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.txt

requirements.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_threshold=0.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(host="0.0.0.0", port=5000, threaded=True)

这里有几个设计细节值得说说。第一,图片用二进制流上传,不要用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默认的threaded=True模式跑着,瓶颈出现了再加gunicorn多worker,再不够再考虑异步化,一步步来,别上来就上RocketMQ、Celery那一套,那是在给自己找活干。

5. 常见问题与排查技巧实录

5.1 训练阶段显存不足

12G显存跑PP-YOLO tiny,batch 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.0
CPU推理特别慢没开MKLDNN或线程数少开启config.enable_mkldnn()和set_cpu_math_library_num_threads
Flask接口第一次请求很慢模型懒加载或初始化耗时在服务启动时预热一次推理,调用detect空图
图片带中文路径报错cv2.imread不支持中文改用np.fromfile + cv2.imdecode
多worker内存爆炸gunicorn多个进程各自加载模型减少worker数量,或用单worker多线程

5.6 部署过程中的一些经验感悟

做这个项目最深的感受是,目标检测模型本身不是难点,难的是把模型变成一条稳定的服务链路。数据质量直接决定了模型上限,而工程细节决定了系统能不能真正用起来。比如图片传输的格式、接口的异常处理、模型的预热加载,这些看起来不起眼的点,在实际部署时往往会花掉一半以上的调试时间。

再分享一个小技巧:给Flask接口加一个简单的预热接口,服务启动后自动用一张测试图跑一次推理,把所有变量初始化和内存分配都完成,这样真实请求进来时响应时间会稳定很多。具体就是在app.run之前,调用一次detector.predict(np.zeros((416,416,3), dtype=np.uint8)),反正就一次,几毫秒的事,能省掉用户感知到的“第一次请求特别慢”的尴尬。

这个项目的源码和训练代码我都整理到本地仓库里了,包括数据预处理脚本、PaddleDetection配置、Flask服务端、以及模型导出的说明文档。后续如果有时间,我打算把这个服务扩展成支持视频文件的猪只统计,顺便加入分栏区域计数的功能,让养殖户对着监控画面就能算出每个栏位的存栏数。这类项目做下来,最大的成就感不在于模型多少分,而在于真的能帮用户解决一个具体问题。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询