简介
现实生活中有这样的场景,摄像头并不能覆盖一个区域的所有地方,现在想不借助其他手段,仅通过摄像头拍摄到的图像,计算一个区域中的行人数目。
首先不能使用人群计数的方法,因为有些有人的地方没有被摄像头覆盖。人群计数或者人头计数只能计算拍摄到的人数。
该项目使用的策略是:
- 仅处理在入口处摄像头拍摄的画面
- 因为摄像头是固定的,由人为划定入口位置,一条直线。
- 行人检测用来检测行人位置。
- 行人重识别用来判断入口内的行人是否被加入到数据库中,如果有则用来刷新信息,如果没有则添加,同时计数加1;同时判断入口外的行人,是否在数据库中,如果有,则从数据库中删除,计数减1。
项目使用当前sota的EANet作为ReID模型,使用YoloV3作为行人检测网络。
项目部分代码展示
YoloV3使用tensorflow所支持的pb模型,然后使用TF C++ API调用,EANet使用LibTorch调用pth文件。
Yolov3模型类定义
class YoloV3 : public Model_Loader_Base {
public:
YoloV3() {};
bool predict(vector<BBoxes>&);
std::string output_node = "post_processing/result";
Tensor t_in = Tensor(DT_FLOAT, TensorShape({ 1,544,544,3 }));
void preProcessing(const Mat &ori);
void drawBBoxes(Mat&, vector<BBoxes>&);
vector<std::pair<string, Tensor> > inputs_for_yolo;
};
EANet模型类定义
class EANet {
public:
EANet(string path) { model_path = path; calDistacneModel = new CalCosDistance("calculateCosHead.pt");
calDistacneModel->InitModel();
};
...
...
void preprocessing(const Mat &img);
void cropPerson(const Mat &, const vector<TfModel::BBoxes> &bboxes, vector<Mat> &personContainer);
at::Tensor predict();
vector<int> countPersonNumber(vector<Mat> &query);
void calFunction(Point p1, Point p2);
bool calDirection( Point center, int);
vector<float> lineFunction;
vector<int> countPersonNumberByRegion(vector<Mat> &query, Point &p1, Point &p2, int direction, vector<TfModel::BBoxes> &bboxes);
void drawInfo(Mat &frame, vector<int> &recorder, vector<TfModel::BBoxes> bboxes);
vector<int> countPersonNumberByRegionV2(vector<Mat> &query, Point &p1, Point &p2, int direction, vector<TfModel::BBoxes> &bboxes);
int delPersonByRegion(const Mat &query);
void writeGalleryForEachFrame();
};
我省略了一些变量,仅仅展示几个比较关键的函数功能。
-
经过行人检测得到行人位置,如何得知行人是在入口内还是入口外?
用户指定一个直线(非平行)作为入口,同时还需给出正方向的位置。
void calFunction(Point p1, Point p2) 函数计算两个点代表的直线方程。 行人的位置用点表示(点可取在中点偏下位置)。该点带入方程,如果点在直线上方,计算得到正值,如果点在直线下方,计算得到负值。用户指定正值还是负值为正方向,正方向即点在门内。以此得知行人和门的关系。
其中 bool calDirection( Point center, int)就是用来判断行人和门的关系。 -
行人信息如何保存
行人一旦进入入口,EANet会通过余弦相似度判断该人和数据库中其他人的相似度,从而得到该人是否本来就在门内。如果不是则计数加一,同时把该人这一帧的形象以及对应的特征向量(经过EANet)保存下来(内存中)。 -
行人首次被保存的形象也许不够表达这个人的全面信息
我们不仅仅保存行人出现的第一个形象,连续6帧保存他的形象。用一个长度为6的队列,保存距离当前帧前的6帧的形象。这6个形象的特征向量会按照比例加权,得到更加能代表行人的特征向量。
//如果 min_index不为-1,则说明当前query和gallery中有一个目标相同,且min_index指向距离最小的gallery。
if (min_index != -1) {
count.push_back(0);
// 如果当前的query和某一个gallery的距离小于0.1,非常确信这是同一个人,则把query添加进gallery
if (galleryVectorMap[min_index].size() < 6 && min_distance < 0.12)
{
galleryVectorMap[min_index].push_back(queryVector);
galleryMap[min_index].push_back(query[i]);
}
else if(galleryVectorMap[min_index].size() == 6 && min_distance < 0.12){
// 保持每一个人的gallery最多有6张。
galleryVectorMap[min_index].push_back(queryVector);
galleryVectorMap[min_index].pop_front();
galleryMap[min_index].push_back(query[i]);
galleryMap[min_index].pop_front();
}
else if(galleryVectorMap[min_index].size() > 6){
std::cerr << "max size of gallery vector container is 6" << endl;
exit(-1);
}
}
//如果 min_index为-1,则说明当前query和gallery中的任何一个目标都不同,则把这个query加入gallery中。
else if (min_index == -1)
{
int new_index = galleryMap.size();
assert(new_index == galleryVectorMap.size());
if (priority_index.empty()) {
// 空
}
else {
new_index = priority_index.front();
priority_index.pop();
}
count.push_back(1);
galleryMap[new_index].push_back(query[i]);
galleryVectorMap[new_index].push_back(queryVector);
}
结果展示
该视频场景非实际应用场景。黄色的线代表入口位置。绿色框代表在入口外。红色代表这个人已经被计数,在数据库中保存。蓝色代表目标第一次进门内。
随着目标渐变移动,他的形象会和之前的不一样,所以我们采用连续6帧保存目标形象来获得更加可靠的目标特征向量。