PCB板图像匹配思路分享
项目场景:
一张pcb板的标准模板的灰度图与RGB实时图做图像配准,要求满足实时性、精度5-10pix。注意:本次图像匹配只需要考虑偏移量,不存在尺度大小、旋转等问题。
匹配与待匹配图(截取)
1. 基于特征点匹配的算法
图像匹配最常用的算法,预处理往往需要加上图像金字塔来提升精度,然后检测图像中的角点,往往是一些梯度较大的地方,再根据图像中角点的各自的属性来计算他们之间的相识性,一般最后还需要对检测到的匹配点对进行过滤,常用RANSAC算法。在这些配准的步骤完成后,再根据匹配点对计算图像的透视矩阵,将其中一份图像透视矩阵变换后就可以与另一副图像相融合了。
但本次项目并没有用,因为相机拍摄出来的图片有点奇怪,大概率是可见光的图像上加入了什么滤镜,导致图像中的边缘特征、灰度特征都有较大的差异。特征匹配的效果比较差。后来开始考虑pcb板上的形状特征,因为pcb板上有一定数量的圆孔,而圆孔之间的相对位置又是固定的。所以我想到计算每个圆孔到周围的其他圆孔的向量关系的相识度数。
2.基于圆孔位置信息的匹配思路
算法预处理后,首先通过Hough圆提取算法获取pcb板中圆孔的位置(tips:1.Hough圆提取算法之间加上滤波效果会好不少,2.我只用了圆孔的位置信息,其实还可以加上半径信息的)。依照如下一个简单的流程就可以得到一个较好的匹配效果了:
1.先将标准图与实时图的圆孔的位置信息存储在vector中;
2.遍历1中的vector,以有序map<double, Point2i>存储每个点与其他点的距离和他点坐标,获取当前点周围最小的5个圆孔点的坐标,存储在一个二维Point2i的vector中。
3.双重for循环遍历两幅图的所有圆孔点的周围最小的5个圆孔点的坐标,计算每次的坐标偏移值,并进行投票。
4.统计票数最多的XY偏移值,对原始图像进行合并。
其中2-3步我用的相似性的指标是比较简陋的,但目前效果还行就没有进行改进。
改算法在有部分聚集的圆孔点时,算法精度较高,但也存在两个局限性:1.比较依赖Hough圆的检测精度。2.可能存在没有圆孔的pcb板。
//标准图中圆孔的位置信息,实时图的圆孔位置信息,返回偏移量
int linkCircles(vector<Point2i> &StdCirclesPoint, vector<Point2i> &RGBCirclesPoint, int &offsetX, int &offsetY)
{
//标准图圆孔及周边5个点的位置信息
vector<vector<Point2i>> StdAllPointLink;
//实时图圆孔及周边5个点的位置信息
vector<vector<Point2i>> RGBAllPointLink;
int StdisFalse = getAllPointLink(StdCirclesPoint, StdAllPointLink);//标准图的圆点的坐标,每个点的关系链接
int RGBisFalse = getAllPointLink(RGBCirclesPoint, RGBAllPointLink); // 实时图的圆点的坐标,每个点的关系链接
if (StdisFalse == -1 || RGBisFalse == -1)
{
return -1;
}
//依照关系链接计算偏移量
calcuPointLink(StdAllPointLink, RGBAllPointLink, offsetX, offsetY);
return 0;
}
//计算周边的向量的相识性
int getAllPointLink(vector<Point2i> &CirclesPoint, vector<vector<Point2i>> &AllPointLink)
{
if (CirclesPoint.size() < 5)
{
return -1;
}
for (int i = 0; i < CirclesPoint.size(); i++)
{
vector<Point2i> pointLink;
Point2i center = CirclesPoint[i];
int offsetx = 0, offsety = 0;
double minDistence = 0;
map<double, Point2i, std::greater<int>> mapDisAndPoint;
for (int j = 0; j < CirclesPoint.size(); j++)
{
if (j == i)
{
continue;
}
//向量的距离
double distence = sqrt((center.x - CirclesPoint[j].x)*(center.x - CirclesPoint[j].x) + (center.y - CirclesPoint[j].y)*(center.y - CirclesPoint[j].y));
//存储距离最小的5个点周边圆孔的坐标
if (mapDisAndPoint.size()<5)
{
mapDisAndPoint.insert(pair<double, Point2i>(distence, Point2i(CirclesPoint[j])));
}
else
{
if (distence < mapDisAndPoint.begin()->first)
{
auto newElem = std::make_pair(distence, Point2i(CirclesPoint[j]));
auto result = mapDisAndPoint.insert(pair<double, Point2i>(distence, Point2i(CirclesPoint[j])));
if (result.second)
{
mapDisAndPoint.erase(std::begin(mapDisAndPoint));//删除第一个元素
}
}
}
}
//获取最小的5个圆孔的坐标
pointLink.push_back(center);
for (const auto& pair : mapDisAndPoint) {
offsetx = pair.second.x - center.x;
offsety = pair.second.y - center.y;
pointLink.push_back(Point2i(offsetx, offsety));
}
AllPointLink.push_back(pointLink);
}
return 0;
}
//Std图的向量关系,rgb图的向量关系,两个偏移量
void calcuPointLink(vector<vector<Point2i>> &StdAllPointLink, vector<vector<Point2i>> &RGBAllPointLink, int &offsetX, int &offsetY)
{
//每个点计算的偏移量,而非整张图
int offsetXEveryPoint = 0, offsetYEveryPoint = 0;
//每两个向量的相似性,与Std图和RGB图的
map<double, vector<Point2i>, std::greater<double>> mapDisAndPoint;
//每个向量的相似性计算
for (int i = 0; i < StdAllPointLink.size(); i++)
{
for (int j = 0; j < RGBAllPointLink.size(); j++)
{
if (abs(StdAllPointLink[i][0].x - RGBAllPointLink[j][0].x) > 2000 / REDUCESCALE ||
abs(StdAllPointLink[i][0].y - RGBAllPointLink[j][0].y) > 2000 / REDUCESCALE) continue;//相差太大暂时不考虑
double SimilarityOffset = 0;
for (int k = 1; k <= 5; k++)
{
SimilarityOffset += abs(StdAllPointLink[i][k].x - RGBAllPointLink[j][k].x);
SimilarityOffset += abs(StdAllPointLink[i][k].y - RGBAllPointLink[j][k].y);
}
vector<Point2i> _2point;
_2point.push_back(StdAllPointLink[i][0]);
_2point.push_back(RGBAllPointLink[j][0]);
mapDisAndPoint.insert(pair<double, vector<Point2i>>(SimilarityOffset, _2point));
}
}
//偏移量投票(-800/REDUCESCALE, 1200/REDUCESCALE)
vector<int> voteValuesX(2800 / REDUCESCALE, 0);
vector<int> voteValuesY(2800 / REDUCESCALE, 0);
// 反向遍历map
for (auto it = mapDisAndPoint.rbegin(); it != mapDisAndPoint.rend(); ++it)
{
offsetXEveryPoint = it->second[0].x - it->second[1].x;
offsetYEveryPoint = it->second[0].y - it->second[1].y;
if (offsetXEveryPoint < 0 || offsetYEveryPoint < 0 || offsetXEveryPoint > 2000 / REDUCESCALE || offsetYEveryPoint > 2000 / REDUCESCALE) continue;//负数与太大暂时不考虑
voteValuesX[offsetXEveryPoint + 800 / REDUCESCALE] += 1;
voteValuesY[offsetYEveryPoint + 800 / REDUCESCALE] += 1;
}
// 初始化最大值为vector的第一个元素
int maxValueX = voteValuesX[0];
int maxValueY = voteValuesY[0];
int maxOffsetX = 0;
int maxOffsetY = 0;
// 遍历vector,查找最大值
for (int i = 1; i < voteValuesX.size(); ++i)
{
if (voteValuesX[i] > maxValueX)
{
maxValueX = voteValuesX[i];
maxOffsetX = i - 800 / REDUCESCALE;
}
}
for (int i = 1; i < voteValuesY.size(); ++i)
{
if (voteValuesY[i] > maxValueY)
{
maxValueY = voteValuesY[i];
maxOffsetY = i - 800 / REDUCESCALE;
}
}
// 输出最大值
std::cout << "The offsetX is: " << maxOffsetX << std::endl;
std::cout << "The offsetY is: " << maxOffsetY << std::endl;
std::cout << "The maxValueX is: " << maxValueX << std::endl;
std::cout << "The maxValueY is: " << maxValueY << std::endl;
offsetX = maxOffsetX;
offsetY = maxOffsetY;
//histImageX.release();
//histImageY.release();
}
3.替换Hough检测算法(失败)
考虑到官方的函数普遍是普适性好,但精度较低,所以我决定自己写Hough检测的代码。
1.首先是根据Hough圆检测的基础理论复现了一遍,但速度和精度不忍直视。后来我去翻HoughCircle()函数的源码,没有找到核心的地方,但感觉应该是加入了Canny边缘检测,并以圆周上的点的梯度来优化计算过程。同样的,加入以后还是效果不好,后来就放弃了,感觉优化的再好速度也跟不上,因为opencv自己函数有官方加速。
2.后来我想到Circle()画圆,半径依次递增的时候可以直接统计圆周上点的数量占总数的比例来确定是否是圆。后来实践过程发现精度确实可以,但时间开销太大,但也可以通过openmp,修改半径或遍历点的步长来加速。但还是比较慢,要是能提前知道圆心可能的坐标,或许就满足要求了。
//待检测原图,遍历到的像素点,圆的最大、最小半径,圆周上点占圆周的比例
int FindCircle(cv::Mat& image, cv::Point center, int min_radius, int max_radius, double ratio)
{
if (center.x + max_radius >= image.cols || center.x - max_radius < 0 || center.y + max_radius >= image.rows || center.y - max_radius < 0)
{
return -1;
}
Mat imageshow;
cvtColor(image, imageshow, CV_GRAY2RGB);
int totalCirclePixNum = 0;
int findCirclePixNum = 0;
std::random_device rd; // 用于获取一个种子
std::mt19937 gen(rd()); // 以 rd() 作为种子,初始化 Mersenne Twister 生成器
// 定义随机数分布
std::uniform_int_distribution<> dis(0, 255);
double squ2 = 1.414;
int squ2radius;//根号2*半径/2
Point p1, p2, p3, p4;//第1234象限中间点
int diffvalueX, diffvalueY;//坐标轴顶点到P1234的距离
for (int i = min_radius; i < max_radius; i+=1)
{
if (image.at<uchar>(center.x - i, center.y) == 0
&& image.at<uchar>(center.x + i, center.y) == 0
&& image.at<uchar>(center.x, center.y - i) == 0
&& image.at<uchar>(center.x, center.y - i) == 0)
{
i += 1;
continue;
}
Vec3b color(dis(gen), dis(gen), dis(gen));
if (i < 8)
{
squ2radius = cvFloor(squ2 * i / 2);
}
else
{
squ2radius = cvRound(squ2 * i / 2);
}
p1.x = center.x + squ2radius;
p1.y = center.y - squ2radius;
diffvalueX = squ2radius;
diffvalueY = i - squ2radius;
if (diffvalueX < 2)
{
cout << "diffX = " << diffvalueX << endl;
continue;
//return -1;
}
int SumDeltad = 0;
if (diffvalueY < 2)
{
continue;
SumDeltad = 1;
}
else
{
for (int s = 1; s < diffvalueY; s++)
{
SumDeltad += s;
}
}
double Deltad = (double)(i - diffvalueY) / SumDeltad;
vector<int> d;//每层点的个数;
int Dsum = 0;
for (int j = 0; j < diffvalueY; j++)
{
int tempD = cvRound(1 + (double)(Deltad * j));
d.push_back(tempD);
Dsum += tempD;
}
//int newDsum = Dsum;
for (int k = 0; k < Dsum - i; k++)
{
d[diffvalueY - 1 - k]--;
//newDsum--;
}
p2.x = center.x - squ2radius;
p2.y = center.y - squ2radius;
p3.x = center.x - squ2radius;
p3.y = center.y + squ2radius;
p4.x = center.x + squ2radius;
p4.y = center.y + squ2radius;
Point circlePoint = Point(p1.x + 1, p1.y);
Point circlePointother;
for (int m = 0; m < d.size(); m++)
{
for (int n = 0; n < d[m]; n++)
{
circlePoint = Point(circlePoint.x - 1, circlePoint.y);
if (image.at<uchar>(circlePoint) > 150)
{
findCirclePixNum++;
}
//image.at<cv::Vec3b>(circlePoint) = color;
circlePointother = Point(2 * center.x - circlePoint.x, circlePoint.y);
if (image.at<uchar>(circlePointother) > 150)
{
findCirclePixNum++;
}
//image.at<cv::Vec3b>(circlePointother) = color;
circlePointother = Point(circlePoint.x, 2 * center.y - circlePoint.y);
if (image.at<uchar>(circlePointother) > 150)
{
findCirclePixNum++;
}
//image.at<cv::Vec3b>(circlePointother) = color;
circlePointother = Point(2 * center.x - circlePoint.x, 2 * center.y - circlePoint.y);
if (image.at<uchar>(circlePointother) > 150)
{
findCirclePixNum++;
}
//image.at<cv::Vec3b>(circlePointother) = color;
totalCirclePixNum += 4;
}
if ((double)findCirclePixNum / totalCirclePixNum < 0.15)
{
break;
}
circlePoint = Point(circlePoint.x + 1, circlePoint.y - 1);
}
circlePoint = Point(p1.x, p1.y - 1);
for (int m = 0; m < d.size(); m++)
{
for (int n = 0; n < d[m]; n++)
{
circlePoint = Point(circlePoint.x, circlePoint.y + 1);
if (image.at<uchar>(circlePointother) > 150)
{
findCirclePixNum++;
}
//image.at<cv::Vec3b>(circlePoint) = color;
circlePointother = Point(2 * center.x - circlePoint.x, circlePoint.y);
if (image.at<uchar>(circlePointother) > 150)
{
findCirclePixNum++;
}
//image.at<cv::Vec3b>(circlePointother) = color;
circlePointother = Point(circlePoint.x, 2 * center.y - circlePoint.y);
if (image.at<uchar>(circlePointother) > 150)
{
findCirclePixNum++;
}
//image.at<cv::Vec3b>(circlePointother) = color;
circlePointother = Point(2 * center.x - circlePoint.x, 2 * center.y - circlePoint.y);
if (image.at<uchar>(circlePointother) > 150)
{
findCirclePixNum++;
}
//image.at<cv::Vec3b>(circlePointother) = color;
totalCirclePixNum += 4;
}
if ((double)findCirclePixNum / totalCirclePixNum < 0.15)
{
break;
}
circlePoint = Point(circlePoint.x + 1, circlePoint.y - 1);
}
if ((double)findCirclePixNum / totalCirclePixNum > ratio)
{
circle(imageshow, center, 1, Scalar(255, 0, 0), -1, 8, 0); // 绘制圆心
circle(imageshow, center, i, Scalar(0, 0, 255), 1, 8, 0); //绘制空心圆
}
findCirclePixNum = 0;
totalCirclePixNum = 0;
}
}
4.Hough圆检测替换为轮廓检测
Hough圆检测算法有个很致命的地方是当圆孔半径较小,才几个像素时(图片分辨率本来很高的,但我为了节约开销,将图片缩小了10倍),有些圆孔部分圆周近似直线,这样这一部分圆周的梯度就消失了,HoughCircle()就会难以检测出来,这也是前文为什么说HoughCircle()要加滤波平滑图像的原因。
后来考虑到目前整套算法最薄弱的地方在于圆孔的检测,然后我就开始考虑能不能替换为其他的特征,比如:轮廓。而且替换为轮廓基本上不需要考虑太多,只需要将原本的圆心换为轮廓的中心(图像的矩很好计算这个)。
void RGBFindContours(Mat &RGBToDetect, vector<Point2i> &RGBCirclesPoint)
{
// 查找轮廓
vector<vector<Point>> contours;
findContours(RGBToDetect, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
// 遍历轮廓
for (size_t i = 0; i < contours.size(); i++) {
double area = contourArea(contours[i]);
//cout << "Contour " << i + 1 << " area: " << area<< endl;
// 检查轮廓大小是否在200到300之间
if (area >= 30 && area <= 1500) {
// 计算轮廓的中心点
Moments M = moments(contours[i], false);
Point center(
cvRound(M.m10 / M.m00),
cvRound(M.m01 / M.m00)
);
//cout << "Contour " << i + 1 << " area: " << area << ", center: (" << center.x << ", " << center.y << ")" << endl;
circle(RGBToDetect, center, 3, 155, -1, 8, 0); // 绘制圆心
// 可以在这里绘制中心点(如果需要)
// circle(src, center, 2, Scalar(0, 255, 0), -1); // 使用绿色绘制中心点
RGBCirclesPoint.push_back(center);
}
}
}
最终的检测效果(截取部分):
其中白灰色为实时图的灰度图,黑灰色为标准图。