From a41cbbdc99906af81a4c26b78a3f7c7f1b301e17 Mon Sep 17 00:00:00 2001 From: Alexander Duda Date: Wed, 19 Feb 2020 23:33:20 +0100 Subject: [PATCH 1/2] findChessboardCornersSB: performance + support for full FOV boards with markers Changes: * UMat for blur + rotate resulting in a speedup of around 2X on an i7 * support for boards larger than specified allowing to cover full FOV * support for markers moving the origin into the center of the board * increase detection accuracy The main change is for supporting boards that are larger than the FOV of the camera and have their origin in the board center. This allows building OEM calibration targets similar to the one from intel real sense utilizing corner points as close as possible to the image border. --- .../calib3d/doc/pics/checkerboard_radon.png | Bin 20142 -> 33771 bytes modules/calib3d/include/opencv2/calib3d.hpp | 33 +- modules/calib3d/src/chessboard.cpp | 988 +++++++++++++++--- modules/calib3d/src/chessboard.hpp | 132 ++- 4 files changed, 1013 insertions(+), 140 deletions(-) diff --git a/modules/calib3d/doc/pics/checkerboard_radon.png b/modules/calib3d/doc/pics/checkerboard_radon.png index 48d6041990317446ffe1e7a9b59e454d00f2363c..63902f7b6e2b8111f11a433f32e6762876f71aba 100644 GIT binary patch literal 33771 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 literal 20142 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 HWb^+3xwcR| diff --git a/modules/calib3d/include/opencv2/calib3d.hpp b/modules/calib3d/include/opencv2/calib3d.hpp index ced32fee9b..b05808367b 100644 --- a/modules/calib3d/include/opencv2/calib3d.hpp +++ b/modules/calib3d/include/opencv2/calib3d.hpp @@ -257,7 +257,9 @@ enum { CALIB_CB_ADAPTIVE_THRESH = 1, CALIB_CB_FILTER_QUADS = 4, CALIB_CB_FAST_CHECK = 8, CALIB_CB_EXHAUSTIVE = 16, - CALIB_CB_ACCURACY = 32 + CALIB_CB_ACCURACY = 32, + CALIB_CB_LARGER = 64, + CALIB_CB_MARKER = 128 }; enum { CALIB_CB_SYMMETRIC_GRID = 1, @@ -1237,7 +1239,16 @@ CV_EXPORTS_W bool checkChessboard(InputArray img, Size size); - **CALIB_CB_NORMALIZE_IMAGE** Normalize the image gamma with equalizeHist before detection. - **CALIB_CB_EXHAUSTIVE** Run an exhaustive search to improve detection rate. - **CALIB_CB_ACCURACY** Up sample input image to improve sub-pixel accuracy due to aliasing effects. +- **CALIB_CB_LARGER** The detected pattern is allowed to be larger than patternSize (see description). +- **CALIB_CB_MARKER** The detected pattern must have a marker (see description). This should be used if an accurate camera calibration is required. +@param meta Optional output arrray of detected corners (CV_8UC1 and size = cv::Size(columns,rows)). +Each entry stands for one corner of the pattern and can have one of the following values: +- 0 = no meta data attached +- 1 = left-top corner of a black cell +- 2 = left-top corner of a white cell +- 3 = left-top corner of a black cell with a white marker dot +- 4 = left-top corner of a white cell with a black marker dot (pattern origin in case of markers otherwise first corner) The function is analog to findchessboardCorners but uses a localized radon transformation approximated by box filters being more robust to all sort of @@ -1248,6 +1259,15 @@ Calibration" demonstrating that the returned sub-pixel positions are more accurate than the one returned by cornerSubPix allowing a precise camera calibration for demanding applications. +In the case, the flags **CALIB_CB_LARGER** or **CALIB_CB_MARKER** are given, +the result can be recovered from the optional meta array. Both flags are +helpful to use calibration patterns exceeding the field of view of the camera. +These oversized patterns allow more accurate calibrations as corners can be +utilized, which are as close as possible to the image borders. For a +consistent coordinate system across all images, the optional marker (see image +below) can be used to move the origin of the board to the location where the +black circle is located. + @note The function requires a white boarder with roughly the same width as one of the checkerboard fields around the whole board to improve the detection in various environments. In addition, because of the localized radon @@ -1257,7 +1277,16 @@ a sample checkerboard optimized for the detection. However, any other checkerboa can be used as well. ![Checkerboard](pics/checkerboard_radon.png) */ -CV_EXPORTS_W bool findChessboardCornersSB(InputArray image,Size patternSize, OutputArray corners,int flags=0); +CV_EXPORTS_AS(findChessboardCornersSBWithMeta) +bool findChessboardCornersSB(InputArray image,Size patternSize, OutputArray corners, + int flags,OutputArray meta); +/** @overload */ +CV_EXPORTS_W static inline +bool findChessboardCornersSB(InputArray image, Size patternSize, OutputArray corners, + int flags = 0) +{ + return findChessboardCornersSB(image, patternSize, corners, flags, noArray()); +} //! finds subpixel-accurate positions of the chessboard corners CV_EXPORTS_W bool find4QuadCornerSubpix( InputArray img, InputOutputArray corners, Size region_size ); diff --git a/modules/calib3d/src/chessboard.cpp b/modules/calib3d/src/chessboard.cpp index 38051b7627..2b0b0a5eb8 100644 --- a/modules/calib3d/src/chessboard.cpp +++ b/modules/calib3d/src/chessboard.cpp @@ -32,6 +32,7 @@ static const int MAX_SYMMETRY_ERRORS = 5; // maximal numbe ///////////////////////////////////////////////////////////////////////////// // some helper methods +static float calcSharpness(cv::InputArray _values,float rise_distance); static bool isPointOnLine(cv::Point2f l1,cv::Point2f l2,cv::Point2f pt,float min_angle); static int testPointSymmetry(const cv::Mat& mat,cv::Point2f pt,float dist,float max_error); static float calcSubpixel(const float &x_l,const float &x,const float &x_r); @@ -40,6 +41,94 @@ static void polyfit(const Mat& src_x, const Mat& src_y, Mat& dst, int order); static float calcSignedDistance(const cv::Vec2f &n,const cv::Point2f &a,const cv::Point2f &pt); static void normalizePoints1D(cv::InputArray _points,cv::OutputArray _T,cv::OutputArray _new_points); static cv::Mat findHomography1D(cv::InputArray _src,cv::InputArray _dst); +static cv::Mat normalizeVector(cv::InputArray _points); + +cv::Mat normalizeVector(cv::InputArray _points) +{ + cv::Mat points = _points.getMat(); + if(points.cols > 1) + { + if(points.rows == 1) + points = points.reshape(points.channels(),points.cols); + else if(points.channels() == 1) + points = points.reshape(points.cols,points.rows); + else + CV_Error(Error::StsBadArg, "unsupported format"); + } + return points; +} + +float calcSharpness(cv::InputArray _values,float rise_distance) +{ + CV_CheckTypeEQ(_values.type(),CV_8UC1, "values must be of the type CV_8UC1"); + cv::Mat values = normalizeVector(_values); + if(values.empty()) + return 0; + if(values.rows != 1 && values.cols != 1) + CV_Error(Error::StsBadArg, "values must be 1xn or nx1"); + if(rise_distance <= 0.0 || rise_distance > 1.0) + CV_Error(Error::StsBadArg, "rise_distance must lie in th interval ]0..1]"); + + // find global min max + cv::Point min_loc,max_loc; + double min_val,max_val; + cv::minMaxLoc(values,&min_val,&max_val,&min_loc,&max_loc); + int max_pos = std::max(max_loc.x,max_loc.y); + int min_pos = std::max(min_loc.x,min_loc.y); + if(max_pos == min_pos) + return 0; + + // calc new interval according to the rise distance + double delta = max_val-min_val; + double min_val2 = min_val+delta*0.5*(1.0-rise_distance); + double max_val2 = max_val-delta*0.5*(1.0-rise_distance); + + // find new max starting at min pos + int dt = 1; + if(max_pos < min_pos) + dt= -1; + int max_pos2 = max_pos; + for(int i=min_pos+dt;i != max_pos;i+=dt) + { + uint8_t val = values.at(i); + if(val >= max_val2) + { + max_pos2 = i; + break; + } + } + + // find new min starting at max pos + int min_pos2 = min_pos; + for(int i=max_pos-dt;i != min_pos;i-=dt) + { + uint8_t val = values.at(i); + if(val <= min_val2) + { + min_pos2 = i; + break; + } + } + + // calc sub pixel max pos + double max_pos3 = max_pos2; + uint8_t val1 = values.at(max_pos2-dt); // <= val2 + uint8_t val2 = values.at(max_pos2); + double m = (val2-val1)/dt; + if(m != 0) + max_pos3 = max_pos2+(max_val2-val2)/m; + + // calc sub pixel min pos + double min_pos3 = min_pos2; + val1 = values.at(min_pos2); // <= val2 + val2 = values.at(min_pos2+dt); + m = (val2-val1)/dt; + if(m != 0) + min_pos3 = min_pos2+(min_val2-val1)/m; + + return float(fabs(max_pos3-min_pos3)); +} + void normalizePoints1D(cv::InputArray _points,cv::OutputArray _T,cv::OutputArray _new_points) { @@ -204,7 +293,7 @@ bool isPointOnLine(cv::Point2f l1,cv::Point2f l2,cv::Point2f pt,float min_angle) } // returns how many tests fails out of 10 -int testPointSymmetry(const cv::Mat& mat,cv::Point2f pt,float dist,float max_error) +int testPointSymmetry(const cv::Mat &mat,cv::Point2f pt,float dist,float max_error) { cv::Rect image_rect(int(0.5*dist),int(0.5*dist),int(mat.cols-0.5*dist),int(mat.rows-0.5*dist)); cv::Size size(int(0.5*dist),int(0.5*dist)); @@ -229,7 +318,7 @@ int testPointSymmetry(const cv::Mat& mat,cv::Point2f pt,float dist,float max_err return count; } -float calcSubpixel(const float &x_l,const float &x,const float &x_r) +inline float calcSubpixel(const float &x_l,const float &x,const float &x_r) { // prevent zero values if(x_l <= 0) @@ -248,7 +337,7 @@ float calcSubpixel(const float &x_l,const float &x,const float &x_r) return delta; } -float calcSubPos(const float &x_l,const float &x,const float &x_r) +inline float calcSubPos(const float &x_l,const float &x,const float &x_r) { float val = 2.0F *(x_l-2.0F*x+x_r); if(val == 0.0F) @@ -273,18 +362,18 @@ void FastX::reconfigure(const Parameters ¶) } // rotates the image around its center -void FastX::rotate(float angle,const cv::Mat &img,cv::Size size,cv::Mat &out)const +void FastX::rotate(float angle,cv::InputArray img,cv::Size size,cv::OutputArray out)const { if(angle == 0) { - out = img; + img.copyTo(out); return; } else { - cv::Matx23d m = cv::getRotationMatrix2D(cv::Point2f(float(img.cols*0.5),float(img.rows*0.5)),float(angle/CV_PI*180),1); - m(0,2) += 0.5*(size.width-img.cols); - m(1,2) += 0.5*(size.height-img.rows); + cv::Matx23d m = cv::getRotationMatrix2D(cv::Point2f(float(img.cols()*0.5),float(img.rows()*0.5)),float(angle/CV_PI*180),1); + m(0,2) += 0.5*(size.width-img.cols()); + m(1,2) += 0.5*(size.height-img.rows()); cv::warpAffine(img,out,m,size); } } @@ -386,52 +475,81 @@ std::vector > FastX::calcAngles(const std::vector &r // assuming all elements of the same channel const int channels = rotated_images.front().channels(); - int channels_1 = channels-1; - float resolution = float(CV_PI/channels); - - float angle; - float val1,val2,val3,wrap_around; - const unsigned char *pimages1,*pimages2,*pimages3,*pimages4; - std::vector > angles; - angles.resize(keypoints.size()); - float scale = float(parameters.super_resolution)+1.0F; + const int channels_1 = channels-1; + const float resolution = float(CV_PI/channels); + const float scale = float(parameters.super_resolution)+1.0F; // for each keypoint - std::vector::iterator pt_iter = keypoints.begin(); - for(int id=0;pt_iter != keypoints.end();++pt_iter,++id) + std::vector > angles; + angles.resize(keypoints.size()); + parallel_for_(Range(0,(int)keypoints.size()),[&](const Range& range) { - int scale_id = pt_iter->octave - parameters.min_scale; - if(scale_id>= int(rotated_images.size()) ||scale_id < 0) - CV_Error(Error::StsBadArg,"no rotated image for requested keypoint octave"); - const cv::Mat &s_rotated_images = rotated_images[scale_id]; - - float x2 = pt_iter->pt.x*scale; - float y2 = pt_iter->pt.y*scale; - int row = int(y2); - int col = int(x2); - x2 -= col; - y2 -= row; - float x1 = 1.0F-x2; float y1 = 1.0F-y2; - float a = x1*y1; float b = x2*y1; float c = x1*y2; float d = x2*y2; - pimages1 = s_rotated_images.ptr(row,col); - pimages2 = s_rotated_images.ptr(row,col+1); - pimages3 = s_rotated_images.ptr(row+1,col); - pimages4 = s_rotated_images.ptr(row+1,col+1); - std::vector &angles_i = angles[id]; - - //calc rating - val1 = a**(pimages1+channels_1)+b**(pimages2+channels_1)+ - c**(pimages3+channels_1)+d**(pimages4+channels_1); // wrap around (last value) - wrap_around = a**(pimages1++)+b**(pimages2++)+c**(pimages3++)+d**(pimages4++); // first value - val2 = wrap_around; // first value - for(int i=0;i::iterator pt_iter = keypoints.begin()+range.start; + std::vector::iterator pt_end = keypoints.begin()+range.end; + for(int id=range.start ;pt_iter != pt_end;++pt_iter,++id) { - val3 = a**(pimages1)+b**(pimages2)+c**(pimages3)+d**(pimages4); - if(val1 <= val2) + int scale_id = pt_iter->octave - parameters.min_scale; + if(scale_id>= int(rotated_images.size()) ||scale_id < 0) + CV_Error(Error::StsBadArg,"no rotated image for requested keypoint octave"); + const cv::Mat &s_rotated_images = rotated_images[scale_id]; + + float x2 = pt_iter->pt.x*scale; + float y2 = pt_iter->pt.y*scale; + int row = int(y2); + int col = int(x2); + x2 -= col; + y2 -= row; + float x1 = 1.0F-x2; float y1 = 1.0F-y2; + float a = x1*y1; float b = x2*y1; float c = x1*y2; float d = x2*y2; + pimages1 = s_rotated_images.ptr(row,col); + pimages2 = s_rotated_images.ptr(row,col+1); + pimages3 = s_rotated_images.ptr(row+1,col); + pimages4 = s_rotated_images.ptr(row+1,col+1); + std::vector &angles_i = angles[id]; + + //calc rating + val1 = a**(pimages1+channels_1)+b**(pimages2+channels_1)+ + c**(pimages3+channels_1)+d**(pimages4+channels_1); // wrap around (last value) + wrap_around = a**(pimages1++)+b**(pimages2++)+c**(pimages3++)+d**(pimages4++); // first value + val2 = wrap_around; // first value + for(int i=0;i CV_PI) + angle -= float(CV_PI); + angles_i.push_back(angle); + pt_iter->angle = 360.0F-angle*RAD2DEG; + } + } + else if(val1 > val2 && val3 >= val2) { angle = float((calcSubPos(val1,val2,val3)+i)*resolution); + if(angle < 0) + angle += float(CV_PI); + else if(angle > CV_PI) + angle -= float(CV_PI); + angles_i.push_back(-angle); + pt_iter->angle = 360.0F-angle*RAD2DEG; + } + val1 = val2; + val2 = val3; + } + // wrap around + if(val1 <= val2) + { + if(wrap_around< val2) + { + angle = float((calcSubPos(val1,val2,wrap_around)+channels-1)*resolution); if(angle < 0) angle += float(CV_PI); else if(angle > CV_PI) @@ -440,9 +558,9 @@ std::vector > FastX::calcAngles(const std::vector &r pt_iter->angle = 360.0F-angle*RAD2DEG; } } - else if(val1 > val2 && val3 >= val2) + else if(val1 > val2 && wrap_around >= val2) { - angle = float((calcSubPos(val1,val2,val3)+i)*resolution); + angle = float((calcSubPos(val1,val2,wrap_around)+channels-1)*resolution); if(angle < 0) angle += float(CV_PI); else if(angle > CV_PI) @@ -450,34 +568,8 @@ std::vector > FastX::calcAngles(const std::vector &r angles_i.push_back(-angle); pt_iter->angle = 360.0F-angle*RAD2DEG; } - val1 = val2; - val2 = val3; } - // wrap around - if(val1 <= val2) - { - if(wrap_around< val2) - { - angle = float((calcSubPos(val1,val2,wrap_around)+channels-1)*resolution); - if(angle < 0) - angle += float(CV_PI); - else if(angle > CV_PI) - angle -= float(CV_PI); - angles_i.push_back(angle); - pt_iter->angle = 360.0F-angle*RAD2DEG; - } - } - else if(val1 > val2 && wrap_around >= val2) - { - angle = float((calcSubPos(val1,val2,wrap_around)+channels-1)*resolution); - if(angle < 0) - angle += float(CV_PI); - else if(angle > CV_PI) - angle -= float(CV_PI); - angles_i.push_back(-angle); - pt_iter->angle = 360.0F-angle*RAD2DEG; - } - } + }); return angles; } @@ -517,11 +609,11 @@ void FastX::findKeyPoints(const std::vector &feature_maps, std::vector< int window_size2i = cvRound(window_size2); const cv::Mat &feature_map = feature_maps[scale-parameters.min_scale]; - int y = ((feature_map.rows)/window_size)-6; - int x = ((feature_map.cols)/window_size)-6; - for(int row=5;row images; - images.resize(2*num); - int scale_size = int(1+pow(2.0,scale+1+super_res)); + int scale_size = int(pow(2.0,scale+1+super_res)-1); int scale_size2 = int((scale_size/10)*2+1); - for(int i=0;i images; + images.resize(2*num); + cv::UMat rotated,filtered_h,filtered_v; + cv::blur(gray_image,images[0],cv::Size(scale_size,scale_size2)); + cv::blur(gray_image,images[num],cv::Size(scale_size2,scale_size)); + for(int i=1;ix)*(bottom_left->y-top_left->y) - (bottom_left->x-top_left->x)*(pt.y-top_left->y); + float l2 = (pt.x-top_right->x)*(top_left->y-top_right->y) - (top_left->x-top_right->x)*(pt.y-top_right->y); + float l3 = (pt.x-bottom_left->x)*(top_right->y-bottom_left->y) - (top_right->x-bottom_left->x)*(pt.y-bottom_left->y); + if((l1>0 && l2>0 && l3>0) || (l1<0 && l2<0 && l3<0)) + return true; + l1 = (pt.x-top_left->x)*(bottom_right->y-top_left->y) - (bottom_right->x-top_left->x)*(pt.y-top_left->y); + l2 = (pt.x-top_right->x)*(top_left->y-top_right->y) - (top_left->x-top_right->x)*(pt.y-top_right->y); + l3 = (pt.x-bottom_right->x)*(top_right->y-bottom_right->y) - (top_right->x-bottom_right->x)*(pt.y-bottom_right->y); + return (l1>0 && l2>0 && l3>0) || (l1<0 && l2<0 && l3<0); +} + bool Chessboard::Board::Cell::empty()const { // check if one of its corners has NaN @@ -780,6 +901,16 @@ int Chessboard::Board::Cell::getRow()const return row; } +cv::Point2f Chessboard::Board::Cell::getCenter()const +{ + if(empty()) + CV_Error(Error::StsBadArg,"Cell is empty"); + cv::Point2f center = *top_left+*top_right+*bottom_left+*bottom_right; + center.x /=4; + center.y /=4; + return center; +} + int Chessboard::Board::Cell::getCol()const { int col = 0; @@ -790,7 +921,7 @@ int Chessboard::Board::Cell::getCol()const Chessboard::Board::Cell::Cell() : top_left(NULL), top_right(NULL), bottom_right(NULL), bottom_left(NULL), - left(NULL), top(NULL), right(NULL), bottom(NULL),black(false) + left(NULL), top(NULL), right(NULL), bottom(NULL),black(false),marker(false) {} Chessboard::Board::PointIter::PointIter(Cell *_cell,CornerIndex _corner_index): @@ -1131,7 +1262,7 @@ Chessboard::Board::Board(const cv::Size &size, const std::vector &p if(!init(ipoints)) return; - // add all cols + // add all cols if more than 3 for(int col=3 ; col< data.cols;++col) { data(cv::Rect(col,0,1,3)).copyTo(temp); @@ -1139,7 +1270,7 @@ Chessboard::Board::Board(const cv::Size &size, const std::vector &p addColumnRight(ipoints); } - // add all rows + // add all rows if more than 3 for(int row=3; row < data.rows;++row) { data(cv::Rect(0,row,cols,1)).copyTo(temp); @@ -1153,12 +1284,76 @@ Chessboard::Board::~Board() clear(); } +void Chessboard::Board::setAngles(float white, float black) +{ + white_angle = white; + black_angle = black; +} + +float Chessboard::Board::getAngle()const +{ + if(isEmpty()) + CV_Error(Error::StsBadArg,"Board is empty"); + if(colCount() < 3) + CV_Error(Error::StsBadArg,"Board is too small"); + + cv::Point2f delta = *(top_left->right->top_right)-*(top_left->top_left); + cv::Point3f pt(delta.x,delta.y,0); + float val; + if(fabs(pt.x) > fabs(pt.y)) + { + cv::Point3f ptx(1,0,0); + val = float(ptx.dot(pt)/cv::norm(pt)); + if(val < 0) + val = -acos(val); + else + val = acos(val); + } + else + { + cv::Point3f ptx(0,1,0); + val = float(ptx.dot(pt)/cv::norm(pt)); + if(val < 0) + val = float(-acos(val)+CV_PI/2); + else + val = float(acos(val)+CV_PI/2); + } + return val; +} + +bool Chessboard::Board::isHorizontal()const +{ + double angle = getAngle(); + if((angle < 0.25*CV_PI && angle > -0.25*CV_PI) || angle > 0.75*CV_PI || angle < -0.75*CV_PI) + return true; + return false; +} + +cv::Mat Chessboard::Board::getObjectPoints(float cell_size)const +{ + cv::Mat points = Chessboard::getObjectPoints(getSize(),cell_size); + + // check for any offset due to a found marker + for(auto &&cell : cells) + { + if(cell->marker && !cell->black) + { + // apply offset + cv::Point3f offset(cell->getCol()*cell_size,cell->getRow()*cell_size,0); + for(int i =0;i < points.rows;++i) + points.at(i) -= offset; + break; + } + } + return points; +} + std::vector Chessboard::Board::getCellCenters()const { int icols = int(colCount()); int irows = int(rowCount()); if(icols < 3 || irows < 3) - throw std::runtime_error("getCellCenters: Chessboard must be at least consist of 3 rows and cols to calculate the cell centers"); + CV_Error(Error::StsBadArg,"Chessboard must be at least consist of 3 rows and cols to calculate the cell centers"); std::vector points; cv::Matx33d H(estimateHomography(DUMMY_FIELD_SIZE)); @@ -1177,6 +1372,56 @@ std::vector Chessboard::Board::getCellCenters()const return points; } +std::vector Chessboard::Board::getCells(float shrink_factor,bool bwhite,bool bblack) const +{ + std::vector result; + int icols = int(colCount()); + int irows = int(rowCount()); + if(icols < 3 || irows < 3) + return result; + + for(int row=0;rowblack) + continue; + if(!bblack && cell->black) + continue; + cv::Mat points = cv::Mat(4,1,CV_32FC2); + points.at(0) = *cell->top_left; + points.at(1) = *cell->top_right; + points.at(2) = *cell->bottom_right; + points.at(3) = *cell->bottom_left; + if(shrink_factor != 1) + { + cv::Point2f center = *cell->top_left+*cell->top_right+*cell->bottom_left+*cell->bottom_right; + center.x /=4; + center.y /=4; + for(int i=0;i<4;++i) + { + auto &pt = points.at(i); + pt = center+(pt-center)*shrink_factor; + } + } + result.push_back(points); + } + } + return result; +} + +cv::Mat Chessboard::Board::warpImage(cv::InputArray image)const +{ + cv::Mat H = estimateHomography(); + cv::Mat mat; + cv::Size size = getSize(); + size.width = (size.width+1)*DUMMY_FIELD_SIZE; + size.height= (size.height+1)*DUMMY_FIELD_SIZE; + cv::warpPerspective(image,mat,H.inv(),size); + return mat; +} + void Chessboard::Board::draw(cv::InputArray m,cv::OutputArray out,cv::InputArray _H)const { cv::Mat H = _H.getMat(); @@ -1202,7 +1447,7 @@ void Chessboard::Board::draw(cv::InputArray m,cv::OutputArray out,cv::InputArray { for(int col=0;colx != iter1->x) // NaN check + if(!H.empty() && iter1->x != iter1->x) // NaN check { // draw search ellipse Ellipse ellipse = estimateSearchArea(H,row,col,0.4F); @@ -1222,18 +1467,42 @@ void Chessboard::Board::draw(cv::InputArray m,cv::OutputArray out,cv::InputArray for(int col=0;coltop_left+*cell->top_right+*cell->bottom_left+*cell->bottom_right; - center.x /=4; - center.y /=4; + cv::Point2f center = cell->getCenter(); int size = 4; if(row==0&&col==0) size=8; if(row==0&&col==1) size=7; - if(cell->black) - cv::circle(image,center,size,cv::Scalar::all(255),-1); + + if(cell->marker) + { + if(cell->black) + cv::circle(image,center,2,cv::Scalar::all(0),-1); + else + { + cv::circle(image,center,2,cv::Scalar::all(255),-1); + // draw coordinate + if(col+1 < icols) + { + const Cell *cell2 = getCell(row,col+1); + cv::Point2f center2 = cell2->getCenter(); + cv::line(image,center,center2,cv::Scalar::all(127),2); + } + if(row+1 < irows) + { + const Cell *cell2 = getCell(row+1,col); + cv::Point2f center2 = cell2->getCenter(); + cv::line(image,center,center2,cv::Scalar::all(127),2); + } + } + } else - cv::circle(image,center,size,cv::Scalar(0,0,10,255),-1); + { + if(cell->black) + cv::circle(image,center,size,cv::Scalar::all(255),-1); + else + cv::circle(image,center,size,cv::Scalar(0,0,10,255),-1); + } } } @@ -1331,6 +1600,7 @@ Chessboard::Board& Chessboard::Board::operator=(const Chessboard::Board &other) cell->bottom_right= point_point_mapping[(*iter2)->bottom_right]; cell->bottom_left = point_point_mapping[(*iter2)->bottom_left]; cell->black = (*iter2)->black; + cell->marker = (*iter2)->marker; cell_cell_mapping[*iter2] = cell; cells.push_back(cell); } @@ -1350,6 +1620,67 @@ Chessboard::Board& Chessboard::Board::operator=(const Chessboard::Board &other) return *this; } +bool Chessboard::Board::normalizeMarkerOrientation() +{ + // use row by row fashion as cells must not be arranged correctly + Cell *pcell = NULL; + int trows = int(rowCount()); + int tcols = int(colCount()); + for(int row=0;row != trows && !pcell;++row) + { + for(int col=0;col != tcols;++col) + { + Cell* current_cell = getCell(row,col); + if(!current_cell->marker || !current_cell->right || !current_cell->right->marker) + continue; + + if(current_cell->black) + { + if(current_cell->right->top && current_cell->right->top->marker) + { + rotateLeft(); + rotateLeft(); + pcell = current_cell->right; + break; + } + if(current_cell->right->bottom && current_cell->right->bottom->marker) + { + rotateLeft(); + pcell = current_cell->right; + break; + } + } + else + { + if(current_cell->top && current_cell->top->marker) + { + rotateRight(); + pcell = current_cell; + break; + } + if(current_cell->bottom && current_cell->bottom->marker) + { + // correct orientation + pcell = current_cell; + break; + } + } + } + } + if(pcell) + { + //check for ambiguity + if(rowCount()-pcell->bottom->getRow() > 2) + { + // std::cout << "FIX board " << pcell->bottom->getRow() << " " << rowCount(); + flipVertical(); + rotateRight(); + } + return true; + } + return false; +} + void Chessboard::Board::normalizeOrientation(bool bblack) { // fix ordering @@ -1864,6 +2195,89 @@ bool Chessboard::Board::isCellBlack(int row,int col)const return getCell(row,col)->black; } +bool Chessboard::Board::hasCellMarker(int row,int col) +{ + return getCell(row,col)->marker; +} + +int Chessboard::Board::detectMarkers(cv::InputArray image) +{ + cv::Mat img = image.getMat(); + CV_CheckTypeEQ(img.type(), CV_8UC1, "Unsupported source type"); + if(img.empty()) + CV_Error(Error::StsBadArg,"image is empty"); + if(isEmpty()) + CV_Error(Error::StsBadArg,"board is is empty"); + + // get undistorted board + cv::Mat board_image = warpImage(image); + + cv::Mat mask = cv::Mat::zeros(DUMMY_FIELD_SIZE,DUMMY_FIELD_SIZE,CV_8UC1); + cv::circle(mask,cv::Point(DUMMY_FIELD_SIZE/2,DUMMY_FIELD_SIZE/2),DUMMY_FIELD_SIZE/7,cv::Scalar::all(255),-1); + int signal_size = cv::countNonZero(mask); + + cv::Mat mask2 = cv::Mat::zeros(DUMMY_FIELD_SIZE,DUMMY_FIELD_SIZE,CV_8UC1); + cv::circle(mask2,cv::Point(DUMMY_FIELD_SIZE/2,DUMMY_FIELD_SIZE/2),DUMMY_FIELD_SIZE/2,cv::Scalar::all(255),-1); + cv::circle(mask2,cv::Point(DUMMY_FIELD_SIZE/2,DUMMY_FIELD_SIZE/2),DUMMY_FIELD_SIZE/5,cv::Scalar::all(0),-1); + int noise_size = cv::countNonZero(mask2); + + std::vector dst,src; + dst.push_back(cv::Point2f(0.0F,0.0F)); + dst.push_back(cv::Point2f(float(DUMMY_FIELD_SIZE),0.0F)); + dst.push_back(cv::Point2f(float(DUMMY_FIELD_SIZE),float(DUMMY_FIELD_SIZE))); + dst.push_back(cv::Point2f(0.0F,float(DUMMY_FIELD_SIZE))); + src.resize(4); + + // check each field + int icols = int(colCount()-1); + int irows = int(rowCount()-1); + int count = 0; + cv::Mat temp; + for(int y=1;ytop_left; + src[1] = *cell->top_right; + src[2] = *cell->bottom_right; + src[3] = *cell->bottom_left; + cv::Mat H = cv::findHomography(src,dst,cv::LMEDS); + cv::Mat field; + cv::warpPerspective(image,field,H,cv::Size(DUMMY_FIELD_SIZE,DUMMY_FIELD_SIZE)); + + // calc signal and noise value + cv::bitwise_and(field,mask,temp); + double signal = cv::sum(temp)[0]/signal_size; + cv::bitwise_and(field,mask2,temp); + double noise= cv::sum(temp)[0]/noise_size; + + // calc refrence value + Cell *cell2 = getCell(y,abs(x-1)); + src[0] = *cell2->top_left; + src[1] = *cell2->top_right; + src[2] = *cell2->bottom_right; + src[3] = *cell2->bottom_left; + H = cv::findHomography(src,dst,cv::LMEDS); + cv::warpPerspective(image,field,H,cv::Size(DUMMY_FIELD_SIZE,DUMMY_FIELD_SIZE)); + cv::bitwise_and(field,mask2,temp); + double reference = cv::sum(temp)[0]/noise_size; + // check if marker is present + if(cell->black) + cell->marker = signal-noise > (reference-noise)*0.5; + else + cell->marker = noise-signal > (noise-reference)*0.5; + if(cell->marker) + count++; + // std::cout << x << "/" << y << " signal " << signal << " noise " << noise << " reference " << reference << " has marker " << int(cell->marker) << std::endl; + } + } + return count; +} + bool Chessboard::Board::isCellEmpty(int row,int col) { return getCell(row,col)->empty(); @@ -1915,7 +2329,7 @@ void Chessboard::Board::drawEllipses(const std::vector &ellipses) #ifdef CV_DETECTORS_CHESSBOARD_DEBUG cv::Mat img; draw(debug_image,img); - std::vector::iterator iter; + std::vector::const_iterator iter = ellipses.begin(); for(;iter != ellipses.end();++iter) iter->draw(img); cv::imshow("chessboard",img); @@ -1924,6 +2338,95 @@ void Chessboard::Board::drawEllipses(const std::vector &ellipses) } +// TODO also delete points +bool Chessboard::Board::shrinkLeft() +{ + if(colCount() < 4) + return false; + + // unlink cells on the left side and delete them + top_left = top_left->right; + PointIter iter(top_left,BOTTOM_RIGHT); + do + { + auto cell = iter.getCell(); + auto citer = std::find(cells.begin(),cells.end(),cell->left); + delete cell->left; + cell->left= NULL; + cells.erase(citer); + } + while(iter.bottom()); + --cols; + return true; +} + +bool Chessboard::Board::shrinkRight() +{ + if(colCount() < 4) + return false; + + // unlink cells on the left side and delete them + PointIter iter(top_left,BOTTOM_RIGHT); + while(iter.right()); + iter.left(); + iter.left(); + do + { + auto cell = iter.getCell(); + auto citer = std::find(cells.begin(),cells.end(),cell->right); + delete cell->right; + cell->right= NULL; + cells.erase(citer); + } + while(iter.bottom()); + --cols; + return true; +} + +bool Chessboard::Board::shrinkTop() +{ + if(rowCount() < 4) + return false; + + // unlink cells on the left side and delete them + top_left = top_left->bottom; + PointIter iter(top_left,BOTTOM_RIGHT); + do + { + auto cell = iter.getCell(); + auto citer = std::find(cells.begin(),cells.end(),cell->top); + delete cell->top; + cell->top= NULL; + cells.erase(citer); + } + while(iter.right()); + --rows; + return true; +} + +bool Chessboard::Board::shrinkBottom() +{ + if(rowCount() < 4) + return false; + + // unlink cells on the left side and delete them + PointIter iter(top_left,BOTTOM_RIGHT); + while(iter.bottom()); + iter.top(); + iter.top(); + do + { + auto cell = iter.getCell(); + auto citer = std::find(cells.begin(),cells.end(),cell->bottom); + delete cell->bottom; + cell->bottom= NULL; + cells.erase(citer); + } + while(iter.right()); + --rows; + return true; +} + void Chessboard::Board::growLeft() { if(isEmpty()) @@ -1979,6 +2482,8 @@ bool Chessboard::Board::growLeft(const cv::Mat &map,cv::flann::Index &flann_inde { ++count; points.push_back(ellipse.getCenter()); + if(points.back().x < 0 || points.back().y <0) + return false; } else if(result != 0) { @@ -2066,6 +2571,8 @@ bool Chessboard::Board::growTop(const cv::Mat &map,cv::flann::Index &flann_index { ++count; points.push_back(ellipse.getCenter()); + if(points.back().x < 0 || points.back().y <0) + return false; } else if(result != 0) { @@ -2153,6 +2660,8 @@ bool Chessboard::Board::growRight(const cv::Mat &map,cv::flann::Index &flann_ind { ++count; points.push_back(ellipse.getCenter()); + if(points.back().x < 0 || points.back().y <0) + return false; } else if(result != 0) { @@ -2241,6 +2750,8 @@ bool Chessboard::Board::growBottom(const cv::Mat &map,cv::flann::Index &flann_in { ++count; points.push_back(ellipse.getCenter()); + if(points.back().x < 0 || points.back().y <0) + return false; } else if(result != 0) { @@ -2588,6 +3099,19 @@ std::vector Chessboard::Board::getContour()const return points; } +void Chessboard::Board::maskImage(cv::InputOutputArray img,const cv::Scalar &color)const +{ + Chessboard::Board temp(*this); + temp.growLeft(); + temp.growRight(); + temp.growTop(); + temp.growBottom(); + cv::Mat contour; + cv::Mat(temp.getContour()).convertTo(contour,CV_32S); + std::vector contours; + contours.push_back(contour); + cv::drawContours(img,contours,0,color,-1); +} cv::Mat Chessboard::Board::estimateHomography(cv::Rect rect,int field_size)const { @@ -2671,30 +3195,43 @@ int Chessboard::Board::grow(const cv::Mat &map,cv::flann::Index &flann_index) int count = 0; do { - // grow to the left - if(bleft) - { - bleft = growLeft(map,flann_index); - if(bleft) - ++count; - } if(btop) { btop= growTop(map,flann_index); if(btop) + { ++count; - } - if(bright) - { - bright= growRight(map,flann_index); - if(bright) - ++count; + continue; + } } if(bbottom) { bbottom= growBottom(map,flann_index); if(bbottom) + { ++count; + continue; + } + } + + // grow to the left + if(bleft) + { + bleft = growLeft(map,flann_index); + if(bleft) + { + ++count; + continue; + } + } + if(bright) + { + bright= growRight(map,flann_index); + if(bright) + { + ++count; + continue; + } } }while(bleft || btop || bright || bbottom ); return count; @@ -2753,6 +3290,112 @@ std::vector Chessboard::Board::getKeyPoints(bool ball)const return keypoints; } + +cv::Scalar Chessboard::Board::calcEdgeSharpness(cv::InputArray _img,float rise_distance,bool vertical,cv::OutputArray _sharpness) +{ + cv::Mat img = _img.getMat(); + if(img.empty()) + CV_Error(Error::StsBadArg,"image is empty"); + if(img.type() != CV_8UC1) + CV_Error(Error::StsBadArg,"image type is not supported. Expect CV_8UC1"); + + int tcols = int(colCount()); + int trows = int(rowCount()); + std::vector centers = getCellCenters(); + + if(int(centers.size()) != trows*tcols) + CV_Error(Error::StsInternal,"internal error - size mismatch"); + + // build horizontal lines + std::vector > pairs; + if(vertical) + { + for(int row = 1;row < trows-1;++row) + { + std::vector::const_iterator iter1 = centers.begin()+row*tcols; + std::vector::const_iterator iter2 = iter1+1; + for(int col= 0;col< tcols-1;++col,++iter1,++iter2) + pairs.push_back(std::make_pair(*iter1,*iter2)); + } + } + else + { + // build vertical lines + for(int col = 1;col< tcols-1;++col) + { + std::vector::const_iterator iter1 = centers.begin()+col; + std::vector::const_iterator iter2 = iter1+tcols; + for(int row= 0;row >::const_iterator iter = pairs.begin(); + int count = 0; + float sharpness = 0; + float max_val= 0; + float min_val= 0; + double dmin,dmax; + cv::Mat data = cv::Mat::zeros(int(pairs.size()),5,CV_32FC1); + for(;iter != pairs.end();++iter) + { + // get values from the image + if(!rect.contains(iter->first) || !rect.contains(iter->second)) + continue; + int delta = int(cv::norm(iter->second-iter->first)); + if(delta < 10) + continue; + + float dx = (iter->second.x-iter->first.x)/delta; + float dy = (iter->second.y-iter->first.y)/delta; + std::vector values; + cv::Mat patch; + for(int i=0;ifirst.x+dx*i,iter->first.y+dy*i); + for(int num=-1;num < 2;++num) + { + cv::Point2f p1(p0.x+dy*num,p0.y-dx*num); + if(!rect.contains(p1)) + continue; + cv::getRectSubPix(img,cv::Size(1,1),p1,patch); + value += patch.at(0,0); + ++count2; + } + values.push_back(uint8_t(value/count2)); + } + + float val = calcSharpness(values,rise_distance); + sharpness += val; + cv::minMaxLoc(values,&dmin,&dmax); + max_val+= float(dmax); + min_val += float(dmin); + data.at(count,0) = iter->first.x+(iter->second.x-iter->first.x)/2; + data.at(count,1) = iter->first.y+(iter->second.y-iter->first.y)/2; + data.at(count,2) = val; + data.at(count,3) = float(dmin); + data.at(count,4) = float(dmax); + count +=1; + } + if(count == 0) + { + std::cout <<"calcEdgeSharpness: checkerboard too small for calculation." << std::endl; + return cv::Scalar::all(9999); + } + sharpness = sharpness/float(count); + max_val = max_val/float(count); + min_val = min_val/float(count); + if(_sharpness.needed()) + data.copyTo(_sharpness); + return cv::Scalar(sharpness,min_val,max_val); +} + + Chessboard::Chessboard(const Parameters ¶) { reconfigure(para); @@ -2781,7 +3424,7 @@ void Chessboard::findKeyPoints(const cv::Mat& image, std::vector& keyp FastX::Parameters para; para.branches = 2; // this is always the case for checssboard corners - para.strength = 10; // minimal threshold + para.strength = 150; // minimal threshold para.resolution = float(CV_PI*0.25); // this gives the best results taking interpolation into account para.filter = 1; para.super_resolution = parameters.super_resolution; @@ -3004,7 +3647,7 @@ Chessboard::Board Chessboard::detectImpl(const Mat& gray,std::vector &f std::vector keypoints_seed; std::vector > angles; findKeyPoints(gray,keypoints_seed,feature_maps,angles,mask); - if(keypoints_seed.empty()) + if(int(keypoints_seed.size()) < parameters.chessboard_size.width * parameters.chessboard_size.height) return Chessboard::Board(); // check how many points are likely a checkerbord corner @@ -3108,19 +3751,70 @@ Chessboard::Board Chessboard::detectImpl(const Mat& gray,std::vector &f } else { - if(iter_boards->getSize().width*iter_boards->getSize().height < chessboard_size2.width*chessboard_size2.height) + if(iter_boards->getSize().width < chessboard_size2.width || iter_boards->getSize().height < chessboard_size2.height) iter_boards->clear(); else if(!parameters.larger) iter_boards->clear(); + else + { + // try to optimize board + while(true) + { + Board temp(*iter_boards); + temp.shrinkRight(); + temp.shrinkLeft(); + temp.shrinkRight(); + temp.shrinkLeft(); + temp.growTop(data,flann_index); + temp.growBottom(data,flann_index); + temp.grow(data,flann_index); + if(temp.rowCount()*temp.colCount() > iter_boards->rowCount()*iter_boards->colCount()) + { + *iter_boards = temp; + continue; + } + + temp = (*iter_boards); + temp = (*iter_boards); + temp.shrinkTop(); + temp.shrinkBottom(); + temp.shrinkTop(); + temp.shrinkBottom(); + temp.growLeft(data,flann_index); + temp.growRight(data,flann_index); + temp.grow(data,flann_index); + if(temp.rowCount()*temp.colCount() > iter_boards->rowCount()*iter_boards->colCount()) + { + *iter_boards = temp; + continue; + } + break; + } + } + } + + // check for markers + if(!iter_boards->isEmpty() && parameters.marker) + { + auto icount = iter_boards->detectMarkers(gray); + if(3 != icount || !iter_boards->normalizeMarkerOrientation()) + iter_boards->clear(); } } }); // check if a good board was found + // check if a good board was found and return largest one + const Board *best_board = NULL; for(const auto &board : boards) { if(!board.isEmpty()) - return board; + { + if(!best_board || best_board->rowCount() * best_board->colCount() < board.colCount()*board.rowCount()) + best_board = &board; + } } + if(best_board) + return *best_board; } return Chessboard::Board(); } @@ -3153,7 +3847,7 @@ void Chessboard::detectImpl(InputArray image, std::vector& keypoints, // public API bool findChessboardCornersSB(cv::InputArray image_, cv::Size pattern_size, - cv::OutputArray corners_, int flags) + cv::OutputArray corners_, int flags, cv::OutputArray meta_) { CV_INSTRUMENT_REGION(); int type = image_.type(), depth = CV_MAT_DEPTH(type), cn = CV_MAT_CN(type); @@ -3176,7 +3870,7 @@ bool findChessboardCornersSB(cv::InputArray image_, cv::Size pattern_size, para.chessboard_size = pattern_size; para.min_scale = 2; para.max_scale = 4; - para.max_tests = 25; + para.max_tests = 30; para.max_points = std::max(100,pattern_size.width*pattern_size.height*2); para.super_resolution = false; @@ -3199,20 +3893,64 @@ bool findChessboardCornersSB(cv::InputArray image_, cv::Size pattern_size, para.super_resolution = true; flags ^= CALIB_CB_ACCURACY; } + if(flags & CALIB_CB_LARGER) + { + para.larger = true; + flags ^= CALIB_CB_LARGER; + } + if(flags & CALIB_CB_MARKER) + { + para.marker = true; + para.max_points *= 4; + flags ^= CALIB_CB_MARKER; + } if(flags) CV_Error(Error::StsOutOfRange, cv::format("Invalid remaining flags %d", (int)flags)); std::vector corners; - details::Chessboard board(para); - board.detect(img,corners); + details::Chessboard detector(para); + + std::vector maps; + details::Chessboard::Board board = detector.detectImpl(img,maps,cv::Mat()); + corners = board.getKeyPoints(); if(corners.empty()) { corners_.release(); + if(meta_.needed()) + meta_.release(); return false; } std::vector points; KeyPoint::convert(corners,points); Mat(points).copyTo(corners_); + + // export meta data + if(meta_.needed()) + { + meta_.create(int(board.rowCount()),int(board.colCount()),CV_8UC1); + cv::Mat meta = meta_.getMat(); + meta = 0; + for(int row =0;row < meta.rows-1;++row) + { + for(int col=0;col< meta.cols-1;++col) + { + if(board.isCellBlack(row,col)) + { + if(board.hasCellMarker(row,col)) + meta.at(row,col) = 3; + else + meta.at(row,col) = 1; + } + else + { + if(board.hasCellMarker(row,col)) + meta.at(row,col) = 4; // origin + else + meta.at(row,col) = 2; + } + } + } + } return true; } diff --git a/modules/calib3d/src/chessboard.hpp b/modules/calib3d/src/chessboard.hpp index 41c64cf347..0261da1614 100644 --- a/modules/calib3d/src/chessboard.hpp +++ b/modules/calib3d/src/chessboard.hpp @@ -36,7 +36,7 @@ class FastX : public cv::Feature2D branches = 2; min_scale = 2; max_scale = 5; - super_resolution = 1; + super_resolution = true; filter = true; } }; @@ -96,7 +96,7 @@ class FastX : public cv::Feature2D void detectImpl(const cv::Mat& _src, std::vector& keypoints, const cv::Mat& mask)const; virtual void detectImpl(cv::InputArray image, std::vector& keypoints, cv::InputArray mask=cv::noArray())const; - void rotate(float angle,const cv::Mat &img,cv::Size size,cv::Mat &out)const; + void rotate(float angle,cv::InputArray img,cv::Size size,cv::OutputArray out)const; void calcFeatureMap(const cv::Mat &images,cv::Mat& out)const; private: @@ -111,6 +111,8 @@ class Ellipse public: Ellipse(); Ellipse(const cv::Point2f ¢er, const cv::Size2f &axes, float angle); + Ellipse(const Ellipse &other); + void draw(cv::InputOutputArray img,const cv::Scalar &color = cv::Scalar::all(120))const; bool contains(const cv::Point2f &pt)const; @@ -149,16 +151,18 @@ class Chessboard: public cv::Feature2D int max_tests; //!< maximal number of tested hypothesis bool super_resolution; //!< use super-repsolution for chessboard detection bool larger; //!< indicates if larger boards should be returned + bool marker; //!< indicates that valid boards must have a white and black cirlce marker used for orientation Parameters() { chessboard_size = cv::Size(9,6); - min_scale = 2; + min_scale = 3; max_scale = 4; super_resolution = true; - max_points = 400; - max_tests = 100; + max_points = 200; + max_tests = 50; larger = false; + marker = false; } Parameters(int scale,int _max_points): @@ -386,6 +390,14 @@ class Chessboard: public cv::Feature2D */ std::vector getCellCenters() const; + /** + * \brief Returns all cells as mats of four points each describing their corners. + * + * The left top cell has index 0 + * + */ + std::vector getCells(float shrink_factor = 1.0,bool bwhite=true,bool bblack = true) const; + /** * \brief Estimates the homography between an ideal board * and reality based on the already recovered points @@ -405,6 +417,12 @@ class Chessboard: public cv::Feature2D */ cv::Mat estimateHomography(int field_size = DUMMY_FIELD_SIZE)const; + /** + * \brief Warp image to match ideal checkerboard + * + */ + cv::Mat warpImage(cv::InputArray image)const; + /** * \brief Returns the size of the board * @@ -431,6 +449,11 @@ class Chessboard: public cv::Feature2D */ std::vector getContour()const; + /** + * \brief Masks the found board in the given image + * + */ + void maskImage(cv::InputOutputArray img,const cv::Scalar &color=cv::Scalar::all(0))const; /** * \brief Grows the board in all direction until no more corners are found in the feature map @@ -467,6 +490,27 @@ class Chessboard: public cv::Feature2D */ bool validateContour()const; + + /** + \brief delete left column of the board + */ + bool shrinkLeft(); + + /** + \brief delete right column of the board + */ + bool shrinkRight(); + + /** + \brief shrink first row of the board + */ + bool shrinkTop(); + + /** + \brief delete last row of the board + */ + bool shrinkBottom(); + /** * \brief Grows the board to the left by adding one column. * @@ -567,6 +611,12 @@ class Chessboard: public cv::Feature2D */ void normalizeOrientation(bool bblack=true); + /** + * \brief Flips and rotates the board so that the marker + * is normalized + */ + bool normalizeMarkerOrientation(); + /** * \brief Exchanges the stored board with the board stored in other */ @@ -594,16 +644,69 @@ class Chessboard: public cv::Feature2D */ std::map getMapping()const; - /** - * \brief Estimates rotation of the board around the camera axis - */ - double estimateRotZ()const; - /** * \brief Returns true if the cell is black * */ - bool isCellBlack(int row,int cola)const; + bool isCellBlack(int row,int col)const; + + /** + * \brief Returns true if the cell has a round marker at its + * center + * + */ + bool hasCellMarker(int row,int col); + + /** + * \brief Detects round markers in the chessboard fields based + * on the given image and the already recoverd board corners + * + * \returns Returns the number of found markes + * + */ + int detectMarkers(cv::InputArray image); + + /** + * \brief Calculates the average edge sharpness for the chessboard + * + * \param[in] image The image where the chessboard was detected + * \param[in] rise_distante Rise distance 0.8 means 10% ... 90% + * \param[in] vertical by default only edge response for horiontal lines are calculated + * + * \returns Scalar(sharpness, average min_val, average max_val) + * + * \author aduda@krakenrobotik.de + */ + cv::Scalar calcEdgeSharpness(cv::InputArray image,float rise_distance=0.8,bool vertical=false,cv::OutputArray sharpness=cv::noArray()); + + + /** + * \brief Gets the 3D objects points for the chessboard + * assuming the left top corner is located at the origin. In + * case the board as a marker, the white marker cell is at position zero + * + * \param[in] cell_size Size of one cell + * + * \returns Returns the object points as CV_32FC3 + */ + cv::Mat getObjectPoints(float cell_size)const; + + + /** + * \brief Returns the angle the board is rotated agains the x-axis of the image plane + * \returns Returns the object points as CV_32FC3 + */ + float getAngle()const; + + /** + * \brief Returns true if the main direction of the board is close to the image x-axis than y-axis + */ + bool isHorizontal()const; + + /** + * \brief Updates the search angles + */ + void setAngles(float white,float black); private: // stores one cell @@ -616,10 +719,13 @@ class Chessboard: public cv::Feature2D cv::Point2f *top_left,*top_right,*bottom_right,*bottom_left; // corners Cell *left,*top,*right,*bottom; // neighbouring cells bool black; // set to true if cell is black + bool marker; // set to true if cell has a round marker in its center Cell(); bool empty()const; // indicates if the cell is empty (one of its corners has NaN) int getRow()const; int getCol()const; + cv::Point2f getCenter()const; + bool isInside(const cv::Point2f &pt)const; // check if point is inside the cell }; // corners @@ -666,8 +772,8 @@ class Chessboard: public cv::Feature2D std::vector cells; // storage for all board cells std::vector corners; // storage for all corners Cell *top_left; // pointer to the top left corner of the board in its local coordinate system - int rows; // number of row cells - int cols; // number of col cells + int rows; // number of inner pattern rows + int cols; // number of inner pattern cols float white_angle,black_angle; }; public: From 44560c3e50516e4ef0116b4dca1dab61c3a36d3a Mon Sep 17 00:00:00 2001 From: Alexander Duda Date: Thu, 20 Feb 2020 16:10:27 +0100 Subject: [PATCH 2/2] calib3d: add estimateChessboardSharpness Image sharpness, as well as brightness, are a critical parameter for accuracte camera calibration. For accessing these parameters for filtering out problematic calibraiton images, this method calculates edge profiles by traveling from black to white chessboard cell centers. Based on this, the number of pixels is calculated required to transit from black to white. This width of the transition area is a good indication of how sharp the chessboard is imaged and should be below ~3.0 pixels. Based on this also motion blur can be detectd by comparing sharpness in vertical and horizontal direction. All unsharp images should be excluded from calibration as they will corrupt the calibration result. The same is true for overexposued images due to a none-linear sensor response. This can be detected by looking at the average cell brightness of the detected chessboard. --- modules/calib3d/include/opencv2/calib3d.hpp | 34 ++++++++++++++- modules/calib3d/src/chessboard.cpp | 46 +++++++++++++++++---- modules/calib3d/test/test_chesscorners.cpp | 13 ++++++ 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/modules/calib3d/include/opencv2/calib3d.hpp b/modules/calib3d/include/opencv2/calib3d.hpp index b05808367b..3fb4b7d42a 100644 --- a/modules/calib3d/include/opencv2/calib3d.hpp +++ b/modules/calib3d/include/opencv2/calib3d.hpp @@ -1281,13 +1281,45 @@ CV_EXPORTS_AS(findChessboardCornersSBWithMeta) bool findChessboardCornersSB(InputArray image,Size patternSize, OutputArray corners, int flags,OutputArray meta); /** @overload */ -CV_EXPORTS_W static inline +CV_EXPORTS_W inline bool findChessboardCornersSB(InputArray image, Size patternSize, OutputArray corners, int flags = 0) { return findChessboardCornersSB(image, patternSize, corners, flags, noArray()); } +/** @brief Estimates the sharpness of a detected chessboard. + +Image sharpness, as well as brightness, are a critical parameter for accuracte +camera calibration. For accessing these parameters for filtering out +problematic calibraiton images, this method calculates edge profiles by traveling from +black to white chessboard cell centers. Based on this, the number of pixels is +calculated required to transit from black to white. This width of the +transition area is a good indication of how sharp the chessboard is imaged +and should be below ~3.0 pixels. + +@param image Gray image used to find chessboard corners +@param patternSize Size of a found chessboard pattern +@param corners Corners found by findChessboardCorners(SB) +@param rise_distance Rise distance 0.8 means 10% ... 90% of the final signal strength +@param vertical By default edge responses for horizontal lines are calculated +@param sharpness Optional output array with a sharpness value for calculated edge responses (see description) + +The optional sharpness array is of type CV_32FC1 and has for each calculated +profile one row with the following five entries: +* 0 = x coordinate of the underlying edge in the image +* 1 = y coordinate of the underlying edge in the image +* 2 = width of the transition area (sharpness) +* 3 = signal strength in the black cell (min brightness) +* 4 = signal strength in the white cell (max brightness) + +@return Scalar(average sharpness, average min brightness, average max brightness,0) +*/ +CV_EXPORTS_W Scalar estimateChessboardSharpness(InputArray image, Size patternSize, InputArray corners, + float rise_distance=0.8F,bool vertical=false, + OutputArray sharpness=noArray()); + + //! finds subpixel-accurate positions of the chessboard corners CV_EXPORTS_W bool find4QuadCornerSubpix( InputArray img, InputOutputArray corners, Size region_size ); diff --git a/modules/calib3d/src/chessboard.cpp b/modules/calib3d/src/chessboard.cpp index 2b0b0a5eb8..04a3bbe6c0 100644 --- a/modules/calib3d/src/chessboard.cpp +++ b/modules/calib3d/src/chessboard.cpp @@ -725,19 +725,19 @@ void FastX::detectImpl(const cv::Mat& _gray_image, // calc images // for each angle step int scale_id = scale-parameters.min_scale; - int scale_size = int(pow(2.0,scale+1+super_res)-1); - int scale_size2 = int((scale_size/10)*2+1); + int scale_size = int(pow(2.0,scale+1+super_res)); + int scale_size2 = int((scale_size/7)*2+1); std::vector images; images.resize(2*num); cv::UMat rotated,filtered_h,filtered_v; - cv::blur(gray_image,images[0],cv::Size(scale_size,scale_size2)); - cv::blur(gray_image,images[num],cv::Size(scale_size2,scale_size)); + cv::boxFilter(gray_image,images[0],-1,cv::Size(scale_size,scale_size2)); + cv::boxFilter(gray_image,images[num],-1,cv::Size(scale_size2,scale_size)); for(int i=1;i points; + corners.reshape(2,corners.rows).convertTo(points,CV_32FC2); + if(int(points.size()) != patternSize.width * patternSize.height) + CV_Error(Error::StsBadArg, "Size mismatch between patternSize and number of provided corners."); + + Mat img; + if (image_.channels() != 1) + cvtColor(image_, img, COLOR_BGR2GRAY); + else + img = image_.getMat(); + + details::Chessboard::Board board(patternSize,points); + return board.calcEdgeSharpness(img,rise_distance,vertical,sharpness); +} + + } // namespace cv diff --git a/modules/calib3d/test/test_chesscorners.cpp b/modules/calib3d/test/test_chesscorners.cpp index 0dfd0b75c1..3d730f6bfd 100644 --- a/modules/calib3d/test/test_chesscorners.cpp +++ b/modules/calib3d/test/test_chesscorners.cpp @@ -235,11 +235,13 @@ void CV_ChessboardDetectorTest::run_batch( const string& filename ) String _filename = folder + (String)board_list[idx * 2 + 1]; bool doesContatinChessboard; + float sharpness; Mat expected; { FileStorage fs1(_filename, FileStorage::READ); fs1["corners"] >> expected; fs1["isFound"] >> doesContatinChessboard; + fs1["sharpness"] >> sharpness ; fs1.release(); } size_t count_exp = static_cast(expected.cols * expected.rows); @@ -259,6 +261,17 @@ void CV_ChessboardDetectorTest::run_batch( const string& filename ) flags = 0; } bool result = findChessboardCornersWrapper(gray, pattern_size,v,flags); + if(result && sharpness && (pattern == CHESSBOARD_SB || pattern == CHESSBOARD)) + { + Scalar s= estimateChessboardSharpness(gray,pattern_size,v); + if(fabs(s[0] - sharpness) > 0.1) + { + ts->printf(cvtest::TS::LOG, "chessboard image has a wrong sharpness in %s. Expected %f but measured %f\n", img_file.c_str(),sharpness,s[0]); + ts->set_failed_test_info( cvtest::TS::FAIL_INVALID_OUTPUT ); + show_points( gray, expected, v, result ); + return; + } + } if(result ^ doesContatinChessboard || (doesContatinChessboard && v.size() != count_exp)) { ts->printf( cvtest::TS::LOG, "chessboard is detected incorrectly in %s\n", img_file.c_str() );