線結(jié)構(gòu)光三維重建(一)https://blog.csdn.net/beyond951/article/details/125771158????????
????????上文主要對線激光的三角測量原理、光平面的標(biāo)定方法和激光條紋提取的方法進(jìn)行了一個簡單的介紹,本文則主要針對線激光三維重建系統(tǒng)的系統(tǒng)參數(shù)標(biāo)定進(jìn)行闡述,同時對采集到的圖片進(jìn)行標(biāo)定。本文主要涉及到的幾個重難點(diǎn):相機(jī)標(biāo)定、激光條紋提取、光平面的標(biāo)定和坐標(biāo)系變換的理解。
相機(jī)標(biāo)定
????????本博客有多篇文章詳細(xì)闡述了相機(jī)標(biāo)定的理論推導(dǎo)過程,可詳細(xì)參考下面鏈接文章的推導(dǎo)和實(shí)現(xiàn)。
北郵魯鵬老師三維重建課程之相機(jī)標(biāo)定https://blog.csdn.net/beyond951/article/details/122201757?spm=1001.2014.3001.5501相機(jī)標(biāo)定-機(jī)器視覺基礎(chǔ)(理論推導(dǎo)、Halcon和OpenCV相機(jī)標(biāo)定)https://blog.csdn.net/beyond951/article/details/126435661?spm=1001.2014.3001.5502????????
????????最后標(biāo)定的結(jié)果如下,標(biāo)定過程中尋找棋盤格角點(diǎn)的檢測圖、標(biāo)定的相機(jī)內(nèi)參、經(jīng)過內(nèi)參矯正的圖片和對應(yīng)標(biāo)定板生成的外參。
?
激光條紋提取
????????本文采用激光條紋提取方式有灰度重心法和Steger算法,對激光條紋的中心點(diǎn)進(jìn)行提取。
灰度重心法
vector<Point2f> GetLinePointsGrayWeight(Mat& src, int gray_Thed, int Min, int Max, int Type)
{
vector<Point2f> points_vec;
if (Type == 0)
{
Min = Min < 0 ? 0 : Min;
Max = Max > src.rows ? src.rows : Max;
for (int i = 0; i < src.cols; i++)
{
float X0 = 0, Y0 = 0;
for (int j = Min; j < Max; j++)
{
if (src.at<ushort>(j, i) > gray_Thed)
{
X0 += src.at<ushort>(j, i) * j;
Y0 += src.at<ushort>(j, i);
}
}
if (Y0 != 0)
{
//Point p = Point(i, X0 / Y0);
points_vec.push_back(Point2f(i, X0 / (float)Y0));
}
else
{
//points_vec.push_back(Point2f(i, -1));
}
}
}
else
{
Min = Min < 0 ? 0 : Min;
Max = Max > src.cols ? src.cols : Max;
for (int i = 0; i < src.rows; i++)
{
int X0 = 0, Y0 = 0;
for (int j = Min; j < Max; j++)
{
if (src.at<Vec3b>(i, j)[0] > gray_Thed)
{
X0 += src.at<Vec3b>(i, j)[0] * j;
Y0 += src.at<Vec3b>(i, j)[0];
}
}
if (Y0 != 0)
{
points_vec.push_back(Point2f(X0 / (float)Y0, i));
}
else
{
points_vec.push_back(Point2f(-1, i));
}
}
}
return points_vec;
}
Steger算法
vector<Point2f> GetLinePoints_Steger(Mat& src, int gray_Thed, int Min, int Max, int Type)
{
Mat srcGray, srcGray1;
cvtColor(src, srcGray1, CV_BGR2GRAY);
//高斯濾波
srcGray = srcGray1.clone();
srcGray.convertTo(srcGray, CV_32FC1);
GaussianBlur(srcGray, srcGray, Size(0, 0), 6, 6);
//一階偏導(dǎo)數(shù)
Mat m1, m2;
m1 = (Mat_<float>(1, 2) << 1, -1);//x方向的偏導(dǎo)數(shù)
m2 = (Mat_<float>(2, 1) << 1, -1);//y方向的偏導(dǎo)數(shù)
Mat dx, dy;
filter2D(srcGray, dx, CV_32FC1, m1);
filter2D(srcGray, dy, CV_32FC1, m2);
//二階偏導(dǎo)數(shù)
Mat m3, m4, m5;
m3 = (Mat_<float>(1, 3) << 1, -2, 1); //二階x偏導(dǎo)
m4 = (Mat_<float>(3, 1) << 1, -2, 1); //二階y偏導(dǎo)
m5 = (Mat_<float>(2, 2) << 1, -1, -1, 1); //二階xy偏導(dǎo)
Mat dxx, dyy, dxy;
filter2D(srcGray, dxx, CV_32FC1, m3);
filter2D(srcGray, dyy, CV_32FC1, m4);
filter2D(srcGray, dxy, CV_32FC1, m5);
//hessian矩陣
double maxD = -1;
vector<double> Pt;
vector<Point2f> points_vec;
if (Type == 0)
{
Min = Min < 0 ? 0 : Min;
Max = Max > src.rows ? src.rows : Max;
for (int i = 0; i < src.cols; i++)
{
for (int j = Min; j < Max; j++)
{
if (srcGray.at<uchar>(j, i) > gray_Thed)
{
Mat hessian(2, 2, CV_32FC1);
hessian.at<float>(0, 0) = dxx.at<float>(j, i);
hessian.at<float>(0, 1) = dxy.at<float>(j, i);
hessian.at<float>(1, 0) = dxy.at<float>(j, i);
hessian.at<float>(1, 1) = dyy.at<float>(j, i);
Mat eValue;
Mat eVectors;
eigen(hessian, eValue, eVectors);
double nx, ny;
double fmaxD = 0;
if (fabs(eValue.at<float>(0, 0)) >= fabs(eValue.at<float>(1, 0))) //求特征值最大時對應(yīng)的特征向量
{
nx = eVectors.at<float>(0, 0);
ny = eVectors.at<float>(0, 1);
fmaxD = eValue.at<float>(0, 0);
}
else
{
nx = eVectors.at<float>(1, 0);
ny = eVectors.at<float>(1, 1);
fmaxD = eValue.at<float>(1, 0);
}
double t = -(nx*dx.at<float>(j, i) + ny*dy.at<float>(j, i)) / (nx*nx*dxx.at<float>(j, i) + 2 * nx*ny*dxy.at<float>(j, i) + ny*ny*dyy.at<float>(j, i));
if (fabs(t*nx) <= 0.5 && fabs(t*ny) <= 0.5)
{
Pt.push_back(i);
Pt.push_back(j);
}
}
}
}
}
else
{
Min = Min < 0 ? 0 : Min;
Max = Max > src.cols ? src.cols : Max;
for (int i = Min; i<Max; i++)
{
for (int j = 0; j<src.rows; j++)
{
if (srcGray.at<uchar>(j, i) > gray_Thed)
{
Mat hessian(2, 2, CV_32FC1);
hessian.at<float>(0, 0) = dxx.at<float>(j, i);
hessian.at<float>(0, 1) = dxy.at<float>(j, i);
hessian.at<float>(1, 0) = dxy.at<float>(j, i);
hessian.at<float>(1, 1) = dyy.at<float>(j, i);
Mat eValue;
Mat eVectors;
eigen(hessian, eValue, eVectors);
double nx, ny;
double fmaxD = 0;
if (fabs(eValue.at<float>(0, 0)) >= fabs(eValue.at<float>(1, 0))) //求特征值最大時對應(yīng)的特征向量
{
nx = eVectors.at<float>(0, 0);
ny = eVectors.at<float>(0, 1);
fmaxD = eValue.at<float>(0, 0);
}
else
{
nx = eVectors.at<float>(1, 0);
ny = eVectors.at<float>(1, 1);
fmaxD = eValue.at<float>(1, 0);
}
double t = -(nx*dx.at<float>(j, i) + ny*dy.at<float>(j, i)) / (nx*nx*dxx.at<float>(j, i) + 2 * nx*ny*dxy.at<float>(j, i) + ny*ny*dyy.at<float>(j, i));
if (fabs(t*nx) <= 0.5 && fabs(t*ny) <= 0.5)
{
Pt.push_back(i);
Pt.push_back(j);
}
}
}
}
}
for (int k = 0; k<Pt.size() / 2; k++)
{
points_vec[k].x = Pt[2 * k + 0];
points_vec[k].y = Pt[2 * k + 1];
}
return points_vec;
}
steger參考的網(wǎng)上算法需要調(diào)試幾個參數(shù)?;叶戎匦姆ǖ奶崛D像如下圖所示:
空間中兩條直線可以確定一個平面,這里用了四副標(biāo)板圖以及上面的激光輪廓??蓪⑵渲幸粔K標(biāo)板的參考坐標(biāo)系作為世界坐標(biāo)系,這里將第一塊標(biāo)板的坐標(biāo)系當(dāng)做世界坐標(biāo)系。另外三幅標(biāo)板的作用在于將激光條紋提取的輪廓點(diǎn)根據(jù)相應(yīng)的標(biāo)板外參轉(zhuǎn)到相機(jī)坐標(biāo)系下,再由相機(jī)坐標(biāo)和標(biāo)板1的外參可將所有的輪廓點(diǎn)坐標(biāo)轉(zhuǎn)到以標(biāo)板1為參考系的世界坐標(biāo)。
上面這段話是理解整個標(biāo)定、重構(gòu)過程的精髓,理解了這段話,整個系統(tǒng)的理解就很簡單。
?標(biāo)板投射的激光圖,為避免棋盤格的影響,將整個圖像的亮度調(diào)低。
?提取出來的激光輪廓圖,為綠顏色的線條
?光平面標(biāo)定
首先加載各副標(biāo)板圖對應(yīng)的外參,并保存下來;
讀取激光條紋圖,先根據(jù)相機(jī)標(biāo)定的內(nèi)參結(jié)果對激光輪廓圖進(jìn)行矯正;
矯正完后,基于灰度重心法對激光條紋輪廓進(jìn)行提取,將提取的點(diǎn)存在vector;
將各副對應(yīng)標(biāo)板圖的平面零點(diǎn)和法向量,(一個平面可由平面上的一點(diǎn)和法向量表示),根據(jù)其對應(yīng)的標(biāo)板外參轉(zhuǎn)換到相機(jī)坐標(biāo)系下,即轉(zhuǎn)換后的標(biāo)板平面為相機(jī)坐標(biāo)系下的平面;
將提取的點(diǎn)由圖像坐標(biāo)系轉(zhuǎn)為相機(jī)坐標(biāo)系下,將該點(diǎn)和相機(jī)光心(相機(jī)坐標(biāo)系的原點(diǎn))連線,可得到一條空間直線;
求解空間直線和平面的交點(diǎn)即為光平面上的點(diǎn)。
看完理清上面這一段話,再看博文一更好理解。
線結(jié)構(gòu)光三維重建(一)https://blog.csdn.net/beyond951/article/details/125771158?spm=1001.2014.3001.5502
//相機(jī)標(biāo)定完,保存下來各副標(biāo)板圖的外參
//讀取外參矩陣
for (int i = 0; i < ImgSource_Vec.size(); i++)
{
Mat rotation_matrix, translation_vectors;
File_Help.Load_RT_Params(".\\image\\source\\" + ImgSource_Vec[i] + ".xml", rotation_matrix, translation_vectors);
R_Vec.push_back(rotation_matrix);
T_Vec.push_back(translation_vectors);
}
for (int i = 0; i < ImgSource_Vec.size(); i++)
{
//先根據(jù)標(biāo)定的內(nèi)參矯正圖像
Mat line_src = imread(Line_path + ImgSource_Vec[i] + ".bmp");
line_src = Api.Correct_Img(line_src, intrin_matrix, distort_coeffs);
//灰度重心法提取激光條紋輪廓點(diǎn)
vector<Point2f> Line_Points_Vec = Api.GetLinePoints_GrayWeight(line_src, 190, 1600, 2000, 1);
//相機(jī)坐標(biāo)系下標(biāo)板的零點(diǎn)
//相機(jī)坐標(biāo)系下平面法向量
//將世界坐標(biāo)系下標(biāo)板的零點(diǎn)和平面法向量轉(zhuǎn)換到對應(yīng)的相機(jī)坐標(biāo)系下
Mat Word_Zreo = (Mat_<double>(3, 1) << 0, 0, 0);
Mat cam_Zero = R_Vec[i] * Word_Zreo + T_Vec[i];
Mat Plane_N = (Mat_<double>(3, 1) << 0, 0, 1);
Mat cam_Plane_N = R_Vec[i] * Plane_N + T_Vec[i];
for each (Point2f L_Point in Line_Points_Vec)
{
//計算激光條紋點(diǎn)在成像面投影點(diǎn)
Mat Cam_XYZ = (Mat_<double>(3, 1) << (L_Point.x - u)* 0.0024, (L_Point.y - v)*0.0024, f);
//計算相機(jī)光心和激光條紋點(diǎn)在成像面投影點(diǎn)的連線和標(biāo)板平面的交點(diǎn)
Point3f point_Cam = Api.CalPlaneLineIntersectPoint(Vec3d(cam_Plane_N.at<double>(0, 0) - cam_Zero.at<double>(0, 0), cam_Plane_N.at<double>(1, 0) - cam_Zero.at<double>(1, 0), cam_Plane_N.at<double>(2, 0) - cam_Zero.at<double>(2, 0)), Point3f(cam_Zero.at<double>(0, 0), cam_Zero.at<double>(1, 0), cam_Zero.at<double>(2, 0)), Vec3f(Cam_XYZ.at<double>(0, 0), Cam_XYZ.at<double>(1, 0), Cam_XYZ.at<double>(2, 0)), Point3f(0, 0, 0));
//將求得點(diǎn)進(jìn)行保存
Plane_Points_Vec.push_back(point_Cam);
}
}
//保存點(diǎn)云
ofstream fout(".\\image\\word_cor_points.txt");
for each(Point3f point in Plane_Points_Vec)
{
fout << point.x << " " << point.y << " " << point.z << endl;
}
//根據(jù)上面得到點(diǎn)擬合光平面
float PlaneLight[4];
Api.Fit_Plane(Plane_Points_Vec, PlaneLight);
//平面誤差
float ems = Api.Get_Plane_Dist(Plane_Points_Vec, PlaneLight);
Mat Plane_V = (Mat_<double>(4, 1) << PlaneLight[0], PlaneLight[1], PlaneLight[2], PlaneLight[3]);
交比不變性標(biāo)定
首先加載標(biāo)板圖對應(yīng)的外參參數(shù);
讀取對應(yīng)的標(biāo)板圖像,進(jìn)行角點(diǎn)檢測,將角點(diǎn)按行存儲,并擬合直線(藍(lán)線);
讀取對應(yīng)的激光條紋投射的標(biāo)板圖,提取激光輪廓點(diǎn),并進(jìn)行直線擬合(黃線),求擬合直線和棋盤格行直線的交點(diǎn)(紅點(diǎn));
求得的交點(diǎn)圖像坐標(biāo)已知,根據(jù)交比不變性,圖像坐標(biāo)系下的交并復(fù)比和標(biāo)板坐標(biāo)系下的交并復(fù)比相等,求得交點(diǎn)在標(biāo)板上的坐標(biāo);
將求得交點(diǎn)在標(biāo)板上的坐標(biāo)根據(jù)標(biāo)板對應(yīng)的外參轉(zhuǎn)到相機(jī)坐標(biāo)系下;
將多副激光標(biāo)板求得點(diǎn)進(jìn)行平面擬合得到其在相機(jī)坐標(biāo)系下的光平面。
//讀取外參矩陣
for (int i = 0; i < ImgSource_Vec.size(); i++)
{
Mat rotation_matrix, translation_vectors;
File_Help.Load_RT_Params(".\\image\\source\\" + ImgSource_Vec[i] + ".xml", rotation_matrix, translation_vectors);
R_Vec.push_back(rotation_matrix);
T_Vec.push_back(translation_vectors);
}
for (int i = 0; i < ImgSource_Vec.size(); i++)
{
//讀取標(biāo)板圖像
Mat src = imread(Chess_path + ImgSource_Vec[i] + ".bmp");
//根據(jù)標(biāo)定的相機(jī)內(nèi)參先對圖像進(jìn)行校正
Mat Correct = Api.Correct_Img(src, intrin_matrix, distort_coeffs);
//提取標(biāo)板圖像上面棋盤格的角點(diǎn)
vector<Point2f> corners = Api.Get_Conners(Correct, Conner_Size, ".\\image\\line_Image\\" + ImgSource_Vec[i], 0);
//標(biāo)板角點(diǎn)按行進(jìn)行直線擬合
vector<vector<Point2f>> Lines_Points;
Mat LineImg = src.clone();
vector<Vec4f> Chess_Lines;
//按行存儲
for (int j = 0; j < Conner_Size.width; j++)
{
vector<Point2f> line_points;
for (int k = 0; k < Conner_Size.height; k++)
{
line_points.push_back(corners[j + k*Conner_Size.width]);
circle(LineImg, corners[j + k*Conner_Size.width], 2, Scalar(0, 0, 255), 2, 8, 0);
}
Lines_Points.push_back(line_points);
}
//直線擬合
for each (vector<Point2f> points in Lines_Points)
{
Vec4f corner_line_fit_temp;
fitLine(points, corner_line_fit_temp, CV_DIST_L2, 0, 0.01, 0.01);
//直線擬合
Chess_Lines.push_back(corner_line_fit_temp);
Mat Temp;
Api.CVT_Gray2RGB(src, Temp);
//圖片轉(zhuǎn)換
CvPoint2D32f startpt;
CvPoint2D32f endpt;
//直線起點(diǎn)&終點(diǎn)
Api.getFitLineStartEndPt(Temp, corner_line_fit_temp, startpt, endpt);
line(LineImg, startpt, endpt, Scalar(0, 255, 0), 1);
}
//首先灰度重心法對激光條紋進(jìn)行輪廓點(diǎn)進(jìn)行提取
//根據(jù)提取到的輪廓點(diǎn)進(jìn)行直線擬合
Vec4f lightline_fitline;
//讀取標(biāo)板圖對應(yīng)的激光標(biāo)板圖
Mat line_src = imread(Line_path + ImgSource_Vec[i] + ".bmp");
//校正圖像
line_src = Api.Correct_Img(line_src, intrin_matrix, distort_coeffs);
//灰度重心法提取激光輪廓中心點(diǎn)
vector<Point2f> Line_Points_Vec = Api.GetLinePoints_GrayWeight(line_src, 254, 1700, 2300, 1);
//直線擬合
fitLine(Line_Points_Vec, lightline_fitline, CV_DIST_L12, 0, 0.01, 0.01);
Mat Temp;
//圖片轉(zhuǎn)換
Api.CVT_Gray2RGB(line_src, Temp);
CvPoint2D32f startpt;
CvPoint2D32f endpt;
//直線起點(diǎn)&終點(diǎn)
Api.getFitLineStartEndPt(Temp, lightline_fitline, startpt, endpt);
line(line_src, startpt, endpt, Scalar(0, 255, 0), 1);
//保存灰度重心法提取光條直線的圖片
imwrite(Line_path + ImgSource_Vec[i] + "_FitLine.bmp", line_src);
//棋盤格角點(diǎn)每一行擬合的直線和激光條紋擬合直線的交點(diǎn)
vector<Point2f> cross_vec;
for each (Vec4f line in Chess_Lines)
{
/*求交點(diǎn)并畫點(diǎn)保存,result.jpg存儲在工程目錄下*/
Point2f crossPoint;
crossPoint = Api.getCrossPoint(line, lightline_fitline);
cross_vec.push_back(crossPoint);
circle(LineImg, crossPoint, 3, Scalar(255, 0, 0), 2, 8, 0);
}
line(LineImg, startpt, endpt, Scalar(0, 255, 0), 1);
imwrite(".\\image\\line_Image\\" + ImgSource_Vec[i] + "_Final.bmp", LineImg);
//激光線標(biāo)板坐標(biāo)系坐標(biāo)提?。ń槐炔蛔冃裕? //根據(jù)標(biāo)板坐標(biāo)系和圖像坐標(biāo)系的交并復(fù)比不變性
//求取交點(diǎn)在靶標(biāo)坐標(biāo)系上的點(diǎn)坐標(biāo)
vector<Point3f> Cor_Points; //標(biāo)板坐標(biāo)系激光點(diǎn)
vector<Point3f> Cam_Points; //相機(jī)坐標(biāo)系激光點(diǎn)
for (int m = 0; m < Lines_Points.size(); m++)
{
vector<Point2f> Chess_points = Lines_Points[m]; //圖像坐標(biāo)系:標(biāo)板直線角點(diǎn)
//圖像坐標(biāo)系的交并復(fù)比
double AC = Api.Point2Point_Dist(Chess_points[10], Chess_points[5]);
double AD = Api.Point2Point_Dist(Chess_points[10], Chess_points[0]);
double BC = Api.Point2Point_Dist(cross_vec[m], Chess_points[5]);
double BD = Api.Point2Point_Dist(cross_vec[m], Chess_points[0]);
double SR = (AC / BC) / (AD / BD);
//標(biāo)板坐標(biāo)系的交并復(fù)比
//棋盤格一格距離代表2mm
double ac = (10 - 5) * 2;
double ad = (10 - 0) * 2;
double X1 = (ad*SR * 2 * 5 - ac * 2 * 0) / (ad*SR - ac);
AC = Api.Point2Point_Dist(Chess_points[10], Chess_points[5]);
AD = Api.Point2Point_Dist(Chess_points[10], Chess_points[4]);
BC = Api.Point2Point_Dist(cross_vec[m], Chess_points[5]);
BD = Api.Point2Point_Dist(cross_vec[m], Chess_points[4]);
SR = (AC / BC) / (AD / BD);
ac = (10 - 5) * 2;
ad = (10 - 4) * 2;
double X2 = (ad*SR * 2 * 5 - ac * 2 * 4) / (ad*SR - ac);
float x = (X1 + X2) / 2.0f;
Point3f Point = Point3f(x, m * 2, 0);
Cor_Points.push_back(Point);
//將標(biāo)板上的交點(diǎn)坐標(biāo)根據(jù)標(biāo)板對應(yīng)的外參轉(zhuǎn)到相機(jī)坐標(biāo)系
Mat cam_Point = (Mat_<double>(3, 1) << x, m * 2, 0);
Mat Trans_Mat = (Mat_<double>(3, 1) << 0, 0, 0);
Trans_Mat = R_Vec[i] * cam_Point + T_Vec[i];
Point3f Trans_Point = Point3f(Trans_Mat.at<double>(0, 0), Trans_Mat.at<double>(1, 0), Trans_Mat.at<double>(2, 0));
Plane_Points_Vec.push_back(Trans_Point);
}
}
//根據(jù)點(diǎn)進(jìn)行平面擬合
float PlaneLight[4];
Api.Fit_Plane(Plane_Points_Vec, PlaneLight);
//平面擬合的誤差
float ems = Api.Get_Plane_Dist(Plane_Points_Vec, PlaneLight);
最后得到的光平面如下文章來源:http://www.zghlxwxcb.cn/news/detail-492234.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-492234.html
到了這里,關(guān)于線結(jié)構(gòu)光三維重建(二)相機(jī)標(biāo)定、光平面標(biāo)定的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!