问题:将两个装甲板的同侧俩灯条识别成装甲板
解决思路:
- 找出图片中所有灯条
- 根据长宽比,面积大小,凸度来筛选灯条
- 合适匹配
- 配对灯条作为候选装甲板,然后提取中间的图案判断是否是数字来进行确定
用红蓝通道相减得到的差作为识别用的灰度图
对于图像中红色的物体来说,其rgb分量中r的值最大,g和b在理想情况下应该是0,同理蓝色物体的b分量应该最大。
如果识别红色物体可以直接用r通道-b通道。由于在低曝光下只有灯条是有颜色的,两通道相减后,其他区域的部分会因为r和b的值差不多而被减去,而蓝色灯条部分由于r通道比b通道的值小,相减后就归0了,也就是剩下的灰度图只留下了红色灯条。
// 把一个3通道图像转换成3个单通道图像
split(_roiImg,channels);//分离色彩通道
//预处理删除己方装甲板颜色
if(_enemy_color==RED)
_grayImg=channels.at(2)-channels.at(0);//Get red-blue image;
else _grayImg=channels.at(0)-channels.at(2);//Get blue-red image;
得到灰度图后需要阈值化处理得到二值图,之后可以进行膨胀处理让图像中的轮廓更明显。
Mat binBrightImg;
//阈值化
threshold(_grayImg, binBrightImg,_param.brightness_threshold
, 255, cv::THRESH_BINARY);
Mat element = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(3, 3));//选元
//膨胀
dilate(binBrightImg, binBrightImg, element);
找轮廓,这步是整个算法中最耗时的部分,如果预处理做的好,可以极大地减少找轮廓中花费的时间。
vector<vector<Point>> lightContours;
//找轮廓
findContours(binBrightImg.clone(), lightContours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
找到轮廓后开始遍历轮廓提取灯条
for(const auto& contour : lightContours)
{
//得到面积
float lightContourArea = contourArea(contour);
//面积太小的不要
if(contour.size() <= 5 ||
lightContourArea < _param.light_min_area) continue;
//椭圆拟合区域得到外接矩形
RotatedRect lightRec = fitEllipse(contour);
//矫正灯条
adjustRec(lightRec, ANGLE_TO_UP);
//宽高比、凸度筛选灯条
if(lightRec.size.width / lightRec.size.height >
_param.light_max_ratio ||
lightContourArea / lightRec.size.area() <
_param.light_contour_min_solidity
)continue;
//对灯条范围适当扩大
lightRec.size.width *= _param.light_color_detect_extend_ratio;
lightRec.size.height *= _param.light_color_detect_extend_ratio;
Rect lightRect = lightRec.boundingRect();
const Rect srcBound(Point(0, 0), _roiImg.size());
lightRect &= srcBound;
//因为颜色通道相减后己方灯条直接过滤,不需要判断颜色了,可以直接将灯条保存
lightInfos.push_back(LightDescriptor(lightRec));
}
//没找到灯条就返回没找到
if(lightInfos.empty())
{
return _flag = ARMOR_NO;
}
对灯条进行匹配筛选
//按灯条中心x从小到大排序
sort(lightInfos.begin(), lightInfos.end(), [](const LightDescriptor& ld1, const LightDescriptor& ld2)
{//Lambda函数,作为sort的cmp函数
return ld1.center.x < ld2.center.x;
});
for(size_t i = 0; i < lightInfos.size(); i++)
{//遍历所有灯条进行匹配
for(size_t j = i + 1; (j < lightInfos.size()); j++)
{
const LightDescriptor& leftLight = lightInfos[i];
const LightDescriptor& rightLight = lightInfos[j];
/*
* Works for 2-3 meters situation
* morphologically similar: // parallel
// similar height
*/
//角差
float angleDiff_ = abs(leftLight.angle - rightLight.angle);
//长度差比率
float LenDiff_ratio = abs(leftLight.length - rightLight.length) / max(leftLight.length, rightLight.length);
//筛选
if(angleDiff_ > _param.light_max_angle_diff_ ||
LenDiff_ratio > _param.light_max_height_diff_ratio_)
{
continue;
}
/*
* proper location: y value of light bar close enough
* ratio of length and width is proper
*/
//左右灯条相距距离
float dis = cvex::distance(leftLight.center, rightLight.center);
//左右灯条长度的平均值
float meanLen = (leftLight.length + rightLight.length) / 2;
//左右灯条中心点y的差值
float yDiff = abs(leftLight.center.y - rightLight.center.y);
//y差比率
float yDiff_ratio = yDiff / meanLen;
//左右灯条中心点x的差值
float xDiff = abs(leftLight.center.x - rightLight.center.x);
//x差比率
float xDiff_ratio = xDiff / meanLen;
//相距距离与灯条长度比值
float ratio = dis / meanLen;
//筛选
if(yDiff_ratio > _param.light_max_y_diff_ratio_ ||
xDiff_ratio < _param.light_min_x_diff_ratio_ ||
ratio > _param.armor_max_aspect_ratio_ ||
ratio < _param.armor_min_aspect_ratio_)
{
continue;
}
// calculate pairs' info
//按比值来确定大小装甲
int armorType = ratio > _param.armor_big_armor_ratio ? BIG_ARMOR : SMALL_ARMOR;
// calculate the rotation score
float ratiOff = (armorType == BIG_ARMOR) ? max(_param.armor_big_armor_ratio - ratio, float(0)) : max(_param.armor_small_armor_ratio - ratio, float(0));
float yOff = yDiff / meanLen;
float rotationScore = -(ratiOff * ratiOff + yOff * yOff);
//得到匹配的装甲板
ArmorDescriptor armor(leftLight, rightLight, armorType, channels.at(1), rotationScore, _param);
_armors.emplace_back(armor);
break;
}
}
//没匹配到装甲板则返回没找到
if(_armors.empty())
{
return _flag = ARMOR_NO;
}
对找到的装甲板进行筛选
//delete the fake armors
_armors.erase(remove_if(_armors.begin(), _armors.end(), [this](ArmorDescriptor& i)
{//lamdba函数判断是不是装甲板,将装甲板中心的图片提取后让识别函数去识别,识别可以用svm或者模板匹配等
return 0==(i.isArmorPattern(_small_Armor_template,_big_Armor_template,lastEnemy));
}), _armors.end());
//全都判断不是装甲板
if(_armors.empty())
{
_targetArmor.clear();
if(_flag == ARMOR_LOCAL)
{
//cout << "Tracking lost" << endl;
return _flag = ARMOR_LOST;
}
else
{
//cout << "No armor pattern detected." << endl;
return _flag = ARMOR_NO;
}
}
判断是不是装甲板我用的是模板匹配的方法,模板匹配特别适合待识别图片不会变化的场景,选择合适的模板可以得到很高的准确率并且花费时间远小于svm等机器学习方法。
//模板匹配 根据装甲板中心的图案判断是不是装甲板
bool ArmorDescriptor::isArmorPattern(std::vector<cv::Mat> &small,
std::vector<cv::Mat> &big ,
LastenemyType &lastEnemy)
{
//若需要判断装甲中间数字
#ifdef IS_ARMOR
vector<pair<int,double>> score;
map<int,double> mp;
Mat regulatedImg=frontImg;
for(int i=0;i<8;i++){
//载入模板,模板是在初始化的时候载入ArmorDetector类,
//因为ArmorDescriptor与其非同类需要间接导入
Mat tepl=small[i];
Mat tepl1=big[i];
//模板匹配得到位置,这里没用
cv::Point matchLoc;
//模板匹配得分
double value;
//匹配小装甲
value = TemplateMatch(regulatedImg, tepl, matchLoc, CV_TM_CCOEFF_NORMED);
mp[i+1]=value;
score.push_back(make_pair(i+1,value));
//匹配大装甲
value = TemplateMatch(regulatedImg, tepl1, matchLoc, CV_TM_CCOEFF_NORMED);
mp[i+11]=value;
score.push_back(make_pair(i+11,value));
}
//对该装甲与所有模板匹配后的得分进行排序
sort(score.begin(),score.end(), [](const pair<int,double> &a, const pair<int,double> &b)
{
return a.second > b.second;
});
//装甲中心位置
cv::Point2f c=(vertex[0]+vertex[1]+vertex[2]+vertex[3])/4;
//装甲数字即为得分最高的那个
int resultNum=score[0].first;
//得分太低认为没识别到数字
if(score[0].second<0.6)
{
if(//与上次识别到的装甲板位置差不多,且丢失次数不超过一定值
std::abs(std::abs(lastEnemy.center.x)-std::abs(c.x))<10&&
std::abs(std::abs(lastEnemy.center.y)-std::abs(c.y))<10&&
lastEnemy.lostTimes<100
)
{//认为该装甲的数字与上次相同
lastEnemy.lostTimes++;
lastEnemy.center=c;
enemy_num=lastEnemy.num;
return true;
}
else
{//认为不是装甲
return false;
}
}
//当装甲板识别为小装甲,而得到的号码为11、22……时,说明把大装甲识别为了小装甲。
if(type==SMALL_ARMOR )
{
if(resultNum>10)
{
type=BIG_ARMOR;
}
}
enemy_num=resultNum%10;
lastEnemy.num=enemy_num;
lastEnemy.center=c;
lastEnemy.lostTimes=0;
return true;
#endif
//若不需要判断匹配的装甲板中的数字则整个函数直接返回true
#ifndef IS_ARMOR
return true;
#endif
}