先看效果, 推理时间不到30ms。
大多数目标检测,目标追踪网络模型部署在终端,用的Python,CUDA环境,只能参考流程,案例Python环境下实现的人流量计数,代码跑跑,效果如下 根据这个案例,了解到要实现人流量计数过程
先目标检测得到目标的位置,再通过跟踪算法根据目标的位置和图像特征,得到目标ID,再计算碰撞线,判断这个目标ID是上还是下。所以重点就是目标检测,目标跟踪。
方案一,NCNN+YOLOV5,推理时间270ms。
yolov5 ncnn ncnn-android-yolov5
ncnn是一个为手机端极致优化的高性能神经网络前向计算框架。之前做了一个挂机打怪助手,自制数据集LabelImg,训练得到pt,转onnx,转ncnn,推理时间是270ms,有兴趣的可以研究下。
方案二,寻找一个第三方部署的平台,只用替换训练模型就跑起来,简单上手。
Paddle-Lite-Demo 百度的,Paddle-Lite 提供了多个应用场景的 demo,并支持 Android、iOS 和 ArmLinux 三个平台。
PaddleDetection 基于飞桨PaddlePaddle的端到端目标检测套件,内置30+模型算法及300+预训练模型,覆盖目标检测、实例分割、跟踪、关键点检测等方向,其中包括服务器端和移动端高精度、轻量级产业级SOTA模型、冠军方案和学术前沿算法,并提供配置化的网络模块组件、十余种数据增强策略和损失函数等高阶优化支持和多种部署方案,在打通数据处理、模型开发、训练、压缩、部署全流程的基础上,提供丰富的案例及教程,加速算法产业落地应用。
基本了解后,可以开始了
1. 准备俯拍人头数据集
2. VOC数据集格式,还需要label_list.txt,train.txt,valid.txt,生成即可
import os
import random
dirpath = 'D://FY/AI/head_datas/annotations'
jpgPath = 'D://FY/AI/head_datas/images'
train_output_file = 'D://FY/AI/head_datas/train.txt'
val_output_file = 'D://FY/AI/head_datas/valid.txt'
ftrain= open ( train_output_file, "w" )
fval= open ( val_output_file, "w" )
for ( path, dirnames, filenames) in os. walk( dirpath) :
for filename in filenames:
if filename. endswith( '.xml' ) :
img_path = './images/' + filename[ : len ( filename) - 4 ] + ".jpg" + " ./annotations/" + filename+ "\n"
number = random. random( )
if number> 0.08 :
ftrain. write( img_path)
else :
fval. write( img_path)
3. 下载Paddle-Lite-Demo,PaddleDetection,例如我用的是
Paddle-Lite-Demo/object_detection/android/app/cxx/picodet_detection_demo
PaddleDetection/configs/picodet/legacy_model/picodet_s_320_coco.yml
如果使用不是Paddle-Lite-Demo中的模型结构, 需要使用Netron查看模型结构,对比原模型,如果输入输出不同,还需要修改对应的预处理和后处理函数。
4. 接下来的模型训练,评估,测试,导出可以参考PaddleDetection README
5. 运行安卓Demo,替换对应的模型db和Label。
6. 添加目标跟踪算法,计数。
参考于Multi-object tracking,包含了DeepSort,ByteTrack两种C++代码的目标跟踪算法,方便移植。 DeepSort还需要依赖onnxruntime,可以自行尝试。
我使用的是ByteTrack,代码移植到项目中app\src\main\cpp下,检测结构Object会和原来的冲突,替换下就行,我这里提取出来到一个头文件了,都引用这个这个。
#ifndef YOLO_DETECTION_DEMO_RESULT_H
#define YOLO_DETECTION_DEMO_RESULT_H
#include // NOLINT
#include // NOLINT
#include // NOLINT
#include // NOLINT
struct Object {
std::string class_name;
cv::Scalar fill_color;
float prob;
cv::Rect rec;
int class_id;
};
在已有的基础上添加跟踪,计数流程
void Pipeline::Process(Obj *obj, cv::Mat &rgbaImage, std::string savedImagePath) {
double preprocessTime = 0, predictTime = 0, postprocessTime = 0, byteTrackTime = 0, countTime = 0;
// Feed the image, run inference and parse the results
std::vector results;
detector_->Predict(rgbaImage, &results, &preprocessTime, &predictTime,
&postprocessTime);
//添加跟踪
std::vector output_stracks = detector_->bytetrack(&rgbaImage, results, &byteTrackTime);
//人流量计数
int upCount = 0, downCount = 0;
detector_->counttrack(&rgbaImage, results, &output_stracks, &countTime, &upCount, &downCount);
if (!savedImagePath.empty()) {
cv::Mat bgrImage;
cv::cvtColor(rgbaImage, bgrImage, cv::COLOR_RGBA2BGR);
imwrite(savedImagePath, bgrImage);
}
//运行时间和进出人数返回
obj->preprocessTime = preprocessTime;
obj->predictTime = predictTime;
obj->postprocessTime = postprocessTime;
obj->byteTrackTime = byteTrackTime;
obj->countTime = countTime;
obj->modified = true;
obj->enter = upCount;
obj->out = downCount;
}
跟踪方法
std::vector Detector::bytetrack(const cv::Mat *frame, const std::vector &results,
double *byteTrackTime) {
auto t = GetCurrentTime();
//LOGD("Detector -------------results size: %d ", objects.size());
std::vector output_stracks = bytetracker.update(results);
*byteTrackTime = GetElapsedTime(t);
// LOGD("Detector -------------output_stracks size: %d ", output_stracks.size());
for (unsigned long i = 0; i < output_stracks.size(); i++) {
std::vector tlwh = output_stracks[i].tlwh;
float tlwh0 = tlwh[0];
float tlwh1 = tlwh[1];
float tlwh2 = tlwh[2];
float tlwh3 = tlwh[3];
bool vertical = tlwh2 / tlwh3 > 1.6;
if (tlwh2 * tlwh3 > 20 && !vertical) {
}
cv::Scalar s = bytetracker.get_color(output_stracks[i].track_id);
cv::putText(*frame, cv::format("%d", output_stracks[i].track_id),
cv::Point(tlwh0, tlwh1 - 5),
0, 5, cv::Scalar(0, 0, 255), 2, cv::LINE_AA);
cv::rectangle(*frame, cv::Rect(tlwh0, tlwh1, tlwh2, tlwh3), s, 2);
}
return output_stracks;
}
计数方法
void Detector::counttrack(const cv::Mat *frame, const std::vector &results,
std::vector *output_strack, double *countTime, int *upCount,
int *downCount) {
auto t = GetCurrentTime();
bool hasPerson = false;
if (results.size() > 0)
hasPerson = true;
if (!hasPerson) {
upTrack.clear();
downTrack.clear();
} else {
//1080
float width = frame->cols;
//607
float height = frame->rows;
float lineY = height * 0.5;
//画上面的这个线
cv::line(*frame, cv::Point(0, lineY), cv::Point(width, lineY), (245, 245, 245), 1);
for (unsigned long i = 0; i < output_strack->size(); i++) {
STrack sTrack = output_strack->at(i);
std::vector tlwh = sTrack.tlwh;
//用下面的坐标值
float pY = tlwh[1] + tlwh[3];
//LOGD("Detector -------------%f",pY);
//在分界线的上面
if (pY < lineY) {
bool exist = false;
for (std::vector::iterator iter = downTrack.begin();
iter != downTrack.end(); iter++) {
if ((*iter) == sTrack.track_id) {
exist = true;
LOGD("Detector -------------存在up++");
(*upCount)++;
downTrack.erase(iter);
break;
}
}
//如果不存在
if (!exist) {
//如果没有找到
if (std::find(upTrack.begin(), upTrack.end(), sTrack.track_id) ==
upTrack.end()) {
upTrack.push_back(sTrack.track_id);
}
}
}
//在分界线的下面
else if (pY > lineY) {
bool exist = false;
for (std::vector::iterator iter = upTrack.begin();
iter != upTrack.end(); iter++) {
if ((*iter) == sTrack.track_id) {
exist = true;
LOGD("Detector -------------存在down++");
(*downCount)++;
upTrack.erase(iter);
break;
}
}
//如果不存在
if (!exist) {
//如果没有找到
if (std::find(downTrack.begin(), downTrack.end(), sTrack.track_id) ==
downTrack.end()) {
downTrack.push_back(sTrack.track_id);
}
}
}
}
}
*countTime = GetElapsedTime(t);
}
总结
凡事都试一试 。
还有其他的一些移动端x86/arm 部署的平台,可以自行研究。 TensorflowLite 腾讯TNN 小米Mace 阿里MNN
计算机视觉学习视频,极力推荐北京邮电大学鲁鹏讲的 计算机视觉与深度学习