diff --git a/modules/calib3d/src/calibinit.cpp b/modules/calib3d/src/calibinit.cpp index d6f5995587..1bd896463c 100644 --- a/modules/calib3d/src/calibinit.cpp +++ b/modules/calib3d/src/calibinit.cpp @@ -76,6 +76,9 @@ #include #include +using namespace cv; +using namespace std; + //#define ENABLE_TRIM_COL_ROW //#define DEBUG_CHESSBOARD @@ -88,13 +91,9 @@ static int PRINTF( const char* fmt, ... ) return vprintf(fmt, args); } #else -static int PRINTF( const char*, ... ) -{ - return 0; -} +#define PRINTF(...) #endif - //===================================================================================== // Implementation for the enhanced calibration object detection //===================================================================================== @@ -155,10 +154,42 @@ struct CvCBQuad //===================================================================================== -//static CvMat* debug_img = 0; +#ifdef DEBUG_CHESSBOARD +#include "opencv2/highgui.hpp" +#include "opencv2/imgproc.hpp" +static void SHOW(const std::string & name, Mat & img) +{ + imshow(name, img); + while ((uchar)waitKey(0) != 'q') {} +} +static void SHOW_QUADS(const std::string & name, const Mat & img_, CvCBQuad * quads, int quads_count) +{ + Mat img = img_.clone(); + if (img.channels() == 1) + cvtColor(img, img, COLOR_GRAY2BGR); + for (int i = 0; i < quads_count; ++i) + { + CvCBQuad & quad = quads[i]; + for (int j = 0; j < 4; ++j) + { + line(img, quad.corners[j]->pt, quad.corners[(j + 1) % 4]->pt, Scalar(0, 240, 0), 1, LINE_AA); + } + } + imshow(name, img); + while ((uchar)waitKey(0) != 'q') {} +} +#else +#define SHOW(...) +#define SHOW_QUADS(...) +#endif + +//===================================================================================== static int icvGenerateQuads( CvCBQuad **quads, CvCBCorner **corners, - CvMemStorage *storage, CvMat *image, int flags, int *max_quad_buf_size); + CvMemStorage *storage, const Mat &image_, int flags, int *max_quad_buf_size); + +static bool processQuads(CvCBQuad *quads, int quad_count, CvSize pattern_size, int max_quad_buf_size, + CvMemStorage * storage, CvCBCorner *corners, CvPoint2D32f *out_corners, int *out_corner_count, int & prev_sqr_size); /*static int icvGenerateQuadsEx( CvCBQuad **out_quads, CvCBCorner **out_corners, @@ -195,203 +226,198 @@ static void icvRemoveQuadFromGroup(CvCBQuad **quads, int count, CvCBQuad *q0); static int icvCheckBoardMonotony( CvPoint2D32f* corners, CvSize pattern_size ); -int cvCheckChessboardBinary(IplImage* src, CvSize size); - /***************************************************************************************************/ //COMPUTE INTENSITY HISTOGRAM OF INPUT IMAGE -static int icvGetIntensityHistogram( unsigned char* pucImage, int iSizeCols, int iSizeRows, std::vector& piHist ); -//SMOOTH HISTOGRAM USING WINDOW OF SIZE 2*iWidth+1 -static int icvSmoothHistogram( const std::vector& piHist, std::vector& piHistSmooth, int iWidth ); -//COMPUTE FAST HISTOGRAM GRADIENT -static int icvGradientOfHistogram( const std::vector& piHist, std::vector& piHistGrad ); -//PERFORM SMART IMAGE THRESHOLDING BASED ON ANALYSIS OF INTENSTY HISTOGRAM -static bool icvBinarizationHistogramBased( unsigned char* pucImg, int iCols, int iRows ); -/***************************************************************************************************/ -int icvGetIntensityHistogram( unsigned char* pucImage, int iSizeCols, int iSizeRows, std::vector& piHist ) +static int icvGetIntensityHistogram( const Mat & img, std::vector& piHist ) { - int iVal; - - // sum up all pixel in row direction and divide by number of columns - for ( int j=0; j& piHist, std::vector& piHistSmooth, int iWidth ) +//SMOOTH HISTOGRAM USING WINDOW OF SIZE 2*iWidth+1 +static int icvSmoothHistogram( const std::vector& piHist, std::vector& piHistSmooth, int iWidth ) { - int iIdx; - for ( int i=0; i<256; i++) - { - int iSmooth = 0; - for ( int ii=-iWidth; ii<=iWidth; ii++) + int iIdx; + for ( int i=0; i<256; i++) { - iIdx = i+ii; - if (iIdx > 0 && iIdx < 256) - { - iSmooth += piHist[iIdx]; - } + int iSmooth = 0; + for ( int ii=-iWidth; ii<=iWidth; ii++) + { + iIdx = i+ii; + if (iIdx > 0 && iIdx < 256) + { + iSmooth += piHist[iIdx]; + } + } + piHistSmooth[i] = iSmooth/(2*iWidth+1); } - piHistSmooth[i] = iSmooth/(2*iWidth+1); - } - return 0; + return 0; } /***************************************************************************************************/ -int icvGradientOfHistogram( const std::vector& piHist, std::vector& piHistGrad ) +//COMPUTE FAST HISTOGRAM GRADIENT +static int icvGradientOfHistogram( const std::vector& piHist, std::vector& piHistGrad ) { - piHistGrad[0] = 0; - for ( int i=1; i<255; i++) - { - piHistGrad[i] = piHist[i-1] - piHist[i+1]; - if ( abs(piHistGrad[i]) < 100 ) + piHistGrad[0] = 0; + for ( int i=1; i<255; i++) { - if ( piHistGrad[i-1] == 0) - piHistGrad[i] = -100; - else - piHistGrad[i] = piHistGrad[i-1]; + piHistGrad[i] = piHist[i-1] - piHist[i+1]; + if ( abs(piHistGrad[i]) < 100 ) + { + if ( piHistGrad[i-1] == 0) + piHistGrad[i] = -100; + else + piHistGrad[i] = piHistGrad[i-1]; + } } - } - return 0; + return 0; } /***************************************************************************************************/ -bool icvBinarizationHistogramBased( unsigned char* pucImg, int iCols, int iRows ) +//PERFORM SMART IMAGE THRESHOLDING BASED ON ANALYSIS OF INTENSTY HISTOGRAM +static bool icvBinarizationHistogramBased( Mat & img ) { - int iMaxPix = iCols*iRows; - int iMaxPix1 = iMaxPix/100; - const int iNumBins = 256; - std::vector piHistIntensity(iNumBins, 0); - std::vector piHistSmooth(iNumBins, 0); - std::vector piHistGrad(iNumBins, 0); - std::vector piAccumSum(iNumBins, 0); - std::vector piMaxPos(20, 0); - int iThresh = 0; - int iIdx; - int iWidth = 1; - - icvGetIntensityHistogram( pucImg, iCols, iRows, piHistIntensity ); - - // get accumulated sum starting from bright - piAccumSum[iNumBins-1] = piHistIntensity[iNumBins-1]; - for ( int i=iNumBins-2; i>=0; i-- ) - { - piAccumSum[i] = piHistIntensity[i] + piAccumSum[i+1]; - } - - // first smooth the distribution - icvSmoothHistogram( piHistIntensity, piHistSmooth, iWidth ); - - // compute gradient - icvGradientOfHistogram( piHistSmooth, piHistGrad ); - - // check for zeros - int iCntMaxima = 0; - for ( int i=iNumBins-2; (i>2) && (iCntMaxima<20); i--) - { - if ( (piHistGrad[i-1] < 0) && (piHistGrad[i] > 0) ) - { - piMaxPos[iCntMaxima] = i; - iCntMaxima++; - } - } - - iIdx = 0; - int iSumAroundMax = 0; - for ( int i=0; i piHistIntensity(iNumBins, 0); + std::vector piHistSmooth(iNumBins, 0); + std::vector piHistGrad(iNumBins, 0); + std::vector piAccumSum(iNumBins, 0); + std::vector piMaxPos(20, 0); + int iThresh = 0; + int iIdx; + int iWidth = 1; + + icvGetIntensityHistogram( img, piHistIntensity ); + + // get accumulated sum starting from bright + piAccumSum[iNumBins-1] = piHistIntensity[iNumBins-1]; + for ( int i=iNumBins-2; i>=0; i-- ) { - for ( int j=i; j= 3 - { - // CHECKING THRESHOLD FOR WHITE - int iIdxAccSum = 0, iAccum = 0; - for (int i=iNumBins-1; i>0; i--) + + // first smooth the distribution + icvSmoothHistogram( piHistIntensity, piHistSmooth, iWidth ); + + // compute gradient + icvGradientOfHistogram( piHistSmooth, piHistGrad ); + + // check for zeros + int iCntMaxima = 0; + for ( int i=iNumBins-2; (i>2) && (iCntMaxima<20); i--) { - iAccum += piHistIntensity[i]; - // iMaxPix/18 is about 5,5%, minimum required number of pixels required for white part of chessboard - if ( iAccum > (iMaxPix/18) ) - { - iIdxAccSum = i; - break; - } + if ( (piHistGrad[i-1] < 0) && (piHistGrad[i] > 0) ) + { + piMaxPos[iCntMaxima] = i; + iCntMaxima++; + } } - int iIdxBGMax = 0; - int iBrightMax = piMaxPos[0]; - // printf("iBrightMax = %d\n", iBrightMax); - for ( int n=0; n= 250 && iIdxBGMax < iCntMaxima ) + if ( iCntMaxima == 1) { - iIdxBGMax++; - iMaxVal = piHistIntensity[piMaxPos[iIdxBGMax]]; + iThresh = piMaxPos[0]/2; } - - for ( int n=iIdxBGMax + 1; n= iMaxVal ) - { - iMaxVal = piHistIntensity[piMaxPos[n]]; - iIdxBGMax = n; - } + iThresh = (piMaxPos[0] + piMaxPos[1])/2; } + else // iCntMaxima >= 3 + { + // CHECKING THRESHOLD FOR WHITE + int iIdxAccSum = 0, iAccum = 0; + for (int i=iNumBins-1; i>0; i--) + { + iAccum += piHistIntensity[i]; + // iMaxPix/18 is about 5,5%, minimum required number of pixels required for white part of chessboard + if ( iAccum > (iMaxPix/18) ) + { + iIdxAccSum = i; + break; + } + } - //SETTING THRESHOLD FOR BINARIZATION - int iDist2 = (iBrightMax - piMaxPos[iIdxBGMax])/2; - iThresh = iBrightMax - iDist2; - PRINTF("THRESHOLD SELECTED = %d, BRIGHTMAX = %d, DARKMAX = %d\n", iThresh, iBrightMax, piMaxPos[iIdxBGMax]); - } + int iIdxBGMax = 0; + int iBrightMax = piMaxPos[0]; + // printf("iBrightMax = %d\n", iBrightMax); + for ( int n=0; n= 250 && iIdxBGMax < iCntMaxima ) + { + iIdxBGMax++; + iMaxVal = piHistIntensity[piMaxPos[iIdxBGMax]]; + } - if ( iThresh > 0 ) - { - for ( int jj=0; jj= iMaxVal ) + { + iMaxVal = piHistIntensity[piMaxPos[n]]; + iIdxBGMax = n; + } + } + + //SETTING THRESHOLD FOR BINARIZATION + int iDist2 = (iBrightMax - piMaxPos[iIdxBGMax])/2; + iThresh = iBrightMax - iDist2; + PRINTF("THRESHOLD SELECTED = %d, BRIGHTMAX = %d, DARKMAX = %d\n", iThresh, iBrightMax, piMaxPos[iIdxBGMax]); + } + + + if ( iThresh > 0 ) { - for ( int ii=0; ii storage; try { int k = 0; const int min_dilations = 0; const int max_dilations = 7; - cv::Ptr norm_img, thresh_img; - cv::Ptr storage; - - CvMat stub, *img = (CvMat*)arr; - cImgSeg = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1 ); - memcpy( cImgSeg->imageData, cvPtr1D( img, 0), img->rows*img->cols ); - - CvMat stub2, *thresh_img_new; - thresh_img_new = cvGetMat( cImgSeg, &stub2, 0, 0 ); - - int expected_corners_num = (pattern_size.width/2+1)*(pattern_size.height/2+1); - - int prev_sqr_size = 0; if( out_corner_count ) *out_corner_count = 0; - int quad_count = 0, group_idx = 0, dilations = 0; - - img = cvGetMat( img, &stub ); - //debug_img = img; + Mat img = cvarrToMat((CvMat*)arr).clone(); - if( CV_MAT_DEPTH( img->type ) != CV_8U || CV_MAT_CN( img->type ) == 2 ) - CV_Error( CV_StsUnsupportedFormat, "Only 8-bit grayscale or color images are supported" ); + if( img.depth() != CV_8U || (img.channels() != 1 && img.channels() != 3) ) + CV_Error( CV_StsUnsupportedFormat, "Only 8-bit grayscale or color images are supported" ); if( pattern_size.width <= 2 || pattern_size.height <= 2 ) CV_Error( CV_StsOutOfRange, "Both width and height of the pattern should have bigger than 2" ); @@ -440,273 +451,124 @@ int cvFindChessboardCorners( const void* arr, CvSize pattern_size, if( !out_corners ) CV_Error( CV_StsNullPtr, "Null pointer to corners" ); - storage.reset(cvCreateMemStorage(0)); - thresh_img.reset(cvCreateMat( img->rows, img->cols, CV_8UC1 )); - - if( CV_MAT_CN(img->type) != 1 || (flags & CV_CALIB_CB_NORMALIZE_IMAGE) ) + if (img.channels() != 1) { - // equalize the input image histogram - - // that should make the contrast between "black" and "white" areas big enough - norm_img.reset(cvCreateMat( img->rows, img->cols, CV_8UC1 )); + cvtColor(img, img, COLOR_BGR2GRAY); + } - if( CV_MAT_CN(img->type) != 1 ) - { - cvCvtColor( img, norm_img, CV_BGR2GRAY ); - img = norm_img; - } - if( flags & CV_CALIB_CB_NORMALIZE_IMAGE ) - { - cvEqualizeHist( img, norm_img ); - img = norm_img; - } - } + Mat thresh_img_new = img.clone(); + icvBinarizationHistogramBased( thresh_img_new ); // process image in-place + SHOW("New binarization", thresh_img_new); if( flags & CV_CALIB_CB_FAST_CHECK) { //perform new method for checking chessboard using a binary image. //image is binarised using a threshold dependent on the image histogram - icvBinarizationHistogramBased( (unsigned char*) cImgSeg->imageData, cImgSeg->width, cImgSeg->height ); - int check_chessboard_result = cvCheckChessboardBinary(cImgSeg, pattern_size); - if(check_chessboard_result <= 0) //fall back to the old method + if (checkChessboardBinary(thresh_img_new, pattern_size) <= 0) //fall back to the old method { - IplImage _img; - cvGetImage(img, &_img); - check_chessboard_result = cvCheckChessboard(&_img, pattern_size); - if(check_chessboard_result <= 0) + if (checkChessboard(img, pattern_size) <= 0) { - return 0; + return found; } } } + storage.reset(cvCreateMemStorage(0)); + + int prev_sqr_size = 0; + // Try our standard "1" dilation, but if the pattern is not found, iterate the whole procedure with higher dilations. // This is necessary because some squares simply do not separate properly with a single dilation. However, // we want to use the minimum number of dilations possible since dilations cause the squares to become smaller, // making it difficult to detect smaller squares. - for( dilations = min_dilations; dilations <= max_dilations; dilations++ ) + for( int dilations = min_dilations; dilations <= max_dilations; dilations++ ) { - if (found) - break; // already found it - - cvFree(&quads); - cvFree(&corners); - - int max_quad_buf_size = 0; - - //USE BINARY IMAGE COMPUTED USING icvBinarizationHistogramBased METHOD - cvDilate( thresh_img_new, thresh_img_new, 0, 1 ); - - // So we can find rectangles that go to the edge, we draw a white line around the image edge. - // Otherwise FindContours will miss those clipped rectangle contours. - // The border color will be the image mean, because otherwise we risk screwing up filters like cvSmooth()... - cvRectangle( thresh_img_new, cvPoint(0,0), cvPoint(thresh_img_new->cols-1, thresh_img_new->rows-1), CV_RGB(255,255,255), 3, 8); - quad_count = icvGenerateQuads( &quads, &corners, storage, thresh_img_new, flags, &max_quad_buf_size ); - PRINTF("Quad count: %d/%d\n", quad_count, expected_corners_num); - - if( quad_count <= 0 ) - { - continue; - } - - // Find quad's neighbors - icvFindQuadNeighbors( quads, quad_count ); - - // allocate extra for adding in icvOrderFoundQuads - cvFree(&quad_group); - cvFree(&corner_group); - quad_group = (CvCBQuad**)cvAlloc( sizeof(quad_group[0]) * max_quad_buf_size); - corner_group = (CvCBCorner**)cvAlloc( sizeof(corner_group[0]) * max_quad_buf_size * 4 ); - - for( group_idx = 0; ; group_idx++ ) - { - int count = 0; - count = icvFindConnectedQuads( quads, quad_count, quad_group, group_idx, storage ); - - int icount = count; - if( count == 0 ) - break; - - // order the quad corners globally - // maybe delete or add some - PRINTF("Starting ordering of inner quads\n"); - count = icvOrderFoundConnectedQuads(count, quad_group, &quad_count, &quads, &corners, pattern_size, max_quad_buf_size, storage ); - PRINTF("Orig count: %d After ordering: %d\n", icount, count); - - if (count == 0) - continue; // haven't found inner quads - - // If count is more than it should be, this will remove those quads - // which cause maximum deviation from a nice square pattern. - count = icvCleanFoundConnectedQuads( count, quad_group, pattern_size ); - PRINTF("Connected group: %d orig count: %d cleaned: %d\n", group_idx, icount, count); - - count = icvCheckQuadGroup( quad_group, count, corner_group, pattern_size ); - PRINTF("Connected group: %d count: %d cleaned: %d\n", group_idx, icount, count); - - int n = count > 0 ? pattern_size.width * pattern_size.height : -count; - n = MIN( n, pattern_size.width * pattern_size.height ); - float sum_dist = 0; - int total = 0; + if (found) + break; // already found it - for(int i = 0; i < n; i++ ) - { - int ni = 0; - float avgi = corner_group[i]->meanDist(&ni); - sum_dist += avgi*ni; - total += ni; - } - prev_sqr_size = cvRound(sum_dist/MAX(total, 1)); + //USE BINARY IMAGE COMPUTED USING icvBinarizationHistogramBased METHOD + dilate( thresh_img_new, thresh_img_new, Mat(), Point(-1, -1), 1 ); - if( count > 0 || (out_corner_count && -count > *out_corner_count) ) - { - // copy corners to output array - for(int i = 0; i < n; i++ ) - out_corners[i] = corner_group[i]->pt; - - if( out_corner_count ) - *out_corner_count = n; - - if( count == pattern_size.width*pattern_size.height && - icvCheckBoardMonotony( out_corners, pattern_size )) - { - found = 1; - break; - } - } - } - }//dilations + // So we can find rectangles that go to the edge, we draw a white line around the image edge. + // Otherwise FindContours will miss those clipped rectangle contours. + // The border color will be the image mean, because otherwise we risk screwing up filters like cvSmooth()... + rectangle( thresh_img_new, Point(0,0), Point(thresh_img_new.cols-1, thresh_img_new.rows-1), Scalar(255,255,255), 3, LINE_8); + int max_quad_buf_size = 0; + cvFree(&quads); + cvFree(&corners); + int quad_count = icvGenerateQuads( &quads, &corners, storage, thresh_img_new, flags, &max_quad_buf_size ); + PRINTF("Quad count: %d/%d\n", quad_count, (pattern_size.width/2+1)*(pattern_size.height/2+1)); + SHOW_QUADS("New quads", thresh_img_new, quads, quad_count); + if (processQuads(quads, quad_count, pattern_size, max_quad_buf_size, storage, corners, out_corners, out_corner_count, prev_sqr_size)) + found = 1; + } PRINTF("Chessboard detection result 0: %d\n", found); // revert to old, slower, method if detection failed if (!found) { - PRINTF("Fallback to old algorithm\n"); - // empiric threshold level - // thresholding performed here and not inside the cycle to save processing time - int thresh_level; - if ( !(flags & CV_CALIB_CB_ADAPTIVE_THRESH) ) - { - double mean = cvAvg( img ).val[0]; - thresh_level = cvRound( mean - 10 ); - thresh_level = MAX( thresh_level, 10 ); - cvThreshold( img, thresh_img, thresh_level, 255, CV_THRESH_BINARY ); - } - for( k = 0; k < 6; k++ ) - { - int max_quad_buf_size = 0; - for( dilations = min_dilations; dilations <= max_dilations; dilations++ ) + if( flags & CV_CALIB_CB_NORMALIZE_IMAGE ) { - if (found) - break; // already found it - - cvFree(&quads); - cvFree(&corners); - - // convert the input grayscale image to binary (black-n-white) - if( flags & CV_CALIB_CB_ADAPTIVE_THRESH ) - { - int block_size = cvRound(prev_sqr_size == 0 ? - MIN(img->cols,img->rows)*(k%2 == 0 ? 0.2 : 0.1): prev_sqr_size*2)|1; - - // convert to binary - cvAdaptiveThreshold( img, thresh_img, 255, - CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY, block_size, (k/2)*5 ); - if (dilations > 0) - cvDilate( thresh_img, thresh_img, 0, dilations-1 ); - } - //if flag CV_CALIB_CB_ADAPTIVE_THRESH is not set it doesn't make sense - //to iterate over k - else - { - k = 6; - cvDilate( thresh_img, thresh_img, 0, 1 ); - } - - // So we can find rectangles that go to the edge, we draw a white line around the image edge. - // Otherwise FindContours will miss those clipped rectangle contours. - // The border color will be the image mean, because otherwise we risk screwing up filters like cvSmooth()... - cvRectangle( thresh_img, cvPoint(0,0), cvPoint(thresh_img->cols-1, - thresh_img->rows-1), CV_RGB(255,255,255), 3, 8); - - quad_count = icvGenerateQuads( &quads, &corners, storage, thresh_img, flags, &max_quad_buf_size); - PRINTF("Quad count: %d/%d\n", quad_count, expected_corners_num); - - if( quad_count <= 0 ) - { - continue; - } - - // Find quad's neighbors - icvFindQuadNeighbors( quads, quad_count ); - - // allocate extra for adding in icvOrderFoundQuads - cvFree(&quad_group); - cvFree(&corner_group); - quad_group = (CvCBQuad**)cvAlloc( sizeof(quad_group[0]) * max_quad_buf_size); - corner_group = (CvCBCorner**)cvAlloc( sizeof(corner_group[0]) * max_quad_buf_size * 4 ); - - for( group_idx = 0; ; group_idx++ ) - { - int count = 0; - count = icvFindConnectedQuads( quads, quad_count, quad_group, group_idx, storage ); - - int icount = count; - if( count == 0 ) - break; - - // order the quad corners globally - // maybe delete or add some - PRINTF("Starting ordering of inner quads\n"); - count = icvOrderFoundConnectedQuads(count, quad_group, &quad_count, &quads, &corners, pattern_size, max_quad_buf_size, storage ); - - PRINTF("Orig count: %d After ordering: %d\n", icount, count); - - if (count == 0) - continue; // haven't found inner quads - - - // If count is more than it should be, this will remove those quads - // which cause maximum deviation from a nice square pattern. - count = icvCleanFoundConnectedQuads( count, quad_group, pattern_size ); - PRINTF("Connected group: %d orig count: %d cleaned: %d\n", group_idx, icount, count); - - count = icvCheckQuadGroup( quad_group, count, corner_group, pattern_size ); - PRINTF("Connected group: %d count: %d cleaned: %d\n", group_idx, icount, count); - - int n = count > 0 ? pattern_size.width * pattern_size.height : -count; - n = MIN( n, pattern_size.width * pattern_size.height ); - float sum_dist = 0; - int total = 0; + equalizeHist( img, img ); + } - for(int i = 0; i < n; i++ ) - { - int ni = 0; - float avgi = corner_group[i]->meanDist(&ni); - sum_dist += avgi*ni; - total += ni; - } - prev_sqr_size = cvRound(sum_dist/MAX(total, 1)); + Mat thresh_img; + prev_sqr_size = 0; - if( count > 0 || (out_corner_count && -count > *out_corner_count) ) + PRINTF("Fallback to old algorithm\n"); + const bool useAdaptive = flags & CV_CALIB_CB_ADAPTIVE_THRESH; + if (!useAdaptive) + { + // empiric threshold level + // thresholding performed here and not inside the cycle to save processing time + double mean = cv::mean(img).val[0]; + int thresh_level = MAX(cvRound( mean - 10 ), 10); + threshold( img, thresh_img, thresh_level, 255, THRESH_BINARY ); + } + //if flag CV_CALIB_CB_ADAPTIVE_THRESH is not set it doesn't make sense to iterate over k + int max_k = useAdaptive ? 6 : 1; + for( k = 0; k < max_k; k++ ) + { + for( int dilations = min_dilations; dilations <= max_dilations; dilations++ ) { - // copy corners to output array - for(int i = 0; i < n; i++ ) - out_corners[i] = corner_group[i]->pt; + if (found) + break; // already found it - if( out_corner_count ) - *out_corner_count = n; + // convert the input grayscale image to binary (black-n-white) + if (useAdaptive) + { + int block_size = cvRound(prev_sqr_size == 0 + ? MIN(img.cols, img.rows) * (k % 2 == 0 ? 0.2 : 0.1) + : prev_sqr_size * 2); + block_size = block_size | 1; + // convert to binary + adaptiveThreshold( img, thresh_img, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, block_size, (k/2)*5 ); + if (dilations > 0) + dilate( thresh_img, thresh_img, Mat(), Point(-1, -1), dilations-1 ); - if( count == pattern_size.width*pattern_size.height && icvCheckBoardMonotony( out_corners, pattern_size )) - { - found = 1; - break; - } + } + else + { + dilate( thresh_img, thresh_img, Mat(), Point(-1, -1), 1 ); + } + SHOW("Old binarization", thresh_img); + + // So we can find rectangles that go to the edge, we draw a white line around the image edge. + // Otherwise FindContours will miss those clipped rectangle contours. + // The border color will be the image mean, because otherwise we risk screwing up filters like cvSmooth()... + rectangle( thresh_img, Point(0,0), Point(thresh_img.cols-1, thresh_img.rows-1), Scalar(255,255,255), 3, LINE_8); + int max_quad_buf_size = 0; + cvFree(&quads); + cvFree(&corners); + int quad_count = icvGenerateQuads( &quads, &corners, storage, thresh_img, flags, &max_quad_buf_size); + PRINTF("Quad count: %d/%d\n", quad_count, (pattern_size.width/2+1)*(pattern_size.height/2+1)); + SHOW_QUADS("Old quads", thresh_img, quads, quad_count); + if (processQuads(quads, quad_count, pattern_size, max_quad_buf_size, storage, corners, out_corners, out_corner_count, prev_sqr_size)) + found = 1; } - } - }//dilations - }// for k = 0 -> 6 + } } PRINTF("Chessboard detection result 1: %d\n", found); @@ -722,8 +584,8 @@ int cvFindChessboardCorners( const void* arr, CvSize pattern_size, const int BORDER = 8; for( k = 0; k < pattern_size.width*pattern_size.height; k++ ) { - if( out_corners[k].x <= BORDER || out_corners[k].x > img->cols - BORDER || - out_corners[k].y <= BORDER || out_corners[k].y > img->rows - BORDER ) + if( out_corners[k].x <= BORDER || out_corners[k].x > img.cols - BORDER || + out_corners[k].y <= BORDER || out_corners[k].y > img.rows - BORDER ) break; } @@ -734,51 +596,35 @@ int cvFindChessboardCorners( const void* arr, CvSize pattern_size, if( found ) { - if ( pattern_size.height % 2 == 0 && pattern_size.width % 2 == 0 ) - { - int last_row = (pattern_size.height-1)*pattern_size.width; - double dy0 = out_corners[last_row].y - out_corners[0].y; - if( dy0 < 0 ) + if ( pattern_size.height % 2 == 0 && pattern_size.width % 2 == 0 ) { - int n = pattern_size.width*pattern_size.height; - for(int i = 0; i < n/2; i++ ) - { - CvPoint2D32f temp; - CV_SWAP(out_corners[i], out_corners[n-i-1], temp); - } + int last_row = (pattern_size.height-1)*pattern_size.width; + double dy0 = out_corners[last_row].y - out_corners[0].y; + if( dy0 < 0 ) + { + int n = pattern_size.width*pattern_size.height; + for(int i = 0; i < n/2; i++ ) + { + CvPoint2D32f temp; + CV_SWAP(out_corners[i], out_corners[n-i-1], temp); + } + } } - } - cv::Ptr gray; - if( CV_MAT_CN(img->type) != 1 ) - { - gray.reset(cvCreateMat(img->rows, img->cols, CV_8UC1)); - cvCvtColor(img, gray, CV_BGR2GRAY); - } - else - { - gray.reset(cvCloneMat(img)); - } - int wsize = 2; - cvFindCornerSubPix( gray, out_corners, pattern_size.width*pattern_size.height, - cvSize(wsize, wsize), cvSize(-1,-1), - cvTermCriteria(CV_TERMCRIT_EPS+CV_TERMCRIT_ITER, 15, 0.1)); + int wsize = 2; + CvMat old_img(img); + cvFindCornerSubPix( &old_img, out_corners, pattern_size.width*pattern_size.height, + cvSize(wsize, wsize), cvSize(-1,-1), + cvTermCriteria(CV_TERMCRIT_EPS+CV_TERMCRIT_ITER, 15, 0.1)); } } catch(...) { cvFree(&quads); cvFree(&corners); - cvFree(&quad_group); - cvFree(&corner_group); - cvFree(&cImgSeg); throw; } - cvFree(&quads); cvFree(&corners); - cvFree(&quad_group); - cvFree(&corner_group); - cvFree(&cImgSeg); return found; } @@ -1866,8 +1712,9 @@ static void icvFindQuadNeighbors( CvCBQuad *quads, int quad_count ) static int icvGenerateQuads( CvCBQuad **out_quads, CvCBCorner **out_corners, - CvMemStorage *storage, CvMat *image, int flags, int *max_quad_buf_size ) + CvMemStorage *storage, const cv::Mat & image_, int flags, int *max_quad_buf_size ) { + CvMat image_old(image_), *image = &image_old; int quad_count = 0; cv::Ptr temp_storage; @@ -2011,6 +1858,88 @@ icvGenerateQuads( CvCBQuad **out_quads, CvCBCorner **out_corners, return quad_count; } +static bool processQuads(CvCBQuad *quads, int quad_count, CvSize pattern_size, int max_quad_buf_size, + CvMemStorage * storage, CvCBCorner *corners, CvPoint2D32f *out_corners, int *out_corner_count, int & prev_sqr_size) +{ + if( quad_count <= 0 ) + return false; + + bool found = false; + + // Find quad's neighbors + icvFindQuadNeighbors( quads, quad_count ); + + // allocate extra for adding in icvOrderFoundQuads + CvCBQuad **quad_group = 0; + CvCBCorner **corner_group = 0; + + quad_group = (CvCBQuad**)cvAlloc( sizeof(quad_group[0]) * max_quad_buf_size); + corner_group = (CvCBCorner**)cvAlloc( sizeof(corner_group[0]) * max_quad_buf_size * 4 ); + + for( int group_idx = 0; ; group_idx++ ) + { + int count = icvFindConnectedQuads( quads, quad_count, quad_group, group_idx, storage ); + + if( count == 0 ) + break; + + // order the quad corners globally + // maybe delete or add some + PRINTF("Starting ordering of inner quads (%d)\n", count); + count = icvOrderFoundConnectedQuads(count, quad_group, &quad_count, &quads, &corners, + pattern_size, max_quad_buf_size, storage ); + PRINTF("Finished ordering of inner quads (%d)\n", count); + + if (count == 0) + continue; // haven't found inner quads + + // If count is more than it should be, this will remove those quads + // which cause maximum deviation from a nice square pattern. + count = icvCleanFoundConnectedQuads( count, quad_group, pattern_size ); + PRINTF("Connected group: %d, count: %d\n", group_idx, count); + + count = icvCheckQuadGroup( quad_group, count, corner_group, pattern_size ); + PRINTF("Connected group: %d, count: %d\n", group_idx, count); + + int n = count > 0 ? pattern_size.width * pattern_size.height : -count; + n = MIN( n, pattern_size.width * pattern_size.height ); + float sum_dist = 0; + int total = 0; + + for(int i = 0; i < n; i++ ) + { + int ni = 0; + float avgi = corner_group[i]->meanDist(&ni); + sum_dist += avgi*ni; + total += ni; + } + prev_sqr_size = cvRound(sum_dist/MAX(total, 1)); + + if( count > 0 || (out_corner_count && -count > *out_corner_count) ) + { + // copy corners to output array + for(int i = 0; i < n; i++ ) + out_corners[i] = corner_group[i]->pt; + + if( out_corner_count ) + *out_corner_count = n; + + if( count == pattern_size.width*pattern_size.height + && icvCheckBoardMonotony( out_corners, pattern_size )) + { + found = true; + break; + } + } + } + + cvFree(&quad_group); + cvFree(&corner_group); + + return found; +} + +//================================================================================================== CV_IMPL void cvDrawChessboardCorners( CvArr* _image, CvSize pattern_size, diff --git a/modules/calib3d/src/checkchessboard.cpp b/modules/calib3d/src/checkchessboard.cpp index 88c6baf107..ea3487846b 100644 --- a/modules/calib3d/src/checkchessboard.cpp +++ b/modules/calib3d/src/checkchessboard.cpp @@ -46,28 +46,26 @@ #include #include -//#define DEBUG_WINDOWS +using namespace cv; +using namespace std; -#if defined(DEBUG_WINDOWS) -# include "opencv2/opencv_modules.hpp" -# ifdef HAVE_OPENCV_HIGHGUI -# include "opencv2/highgui.hpp" -# else -# undef DEBUG_WINDOWS -# endif -#endif - -int cvCheckChessboardBinary(IplImage* src, CvSize size); - -static void icvGetQuadrangleHypotheses(CvSeq* contours, std::vector >& quads, int class_id) +static void icvGetQuadrangleHypotheses(const std::vector > & contours, const std::vector< cv::Vec4i > & hierarchy, std::vector >& quads, int class_id) { const float min_aspect_ratio = 0.3f; const float max_aspect_ratio = 3.0f; const float min_box_size = 10.0f; - for(CvSeq* seq = contours; seq != NULL; seq = seq->h_next) + typedef std::vector< std::vector< cv::Point > >::const_iterator iter_t; + iter_t i; + for (i = contours.begin(); i != contours.end(); ++i) { - CvBox2D box = cvMinAreaRect2(seq); + const iter_t::difference_type idx = i - contours.begin(); + if (hierarchy.at(idx)[3] != -1) + continue; // skip holes + + const std::vector< cv::Point > & c = *i; + cv::RotatedRect box = cv::minAreaRect(c); + float box_size = MAX(box.size.width, box.size.height); if(box_size < min_box_size) { @@ -98,113 +96,98 @@ inline bool less_pred(const std::pair& p1, const std::pair > & quads) { - if(src->nChannels > 1) + Mat thresh; { - cvError(CV_BadNumChannels, "cvCheckChessboard", "supports single-channel images only", - __FILE__, __LINE__); + vector< vector > contours; + vector< Vec4i > hierarchy; + threshold(white, thresh, white_thresh, 255, THRESH_BINARY); + findContours(thresh, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE); + icvGetQuadrangleHypotheses(contours, hierarchy, quads, 1); } - if(src->depth != 8) { - cvError(CV_BadDepth, "cvCheckChessboard", "supports depth=8 images only", - __FILE__, __LINE__); + vector< vector > contours; + vector< Vec4i > hierarchy; + threshold(black, thresh, black_thresh, 255, THRESH_BINARY_INV); + findContours(thresh, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE); + icvGetQuadrangleHypotheses(contours, hierarchy, quads, 0); } +} - const int erosion_count = 1; - const float black_level = 20.f; - const float white_level = 130.f; - const float black_white_gap = 70.f; - -#if defined(DEBUG_WINDOWS) - cvNamedWindow("1", 1); - cvShowImage("1", src); - cvWaitKey(0); -#endif //DEBUG_WINDOWS - - CvMemStorage* storage = cvCreateMemStorage(); - - IplImage* white = cvCloneImage(src); - IplImage* black = cvCloneImage(src); +static bool checkQuads(vector > & quads, const cv::Size & size) +{ + const size_t min_quads_count = size.width*size.height/2; + std::sort(quads.begin(), quads.end(), less_pred); - cvErode(white, white, NULL, erosion_count); - cvDilate(black, black, NULL, erosion_count); - IplImage* thresh = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1); + // now check if there are many hypotheses with similar sizes + // do this by floodfill-style algorithm + const float size_rel_dev = 0.4f; - int result = 0; - for(float thresh_level = black_level; thresh_level < white_level && !result; thresh_level += 20.0f) + for(size_t i = 0; i < quads.size(); i++) { - cvThreshold(white, thresh, thresh_level + black_white_gap, 255, CV_THRESH_BINARY); - -#if defined(DEBUG_WINDOWS) - cvShowImage("1", thresh); - cvWaitKey(0); -#endif //DEBUG_WINDOWS - - CvSeq* first = 0; - std::vector > quads; - cvFindContours(thresh, storage, &first, sizeof(CvContour), CV_RETR_CCOMP); - icvGetQuadrangleHypotheses(first, quads, 1); - - cvThreshold(black, thresh, thresh_level, 255, CV_THRESH_BINARY_INV); - -#if defined(DEBUG_WINDOWS) - cvShowImage("1", thresh); - cvWaitKey(0); -#endif //DEBUG_WINDOWS - - cvFindContours(thresh, storage, &first, sizeof(CvContour), CV_RETR_CCOMP); - icvGetQuadrangleHypotheses(first, quads, 0); - - const size_t min_quads_count = size.width*size.height/2; - std::sort(quads.begin(), quads.end(), less_pred); - - // now check if there are many hypotheses with similar sizes - // do this by floodfill-style algorithm - const float size_rel_dev = 0.4f; - - for(size_t i = 0; i < quads.size(); i++) + size_t j = i + 1; + for(; j < quads.size(); j++) { - size_t j = i + 1; - for(; j < quads.size(); j++) + if(quads[j].first/quads[i].first > 1.0f + size_rel_dev) { - if(quads[j].first/quads[i].first > 1.0f + size_rel_dev) - { - break; - } + break; } + } - if(j + 1 > min_quads_count + i) + if(j + 1 > min_quads_count + i) + { + // check the number of black and white squares + std::vector counts; + countClasses(quads, i, j, counts); + const int black_count = cvRound(ceil(size.width/2.0)*ceil(size.height/2.0)); + const int white_count = cvRound(floor(size.width/2.0)*floor(size.height/2.0)); + if(counts[0] < black_count*0.75 || + counts[1] < white_count*0.75) { - // check the number of black and white squares - std::vector counts; - countClasses(quads, i, j, counts); - const int black_count = cvRound(ceil(size.width/2.0)*ceil(size.height/2.0)); - const int white_count = cvRound(floor(size.width/2.0)*floor(size.height/2.0)); - if(counts[0] < black_count*0.75 || - counts[1] < white_count*0.75) - { - continue; - } - result = 1; - break; + continue; } + return true; } } + return false; +} +// does a fast check if a chessboard is in the input image. This is a workaround to +// a problem of cvFindChessboardCorners being slow on images with no chessboard +// - src: input image +// - size: chessboard size +// Returns 1 if a chessboard can be in this image and findChessboardCorners should be called, +// 0 if there is no chessboard, -1 in case of error +int cvCheckChessboard(IplImage* src, CvSize size) +{ + cv::Mat img = cv::cvarrToMat(src); + return checkChessboard(img, size); +} - cvReleaseImage(&thresh); - cvReleaseImage(&white); - cvReleaseImage(&black); - cvReleaseMemStorage(&storage); +int checkChessboard(const cv::Mat & img, const cv::Size & size) +{ + CV_Assert(img.channels() == 1 && img.depth() == CV_8U); + const int erosion_count = 1; + const float black_level = 20.f; + const float white_level = 130.f; + const float black_white_gap = 70.f; + + Mat white; + Mat black; + erode(img, white, Mat(), Point(-1, -1), erosion_count); + dilate(img, black, Mat(), Point(-1, -1), erosion_count); + + int result = 0; + for(float thresh_level = black_level; thresh_level < white_level && !result; thresh_level += 20.0f) + { + vector > quads; + fillQuads(white, black, thresh_level + black_white_gap, thresh_level, quads); + if (checkQuads(quads, size)) + result = 1; + } return result; } @@ -214,90 +197,29 @@ int cvCheckChessboard(IplImage* src, CvSize size) // - size: chessboard size // Returns 1 if a chessboard can be in this image and findChessboardCorners should be called, // 0 if there is no chessboard, -1 in case of error -int cvCheckChessboardBinary(IplImage* src, CvSize size) +int checkChessboardBinary(const cv::Mat & img, const cv::Size & size) { - if(src->nChannels > 1) - { - cvError(CV_BadNumChannels, "cvCheckChessboard", "supports single-channel images only", - __FILE__, __LINE__); - } - - if(src->depth != 8) - { - cvError(CV_BadDepth, "cvCheckChessboard", "supports depth=8 images only", - __FILE__, __LINE__); - } - - CvMemStorage* storage = cvCreateMemStorage(); + CV_Assert(img.channels() == 1 && img.depth() == CV_8U); - IplImage* white = cvCloneImage(src); - IplImage* black = cvCloneImage(src); - IplImage* thresh = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1); + Mat white = img.clone(); + Mat black = img.clone(); int result = 0; - for ( int erosion_count = 0; erosion_count <= 3; erosion_count++ ) { - if ( 1 == result ) - break; - - if ( 0 != erosion_count ) // first iteration keeps original images - { - cvErode(white, white, NULL, 1); - cvDilate(black, black, NULL, 1); - } - - cvThreshold(white, thresh, 128, 255, CV_THRESH_BINARY); - - CvSeq* first = 0; - std::vector > quads; - cvFindContours(thresh, storage, &first, sizeof(CvContour), CV_RETR_CCOMP); - icvGetQuadrangleHypotheses(first, quads, 1); + if ( 1 == result ) + break; - cvThreshold(black, thresh, 128, 255, CV_THRESH_BINARY_INV); - cvFindContours(thresh, storage, &first, sizeof(CvContour), CV_RETR_CCOMP); - icvGetQuadrangleHypotheses(first, quads, 0); - - const size_t min_quads_count = size.width*size.height/2; - std::sort(quads.begin(), quads.end(), less_pred); - - // now check if there are many hypotheses with similar sizes - // do this by floodfill-style algorithm - const float size_rel_dev = 0.4f; - - for(size_t i = 0; i < quads.size(); i++) - { - size_t j = i + 1; - for(; j < quads.size(); j++) - { - if(quads[j].first/quads[i].first > 1.0f + size_rel_dev) - { - break; - } - } + if ( 0 != erosion_count ) // first iteration keeps original images + { + erode(white, white, Mat(), Point(-1, -1), 1); + dilate(black, black, Mat(), Point(-1, -1), 1); + } - if(j + 1 > min_quads_count + i) - { - // check the number of black and white squares - std::vector counts; - countClasses(quads, i, j, counts); - const int black_count = cvRound(ceil(size.width/2.0)*ceil(size.height/2.0)); - const int white_count = cvRound(floor(size.width/2.0)*floor(size.height/2.0)); - if(counts[0] < black_count*0.75 || - counts[1] < white_count*0.75) - { - continue; - } - result = 1; - break; - } - } + vector > quads; + fillQuads(white, black, 128, 128, quads); + if (checkQuads(quads, size)) + result = 1; } - - cvReleaseImage(&thresh); - cvReleaseImage(&white); - cvReleaseImage(&black); - cvReleaseMemStorage(&storage); - return result; -} \ No newline at end of file +} diff --git a/modules/calib3d/src/precomp.hpp b/modules/calib3d/src/precomp.hpp index 83a513dcaa..0005f234e2 100644 --- a/modules/calib3d/src/precomp.hpp +++ b/modules/calib3d/src/precomp.hpp @@ -117,4 +117,7 @@ template inline int compressElems( T* ptr, const uchar* mask, int ms } +int checkChessboard(const cv::Mat & img, const cv::Size & size); +int checkChessboardBinary(const cv::Mat & img, const cv::Size & size); + #endif diff --git a/modules/calib3d/test/test_chesscorners.cpp b/modules/calib3d/test/test_chesscorners.cpp index fd3da2e440..f4208a574c 100644 --- a/modules/calib3d/test/test_chesscorners.cpp +++ b/modules/calib3d/test/test_chesscorners.cpp @@ -51,29 +51,31 @@ using namespace cv; #define _L2_ERR -void show_points( const Mat& gray, const Mat& u, const vector& v, Size pattern_size, bool was_found ) +//#define DEBUG_CHESSBOARD + +#ifdef DEBUG_CHESSBOARD +#include "opencv2/highgui.hpp" +void show_points( const Mat& gray, const Mat& expected, const vector& actual, bool was_found ) { Mat rgb( gray.size(), CV_8U); merge(vector(3, gray), rgb); - for(size_t i = 0; i < v.size(); i++ ) - circle( rgb, v[i], 3, Scalar(255, 0, 0), FILLED); + for(size_t i = 0; i < actual.size(); i++ ) + circle( rgb, actual[i], 5, Scalar(0, 0, 200), 1, LINE_AA); - if( !u.empty() ) + if( !expected.empty() ) { - const Point2f* u_data = u.ptr(); - size_t count = u.cols * u.rows; + const Point2f* u_data = expected.ptr(); + size_t count = expected.cols * expected.rows; for(size_t i = 0; i < count; i++ ) - circle( rgb, u_data[i], 3, Scalar(0, 255, 0), FILLED); - } - if (!v.empty()) - { - Mat corners((int)v.size(), 1, CV_32FC2, (void*)&v[0]); - drawChessboardCorners( rgb, pattern_size, corners, was_found ); + circle(rgb, u_data[i], 4, Scalar(0, 240, 0), 1, LINE_AA); } - //namedWindow( "test", 0 ); imshow( "test", rgb ); waitKey(0); + putText(rgb, was_found ? "FOUND !!!" : "NOT FOUND", Point(5, 20), FONT_HERSHEY_PLAIN, 1, Scalar(0, 240, 0)); + imshow( "test", rgb ); while ((uchar)waitKey(0) != 'q') {}; } - +#else +#define show_points(...) +#endif enum Pattern { CHESSBOARD, CIRCLES_GRID, ASYMMETRIC_CIRCLES_GRID }; @@ -253,7 +255,6 @@ void CV_ChessboardDetectorTest::run_batch( const string& filename ) result = findCirclesGrid(gray, pattern_size, v, CALIB_CB_ASYMMETRIC_GRID | algorithmFlags); break; } - show_points( gray, Mat(), v, pattern_size, result ); if( result ^ doesContatinChessboard || v.size() != count_exp ) { @@ -280,7 +281,7 @@ void CV_ChessboardDetectorTest::run_batch( const string& filename ) if( pattern == CHESSBOARD ) cornerSubPix( gray, v, Size(5, 5), Size(-1,-1), TermCriteria(TermCriteria::EPS|TermCriteria::MAX_ITER, 30, 0.1)); //find4QuadCornerSubpix(gray, v, Size(5, 5)); - show_points( gray, expected, v, pattern_size, result ); + show_points( gray, expected, v, result ); #ifndef WRITE_POINTS // printf("called find4QuadCornerSubpix\n"); err = calcError(v, expected); @@ -298,6 +299,10 @@ void CV_ChessboardDetectorTest::run_batch( const string& filename ) max_precise_error = MAX( max_precise_error, err ); #endif } + else + { + show_points( gray, Mat(), v, result ); + } #ifdef WRITE_POINTS Mat mat_v(pattern_size, CV_32FC2, (void*)&v[0]); diff --git a/modules/python/test/test_calibration.py b/modules/python/test/test_calibration.py index 665521862c..584c7e6cbb 100644 --- a/modules/python/test/test_calibration.py +++ b/modules/python/test/test_calibration.py @@ -57,7 +57,7 @@ class calibration_test(NewOpenCVTests): eps = 0.01 normCamEps = 10.0 - normDistEps = 0.001 + normDistEps = 0.05 cameraMatrixTest = [[ 532.80992189, 0., 342.4952186 ], [ 0., 532.93346422, 233.8879292 ], @@ -68,4 +68,4 @@ class calibration_test(NewOpenCVTests): self.assertLess(abs(rms - 0.196334638034), eps) self.assertLess(cv2.norm(camera_matrix - cameraMatrixTest, cv2.NORM_L1), normCamEps) - self.assertLess(cv2.norm(dist_coefs - distCoeffsTest, cv2.NORM_L1), normDistEps) \ No newline at end of file + self.assertLess(cv2.norm(dist_coefs - distCoeffsTest, cv2.NORM_L1), normDistEps)