公司动态

基于PaddlePaddle与Flask的猪只自动计数系统实现

📅 2026/8/30 4:22:39
基于PaddlePaddle与Flask的猪只自动计数系统实现
简介本资源是一套基于PaddlePaddle与Flask构建的猪只智能识别与自动计数系统面向农业AI初学者、计算机视觉实践者及智慧养殖技术开发者解决养殖场人工清点效率低、误差大等实际问题。压缩包共29个文件含5个核心Python脚本app.py、infer.py、visualize.py等、10张标注测试图像、2个模型文件.pdmodel/.pdiparams、1个Dockerfile及配套配置文件yml、txt、requirements.txt整体34.17MB结构清晰覆盖模型推理、Web服务封装与容器化部署全流程。已有64人学习下载资源提供完整可运行代码、预训练PP-YOLO模型、详细Postman调用说明及Docker同步路径规范特别适合希望快速上手目标检测落地、理解CV模型服务化封装与轻量级部署的开发者。 搞养殖的朋友应该都懂数猪这件事看着简单真做起来全是泪。几百上千头猪挤在一起人工点数慢、容易错猪还一刻不停在动同一栏数两遍结果对不上是常态。我接过一个生猪养殖场的智能化改造项目客户提的第一个需求就是能不能用摄像头自动数猪。当时我选了PaddlePaddle做目标检测模型训练再用Flask把推理能力包成Web服务最终落地了一套上传图片或视频帧就能实时返回猪只数量、位置框和置信度的系统。模型、源码、部署脚本现在都整理好了这篇文章就把整套实现过程拆开讲一遍包括数据怎么标、模型怎么训、接口怎么写以及那些文档里不会写的坑给正准备做农业视觉项目的朋友做个参考。1. 项目概述养殖场里的数猪难题1.1 为什么智能数猪是个真需求养猪场的盘点需求比想象中频繁得多。母猪产仔后要统计产仔数育肥猪转栏、出栏时要核对数量保险公司理赔时也要提供存栏数。传统做法是饲养员人眼点数几千头猪数下来至少半小时碰到猪舍光线差、猪群密度大漏数错数基本无法避免。更麻烦的是人工计数会惊动猪群猪一应激就跑动越数越乱。用摄像头配合目标检测算法来做识别计数能同时解决效率和准确率两个问题。模型对单张图片做检测每头猪输出一个检测框数量直接由检测框数目决定。整个过程不需要接触猪群架在猪舍顶部的摄像头固定视角猪正常活动就能完成统计。这套方案还能用在无人机巡场、运输车辆装卸计数、以及生猪保险核验等场景里本质上都是同一套检测能力在不同拍摄条件下的适配。1.2 技术方案选型为什么是PaddleFlask选PaddlePaddle而不是PyTorch或TensorFlow主要基于三个考虑。第一PaddleDetection仓库里集成了大量训练好的检测模型和配置文件PP-YOLO、PicoDet这些模型在精度和速度上都不错省去了从零搭建训练流程的功夫。第二Paddle Inference的部署链路非常成熟训练好的模型可以导出成推理格式在CPU和GPU上跑起来都很快而且对第三方依赖很少放到Flask服务里很干净。第三Paddle提供了完善的移动端和嵌入式部署方案Paddle Lite后续如果要把模型跑到边缘设备上不需要换框架。Flask的选择就很简单了。这个项目需要的是轻量HTTP服务把图片上传、推理、结果返回这几个接口暴露出来不需要复杂的用户体系、数据库和消息队列。Flask用几十行代码就能起一个服务配合模板引擎还能顺带做个简单的Web演示页面对中小型养殖场做私有化部署特别合适。相比FastAPIFlask的生态更成熟各种部署文档也多团队上手成本低。这里要说明一下整套方案里Paddle负责识别Flask负责对外服务是典型的两层模型部署架构模型层关注检测能力本身服务层关注接口可用性和并发处理。两者通过Python对象直接调用中间不需要再引入消息中间件单机部署足够应对单路摄像头的实时计数需求。2. 整体架构设计与识别原理2.1 系统整体架构整个系统从数据流方向来看分为三个模块数据接入、模型推理、结果服务。数据接入层负责接收图片、视频帧或者摄像头RTSP流。图片直接从HTTP请求里拿视频流则通过OpenCV逐帧读取。模型推理层是核心里面跑着一个已导出的Paddle Inference检测模型输入预处理后的图像张量输出检测框、类别和置信度。结果服务层由Flask负责把推理结果组装成JSON返回给前端同时把带框标注的图像编码成Base64字符串回传方便前端直接展示。模块之间用Python类做了隔离。推理部分封装成PigDetector类只暴露一个predict(image)方法Flask路由里不写任何模型逻辑。这样后续想换模型、加日志、做缓存都只改推理层不影响接口层。项目目录结构大概是这样pig_counting/ ├── app.py # Flask主程序 ├── inference/ │ ├── __init__.py │ ├── detector.py # Paddle Inference封装 │ └── counter.py # 计数与去重逻辑 ├── templates/ │ └── index.html # 演示页面 ├── static/ │ └── css/ js/ # 前端资源 ├── models/ │ ├── inference.pdmodel │ ├── inference.pdiparams │ └── infer_cfg.yml └── requirements.txt2.2 目标检测模型的选型逻辑目标检测模型选择决定了系统的精度上限和推理速度。PaddleDetection里可选的主流模型有PP-YOLO系列、PicoDet系列和YOLOv3系列我实际测试对比过三个方案模型骨干网络输入尺寸推理耗时(GPU)mAP(自测集)适用场景PP-YOLOResNet50-vd608×608约18ms0.91精度优先服务器部署PP-YOLO-TinyMobileNetV3320×320约6ms0.84速度优先边缘设备PicoDet-SLCNet320×320约8ms0.87均衡方案CPU可跑从实测结果看PP-YOLO在猪只密集、互相遮挡的场景下漏检率最低所以最终上线用了PP-YOLO加上ResNet50-vd骨干网络。但要注意输入尺寸越大越吃显存如果显卡只有4GB显存608×608的输入配合默认batch size会直接OOM这种情况要么换小模型要么把输入降成416×416。这里还有一个容易被忽略的点猪舍照片和COCO数据集里的自然场景差异很大。猪舍光照偏暗、地面和猪体颜色接近、猪群密集重叠这些都会明显影响检测效果。所以即使PP-YOLO在COCO上mAP很高也必须用自采的猪舍数据做微调finetune不能直接拿官方权重上线。2.3 计数核心思路检测框到数量的映射模型输出的原始结果是若干组[x1, y1, x2, y2, score, class_id]分别表示检测框左上角坐标、右下角坐标、置信度和类别。计数逻辑看起来非常简单统计满足置信度阈值的检测框数量就是猪的数量。实际落地时有三个细节必须处理。第一是置信度阈值的选择设得太低会把料槽、饮水器误检成猪设得太高会漏掉遮挡严重的个体。我在自测集上做了阈值扫描实验0.45到0.55之间F1值最高最终线上取0.5。第二是NMS非极大值抑制的处理训练时模型已经带了NMS后处理如果导出模型时保留了后处理拿到的检测框就是去重后的如果导出的是裸模型就需要在推理代码里手动做NMS否则同一头猪可能输出多个重叠框导致数量虚高。第三是遮挡场景的漏检补偿当猪群密度特别大时一头猪的身体大部分被旁边猪挡住检测框可能只框住露出来的头部甚至检测不到。这种情况单靠检测框计数会有系统性偏低后面我会讲几种补偿策略。3. 模型训练全流程从数据到推理模型3.1 环境准备训练环境我建议直接用conda管理避免Python包互相污染。下面是完整的环境搭建步骤依赖版本都是验证过的conda create -n pigcount python3.8 conda activate pigcount # 安装PaddlePaddle GPU版CUDA 11.2对应2.5.x版本 python -m pip install paddlepaddle-gpu2.5.2.post112 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html # 克隆PaddleDetection仓库 git clone https://github.com/PaddlePaddle/PaddleDetection.git cd PaddleDetection pip install -r requirements.txt # 安装pycocotools等辅助库 pip install pycocotools这里有几个容易踩的坑。第一PaddlePaddle的GPU版本必须和本机CUDA、cuDNN版本严格对应装错版本会出现libcudart.so: cannot open shared object file之类的报错。装之前先执行nvidia-smi确认驱动支持的CUDA版本再执行nvcc -V看本机CUDA版本。第二PaddleDetection对Python版本有要求3.8是兼容性最好的3.10在部分老版本上可能有依赖兼容问题。第三如果显卡显存小于6GB建议直接装CPU版PaddlePaddle先把流程跑通GPU版留到正式训练再去解决。3.2 数据采集与标注数据质量直接决定模型上限。我前后采集了约1800张猪舍图片覆盖了白天、傍晚、夜间补光灯三种光照条件拍摄角度包括猪舍顶部俯拍、45度斜拍和栏外平视图片分辨率统一在1280×720以上。数据采集时注意不要只在同一个栏舍拍不同栏舍的栏杆颜色、地面材质、墙面反光都会不一样模型如果没见过这些变化换一个环境就掉点。标注工具用的X-AnyLabeling这是我个人比较推荐的开源标注工具支持PaddleDetection的COCO格式导出。标注时所有类别统一为pig单类别不需要区分母猪和仔猪因为计数功能不关心猪的类别。标注原则是只要人能看清是猪就框出来严重遮挡导致无法判断边界的框住可见部分像素小于10×10的极小目标不标标了反而给模型引入噪声。标注完成后把数据划分成训练集、验证集和测试集比例建议7:2:1。注意划分时按栏舍分不要把同一栏舍的图片既放训练集又放验证集否则验证集和训练集高度相似评估出来的mAP会虚高上线后泛化能力被高估。3.3 训练配置与执行PaddleDetection的训练通过修改配置文件来完成。我直接在configs/ppyolo/ppyolo_r50vd_dcn_1x_coco.yml基础上改出一个自定义配置主要改动是类别数、数据集路径和数据增强# configs/pig_ppyolo.yml epoch: 200 LearningRate: base_lr: 0.01 schedulers: - !PiecewiseDecay gamma: 0.1 milestones: [120, 160] - !LinearWarmup start_factor: 0.1 steps: 1000 OptimizerBuilder: optimizer: type: Momentum momentum: 0.9 regularizer: type: L2 factor: 0.0001 architecture: PPYOLO use_gpu: true max_iters: 35000 snapshot_epoch: 10 weights: output/pig_ppyolo/best_model启动训练和评估的命令如下export CUDA_VISIBLE_DEVICES0 python tools/train.py -c configs/pig_ppyolo.yml python tools/eval.py -c configs/pig_ppyolo.yml \ -o weightsoutput/pig_ppyolo/best_model.pdparams训练过程中有两个指标要盯loss曲线和验证集mAP。如果训练集loss下降但验证集mAP长期不动说明过拟合需要加大数据增强、增加数据量或加正则。PP-YOLO配置里默认带MixUp、Mosaic增强这两个增强对猪群密集遮挡场景效果非常明显建议保留。我这边最终训练到约180个epoch收敛验证集mAP达到0.91。3.4 模型评估与导出模型评估别只看mAP还要按实际场景看漏检率和误检率。我单独写了一个评估脚本把验证集图片逐张过一遍模型统计三种错误类型漏检有猪未检出、重复检一头猪输出多个框、误检非猪目标被当成猪。重复检通常在NMS阈值设置不合理时出现误检多发生在料槽、水嘴、墙上有污渍的区域。确认效果满意后把训练好的模型导出为Paddle Inference格式python tools/export_model.py \ -c configs/pig_ppyolo.yml \ -o weightsoutput/pig_ppyolo/best_model.pdparams \ --output_dirdeploy/pig_model导出完成后deploy/pig_model目录下会生成三个文件inference.pdmodel模型结构、inference.pdiparams模型参数、infer_cfg.yml预处理和后处理配置。这个目录就是Flask服务要加载的模型目录可以直接拷贝到部署机器上。4. Flask后端与推理服务实现4.1 项目结构与接口设计Flask服务端提供三个核心接口接口方法入参返回/GET无演示页面HTML/api/countPOST图片文件 (multipart/form-data)JSON数量、检测框列表、标注图Base64/api/healthGET无服务状态JSON接口设计上做了两个刻意取舍。第一返回的检测框坐标全部映射回原始图像坐标系前端拿到后直接画框不需要知道模型输入尺寸避免前端和后端耦合。第二标注图以Base64字符串返回而不是单独开一个静态文件接口这样单机部署时不需要额外处理临时文件清理客户端直接img srcdata:image/jpeg;base64,...就能显示。4.2 推理模块封装Paddle Inference实战推理模块是整个服务里最重要的一段代码封装了模型加载、预处理、推理、后处理四个环节。直接看代码import numpy as np import paddle.inference as paddle_infer class PigDetector: def __init__(self, model_dir, devicegpu, threshold0.5): self.threshold threshold config paddle_infer.Config( f{model_dir}/inference.pdmodel, f{model_dir}/inference.pdiparams, ) if device gpu: config.enable_use_gpu(500, 0) else: config.disable_gpu() config.set_cpu_math_library_num_threads(4) config.enable_memory_optim() self.predictor paddle_infer.create_predictor(config) self.input_names self.predictor.get_input_names() self.output_names self.predictor.get_output_names() def preprocess(self, image, target_size608): 缩放、归一化、CHW转换 import cv2 img cv2.cvtColor(image, cv2.COLOR_BGR2RGB) h, w img.shape[:2] scale min(target_size / h, target_size / w) new_h, new_w int(h * scale), int(w * scale) resized cv2.resize(img, (new_w, new_h)) canvas np.full((target_size, target_size, 3), 114, dtypenp.float32) canvas[:new_h, :new_w] resized # 转CHW并归一化到[0,1] tensor canvas.transpose(2, 0, 1).astype(np.float32) / 255.0 tensor np.expand_dims(tensor, axis0).copy() return tensor, scale def predict(self, image): 返回 [(x1, y1, x2, y2, score), ...] tensor, scale self.preprocess(image) input_handle self.predictor.get_input_handle(self.input_names[0]) input_handle.copy_from_cpu(tensor) self.predictor.run() # PaddleDetection导出模型一般输出3个数组: # bboxes[N,6], 其中每行是[x1,y1,x2,y2,score,class_id] output_handle self.predictor.get_output_handle(self.output_names[0]) results output_handle.copy_to_cpu() boxes [] for item in results: score float(item[4]) if score self.threshold: continue # 把缩放到608的坐标还原回原图坐标 x1, y1, x2, y2 map(float, item[:4]) boxes.append((x1 / scale, y1 / scale, x2 / scale, y2 / scale, score)) return boxes这段代码里有三个细节值得展开讲。预处理里的letterbox保持宽高比的缩放填充非常关键。如果把图片直接resize成608×608猪会被横向或纵向拉伸变形模型检测精度明显下降。letterbox的做法是先按比例缩放图片剩余区域用灰色填充保证送入模型的图片内容不变形。坐标还原要乘以缩放比例的倒数。这个坑我一开始就踩过忘记还原坐标前端画框全部偏到左上角。因为模型输出的是608×608坐标系里的坐标而原图是1920×1080不换算画出来的框肯定是错的。单例模式的加载方式值得坚持。PigDetector在Flask应用启动时初始化一次反复使用同一个Predictor实例而不是每次请求都重新加载模型。重新加载模型一次要几百毫秒而一次推理只要十几毫秒如果放在接口里做性能直接被打崩。4.3 图片上传处理接口Flask路由与JSON返回Flask路由部分逻辑不复杂但有一个并发问题必须处理。直接看代码import base64 import io import cv2 import numpy as np from flask import Flask, render_template, request, jsonify from inference.detector import PigDetector from inference.counter import count_pigs app Flask(__name__) detector PigDetector(models/pig_model, devicegpu) def draw_boxes(image, boxes): 在图像上绘制检测框 for (x1, y1, x2, y2, score) in boxes: cv2.rectangle(image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(image, f{score:.2f}, (int(x1), int(y1) - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) return image app.route(/api/count, methods[POST]) def api_count(): if file not in request.files: return jsonify({code: 400, message: 缺少file字段}), 400 file request.files[file] 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({code: 400, message: 图片解码失败}), 400 boxes detector.predict(image) result count_pigs(boxes) annotated draw_boxes(image.copy(), boxes) _, encoded cv2.imencode(.jpg, annotated, [cv2.IMWRITE_JPEG_QUALITY, 90]) img_b64 base64.b64encode(encoded.tobytes()).decode(utf-8) return jsonify({ code: 0, count: result[total], boxes: [{x1: b[0], y1: b[1], x2: b[2], y2: b[3], score: b[4]} for b in boxes], annotated_image: img_b64, }) app.route(/api/health, methods[GET]) def health(): return jsonify({status: ok}) if __name__ __main__: app.run(host0.0.0.0, port8080, threadedTrue)并发问题在于Paddle的Predictor实例并不是线程安全的。Flask默认开发服务器在多线程模式下会同时处理多个请求如果多个请求同时调用同一个Predictor执行推理可能报错或得到错乱结果。解决方式有两种一是给推理加线程锁每次推理前获取锁缺点是并发能力会打折扣二是给每个线程创建独立的Predictor实例用threading.local做线程隔离。实际部署我建议第一种方案因为推理本身只要十几毫秒加锁后单机也能稳定支撑20路左右的并发请求养殖场场景完全够用。4.4 视频帧与连续帧去重计数图片计数只能解决静态场景需求真正上线时客户往往还要数视频里走动的猪。这里面的核心问题不是识别而是去重。同一头猪连续出现在多个视频帧里如果每帧都数一次会把同一头猪重复计算。我采用的方案是简单的目标匹配去重法把每一帧的检测框和上一帧的检测框做IoU匹配如果当前帧某个检测框与上一帧某个检测框的IoU大于0.4就认为是同一头猪不重复计数只有当检测框与上一帧所有框的IoU都小于0.4时才认为出现了一头新猪。这个方案适用于摄像头固定、猪缓慢运动的场景实测下来误差率在5%以内。如果要实现更精确的跨帧跟踪可以引入SORT或DeepSORT算法给每头猪分配唯一ID。但说实话对于数猪这个需求SORT的卡尔曼滤波在猪相互遮挡时容易出现ID切换反而导致计数错误简单IoU匹配在这个场景下更稳定。如果猪的移动速度快、遮挡频繁那更应该考虑的是计数线方式检测框中心点跨过预设线才计数这是工业上比较成熟的流量统计方案。5. 前端页面与交互实现5.1 上传与展示页面演示页面用Flask的模板引擎渲染逻辑非常简单但足够直观。页面上放了一个文件选择控件和一个开始识别按钮用户选择图片后前端立即预览原图点击按钮后通过fetch把图片POST到/api/count接口。收到响应后把返回的图片Base64放到img标签里显示同时把数量显示在页面顶部。!DOCTYPE html html langzh head meta charsetUTF-8 title猪只智能识别计数系统/title /head body h1猪只智能识别计数系统/h1 input typefile idfileInput acceptimage/* button idbtnCount开始识别/button div识别结果span idcountResult-/span 头/div div p原图/p img idsrcImg stylemax-width: 480px; p识别结果/p img idresImg stylemax-width: 480px; /div script const fileInput document.getElementById(fileInput); const btnCount document.getElementById(btnCount); const countResult document.getElementById(countResult); const srcImg document.getElementById(srcImg); const resImg document.getElementById(resImg); fileInput.addEventListener(change, function () { const file fileInput.files[0]; if (!file) return; srcImg.src URL.createObjectURL(file); }); btnCount.addEventListener(click, async function () { const file fileInput.files[0]; if (!file) return; const formData new FormData(); formData.append(file, file); const resp await fetch(/api/count, { method: POST, body: formData }); const data await resp.json(); if (data.code 0) { countResult.textContent data.count; resImg.src data:image/jpeg;base64, data.annotated_image; } else { alert(data.message); } }); /script /body /html5.2 结果展示与API联调细节联调时要注意Base64图片在数据量大的时候会占很多内存。一张1920×1080的标注图JPEG编码后大概200KB到500KBBase64编码后体积再膨胀约1.3倍。如果一次检测多张图片响应体可能达到几MB。局域网内问题不大但如果通过公网访问会明显变慢可以考虑把标注图接口独立出来用静态文件URL替代Base64回传。另外建议在页面上加一个置信度显示区域把每头猪的检测分数列出来。这个功能看起来不起眼但对调试非常有用。当客户反馈数少了的时候你能快速看到是不是某头猪的置信度低于阈值被过滤掉了而不是只能在后台翻日志。6. 常见问题与排查技巧实录6.1 典型问题与解决方案整理一下我在这个项目里遇到的高频问题做成速查表按出现概率排序问题现象可能原因解决方案漏检严重猪群密集区数不到输入分辨率低、遮挡严重提高输入尺寸到608或768检查是否开启Mosaic增强把料槽、饮水器识别成猪训练数据缺少负样本采集包含料槽、水嘴、工具的图片并标注为ignored或加入背景图同一头猪出现两个框NMS后处理被跳过或阈值过高确认导出模型时NMS配置或手动调用PaddleDetection的后处理逻辑GPU显存溢出输入过大或batch size过大降输入尺寸为416关闭enable_memory_optim等优化推理结果线程错乱Flask多线程同时调用Predictor加线程锁或使用threading.local做实例隔离模型加载慢首次请求卡顿Flask启动时才加载模型在app.run之前初始化PigDetector或加一个预热路由图像颜色偏蓝绿OpenCV BGR与RGB通道顺序问题预处理时先做cv2.COLOR_BGR2RGB部署机器无CUDA环境目标机器没有GPU使用CPU版PaddleInference换PicoDet-S小模型推理耗时可控制在50ms内6.2 经验心得与避坑指南最后分享几条这个项目里真正花时间摸索出来的经验。数据标注的粒度比想象中重要。我一开始只标能完整看到身体轮廓的猪漏掉了很多被遮挡的个体结果模型在密集场景下漏检率特别高。后来把标准改成凡是能确认是猪的目标都要标哪怕只露出一个头也要用框精确框住头部。这个改动让密集场景的召回率提升了差不多10个百分点。遮挡目标的边界不好画但模型恰恰需要这些难例来学习局部特征。置信度阈值不要死磕0.5。不同场景最优阈值不一样夜间补光场景整体置信度会比白天低0.1左右如果统一用0.5会漏检。建议把阈值做成配置项按白天、夜间分别调参。我是在服务端加了一个可选参数threshold前端页面放一个滑块现场调试时直接拖滑块看效果非常实用。如果目标机器没有GPU不要直接上PP-YOLO。我之前在一台4核CPU服务器上跑推理一张608图片要将近1秒根本没法用。换成PicoDet-S后降到80毫秒左右虽然精度掉了几个点但满足了实时需求。农业场景的算力条件往往比互联网公司差很多模型选型要提前确认部署环境而不是等上线才发现跑不动。模型更新是另一个容易被忽视的运维问题。猪舍结构翻新、更换地面材质、摄像头位置调整都会导致模型效果下降。建议在服务里留一个模型版本号字段每次迭代后记录训练数据分布和mAP变化现场反馈效果变差时能快速定位是环境变了还是模型回退了。这套系统的扩展空间其实比想象中大。当前做的是单帧图片检测计数只要把视频帧去重逻辑换成跟踪算法就能扩展到猪只行为分析比如统计某头猪的进食时间把检测框换成关键点检测还能做猪的体长、体重估测。基础能力搭好之后具体的业务场景只是往上加模块的事。本文还有配套的精品资源点击获取