单目标定分析+外参数求解+重投影误差验证
可以验证 RANSAC + PNP + BA 对单目标定中重投影误差的计算和 opencv 中 calibrateCamera输出的误差一模一样!!!,当然你也可以用Projection直接计算重投影误差。
看代码,此代码改编自opencv安装目录下 “calibration.cpp”,这里我以三种方式打印每张棋盘的重投影误差:
1、calibrateCamera 输出的每张图的重投影误差(cv::Mat perViewErrors 中每一行代表一张棋盘的重投影误差)
2、借助 calibrateCamera 输出的棋盘外参数 rvecs, tvecs 和 projectPoints()计算重投影点,接着计算每张棋盘重投影误差
3、借助 solvePnPRansac + solvePnPRefineLM 求得 rvecs, tvecs,接着..同2
你看看输出,我测了三组棋盘(SB VS2019让第二组图挂了,我这里暂时上传两组图,请放大看,或者下载看)
其实在实验过程中, solvePnPRansac的作用在于梯度下降过程中取一个好的起点。如果我把solvePnPRansac 参数误差阈值 float reprojectionError 设置得非常小,我们从 inliers 可知内点比例不是100%,甚至不到50%。这时候,起点选的不够好,容易陷入极小值。ransanc的作用在于选择最优起点,所以我们应该把迭代次数设置得越高越好,把门槛误差设置得越松(大)越好。而在梯度下降过程中,迭代次数也要要高,至于门槛误差,,那当然是要无限小最好。
1 #define _CRT_SECURE_NO_WARNINGS 2 #include <iostream> // cout 3 #include<fstream> 4 #include <algorithm> // std::sort 5 using namespace std; 6 using std::string; 7 using std::cout; 8 using std::cerr; 9 using std::endl; 10 11 #include <opencv2/opencv.hpp> 12 using namespace cv; 13 14 void readFileNames(const string& fileList, vector<string>& fileNames) 15 { 16 fstream txt; 17 txt.open(fileList, ios::in); 18 while (!txt.eof()) 19 { 20 string s; 21 getline(txt, s); 22 if (!s.empty()) 23 { 24 fileNames.push_back(s); 25 } 26 } 27 txt.close(); 28 return; 29 } 30 31 // camera -> pixel 32 // (X Y Z)-> (u v) 33 void CameraToPixel(const Point3d& pt3d, const Mat& M, Point2d& pt2d) 34 { 35 double fx = M.at<double>(0, 0); 36 double fy = M.at<double>(1, 1); 37 double cx = M.at<double>(0, 2); 38 double cy = M.at<double>(1, 2); 39 pt2d.x = fx * pt3d.x / pt3d.z + cx; 40 pt2d.y = fy * pt3d.y / pt3d.z + cy; 41 } 42 43 int main(int argc, const char* argv[]) 44 { 45 // 第一组棋盘 46 //string folder = "calibration1\\";// 47 //string fileList = "filelist_1.txt"; 48 49 //float image_sf = 1.f; // 后面要resize 50 //int delay = 250; // 延时,方便观察 51 //int board_w = 12; 52 //int board_h = 12; 53 54 // 第二组棋盘 55 string folder = "calibration2\\";// 56 string fileList = "filelist_2.txt"; 57 58 float image_sf = 1.f; // 后面要resize 59 int delay = 250; // 延时,方便观察 60 int board_w = 7; 61 int board_h = 5; 62 63 // 第三组棋盘 64 //string folder = "calibration3\\";// 65 //string fileList = "filelist_3.txt"; 66 67 //float image_sf = 1.0; // 后面要resize 68 //int delay = 10; // 延时,方便观察 69 //int board_w = 9; 70 //int board_h = 6; 71 72 vector<string> fileNames; 73 readFileNames(fileList, fileNames); 74 int num_files = (int)fileNames.size(); 75 cout << " ... Done. Number of files = " << num_files << "\n" << endl; 76 int n_boards = num_files; 77 78 int board_n = board_w * board_h; // 一张图棋盘点数量 79 cv::Size board_sz = cv::Size(board_w, board_h); //棋盘的是12X12 80 81 // 提取样本点 82 vector<vector<cv::Point2f> > image_points; 83 vector<vector<cv::Point3f> > object_points; 84 85 // <1>、检测2D角点、指定3D棋盘点 86 cv::Size image_size; 87 int board_count = 0; 88 for (size_t i = 0; (i < fileNames.size()) && (board_count < n_boards); ++i) { 89 cv::Mat image, image0 = cv::imread(folder + fileNames[i]); 90 board_count += 1; 91 if (!image0.data) 92 { 93 cerr << folder + fileNames[i] << ", file #" << i << ", is not an image" << endl; 94 continue; 95 } 96 image_size = image0.size(); 97 cv::resize(image0, image, cv::Size(), image_sf, image_sf, cv::INTER_LINEAR);//这里是在干鸡毛??? 98 99 // 在图上找到棋盘 100 101 vector<cv::Point2f> corners; 102 bool found = cv::findChessboardCorners(image, board_sz, corners); 103 104 // 画出来 105 drawChessboardCorners(image, board_sz, corners, found); // 当然是找到了才能画出来 106 107 // 如果找到了,就可以提取2D焦点了 108 if (found) { 109 image ^= cv::Scalar::all(255); // 异或运算 110 cv::Mat mcorners(corners); 111 112 // do not copy the data 113 mcorners *= (1.0 / image_sf); // 角点坐标也要缩放 114 115 // 缩放角点坐标 116 image_points.push_back(corners); 117 object_points.push_back(vector<cv::Point3f>());// malloc? 118 vector<cv::Point3f>& opts = object_points.back(); 119 120 opts.resize(board_n); 121 for (int j = 0; j < board_n; j++) { 122 opts[j] = cv::Point3f(static_cast<float>(j / board_w),static_cast<float>(j % board_w), 0.0f); 123 } 124 cout << "Collected " << static_cast<int>(image_points.size()) 125 << "total boards. This one from chessboard image #" 126 << i << ", " << folder + fileNames[i] << endl; 127 } 128 cv::namedWindow("Calibration", cv::WINDOW_NORMAL); 129 cv::imshow("Calibration", image); 130 131 // show in color if we did collect the image 132 if ((cv::waitKey(delay) & 255) == 27) { 133 return -1; 134 } 135 } 136 137 cv::destroyWindow("Calibration"); 138 cout << "\n\n*** CALIBRATING THE CAMERA...\n" << endl; 139 140 // <2>、调用算法开始标定 141 cv::Mat intrinsic_matrix, distortion_coeffs; 142 std::vector<cv::Mat> rvecs; 143 std::vector<cv::Mat> tvecs; 144 cv::Mat perViewErrors;// 每一张棋盘的重投影误差 145 double err = cv::calibrateCamera( object_points, image_points, image_size, intrinsic_matrix, distortion_coeffs, 146 rvecs, tvecs, cv::noArray(), cv::noArray(), perViewErrors/*, // 我劝你不会用这些参数,就不要设置了 147 cv::CALIB_ZERO_TANGENT_DIST | cv::CALIB_FIX_PRINCIPAL_POINT*/); 148 149 double fx = intrinsic_matrix.at<double>(0, 0); 150 double fy = intrinsic_matrix.at<double>(1, 1); 151 double cx = intrinsic_matrix.at<double>(0, 2); 152 double cy = intrinsic_matrix.at<double>(1, 2); 153 154 // <2.1> 验证 perViewErrors 155 cout << "err = " << endl; 156 cout << err << endl; 157 cout << "sum(perViewErrors) / rows =" << endl; 158 double s1 = 0.; 159 for (size_t i = 0; i < perViewErrors.rows; i++) 160 { 161 s1 += std::pow(perViewErrors.at<double>((int)i, 0), 2); 162 } 163 cout << std::sqrt(s1 / perViewErrors.rows) << endl; 164 cout << "---------------" << endl; 165 166 // <2.2> 验证重投影误差 167 cout << "每张棋盘重投影误差【calibrateCamera输出】:" << endl; 168 cout << perViewErrors.t() << endl; 169 170 // 遍历所有棋盘(利用张正友棋盘法估算的外参) 171 cout << "每张棋盘重投影误差【利用外参计算】:" << endl; 172 for (size_t k = 0; k < object_points.size(); k++) 173 { 174 vector<Point2f> pts2d1; // [u' v'] 重投影后的点 175 double perViewError1 = 0.; 176 projectPoints(Mat(object_points[k]), rvecs[k], tvecs[k], intrinsic_matrix, distortion_coeffs, pts2d1); 177 perViewError1 = norm(Mat(image_points[k]), Mat(pts2d1), NORM_L2); 178 cout << std::sqrt(perViewError1 * perViewError1/ (int)pts2d1.size()) << " "; 179 // // 外参 180 // Mat rvec = rvecs[k]; 181 // Mat t = tvecs[k]; 182 // Mat R; 183 // Rodrigues(rvec, R); 184 // // T = (R|t) 185 // cv::Affine3d T( 186 // cv::Affine3d::Mat3( R.at<double>(0, 0), R.at<double>(0, 1), R.at<double>(0, 2), 187 // R.at<double>(1, 0), R.at<double>(1, 1), R.at<double>(1, 2), 188 // R.at<double>(2, 0), R.at<double>(2, 1), R.at<double>(2, 2)), 189 // cv::Affine3d::Vec3(t.at<double>(0, 0), t.at<double>(1, 0), t.at<double>(2, 0))); 190 191 ///* cout << T.rotation() << endl; 192 // cout << T.translation() << endl;*/ 193 194 // // 遍历一张棋盘中所有点 195 // for (size_t i = 0; i < object_points[k].size(); i++) 196 // { 197 // Point3d ptR3d = (Point3d)object_points[k][i]; 198 // Point2d ptL2d = (Point2d) image_points[k][i]; 199 200 // // <1> 仿射 201 // Point3d ptL3d = T * ptR3d; 202 203 // // <2> 投影 + 畸变矫正 204 // Point2d ptL2d_; 205 // CameraToPixel(ptL3d, intrinsic_matrix, ptL2d_); 206 // undistortPoints(); 207 208 // 209 // } 210 //cv::projectPoints 211 } 212 213 // <2.3> 外参数验证 214 double sum_errs_t = 0.; 215 Mat rvec1, t1, inliers; 216 // 遍历所有棋盘 217 cout << endl; 218 cout << "每张棋盘重投影误差【非线性优化】:" << endl; 219 for (size_t k = 0; k < object_points.size(); k++) 220 { 221 // 标定的外参数t2 222 Mat t2 = tvecs[k]; 223 //cout << "t2 = " << t2.t() << " "; 224 225 // pnp 226 // 注意调参数,如果误差阈值卡得太严格,尽量大,反正迭代会让误差收敛,可能需要在GUI做一个手动设置 227 // 当你误差设得很大的时候,此时,你需要将迭代次数射得大一些,这样才能确保得到一个好的姿态 228 // 置信度? 229 solvePnPRansac(object_points[k], image_points[k], intrinsic_matrix, distortion_coeffs, rvec1, t1, false, 300, 30.0, 0.99, inliers); // 为什么内点为空? 因为阈值太低 230 /* if (inliers.rows < ((int)object_points[k].size() / 3)) 231 { 232 cout << " 内点比例:"<< (float)inliers.rows / (int)object_points[k].size() << endl; 233 return -1; 234 }*/ 235 // TermCriteria::EPS = FLT_EPSILON,这个精度不可能达到,所以还是迭代300次 236 // TermCriteria::COUNT = 300 237 // + 表示或的关系不是与 238 solvePnPRefineLM(object_points[k], image_points[k], intrinsic_matrix, distortion_coeffs, rvec1, t1, TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 300, FLT_EPSILON)); 239 //cout << "t1 = " << t1.t() << " "; 240 241 sum_errs_t += norm(t1, t2, NORM_L2); 242 //cout << norm(t1, t2, NORM_L2) << endl;// 梯度下降后的外参数t1 与 张正友外参数差距 243 244 // 2D点重投影误差(利用solvePnPRefineLM 求解出的外参数) 245 vector<Point2f> pts2d2; // [u' v'] 重投影后的点 246 double perViewError2 = 0.; 247 projectPoints(Mat(object_points[k]), rvec1, t1, intrinsic_matrix, distortion_coeffs, pts2d2); 248 perViewError2 = norm(Mat(image_points[k]), Mat(pts2d2), NORM_L2); 249 cout << std::sqrt(perViewError2 * perViewError2 / (int)pts2d2.size()) << " "; 250 } 251 cout << endl; 252 cout << "内点数量 =" << inliers.rows << endl; 253 cout << sum_errs_t / (int)object_points.size() << endl; 254 255 256 // undistortPoints 257 /*cvProjectPoints2*/ 258 return 0; 259 }
CV&DL