Home | History | Annotate | Download | only in camera_calibration
      1 #include <iostream>
      2 #include <sstream>
      3 #include <time.h>
      4 #include <stdio.h>
      5 
      6 #include <opencv2/core.hpp>
      7 #include <opencv2/core/utility.hpp>
      8 #include <opencv2/imgproc.hpp>
      9 #include <opencv2/calib3d.hpp>
     10 #include <opencv2/imgcodecs.hpp>
     11 #include <opencv2/videoio.hpp>
     12 #include <opencv2/highgui.hpp>
     13 
     14 #ifndef _CRT_SECURE_NO_WARNINGS
     15 # define _CRT_SECURE_NO_WARNINGS
     16 #endif
     17 
     18 using namespace cv;
     19 using namespace std;
     20 
     21 static void help()
     22 {
     23     cout <<  "This is a camera calibration sample." << endl
     24          <<  "Usage: calibration configurationFile"  << endl
     25          <<  "Near the sample file you'll find the configuration file, which has detailed help of "
     26              "how to edit it.  It may be any OpenCV supported file format XML/YAML." << endl;
     27 }
     28 class Settings
     29 {
     30 public:
     31     Settings() : goodInput(false) {}
     32     enum Pattern { NOT_EXISTING, CHESSBOARD, CIRCLES_GRID, ASYMMETRIC_CIRCLES_GRID };
     33     enum InputType { INVALID, CAMERA, VIDEO_FILE, IMAGE_LIST };
     34 
     35     void write(FileStorage& fs) const                        //Write serialization for this class
     36     {
     37         fs << "{"
     38                   << "BoardSize_Width"  << boardSize.width
     39                   << "BoardSize_Height" << boardSize.height
     40                   << "Square_Size"         << squareSize
     41                   << "Calibrate_Pattern" << patternToUse
     42                   << "Calibrate_NrOfFrameToUse" << nrFrames
     43                   << "Calibrate_FixAspectRatio" << aspectRatio
     44                   << "Calibrate_AssumeZeroTangentialDistortion" << calibZeroTangentDist
     45                   << "Calibrate_FixPrincipalPointAtTheCenter" << calibFixPrincipalPoint
     46 
     47                   << "Write_DetectedFeaturePoints" << writePoints
     48                   << "Write_extrinsicParameters"   << writeExtrinsics
     49                   << "Write_outputFileName"  << outputFileName
     50 
     51                   << "Show_UndistortedImage" << showUndistorsed
     52 
     53                   << "Input_FlipAroundHorizontalAxis" << flipVertical
     54                   << "Input_Delay" << delay
     55                   << "Input" << input
     56            << "}";
     57     }
     58     void read(const FileNode& node)                          //Read serialization for this class
     59     {
     60         node["BoardSize_Width" ] >> boardSize.width;
     61         node["BoardSize_Height"] >> boardSize.height;
     62         node["Calibrate_Pattern"] >> patternToUse;
     63         node["Square_Size"]  >> squareSize;
     64         node["Calibrate_NrOfFrameToUse"] >> nrFrames;
     65         node["Calibrate_FixAspectRatio"] >> aspectRatio;
     66         node["Write_DetectedFeaturePoints"] >> writePoints;
     67         node["Write_extrinsicParameters"] >> writeExtrinsics;
     68         node["Write_outputFileName"] >> outputFileName;
     69         node["Calibrate_AssumeZeroTangentialDistortion"] >> calibZeroTangentDist;
     70         node["Calibrate_FixPrincipalPointAtTheCenter"] >> calibFixPrincipalPoint;
     71         node["Input_FlipAroundHorizontalAxis"] >> flipVertical;
     72         node["Show_UndistortedImage"] >> showUndistorsed;
     73         node["Input"] >> input;
     74         node["Input_Delay"] >> delay;
     75         validate();
     76     }
     77     void validate()
     78     {
     79         goodInput = true;
     80         if (boardSize.width <= 0 || boardSize.height <= 0)
     81         {
     82             cerr << "Invalid Board size: " << boardSize.width << " " << boardSize.height << endl;
     83             goodInput = false;
     84         }
     85         if (squareSize <= 10e-6)
     86         {
     87             cerr << "Invalid square size " << squareSize << endl;
     88             goodInput = false;
     89         }
     90         if (nrFrames <= 0)
     91         {
     92             cerr << "Invalid number of frames " << nrFrames << endl;
     93             goodInput = false;
     94         }
     95 
     96         if (input.empty())      // Check for valid input
     97                 inputType = INVALID;
     98         else
     99         {
    100             if (input[0] >= '0' && input[0] <= '9')
    101             {
    102                 stringstream ss(input);
    103                 ss >> cameraID;
    104                 inputType = CAMERA;
    105             }
    106             else
    107             {
    108                 if (readStringList(input, imageList))
    109                 {
    110                     inputType = IMAGE_LIST;
    111                     nrFrames = (nrFrames < (int)imageList.size()) ? nrFrames : (int)imageList.size();
    112                 }
    113                 else
    114                     inputType = VIDEO_FILE;
    115             }
    116             if (inputType == CAMERA)
    117                 inputCapture.open(cameraID);
    118             if (inputType == VIDEO_FILE)
    119                 inputCapture.open(input);
    120             if (inputType != IMAGE_LIST && !inputCapture.isOpened())
    121                     inputType = INVALID;
    122         }
    123         if (inputType == INVALID)
    124         {
    125             cerr << " Input does not exist: " << input;
    126             goodInput = false;
    127         }
    128 
    129         flag = 0;
    130         if(calibFixPrincipalPoint) flag |= CALIB_FIX_PRINCIPAL_POINT;
    131         if(calibZeroTangentDist)   flag |= CALIB_ZERO_TANGENT_DIST;
    132         if(aspectRatio)            flag |= CALIB_FIX_ASPECT_RATIO;
    133 
    134 
    135         calibrationPattern = NOT_EXISTING;
    136         if (!patternToUse.compare("CHESSBOARD")) calibrationPattern = CHESSBOARD;
    137         if (!patternToUse.compare("CIRCLES_GRID")) calibrationPattern = CIRCLES_GRID;
    138         if (!patternToUse.compare("ASYMMETRIC_CIRCLES_GRID")) calibrationPattern = ASYMMETRIC_CIRCLES_GRID;
    139         if (calibrationPattern == NOT_EXISTING)
    140         {
    141             cerr << " Camera calibration mode does not exist: " << patternToUse << endl;
    142             goodInput = false;
    143         }
    144         atImageList = 0;
    145 
    146     }
    147     Mat nextImage()
    148     {
    149         Mat result;
    150         if( inputCapture.isOpened() )
    151         {
    152             Mat view0;
    153             inputCapture >> view0;
    154             view0.copyTo(result);
    155         }
    156         else if( atImageList < imageList.size() )
    157             result = imread(imageList[atImageList++], IMREAD_COLOR);
    158 
    159         return result;
    160     }
    161 
    162     static bool readStringList( const string& filename, vector<string>& l )
    163     {
    164         l.clear();
    165         FileStorage fs(filename, FileStorage::READ);
    166         if( !fs.isOpened() )
    167             return false;
    168         FileNode n = fs.getFirstTopLevelNode();
    169         if( n.type() != FileNode::SEQ )
    170             return false;
    171         FileNodeIterator it = n.begin(), it_end = n.end();
    172         for( ; it != it_end; ++it )
    173             l.push_back((string)*it);
    174         return true;
    175     }
    176 public:
    177     Size boardSize;              // The size of the board -> Number of items by width and height
    178     Pattern calibrationPattern;  // One of the Chessboard, circles, or asymmetric circle pattern
    179     float squareSize;            // The size of a square in your defined unit (point, millimeter,etc).
    180     int nrFrames;                // The number of frames to use from the input for calibration
    181     float aspectRatio;           // The aspect ratio
    182     int delay;                   // In case of a video input
    183     bool writePoints;            // Write detected feature points
    184     bool writeExtrinsics;        // Write extrinsic parameters
    185     bool calibZeroTangentDist;   // Assume zero tangential distortion
    186     bool calibFixPrincipalPoint; // Fix the principal point at the center
    187     bool flipVertical;           // Flip the captured images around the horizontal axis
    188     string outputFileName;       // The name of the file where to write
    189     bool showUndistorsed;        // Show undistorted images after calibration
    190     string input;                // The input ->
    191 
    192     int cameraID;
    193     vector<string> imageList;
    194     size_t atImageList;
    195     VideoCapture inputCapture;
    196     InputType inputType;
    197     bool goodInput;
    198     int flag;
    199 
    200 private:
    201     string patternToUse;
    202 
    203 
    204 };
    205 
    206 static inline void read(const FileNode& node, Settings& x, const Settings& default_value = Settings())
    207 {
    208     if(node.empty())
    209         x = default_value;
    210     else
    211         x.read(node);
    212 }
    213 
    214 static inline void write(FileStorage& fs, const String&, const Settings& s )
    215 {
    216     s.write(fs);
    217 }
    218 
    219 enum { DETECTION = 0, CAPTURING = 1, CALIBRATED = 2 };
    220 
    221 bool runCalibrationAndSave(Settings& s, Size imageSize, Mat&  cameraMatrix, Mat& distCoeffs,
    222                            vector<vector<Point2f> > imagePoints );
    223 
    224 int main(int argc, char* argv[])
    225 {
    226     help();
    227 
    228     //! [file_read]
    229     Settings s;
    230     const string inputSettingsFile = argc > 1 ? argv[1] : "default.xml";
    231     FileStorage fs(inputSettingsFile, FileStorage::READ); // Read the settings
    232     if (!fs.isOpened())
    233     {
    234         cout << "Could not open the configuration file: \"" << inputSettingsFile << "\"" << endl;
    235         return -1;
    236     }
    237     fs["Settings"] >> s;
    238     fs.release();                                         // close Settings file
    239     //! [file_read]
    240 
    241     //FileStorage fout("settings.yml", FileStorage::WRITE); // write config as YAML
    242     //fout << "Settings" << s;
    243 
    244     if (!s.goodInput)
    245     {
    246         cout << "Invalid input detected. Application stopping. " << endl;
    247         return -1;
    248     }
    249 
    250     vector<vector<Point2f> > imagePoints;
    251     Mat cameraMatrix, distCoeffs;
    252     Size imageSize;
    253     int mode = s.inputType == Settings::IMAGE_LIST ? CAPTURING : DETECTION;
    254     clock_t prevTimestamp = 0;
    255     const Scalar RED(0,0,255), GREEN(0,255,0);
    256     const char ESC_KEY = 27;
    257 
    258     //! [get_input]
    259     for(;;)
    260     {
    261         Mat view;
    262         bool blinkOutput = false;
    263 
    264         view = s.nextImage();
    265 
    266         //-----  If no more image, or got enough, then stop calibration and show result -------------
    267         if( mode == CAPTURING && imagePoints.size() >= (size_t)s.nrFrames )
    268         {
    269           if( runCalibrationAndSave(s, imageSize,  cameraMatrix, distCoeffs, imagePoints))
    270               mode = CALIBRATED;
    271           else
    272               mode = DETECTION;
    273         }
    274         if(view.empty())          // If there are no more images stop the loop
    275         {
    276             // if calibration threshold was not reached yet, calibrate now
    277             if( mode != CALIBRATED && !imagePoints.empty() )
    278                 runCalibrationAndSave(s, imageSize,  cameraMatrix, distCoeffs, imagePoints);
    279             break;
    280         }
    281         //! [get_input]
    282 
    283         imageSize = view.size();  // Format input image.
    284         if( s.flipVertical )    flip( view, view, 0 );
    285 
    286         //! [find_pattern]
    287         vector<Point2f> pointBuf;
    288 
    289         bool found;
    290         switch( s.calibrationPattern ) // Find feature points on the input format
    291         {
    292         case Settings::CHESSBOARD:
    293             found = findChessboardCorners( view, s.boardSize, pointBuf,
    294                 CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_FAST_CHECK | CALIB_CB_NORMALIZE_IMAGE);
    295             break;
    296         case Settings::CIRCLES_GRID:
    297             found = findCirclesGrid( view, s.boardSize, pointBuf );
    298             break;
    299         case Settings::ASYMMETRIC_CIRCLES_GRID:
    300             found = findCirclesGrid( view, s.boardSize, pointBuf, CALIB_CB_ASYMMETRIC_GRID );
    301             break;
    302         default:
    303             found = false;
    304             break;
    305         }
    306         //! [find_pattern]
    307         //! [pattern_found]
    308         if ( found)                // If done with success,
    309         {
    310               // improve the found corners' coordinate accuracy for chessboard
    311                 if( s.calibrationPattern == Settings::CHESSBOARD)
    312                 {
    313                     Mat viewGray;
    314                     cvtColor(view, viewGray, COLOR_BGR2GRAY);
    315                     cornerSubPix( viewGray, pointBuf, Size(11,11),
    316                         Size(-1,-1), TermCriteria( TermCriteria::EPS+TermCriteria::COUNT, 30, 0.1 ));
    317                 }
    318 
    319                 if( mode == CAPTURING &&  // For camera only take new samples after delay time
    320                     (!s.inputCapture.isOpened() || clock() - prevTimestamp > s.delay*1e-3*CLOCKS_PER_SEC) )
    321                 {
    322                     imagePoints.push_back(pointBuf);
    323                     prevTimestamp = clock();
    324                     blinkOutput = s.inputCapture.isOpened();
    325                 }
    326 
    327                 // Draw the corners.
    328                 drawChessboardCorners( view, s.boardSize, Mat(pointBuf), found );
    329         }
    330         //! [pattern_found]
    331         //----------------------------- Output Text ------------------------------------------------
    332         //! [output_text]
    333         string msg = (mode == CAPTURING) ? "100/100" :
    334                       mode == CALIBRATED ? "Calibrated" : "Press 'g' to start";
    335         int baseLine = 0;
    336         Size textSize = getTextSize(msg, 1, 1, 1, &baseLine);
    337         Point textOrigin(view.cols - 2*textSize.width - 10, view.rows - 2*baseLine - 10);
    338 
    339         if( mode == CAPTURING )
    340         {
    341             if(s.showUndistorsed)
    342                 msg = format( "%d/%d Undist", (int)imagePoints.size(), s.nrFrames );
    343             else
    344                 msg = format( "%d/%d", (int)imagePoints.size(), s.nrFrames );
    345         }
    346 
    347         putText( view, msg, textOrigin, 1, 1, mode == CALIBRATED ?  GREEN : RED);
    348 
    349         if( blinkOutput )
    350             bitwise_not(view, view);
    351         //! [output_text]
    352         //------------------------- Video capture  output  undistorted ------------------------------
    353         //! [output_undistorted]
    354         if( mode == CALIBRATED && s.showUndistorsed )
    355         {
    356             Mat temp = view.clone();
    357             undistort(temp, view, cameraMatrix, distCoeffs);
    358         }
    359         //! [output_undistorted]
    360         //------------------------------ Show image and check for input commands -------------------
    361         //! [await_input]
    362         imshow("Image View", view);
    363         char key = (char)waitKey(s.inputCapture.isOpened() ? 50 : s.delay);
    364 
    365         if( key  == ESC_KEY )
    366             break;
    367 
    368         if( key == 'u' && mode == CALIBRATED )
    369            s.showUndistorsed = !s.showUndistorsed;
    370 
    371         if( s.inputCapture.isOpened() && key == 'g' )
    372         {
    373             mode = CAPTURING;
    374             imagePoints.clear();
    375         }
    376         //! [await_input]
    377     }
    378 
    379     // -----------------------Show the undistorted image for the image list ------------------------
    380     //! [show_results]
    381     if( s.inputType == Settings::IMAGE_LIST && s.showUndistorsed )
    382     {
    383         Mat view, rview, map1, map2;
    384         initUndistortRectifyMap(cameraMatrix, distCoeffs, Mat(),
    385             getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, imageSize, 1, imageSize, 0),
    386             imageSize, CV_16SC2, map1, map2);
    387 
    388         for(size_t i = 0; i < s.imageList.size(); i++ )
    389         {
    390             view = imread(s.imageList[i], 1);
    391             if(view.empty())
    392                 continue;
    393             remap(view, rview, map1, map2, INTER_LINEAR);
    394             imshow("Image View", rview);
    395             char c = (char)waitKey();
    396             if( c  == ESC_KEY || c == 'q' || c == 'Q' )
    397                 break;
    398         }
    399     }
    400     //! [show_results]
    401 
    402     return 0;
    403 }
    404 
    405 //! [compute_errors]
    406 static double computeReprojectionErrors( const vector<vector<Point3f> >& objectPoints,
    407                                          const vector<vector<Point2f> >& imagePoints,
    408                                          const vector<Mat>& rvecs, const vector<Mat>& tvecs,
    409                                          const Mat& cameraMatrix , const Mat& distCoeffs,
    410                                          vector<float>& perViewErrors)
    411 {
    412     vector<Point2f> imagePoints2;
    413     size_t totalPoints = 0;
    414     double totalErr = 0, err;
    415     perViewErrors.resize(objectPoints.size());
    416 
    417     for(size_t i = 0; i < objectPoints.size(); ++i )
    418     {
    419         projectPoints(objectPoints[i], rvecs[i], tvecs[i], cameraMatrix, distCoeffs, imagePoints2);
    420         err = norm(imagePoints[i], imagePoints2, NORM_L2);
    421 
    422         size_t n = objectPoints[i].size();
    423         perViewErrors[i] = (float) std::sqrt(err*err/n);
    424         totalErr        += err*err;
    425         totalPoints     += n;
    426     }
    427 
    428     return std::sqrt(totalErr/totalPoints);
    429 }
    430 //! [compute_errors]
    431 //! [board_corners]
    432 static void calcBoardCornerPositions(Size boardSize, float squareSize, vector<Point3f>& corners,
    433                                      Settings::Pattern patternType /*= Settings::CHESSBOARD*/)
    434 {
    435     corners.clear();
    436 
    437     switch(patternType)
    438     {
    439     case Settings::CHESSBOARD:
    440     case Settings::CIRCLES_GRID:
    441         for( int i = 0; i < boardSize.height; ++i )
    442             for( int j = 0; j < boardSize.width; ++j )
    443                 corners.push_back(Point3f(j*squareSize, i*squareSize, 0));
    444         break;
    445 
    446     case Settings::ASYMMETRIC_CIRCLES_GRID:
    447         for( int i = 0; i < boardSize.height; i++ )
    448             for( int j = 0; j < boardSize.width; j++ )
    449                 corners.push_back(Point3f((2*j + i % 2)*squareSize, i*squareSize, 0));
    450         break;
    451     default:
    452         break;
    453     }
    454 }
    455 //! [board_corners]
    456 static bool runCalibration( Settings& s, Size& imageSize, Mat& cameraMatrix, Mat& distCoeffs,
    457                             vector<vector<Point2f> > imagePoints, vector<Mat>& rvecs, vector<Mat>& tvecs,
    458                             vector<float>& reprojErrs,  double& totalAvgErr)
    459 {
    460     //! [fixed_aspect]
    461     cameraMatrix = Mat::eye(3, 3, CV_64F);
    462     if( s.flag & CALIB_FIX_ASPECT_RATIO )
    463         cameraMatrix.at<double>(0,0) = s.aspectRatio;
    464     //! [fixed_aspect]
    465     distCoeffs = Mat::zeros(8, 1, CV_64F);
    466 
    467     vector<vector<Point3f> > objectPoints(1);
    468     calcBoardCornerPositions(s.boardSize, s.squareSize, objectPoints[0], s.calibrationPattern);
    469 
    470     objectPoints.resize(imagePoints.size(),objectPoints[0]);
    471 
    472     //Find intrinsic and extrinsic camera parameters
    473     double rms = calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix,
    474                                  distCoeffs, rvecs, tvecs, s.flag|CALIB_FIX_K4|CALIB_FIX_K5);
    475 
    476     cout << "Re-projection error reported by calibrateCamera: "<< rms << endl;
    477 
    478     bool ok = checkRange(cameraMatrix) && checkRange(distCoeffs);
    479 
    480     totalAvgErr = computeReprojectionErrors(objectPoints, imagePoints,
    481                                              rvecs, tvecs, cameraMatrix, distCoeffs, reprojErrs);
    482 
    483     return ok;
    484 }
    485 
    486 // Print camera parameters to the output file
    487 static void saveCameraParams( Settings& s, Size& imageSize, Mat& cameraMatrix, Mat& distCoeffs,
    488                               const vector<Mat>& rvecs, const vector<Mat>& tvecs,
    489                               const vector<float>& reprojErrs, const vector<vector<Point2f> >& imagePoints,
    490                               double totalAvgErr )
    491 {
    492     FileStorage fs( s.outputFileName, FileStorage::WRITE );
    493 
    494     time_t tm;
    495     time( &tm );
    496     struct tm *t2 = localtime( &tm );
    497     char buf[1024];
    498     strftime( buf, sizeof(buf), "%c", t2 );
    499 
    500     fs << "calibration_time" << buf;
    501 
    502     if( !rvecs.empty() || !reprojErrs.empty() )
    503         fs << "nr_of_frames" << (int)std::max(rvecs.size(), reprojErrs.size());
    504     fs << "image_width" << imageSize.width;
    505     fs << "image_height" << imageSize.height;
    506     fs << "board_width" << s.boardSize.width;
    507     fs << "board_height" << s.boardSize.height;
    508     fs << "square_size" << s.squareSize;
    509 
    510     if( s.flag & CALIB_FIX_ASPECT_RATIO )
    511         fs << "fix_aspect_ratio" << s.aspectRatio;
    512 
    513     if (s.flag)
    514     {
    515         sprintf(buf, "flags: %s%s%s%s",
    516                 s.flag & CALIB_USE_INTRINSIC_GUESS ? " +use_intrinsic_guess" : "",
    517                 s.flag & CALIB_FIX_ASPECT_RATIO ? " +fix_aspect_ratio" : "",
    518                 s.flag & CALIB_FIX_PRINCIPAL_POINT ? " +fix_principal_point" : "",
    519                 s.flag & CALIB_ZERO_TANGENT_DIST ? " +zero_tangent_dist" : "");
    520         cvWriteComment(*fs, buf, 0);
    521     }
    522 
    523     fs << "flags" << s.flag;
    524 
    525     fs << "camera_matrix" << cameraMatrix;
    526     fs << "distortion_coefficients" << distCoeffs;
    527 
    528     fs << "avg_reprojection_error" << totalAvgErr;
    529     if (s.writeExtrinsics && !reprojErrs.empty())
    530         fs << "per_view_reprojection_errors" << Mat(reprojErrs);
    531 
    532     if(s.writeExtrinsics && !rvecs.empty() && !tvecs.empty() )
    533     {
    534         CV_Assert(rvecs[0].type() == tvecs[0].type());
    535         Mat bigmat((int)rvecs.size(), 6, rvecs[0].type());
    536         for( size_t i = 0; i < rvecs.size(); i++ )
    537         {
    538             Mat r = bigmat(Range(int(i), int(i+1)), Range(0,3));
    539             Mat t = bigmat(Range(int(i), int(i+1)), Range(3,6));
    540 
    541             CV_Assert(rvecs[i].rows == 3 && rvecs[i].cols == 1);
    542             CV_Assert(tvecs[i].rows == 3 && tvecs[i].cols == 1);
    543             //*.t() is MatExpr (not Mat) so we can use assignment operator
    544             r = rvecs[i].t();
    545             t = tvecs[i].t();
    546         }
    547         //cvWriteComment( *fs, "a set of 6-tuples (rotation vector + translation vector) for each view", 0 );
    548         fs << "extrinsic_parameters" << bigmat;
    549     }
    550 
    551     if(s.writePoints && !imagePoints.empty() )
    552     {
    553         Mat imagePtMat((int)imagePoints.size(), (int)imagePoints[0].size(), CV_32FC2);
    554         for( size_t i = 0; i < imagePoints.size(); i++ )
    555         {
    556             Mat r = imagePtMat.row(int(i)).reshape(2, imagePtMat.cols);
    557             Mat imgpti(imagePoints[i]);
    558             imgpti.copyTo(r);
    559         }
    560         fs << "image_points" << imagePtMat;
    561     }
    562 }
    563 
    564 //! [run_and_save]
    565 bool runCalibrationAndSave(Settings& s, Size imageSize, Mat& cameraMatrix, Mat& distCoeffs,
    566                            vector<vector<Point2f> > imagePoints)
    567 {
    568     vector<Mat> rvecs, tvecs;
    569     vector<float> reprojErrs;
    570     double totalAvgErr = 0;
    571 
    572     bool ok = runCalibration(s, imageSize, cameraMatrix, distCoeffs, imagePoints, rvecs, tvecs, reprojErrs,
    573                              totalAvgErr);
    574     cout << (ok ? "Calibration succeeded" : "Calibration failed")
    575          << ". avg re projection error = " << totalAvgErr << endl;
    576 
    577     if (ok)
    578         saveCameraParams(s, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, reprojErrs, imagePoints,
    579                          totalAvgErr);
    580     return ok;
    581 }
    582 //! [run_and_save]
    583