2. 思路
路标外围的红色还是挺有区分度的,所以可以利用hsv先将场景中的红色区域提取出来,然后通过中值滤波和形态学处理去除噪音,接着就可以把每个红色区域的轮廓提取出来了,由于路标是椭圆形(视角原因,本来是圆形),所以利用每个轮廓去拟合一个椭圆并计算面积,同样的,轮廓也可以计算面积。如果当前的轮廓是路标的话,这两个面积比应该接近于1.具体的细节请看代码和注释。
3. 代码
#include
#include
#define PI 3.1415926
using namespace std;
using namespace cv;
//将RGB色域转换为HSV色域,可以直接网上搜公式
void RGB2HSV(double red, double green, double blue, double& hue, double& saturation, double& intensity)
{
double r, g, b;
double h, s, i;
double sum;
double minRGB, maxRGB;
double theta;
r = red / 255.0;
g = green / 255.0;
b = blue / 255.0;
minRGB = ((r < g) ? (r) : (g));
minRGB = (minRGB < b) ? (minRGB) : (b);
maxRGB = ((r > g) ? (r) : (g));
maxRGB = (maxRGB > b) ? (maxRGB) : (b);
sum = r + g + b;
i = sum / 3.0;
if (i < 0.001 || maxRGB - minRGB < 0.001)
{
h = 0.0;
s = 0.0;
}
else
{
s = 1.0 - 3.0*minRGB / sum;
theta = sqrt((r - g)*(r - g) + (r - b)*(g - b));
theta = acos((r - g + r - b)*0.5 / theta);
if (b <= g)
h = theta;
else
h = 2 * PI - theta;
if (s <= 0.01)
h = 0;
}
hue = (int)(h * 180 / PI);
saturation = (int)(s * 100);
intensity = (int)(i * 100);
}
//由于路标里面有空洞,先使用floodFill算法将红色区域的外围覆盖成255,然后通过最后一行代码取出红色区域,此时路标里的空洞以被填充。
//这里的srcBw是一通道的mask,原图像中红色的区域为255,其他为0,dstBw 为填充后的mask
void fillHole(const Mat &srcBw, Mat &dstBw)
{
Size m_Size = srcBw.size();
Mat Temp = Mat::zeros(m_Size.height + 2, m_Size.width + 2, srcBw.type());
srcBw.copyTo(Temp(Range(1, m_Size.height + 1), Range(1, m_Size.width + 1)));
cv::floodFill(Temp, Point(0, 0), Scalar(255));
Mat cutImg;
Temp(Range(1, m_Size.height + 1), Range(1, m_Size.width + 1)).copyTo(cutImg);
dstBw = srcBw | (~cutImg);
}
/*
*image: 原图像,三通道
*contours: 从原图像提取出的轮廓
*/
void getContours(const Mat & image,vector<vector<Point>> &contours)
{
int width = image.cols;//图像宽度
int height = image.rows;//图像高度
double B = 0.0, G = 0.0, R = 0.0, H = 0.0, S = 0.0, V = 0.0;
Mat matRgb = Mat::zeros(image.size(), CV_8UC1);
int x, y; //循环,根据hsv值挑选出红色的区域,红色区域会在matRgb中相应位置取值255
for (y = 0; y < height; y++)
{
for (x = 0; x < width; x++)
{
// 获取BGR值
B = image.at<Vec3b>(y, x)[0];
G = image.at<Vec3b>(y, x)[1];
R = image.at<Vec3b>(y, x)[2];
RGB2HSV(R, G, B, H, S, V);
//红色范围
if ((H >= 330 && H <= 360 || H >= 0 && H <= 10) && S >= 11 && S <= 100 && V > 10 && V < 99) //H不能低于10,H不能大于344,S不能高于21,V不能变
{
matRgb.at<uchar>(y, x) = 255;
}
}
}
medianBlur(matRgb, matRgb, 3); // 中值滤波,去除噪声,下面的形态学处理也是
Mat element = getStructuringElement(MORPH_ELLIPSE, Size(5, 5), Point(0, 0));
morphologyEx(matRgb,matRgb,MORPH_CLOSE,element,Point(-1,-1),1);
fillHole(matRgb,matRgb);// 填充路标的空洞,看fillHole的函数注释
vector<Vec4i> hierarchy;
findContours(matRgb, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0)); // opencv 函数,计算轮廓
}
// 根据一个轮廓,把这个轮廓里的图像(in)扣出来
void Segment(const Mat &in,const vector<Point> &contour,Mat &out,Rect &box)
{
Mat temp(in.size(),in.type());
Mat img;
in.copyTo(img);
temp.setTo(Scalar(0,0,0));
RotatedRect ell = fitEllipse(contour); //轮廓拟合椭圆
box = boundingRect(contour); //轮廓的最小矩形
cout<<box<<endl;
ellipse(temp,ell,Scalar(255,255,255),-1); //将椭圆填充到temp,成为一个mask
bitwise_and(in,temp,img); //根据椭圆切割图像
img(box).copyTo(out);
}
// 简单计算两个图像的相似度,直接求差值,一个为路标template,另一个为自然场景中扣出来的图片,这里是很简单的计算方法,完全可以改进
// 函数返回值越小2,说明两张图片越相似
double score(const Mat &queryImg,const Mat &tempImg)
{
Mat tmp1,tmp2;
queryImg.copyTo(tmp1);
tempImg.copyTo(tmp2);
Scalar mean1 = cv::mean(tmp1);
Scalar mean2 = cv::mean(tmp2);
tmp1 -= mean1;
tmp2 -= mean2;
return cv::norm(tmp1,tmp2);
}
int main(int argc,char *argv[])
{
/*
* idlefish-msg-1640693329982.jpg
* idlefish-msg-1640693332987.jpg
* idlefish-msg-1640693317275.jpg
* idlefish-msg-1640693319435.jpg
* */
if(argc!=3)
{
cerr << "Usage: "<<argv[0] <<" image_path template_image_path"<<endl;
}
Mat srcImg = imread(argv[1]);
Mat template_Img = imread(argv[2]);
Mat srcImgCopy;
srcImg.copyTo(srcImgCopy);
vector<vector<Point>> srcImg_contours,templateImg_contours;
getContours(srcImg,srcImg_contours); //先计算template和image的轮廓
getContours(template_Img,templateImg_contours);
Rect template_box;
Segment(template_Img,templateImg_contours[0],template_Img,template_box);// 对template的图片也切割一下,把不是红色轮廓里面的背景置0
double best_score = 0;
Rect best_rect;
for(auto & contour : srcImg_contours)
{
double ConArea = contourArea(contour);// 场景image中的某个轮廓
if(ConArea < 500) // 这个轮廓面积太小,就跳过
continue;
RotatedRect ell = fitEllipse(contour); //这个轮廓的拟合椭圆
double EllArea = ell.size.area() * PI/4; // 拟合椭圆的面积
if(ConArea/EllArea > 1.1 || ConArea/EllArea < 0.9) // 如果这个两个面积比相差太大,则跳过
continue;
Mat queryImg;
Rect queryBox;
Segment(srcImgCopy,contour,queryImg,queryBox); // 将这个轮廓对应的图片扣出来与template做对比,首先要resize到和template 一样的大小才行。
resize(queryImg,queryImg,template_Img.size());
Mat result;
double s = score(queryImg,template_Img); //计算相似性
if(best_score == 0) { // 如果是第一个轮廓,就把best_score赋值
best_score = s;
best_rect = queryBox;
}
if(s<best_score) // 如果目前的score比以前的best_score小,说明当前扣出来的图片与template的相似度高一点,则替换以前的值
{
best_score = s;
best_rect = queryBox;
}
std::cout<<"ConArea: "<<ConArea<<" EllArea "<<EllArea<<endl;
}
cv::rectangle(srcImgCopy,best_rect,Scalar(255,0,0),2);
imshow("srcImgCopy",srcImgCopy);
imwrite("result.png",srcImgCopy);
waitKey(0);
return 0;
}