Monday, April 4, 2011

Offline camera calibration for iPhone/iPad-- or any camera, really

Creating a GUI to perform camera calibration on a mobile device like an iPhone or iPad sounded like more work than it would be worth, so I wrote a short program to do it offline. The cameras used on these devices can be assumed to be consistent within the same model, so it makes more sense for an app developer to have several precomputed calibration matrices available rather than asking the user to do this step on their own device.

The program I wrote is adapted from a tutorial I found to do camera calibration from live input. My version instead looks for a sequence of files name 00.jpg, 01.jpg, etc and calibrates from those. So the way I used it was to take several pictures of the checkerboard pattern from my iPad, upload them to my computer, edit out the rest of the stuff on my desktop in Photoshop so finding the corners was more likely to be correct, and rename them. The output of the program is two XML files which include the camera intrinsic parameters and distortion coefficients. The code for the program is attached after the jump.

And results:
For camera Matrix

f_x = 786.42938232
f_y = 786.42938232
c_x = 311.25384521 // See update below
c_y = 217.01358032 // See update below

And distortion coefficients were: -0.10786291, 1.23078966, -4.54779295e-03, -3.28966696e-03, -5.54199600

I hope I did that right. The center is slightly off from where it ideally should be (320, 240).

Note: I found this precompiled private framework of OpenCV built for OSX rather handy. It is only built with 32-bit support, so set your target in XCode accordingly.

UPDATE: The primary point obtained from this calibration was wrong! It was throwing off the pose estimates at glancing angles. I set it to 320,240 and everything works better now...




//  main.mm
//  CameraCalibration
//
//  Adapted from http://dasl.mem.drexel.edu/~noahKuntz/openCVTut10.html
//

#import <cocoa cocoa.h>
#import <opencv opencv.h>
#import <opencv highgui.h>

int main(int argc, char *argv[])
{
 const int NUMBER_OF_PICTURES = 23; 
 
 int board_w = 5; // Board width in squares
 int board_h = 8; // Board height 
 int n_boards = NUMBER_OF_PICTURES; // Maximum number of boards
 int board_n = board_w * board_h;
 CvSize board_sz = cvSize( board_w, board_h );
 
 cvNamedWindow( "Calibration", 1);
 // Allocate Sotrage
 CvMat* image_points  = cvCreateMat( n_boards*board_n, 2, CV_32FC1 );
 CvMat* object_points  = cvCreateMat( n_boards*board_n, 3, CV_32FC1 );
 CvMat* point_counts   = cvCreateMat( n_boards, 1, CV_32SC1 );
 CvMat* intrinsic_matrix  = cvCreateMat( 3, 3, CV_32FC1 );
 CvMat* distortion_coeffs = cvCreateMat( 5, 1, CV_32FC1 );
 
 CvPoint2D32f* corners = new CvPoint2D32f[ board_n ];
 int corner_count;
 int successes = 0;
 int step, frame = 0;
 
 const CFIndex FILENAME_LEN = 2048;
 char filename[FILENAME_LEN] = "";
 
 // (this is the mac way to package application resources)
    CFBundleRef mainBundle  = CFBundleGetMainBundle();
    assert (mainBundle);
    CFURLRef image_url = CFBundleCopyResourceURL (mainBundle, CFSTR("00"), CFSTR("jpg"), NULL);
    assert (image_url);
    Boolean got_it      = CFURLGetFileSystemRepresentation (image_url, true, 
                                                                reinterpret_cast<uint8 *>(filename), FILENAME_LEN);
    if (! got_it) abort ();
 
 IplImage *image = cvLoadImage(filename, 1);
 IplImage *gray_image = cvCreateImage( cvGetSize( image ), 8, 1 );
 NSLog(@"Loaded image.");
 
 // Capture Corner views loop until we've got n_boards
 // succesful captures (all corners on the board are found)
 int pictureNumber = 0;
 
 while( successes < n_boards && pictureNumber < NUMBER_OF_PICTURES-1){
  // Find chessboard corners:
  int found = cvFindChessboardCorners( image, board_sz, corners,
           &corner_count, CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_FILTER_QUADS );
  
  // Get subpixel accuracy on those corners
  cvCvtColor( image, gray_image, CV_BGR2GRAY );
  cvFindCornerSubPix( gray_image, corners, corner_count, cvSize( 11, 11 ), 
         cvSize( -1, -1 ), cvTermCriteria( CV_TERMCRIT_EPS+CV_TERMCRIT_ITER, 30, 0.1 ));
  
  // Draw it
  cvDrawChessboardCorners( image, board_sz, corners, corner_count, found );
  cvShowImage( "Calibration", image );
  
  // If we got a good board, add it to our data
  if( corner_count == board_n ){
   step = successes*board_n;
   for( int i=step, j=0; j < board_n; ++i, ++j ){
    CV_MAT_ELEM( *image_points, float, i, 0 ) = corners[j].x;
    CV_MAT_ELEM( *image_points, float, i, 1 ) = corners[j].y;
    CV_MAT_ELEM( *object_points, float, i, 0 ) = j/board_w;
    CV_MAT_ELEM( *object_points, float, i, 1 ) = j%board_w;
    CV_MAT_ELEM( *object_points, float, i, 2 ) = 0.0f;
   }
   CV_MAT_ELEM( *point_counts, int, successes, 0 ) = board_n;
   successes++;
  }
  
  pictureNumber++;
  NSLog(@"%i chessboards found in %i pictures", successes, pictureNumber);
  
  CFStringRef number = CFStringCreateWithFormat(NULL, NULL, CFSTR("%02i"), pictureNumber);
  
  CFURLRef image_url = CFBundleCopyResourceURL (mainBundle, (CFStringRef)number, CFSTR("jpg"), NULL);
  assert (image_url);
  CFURLGetFileSystemRepresentation (image_url, true, reinterpret_cast(filename), FILENAME_LEN);
  
  image = cvLoadImage(filename, 1); // Get next image
 } // End collection while loop
 
 // Allocate matrices according to how many chessboards found
 CvMat* object_points2 = cvCreateMat( successes*board_n, 3, CV_32FC1 );
 CvMat* image_points2 = cvCreateMat( successes*board_n, 2, CV_32FC1 );
 CvMat* point_counts2 = cvCreateMat( successes, 1, CV_32SC1 );
 
 // Transfer the points into the correct size matrices
 for( int i = 0; i < successes*board_n; ++i ){
  CV_MAT_ELEM( *image_points2, float, i, 0) = CV_MAT_ELEM( *image_points, float, i, 0 );
  CV_MAT_ELEM( *image_points2, float, i, 1) = CV_MAT_ELEM( *image_points, float, i, 1 );
  CV_MAT_ELEM( *object_points2, float, i, 0) = CV_MAT_ELEM( *object_points, float, i, 0 );
  CV_MAT_ELEM( *object_points2, float, i, 1) = CV_MAT_ELEM( *object_points, float, i, 1 );
  CV_MAT_ELEM( *object_points2, float, i, 2) = CV_MAT_ELEM( *object_points, float, i, 2 );
 }
 
 for( int i=0; i < successes; ++i ){
  CV_MAT_ELEM( *point_counts2, int, i, 0 ) = CV_MAT_ELEM( *point_counts, int, i, 0 );
 }
 cvReleaseMat( &object_points );
 cvReleaseMat( &image_points );
 cvReleaseMat( &point_counts );
 
 // At this point we have all the chessboard corners we need
 // Initiliazie the intrinsic matrix such that the two focal lengths
 // have a ratio of 1.0
 
 CV_MAT_ELEM( *intrinsic_matrix, float, 0, 0 ) = 1.0;
 CV_MAT_ELEM( *intrinsic_matrix, float, 1, 1 ) = 1.0;
 
 // Calibrate the camera
 cvCalibrateCamera2( object_points2, image_points2, point_counts2, cvGetSize( image ), 
        intrinsic_matrix, distortion_coeffs, NULL, NULL, CV_CALIB_FIX_ASPECT_RATIO ); 
 
 // Save the intrinsics and distortions
 cvSave( "Intrinsics.xml", intrinsic_matrix );
 cvSave( "Distortion.xml", distortion_coeffs );
 
 // Example of loading these matrices back in
 CvMat *intrinsic = (CvMat*)cvLoad( "Intrinsics.xml" );
 CvMat *distortion = (CvMat*)cvLoad( "Distortion.xml" );
 
 // Build the undistort map that we will use for all subsequent frames
 IplImage* mapx = cvCreateImage( cvGetSize( image ), IPL_DEPTH_32F, 1 );
 IplImage* mapy = cvCreateImage( cvGetSize( image ), IPL_DEPTH_32F, 1 );
 cvInitUndistortMap( intrinsic, distortion, mapx, mapy );
 
 
 return 0;
}

2 comments:

  1. My cousin recommended this blog and she was totally right keep up the fantastic work!

    ReplyDelete