一、项目来源
本例项目来源于群里面网友提问“在流水线上采集到的图片,相互之间位移基本确定,需要进行进一步精细拼接”。原始图片和拼接结果见附件,已经获得图片提供者同意。
具体而言,这是一块大型服务器板子,会走点拍100张图【特定设备】,每张图有部分重合,算下来应该七百多宽度重合,图像大小为5000多。难点是重合的全是cpu socket pin,全部长一个样子。
二、算法研究
如果是有定位点的图片,我们当然会优先考虑采用“特征定位”的方式,但是难点在于这里重合的部分基本长的都一样。这种情况下,我们经过讨论,得出了采用“相位变换”的方法来解决“配准”问题。
算法参考:
Mat src1 = cv::imread("e:/template/pcb/1.bmp");
Mat src2 = cv::imread("e:/template/pcb/2.bmp");
Mat gray1; Mat gray2;
Mat dst1; Mat dst2;
cvtColor(src1, gray1, COLOR_BGR2GRAY); //转换为灰度图像
gray1.convertTo(dst1, CV_32FC1); //转换为32位浮点型
cvtColor(src2, gray2, COLOR_BGR2GRAY);
gray2.convertTo(dst2, CV_32FC1);
Mat roi1; Mat roi2;
roi1 = dst1(Rect(dst1.cols - 700, 0, 700, dst1.rows));
roi2 = dst2(Rect(0, 0, 700, dst2.rows));
//TODO 主动选取ROI 区域
//获得相位差距
Point2d phase_shift;
phase_shift = phaseCorrelate(roi1, roi2);
cout << endl << "warp :" << endl << " X shift : " << phase_shift.x << " Y shift : " << phase_shift.y << endl;
//图片对准
Mat result(cv::Size(src1.cols + src2.cols, src1.rows + 200), CV_8UC3, Scalar::all(0));
Mat leftHalf(cv::Size(src1.cols, src1.rows + 200), CV_8UC3, Scalar::all(0));
Mat rightHalf(cv::Size(src2.cols, src2.rows + 200), CV_8UC3, Scalar::all(0));
Mat copy1 = result(Rect(0, 100, src1.cols, src1.rows));
src1.copyTo(copy1);
copy1 = leftHalf(Rect(0, 100, src1.cols, src1.rows));
src1.copyTo(copy1);
Mat copy2 = result(Rect(src1.cols - 700- phase_shift.x, 100-phase_shift.y, src2.cols, src2.rows));
src2.copyTo(copy2);
copy2 = rightHalf(Rect(0, 100 - phase_shift.y, src2.cols, src2.rows));
src2.copyTo(copy2);
//绘制融合线
//cv::line(result, cv::Point(src1.cols - 700 - phase_shift.x, 0), cv::Point(src1.cols - 700 - phase_shift.x, result.cols), cv::Scalar(0, 0, 255));
//cv::line(result, cv::Point(src1.cols - 700 - phase_shift.x-50, 0), cv::Point(src1.cols - 700 - phase_shift.x-50, result.cols), cv::Scalar(0, 0, 255));
//cv::line(result, cv::Point(src1.cols - 700 - phase_shift.x+50, 0), cv::Point(src1.cols - 700 - phase_shift.x+50, result.cols), cv::Scalar(0, 0, 255));
//图像融合
leftHalf.convertTo(leftHalf, CV_32FC3, 1.0 / 255);
rightHalf.convertTo(rightHalf, CV_32FC3, 1.0 / 255);
result.convertTo(result, CV_32FC3, 1.0 / 255);
double dblend = 0.0;
int istart = src1.cols - 700 - phase_shift.x ;//col的初始定位
for (int i = 0; i < 100; i++)
{
//使用col,必须保证row是一样的
result.col(istart + i) = leftHalf.col(istart + i)*(1-dblend)+ rightHalf.col(i)*dblend;
dblend = dblend + 0.01;
}
result.convertTo(result, CV_8UC3, 255);
imwrite("e:/template/pcb/result.jpg", result);
cv::waitKey(0);在这种情况下,基本上能够得到这样的结果:


Mat result(cv::Size(src1.cols + src2.cols, src1.rows + 200), CV_8UC3, Scalar::all(0));
Mat leftHalf(cv::Size(src1.cols, src1.rows + 200), CV_8UC3, Scalar::all(0));
Mat rightHalf(cv::Size(src2.cols, src2.rows + 200), CV_8UC3, Scalar::all(0));
Mat copy1 = result(Rect(0, 100, src1.cols, src1.rows));
src1.copyTo(copy1);
copy1 = leftHalf(Rect(0, 100, src1.cols, src1.rows));
src1.copyTo(copy1);
Mat copy2 = result(Rect(src1.cols - 700- phase_shift.x, 100-phase_shift.y, src2.cols, src2.rows));
src2.copyTo(copy2);
copy2 = rightHalf(Rect(0, 100 - phase_shift.y, src2.cols, src2.rows));
src2.copyTo(copy2);
Mat leftHalf(cv::Size(src1.cols, src1.rows + 200), CV_8UC3, Scalar::all(0));
Mat rightHalf(cv::Size(src2.cols, src2.rows + 200), CV_8UC3, Scalar::all(0));
Mat copy1 = result(Rect(0, 100, src1.cols, src1.rows));
src1.copyTo(copy1);
copy1 = leftHalf(Rect(0, 100, src1.cols, src1.rows));
src1.copyTo(copy1);
Mat copy2 = result(Rect(src1.cols - 700- phase_shift.x, 100-phase_shift.y, src2.cols, src2.rows));
src2.copyTo(copy2);
copy2 = rightHalf(Rect(0, 100 - phase_shift.y, src2.cols, src2.rows));
src2.copyTo(copy2);
从细节来看,已经对准,只是融合的问题。准确地找到融合线是成功融合的前提条件 。
//绘制融合线
cv::line(result, cv::Point(src1.cols - 700 - phase_shift.x, 0), cv::Point(src1.cols - 700 - phase_shift.x, result.cols), cv::Scalar(0, 0, 255));
cv::line(result, cv::Point(src1.cols - 700 - phase_shift.x, 0), cv::Point(src1.cols - 700 - phase_shift.x, result.cols), cv::Scalar(0, 0, 255));

基于lineblender算法进行融合,算法原理是非常简单的,但是如果想正确的实现,需要清晰的思路。

//图像融合
leftHalf.convertTo(leftHalf, CV_32FC3, 1.0 / 255);
rightHalf.convertTo(rightHalf, CV_32FC3, 1.0 / 255);
result.convertTo(result, CV_32FC3, 1.0 / 255);
double dblend = 0.0;
int istart = src1.cols - 700 - phase_shift.x ;//col的初始定位
for (int i = 0; i < 100; i++)
{
//使用col,必须保证row是一样的
result.col(istart + i) = leftHalf.col(istart + i)*(1-dblend)+ rightHalf.col(i)*dblend;
dblend = dblend + 0.01;
}
leftHalf.convertTo(leftHalf, CV_32FC3, 1.0 / 255);
rightHalf.convertTo(rightHalf, CV_32FC3, 1.0 / 255);
result.convertTo(result, CV_32FC3, 1.0 / 255);
double dblend = 0.0;
int istart = src1.cols - 700 - phase_shift.x ;//col的初始定位
for (int i = 0; i < 100; i++)
{
//使用col,必须保证row是一样的
result.col(istart + i) = leftHalf.col(istart + i)*(1-dblend)+ rightHalf.col(i)*dblend;
dblend = dblend + 0.01;
}
能够得到较好的效果。
三、扩展分析
1、在融合结果的中心区域,能够获得较好的结果:

这里的融合,几乎看不错错误。但是在边缘的部分,则错误比较明显:

出现这个错误的原因,并不是原始图片发生了偏转,而是图片在采集的时候发生了“镜像失真”。在图像处理之前,首先要进行更为细致的标定,这应该也是工业化必须的步骤。
2、多图片批量拼接。
比如之前遇到过的显微镜问题:
虽然其原理可以简介本例算法,但是在涉及到“横向”“竖向”拼接的时候,算法的复杂度将指数级上升,写出更有弹性的代码、处理大像素的图片,都是非常考验能力的。
四、项目小结
总的来说,这里提出了一种关于行业模式下拼接的新方法,是有效的,值得在实际的项目中继续研究。而如果能够指导获得高质量的原始图片,也是非常有价值的。
相关参考:
2、LogPolarFFTTemplateMatcher https://github.com/Smorodov/LogPolarFFTTemplateMatcher
附件列表:
链接:https://pan.baidu.com/s/1qrgOK7r-xmRvtyoLniZdGA
提取码:o8hg