复杂动背景下的“低小慢”目标检测技术,论文阅读和复现代码
本文的目的是要检测低空中的慢速目标,我复现了本文算法(修正了一些模块),下面给出复现的细节。
- 视觉显著性模型
视觉显著性反应了人眼感知系统对视觉信号灯响应。原文给出的模型描述存在问题
正确的模型如下:
S
(
x
,
y
)
=
∑
i
=
0
255
f
i
C
(
I
(
x
,
y
)
,
i
)
S(x, y) = \sum_{i=0}^{255} f_i C(I(x, y), i)
S(x,y)=i=0∑255fiC(I(x,y),i)
其中,
I
(
x
,
y
)
I(x, y)
I(x,y)表示点x, y处的像素点值,
f
f
f表示灰度直方图,
f
i
f_i
fi表示灰度等级i点频数。C(.,.)表示距离度量,一般地
C
(
a
,
b
)
=
∣
a
−
b
∣
C(a, b) = |a - b|
C(a,b)=∣a−b∣
S ( x , y ) S(x, y) S(x,y)的取值范围超出了255,因此要将其极差归一化到[0, 255]区间。
函数原型:cv::Mat computeSaliencyMap(cv::Mat& image);
- 形态学梯度
形态学梯度可以获取目标的边缘轮廓信息,本质上是膨胀结果减去腐蚀结果,函数原型:cv::Mat computeGrad(cv::Mat& image);
- 三帧差分
论文里面三帧差分是差分后的二值图,原理可参考原文。函数原型:cv::Mat computeDiff3(cv::Mat& image0, cv::Mat& image1, cv::Mat& image2);
- 区域生长(没有用到)
论文里面对区域生长这块的描述不清晰,特别是种子如何选取的问题,原文是没有描述的。我复现了经典的区域生长算法。发现效果不理想,分析原因是:三帧差分的值会在目标的边缘外部是255。
意味着如果将这些点当做是种子点,那么目标边缘外部会向四周生长,如果背景比较类似的话,背景全部会生长;另外,由于三帧差分的值在物体内部也极有可能有255。所以,大概率区域生长的结果是全图变白。
尝试改进方案1:腐蚀+膨胀+连通结构计算,得出目标质心,将质心当成种子迭代生长。
这种方法存在偶然性,因为种子较少,如果种子所在的像素点恰好和周围的像素点差别很大,那么将完全无法生长。
所以我的原始实现没有采用区域生长算法,而是做了一些改进。不搞那些虚的,直接针对三帧差分的结果腐蚀+膨胀+计算连通结构,连通结构即为检测到的目标。
函数原型:vetor<cv::Rect> getObjRects(cv::Mat srcImage, cv::Mat mask);
区域生长算法的函数原型:cv::Mat alg_utils::regionGrow(cv::Mat srcImage, cv::Point pt, int ch1Thres,int ch2Thres, int ch3Thres);
- 合并距离比较近近的矩形框
在前面生成的结果中,可能存在着一些零散的小矩形框挨的比较近,一个合理的办法是将这些矩形框合并。
函数原型: vector<cv::Rect> combineNearRects();
下面给出完整代码
// LSSDetector1.h
/**
*
* 复杂动背景下的“低小慢”目标检测技术,吴言枫, 王延杰, 孙海江, 刘培勋。
* 该算法实现完成后,效果并不好,分析原因是:三帧差分的值会在目标的边缘外部是255。
* 意味着如果将这些点当做是种子点,那么目标边缘外部会向四周生长,如果背景比较类似的话,背景全部会生长;
* 另外,由于三帧差分的值在物体内部也极有可能有255。所以,大概率区域生长的结果是全图变白。
* 改进:直接将三帧差分的二值图做腐蚀膨胀操作,然后计算连通图区域,即为目标的bbox。
*/
#ifndef LSSDETECTOR1_H_
#define LSSDETECTOR1_H_
#include "detectors/detector.h"
using namespace std;
class LSSDetector1 : public Detector
{
public:
LSSDetector1(bool debug);
void init(); // 初始化方法
void detect(cv::Mat& image); // 检测方法
cv::Mat computeSaliencyMap(cv::Mat& image); //计算显著性图像
cv::Mat computeGrad(cv::Mat& image); // 获取形态学梯度图
cv::Mat computeDiff3(cv::Mat& image0, cv::Mat& image1, cv::Mat& image2); //计算三帧差分
vector<cv::Rect> getObjRects(cv::Mat srcImage, cv::Mat mask);
vector<cv::Rect> combineNearRects(); // 连接比较近的矩形框
bool needCombine(cv::Rect r1, cv::Rect r2);
cv::Rect combine(vector<cv::Rect> rects);
protected:
cv::Mat image;
cv::Mat saliencyMap;
cv::Mat diff3;
cv::Size_<int> kernel_size;
uint8_t Q; // 三帧差分阈值
cv::Mat grad; // 形态学梯度图
double connectRectsRatio; // 合并矩形框的阈值,基于视频图像长宽的比例, 小于connectRectsRatio * imageWidth或者imageHeight 距离的矩形框会被合并
vector<cv::Rect> objRects;
vector<cv::Rect> combinedRects;
};
#endif
// LSSDetector1.cpp
#include "LSSDetector1.h"
LSSDetector1::LSSDetector1(bool debug)
{
DEBUG = debug;
}
void LSSDetector1::init()
{
kernel_size=cv::Size_<int>(5, 5);
Q = (uint8_t)30;
connectRectsRatio = 0.1;
}
void LSSDetector1::detect(cv::Mat& image){}
cv::Mat LSSDetector1::computeSaliencyMap(cv::Mat& image)
{
this->image = image;
assert(image.channels() == 1 && image.type()== CV_8U); // 灰度图
cv::Mat r_hist;
float range[] = { 0, 255 } ;
const float* histRange = { range };
cv::calcHist( &image, 1, new int[1]{0}, cv::Mat(), r_hist, 1, new int[1]{255}, &histRange);
std::vector<int32_t> r_hist_arr;
for (int i = 0; i < 255; i++)
{
r_hist_arr.push_back((int32_t)r_hist.at<float>(i));
}
// 计算显著性值
cv::Mat sals = cv::Mat(255, 1, CV_32SC1, cv::Scalar_<int32_t>(0));
for (int i = 0; i < 255; i++)
{
for (int j = 0; j < 255; j++)
{
if (j == i) continue;
int dist = abs(i - j); // 可以有其他的距离度量方法
sals.ptr<int32_t>(i)[0] += r_hist_arr[j] * dist;
}
}
cv::Mat norm_sals; // int32 type
cv::normalize(sals, norm_sals, 0, 255, cv::NORM_MINMAX, CV_8U);
// mapping to each point
int width = image.cols;
int height = image.rows;
cv::Mat saliencyMap = cv::Mat(cv::Size_<int>(width, height), CV_8UC1);
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
// uint8_t pix = image.at<uint8_t>(j, i);
// 获得当前坐标地像素值
uint8_t pix = image.ptr<uint8_t>(j)[i];
// 获得当前坐标像素值的显著性
saliencyMap.ptr<uint8_t>(j)[i] = norm_sals.ptr<uint8_t>(pix)[0];
}
}
if(DEBUG)
{
cv::imshow("SaliencyMap", saliencyMap);
cv::waitKey(0);
}
this->saliencyMap = saliencyMap;
return saliencyMap;
}
cv::Mat LSSDetector1::computeGrad(cv::Mat& image)
{
assert(image.channels() == 1 && image.type() == CV_8U);
// 设置结构元素
cv::Mat grad;
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_CROSS, this->kernel_size);
cv::morphologyEx(image, grad, cv::MORPH_GRADIENT, kernel);
if (DEBUG)
{
cv::imshow("Grad", grad);
cv::waitKey();
}
this->grad = grad;
return grad;
}
cv::Mat LSSDetector1::computeDiff3(cv::Mat& image0, cv::Mat& image1, cv::Mat& image2) //计算三帧差分
{
assert(image0.channels() == 1 && image0.type() == CV_8U);
assert(image1.channels() == 1 && image1.type() == CV_8U);
assert(image2.channels() == 1 && image2.type() == CV_8U);
cv::Mat diff0, diff1;
cv::absdiff(image1, image0, diff0);
cv::absdiff(image2, image1, diff1);
cv::threshold(diff0, diff0, Q-1/*Q*/, 255, cv::THRESH_BINARY);
cv::threshold(diff1, diff1, Q-1/*Q*/, 255, cv::THRESH_BINARY);
// Logical and
cv::Mat diff3;
cv::bitwise_and(diff0, diff1, diff3, cv::Mat());
if (DEBUG)
{
cv::imshow("Diff3", diff3);
cv::waitKey(0);
}
this->diff3 = diff3;
return diff3;
}
vector<cv::Rect> LSSDetector1::getObjRects(cv::Mat srcImage, cv::Mat mask)
{
// 腐蚀膨胀操作
cv::Mat struct_ele = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3), cv::Point(-1, -1));
cv::erode(mask, mask, struct_ele);
cv::dilate(mask, mask, struct_ele, cv::Point(-1, -1), 10);
// 获得连通域的质心
cv::Mat labels, stats, centroids;
cv::connectedComponentsWithStats(mask, labels, stats, centroids); //通过连通域获得不同目标的Rect
vector<cv::Rect> rects;
for (int i = 1; i < stats.rows; i++)
{
cv::Rect c_rect;
c_rect.x = stats.at<int>(i, 0);
c_rect.y = stats.at<int>(i, 1);
c_rect.width = stats.at<int>(i, 2);
c_rect.height = stats.at<int>(i, 3);
rects.push_back(c_rect);
}
this->objRects = rects;
return rects;
}
vector<cv::Rect> LSSDetector1::combineNearRects() //这些矩形框肯定是不交叉的,因为他们不连通
{
vector<cv::Rect> ret_rects;
vector<cv::Rect> alt_rects;
for (int i = 0; i < objRects.size(); i++)
{
alt_rects.push_back(objRects[i]);
}
cv::Rect t_rect;
while (!alt_rects.empty())
{
vector<cv::Rect> t_rects; //需要合并的rects组
t_rects.push_back(alt_rects.back());
alt_rects.pop_back();
vector<int> indices;
for (int i = 0; i < alt_rects.size(); i++) // 记录哪些rect需要合并
{
cv::Rect cand_rect = alt_rects[i];
for (int j = 0; j < t_rects.size(); j++)
{
// 判断是否需要合并
if (needCombine(cand_rect, t_rects[j]))
{
t_rects.push_back(cand_rect);
indices.push_back(i);
break;
}
}
}
// 去除掉alt_rects中的需要合并的样本
vector<cv::Rect> filter_alt_rects;
for (int i = 0; i < alt_rects.size(); i++)
{
auto it = std::find(indices.begin(), indices.end(), i);
if (it==indices.end()) //没有找到
{
filter_alt_rects.push_back(alt_rects[i]);
}
}
alt_rects = filter_alt_rects;
t_rect = combine(t_rects);
ret_rects.push_back(t_rect);
}
this->combinedRects = ret_rects;
return ret_rects;
}
bool LSSDetector1::needCombine(cv::Rect r1, cv::Rect r2)
{
int height = image.rows;
int width = image.cols;
cv::Point c1 = cv::Point(r1.x + r1.width / 2, r1.y + r1.height / 2);
cv::Point c2 = cv::Point(r2.x + r2.width / 2, r2.y + r2.height / 2);
if (abs(c1.x - c2.x) < this->connectRectsRatio * width &&
abs(c1.y- c2.y) < this->connectRectsRatio * height)
{
return true;
}
return false;
}
cv::Rect LSSDetector1::combine(vector<cv::Rect> rects)
{
assert(rects.size() >= 1);
int xmin = rects[0].x, ymin=rects[0].y, xmax=rects[0].x+rects[0].width, ymax=rects[0].y+rects[0].height;
for (int i = 1; i < rects.size(); i++)
{
if (rects[i].x < xmin) xmin = rects[i].x;
if (rects[i].y < ymin) ymin = rects[i].y;
if (rects[i].x + rects[i].width > xmax) xmax = rects[i].x + rects[i].width;
if (rects[i].y + rects[i].height > ymax) ymax = rects[i].y + rects[i].height;
}
return cv::Rect(xmin, ymin, xmax-xmin, ymax-ymin);
}
//run_detector.cpp
/**
*
g++-7 -o \
detectors/lssdetectors/run_detector \
detectors/lssdetectors/run_detector.o \
detectors/lssdetectors/LSSDetector1.o \
utils/common_utils.o \
-I /usr/local/include \
-I /usr/local/include/opencv \
-I /usr/local/include/opencv2 \
-I /home/wyx/software/opencv_contrib-3.4.9/modules/tracking/include \
-l opencv_world
run shell:
detectors/lssdetectors/run_detector `pwd`/dataset/靶机5
*/
#include "LSSDetector1.h"
#include "utils/common_utils.h"
int main(int argc, char** argv)
{
string img_dir = argv[1];
vector<string> files;
common_utils::getFiles(img_dir, files);
std::sort(files.begin(), files.end());
LSSDetector1 det(false);
det.init();
for (int i = 0; i < files.size() - 3; i++)
{
cv::Mat img = cv::imread(img_dir + "/" + files[i]);
cv::Mat img1 = cv::imread(img_dir + "/" + files[i+1]);
cv::Mat img2 = cv::imread(img_dir + "/" + files[i+2]);
cv::cvtColor(img, img, cv::COLOR_BGR2GRAY);
cv::cvtColor(img1, img1, cv::COLOR_BGR2GRAY);
cv::cvtColor(img2, img2, cv::COLOR_BGR2GRAY);
cv::Mat map = det.computeSaliencyMap(img);
cv::Mat map1 = det.computeSaliencyMap(img1);
cv::Mat map2 = det.computeSaliencyMap(img2);
cv::Mat grad = det.computeGrad(map);
cv::Mat grad1 = det.computeGrad(map1);
cv::Mat grad2 = det.computeGrad(map2);
cv::Mat mask = det.computeDiff3(grad, grad1, grad2);
// 对mask取连通域
img2 = cv::imread(img_dir + "/" + files[i+2]);
vector<cv::Rect> rects = det.getObjRects(img2, mask);
vector<cv::Rect> combinedRects = det.combineNearRects();
for (size_t i = 0; i < rects.size(); i++)
{
cv::rectangle(img2, rects[i], cv::Scalar(0, 255, 0),2);
cv::imshow("Image", img2);
cv::waitKey(0);
}
for (auto rect:combinedRects)
{
cv::rectangle(img2, rect, cv::Scalar(0, 0, 255),2);
cv::imshow("Image", img2);
cv::waitKey(0);
}
}
return EXIT_SUCCESS;
}