I have successfully integrated the ofxCv libraries calibrate functionality into my project (a threaded multi-camera system for Point Grey GigE Cameras) in Ubuntu 15.04 x64 with OF 0.8.4.
The problem is the camera is a 1/1.2 sensor with a 2.7mm lens. The calibration function does not appear to work with Fisheye lenses directly. I have done quite a bit of research, but admittedly I am new to working with calibration in OpenCv. It appears that I need to set flags to something like:
int calibFlags = CV_CALIB_FIX_K1 | CV_CALIB_FIX_K2 | CV_CALIB_FIX_K3 | CV_CALIB_FIX_K4 | CV_CALIB_FIX_K5 | CV_CALIB_FIX_K6 | CV_CALIB_FIX_FOCAL_LENGTH | CV_CALIB_USE_INTRINSIC_GUESS;
and apply this to calibrateCamera();
I am adapting the ofxCv code directly, I have backed up the original and when I get it down my plan is to make another header and c++ file to extend it's functionality.
Even with the flags I cannot get the fisheye distortion to clean up like I can lenses in the normal range, it is also apparently unable to automatically calculate the sensor size or the focal length, I could also use help on how to set that manually since I know the numbers.
Essentially, any help I can get in adapting the ofxCv calibration to function with Fisheye lenses would be great, and how to manually handle sensorWidth/sensorHeight, and focalLength.
Also, regarding the settings.yml the xcount and ycount, does the counting start from 0 or 1. my actual x=11 and y=8, but it only works if I list as 10,7.
Here is the calibration.h file I am using in my project to reference the ofxCv calibration:
#include "ofApp.h"
using namespace ofxCv;
using namespace cv;
class GigECalibration{
public:
GigECalibration(){
active = true;
}
virtual ~GigECalibration(){
//calibration.reset();
}
void setup(ofImage cam) {
camImage = cam;
calibration.setFillFrame(false);
FileStorage settings(ofToDataPath("settings.yml"), FileStorage::READ);
if(settings.isOpened()) {
int xCount = settings["xCount"], yCount = settings["yCount"];
calibration.setPatternSize(xCount, yCount);
float squareSize = settings["squareSize"];
calibration.setSquareSize(squareSize);
CalibrationPattern patternType;
switch(settings["patternType"]) {
case 0: patternType = CHESSBOARD; break;
case 1: patternType = CIRCLES_GRID; break;
case 2: patternType = ASYMMETRIC_CIRCLES_GRID; break;
}
calibration.setPatternType(patternType);
}
imitate(undistorted, camImage);
imitate(previous, camImage);
imitate(diff, camImage);
lastTime = 0;
//active = true;
}
void update(ofImage cam) {
camImage = cam;
Mat camMat = toCv(camImage);
Mat prevMat = toCv(previous);
Mat diffMat = toCv(diff);
absdiff(prevMat, camMat, diffMat);
camMat.copyTo(prevMat);
diffMean = mean(Mat(mean(diffMat)))[0];
float curTime = ofGetElapsedTimef();
if(active && curTime - lastTime > timeThreshold && diffMean < diffThreshold) {
if(calibration.add(camMat)) {
cout << "re-calibrating" << endl;
calibration.calibrate();
if(calibration.size() > startCleaning) {
calibration.clean();
}
calibration.save("calibration.yml");
lastTime = curTime;
}
}
if(calibration.size() > 0) {
calibration.undistort(toCv(camImage), toCv(undistorted));
undistorted.update();
}
}
void draw(int camW, int camH) {
ofSetColor(255);
camImage.draw(0, 0,camW/2, camH/2);
undistorted.draw(camW/2, 0, camW/2,camH/2);
if(active){
ofDrawBitmapString("Calibration Active",ofGetWidth()-200,20);
}else{
ofDrawBitmapString("Calibration Inactive",ofGetWidth()-200,20);
}
stringstream intrinsics;
intrinsics << "fov: " << toOf(calibration.getDistortedIntrinsics().getFov()) << " distCoeffs: " << calibration.getDistCoeffs();
drawHighlightString(intrinsics.str(), 10, 20, yellowPrint, ofColor(0));
drawHighlightString("movement: " + ofToString(diffMean), 10, 40, cyanPrint);
drawHighlightString("reproj error: " + ofToString(calibration.getReprojectionError()) + " from " + ofToString(calibration.size()), 10, 60, magentaPrint);
for(int i = 0; i < calibration.size(); i++) {
drawHighlightString(ofToString(i) + ": " + ofToString(calibration.getReprojectionError(i)), 10, 80 + 16 * i, magentaPrint);
}
}
bool active;
protected:
const float diffThreshold = 2.5; // maximum amount of movement
const float timeThreshold = 1; // minimum time between snapshots
const int startCleaning = 20; // start cleaning outliers after this many samples
ofImage camImage;
ofImage undistorted;
ofPixels previous;
ofPixels diff;
float diffMean;
float lastTime;
ofxCv::Calibration calibration;
};
Here is the slightly adapted ofxCv/Calibration.cpp
#include "ofxCv/Calibration.h"
#include "ofxCv/Helpers.h"
#include "ofFileUtils.h"
namespace ofxCv {
using namespace cv;
void Intrinsics::setup(Mat cameraMatrix, cv::Size imageSize, cv::Size sensorSize) {
this->cameraMatrix = cameraMatrix;
this->imageSize = imageSize;
this->sensorSize = sensorSize;
calibrationMatrixValues(cameraMatrix, imageSize, sensorSize.width, sensorSize.height,
fov.x, fov.y, focalLength, principalPoint, aspectRatio);
}
void Intrinsics::setImageSize(cv::Size imgSize) {
imageSize = imgSize;
}
Mat Intrinsics::getCameraMatrix() const {
return cameraMatrix;
}
cv::Size Intrinsics::getImageSize() const {
return imageSize;
}
cv::Size Intrinsics::getSensorSize() const {
return sensorSize;
}
cv::Point2d Intrinsics::getFov() const {
return fov;
}
double Intrinsics::getFocalLength() const {
return focalLength;
}
double Intrinsics::getAspectRatio() const {
return aspectRatio;
}
Point2d Intrinsics::getPrincipalPoint() const {
return principalPoint;
}
void Intrinsics::loadProjectionMatrix(float nearDist, float farDist, cv::Point2d viewportOffset) const {
ofViewport(viewportOffset.x, viewportOffset.y, imageSize.width, imageSize.height);
ofSetMatrixMode(OF_MATRIX_PROJECTION);
ofLoadIdentityMatrix();
float w = imageSize.width;
float h = imageSize.height;
float fx = cameraMatrix.at<double>(0, 0);
float fy = cameraMatrix.at<double>(1, 1);
float cx = principalPoint.x;
float cy = principalPoint.y;
ofMatrix4x4 frustum;
frustum.makeFrustumMatrix(
nearDist * (-cx) / fx, nearDist * (w - cx) / fx,
nearDist * (cy) / fy, nearDist * (cy - h) / fy,
nearDist, farDist);
ofMultMatrix(frustum);
ofSetMatrixMode(OF_MATRIX_MODELVIEW);
ofLoadIdentityMatrix();
ofMatrix4x4 lookAt;
lookAt.makeLookAtViewMatrix(ofVec3f(0,0,0), ofVec3f(0,0,1), ofVec3f(0,-1,0));
ofMultMatrix(lookAt);
}
Calibration::Calibration() :
patternType(CHESSBOARD),
patternSize(cv::Size(10, 7)), // based on Chessboard_A4.pdf, assuming world units are centimeters
subpixelSize(cv::Size(11,11)),
squareSize(2.5),
reprojectionError(0),
fillFrame(true),
ready(false) {
}
void Calibration::save(string filename, bool absolute) const {
if(!ready){
ofLog(OF_LOG_ERROR, "Calibration::save() failed, because your calibration isn't ready yet!");
}
FileStorage fs(ofToDataPath(filename, absolute), FileStorage::WRITE);
cv::Size imageSize = distortedIntrinsics.getImageSize();
cv::Size sensorSize = distortedIntrinsics.getSensorSize();
Mat cameraMatrix = distortedIntrinsics.getCameraMatrix();
fs << "cameraMatrix" << cameraMatrix;
fs << "imageSize_width" << imageSize.width;
fs << "imageSize_height" << imageSize.height;
fs << "sensorSize_width" << sensorSize.width;
fs << "sensorSize_height" << sensorSize.height;
fs << "distCoeffs" << distCoeffs;
fs << "reprojectionError" << reprojectionError;
fs << "features" << "[";
for(int i = 0; i < (int)imagePoints.size(); i++) {
fs << "[:" << imagePoints[i] << "]";
}
fs << "]";
}
void Calibration::load(string filename, bool absolute) {
imagePoints.clear();
FileStorage fs(ofToDataPath(filename, absolute), FileStorage::READ);
cv::Size imageSize, sensorSize;
Mat cameraMatrix;
fs["cameraMatrix"] >> cameraMatrix;
fs["imageSize_width"] >> imageSize.width;
fs["imageSize_height"] >> imageSize.height;
fs["sensorSize_width"] >> sensorSize.width;
fs["sensorSize_height"] >> sensorSize.height;
fs["distCoeffs"] >> distCoeffs;
fs["reprojectionError"] >> reprojectionError;
FileNode features = fs["features"];
for(FileNodeIterator it = features.begin(); it != features.end(); it++) {
vector<Point2f> cur;
(*it) >> cur;
imagePoints.push_back(cur);
}
addedImageSize = imageSize;
distortedIntrinsics.setup(cameraMatrix, imageSize, sensorSize);
updateUndistortion();
ready = true;
}
void Calibration::setIntrinsics(Intrinsics& distortedIntrinsics, Mat& distortionCoefficients){
this->distortedIntrinsics = distortedIntrinsics;
this->distCoeffs = distortionCoefficients;
this->addedImageSize = distortedIntrinsics.getImageSize();
updateUndistortion();
this->ready = true;
}
void Calibration::reset(){
this->ready = false;
this->reprojectionError = 0.0;
this->imagePoints.clear();
this->objectPoints.clear();
this->perViewErrors.clear();
}
void Calibration::setPatternType(CalibrationPattern patternType) {
this->patternType = patternType;
}
void Calibration::setPatternSize(int xCount, int yCount) {
patternSize = cv::Size(xCount, yCount);
}
void Calibration::setSquareSize(float squareSize) {
this->squareSize = squareSize;
}
void Calibration::setFillFrame(bool fillFrame) {
this->fillFrame = fillFrame;
}
void Calibration::setSubpixelSize(int subpixelSize) {
subpixelSize = MAX(subpixelSize,2);
this->subpixelSize = cv::Size(subpixelSize,subpixelSize);
}
bool Calibration::add(Mat img) {
addedImageSize = img.size();
vector<Point2f> pointBuf;
// find corners
bool found = findBoard(img, pointBuf);
if (found)
imagePoints.push_back(pointBuf);
else
ofLog(OF_LOG_ERROR, "Calibration::add() failed, maybe your patternSize is wrong or the image has poor lighting?");
return found;
}
bool Calibration::findBoard(Mat img, vector<Point2f>& pointBuf, bool refine) {
bool found=false;
if(patternType == CHESSBOARD) {
// no CV_CALIB_CB_FAST_CHECK, because it breaks on dark images (e.g., dark IR images from kinect)
int chessFlags = CV_CALIB_CB_ADAPTIVE_THRESH;// | CV_CALIB_CB_FILTER_QUADS;// | CV_CALIB_CB_NORMALIZE_IMAGE;
found = findChessboardCorners(img, patternSize, pointBuf, chessFlags);
// improve corner accuracy
if(found) {
if(img.type() != CV_8UC1) {
copyGray(img, grayMat);
} else {
grayMat = img;
}
if(refine) {
// the 11x11 dictates the smallest image space square size allowed
// in other words, if your smallest square is 11x11 pixels, then set this to 11x11
cornerSubPix(grayMat, pointBuf, subpixelSize, cv::Size(-1,-1), TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 30, 0.1 ));
}
}
}
#ifdef USING_OPENCV_2_3
else {
int flags = (patternType == CIRCLES_GRID ? CALIB_CB_SYMMETRIC_GRID : CALIB_CB_ASYMMETRIC_GRID); // + CALIB_CB_CLUSTERING
found = findCirclesGrid(img, patternSize, pointBuf, flags);
}
#endif
return found;
}
bool Calibration::clean(float minReprojectionError) {
int removed = 0;
for(int i = size() - 1; i >= 0; i--) {
if(getReprojectionError(i) > minReprojectionError) {
objectPoints.erase(objectPoints.begin() + i);
imagePoints.erase(imagePoints.begin() + i);
removed++;
}
}
if(size() > 0) {
if(removed > 0) {
return calibrate();
} else {
return true;
}
} else {
ofLog(OF_LOG_ERROR, "Calibration::clean() removed the last object/image point pair");
return false;
}
}
bool Calibration::calibrate() {
if(size() < 1) {
ofLog(OF_LOG_ERROR, "Calibration::calibrate() doesn't have any image data to calibrate from.");
if(ready) {
ofLog(OF_LOG_ERROR, "Calibration::calibrate() doesn't need to be called after Calibration::load().");
}
return ready;
}
Mat cameraMatrix = Mat::eye(3, 3, CV_64F);
distCoeffs = Mat::zeros(8, 1, CV_64F);
updateObjectPoints();
//int calibFlags = 0;
int calibFlags = CV_CALIB_FIX_K1 | CV_CALIB_FIX_K2 | CV_CALIB_FIX_K3 | CV_CALIB_FIX_K4 | CV_CALIB_FIX_K5 | CV_CALIB_FIX_K6 | CV_CALIB_FIX_FOCAL_LENGTH | CV_CALIB_USE_INTRINSIC_GUESS;
//CV_CALIB_FIX_PRINCIPAL_POINT | CV_CALIB_USE_INTRINSIC_GUESS | CV_CALIB_FIX_ASPECT_RATIO
//int calibFlags = CV_CALIB_FIX_K1 | CV_CALIB_FIX_K2 | CV_CALIB_FIX_K3 | CV_CALIB_FIX_K4 | CV_CALIB_FIX_K5 | CV_CALIB_FIX_K6 | CV_CALIB_ZERO_TANGENT_DIST | CV_CALIB_USE_INTRINSIC_GUESS |CV_CALIB_ZERO_TANGENT_DIST;
float rms = calibrateCamera(objectPoints, imagePoints, addedImageSize, cameraMatrix, distCoeffs, boardRotations, boardTranslations, calibFlags);
ofLog(OF_LOG_VERBOSE, "calibrateCamera() reports RMS error of " + ofToString(rms));
ready = checkRange(cameraMatrix) && checkRange(distCoeffs);
if(!ready) {
ofLog(OF_LOG_ERROR, "Calibration::calibrate() failed to calibrate the camera");
}
distortedIntrinsics.setup(cameraMatrix, addedImageSize);
updateReprojectionError();
updateUndistortion();
return ready;
}
bool Calibration::isReady(){
return ready;
}
bool Calibration::calibrateFromDirectory(string directory) {
ofDirectory dirList;
ofImage cur;
dirList.listDir(directory);
for(int i = 0; i < (int)dirList.size(); i++) {
cur.loadImage(dirList.getPath(i));
if(!add(toCv(cur))) {
ofLog(OF_LOG_ERROR, "Calibration::add() failed on " + dirList.getPath(i));
}
}
return calibrate();
}
void Calibration::undistort(Mat img, int interpolationMode) {
img.copyTo(undistortBuffer);
undistort(undistortBuffer, img, interpolationMode);
}
void Calibration::undistort(Mat src, Mat dst, int interpolationMode) {
remap(src, dst, undistortMapX, undistortMapY, interpolationMode);
}
ofVec2f Calibration::undistort(ofVec2f& src) const {
ofVec2f dst;
Mat matSrc = Mat(1, 1, CV_32FC2, &src.x);
Mat matDst = Mat(1, 1, CV_32FC2, &dst.x);;
undistortPoints(matSrc, matDst, distortedIntrinsics.getCameraMatrix(), distCoeffs);
return dst;
}
void Calibration::undistort(vector<ofVec2f>& src, vector<ofVec2f>& dst) const {
int n = src.size();
dst.resize(n);
Mat matSrc = Mat(n, 1, CV_32FC2, &src[0].x);
Mat matDst = Mat(n, 1, CV_32FC2, &dst[0].x);
undistortPoints(matSrc, matDst, distortedIntrinsics.getCameraMatrix(), distCoeffs);
}
bool Calibration::getTransformation(Calibration& dst, Mat& rotation, Mat& translation) {
//if(imagePoints.size() == 0 || dst.imagePoints.size() == 0) {
if(!ready) {
ofLog(OF_LOG_ERROR, "getTransformation() requires both Calibration objects to have just been calibrated");
return false;
}
if(imagePoints.size() != dst.imagePoints.size() || patternSize != dst.patternSize) {
ofLog(OF_LOG_ERROR, "getTransformation() requires both Calibration objects to be trained simultaneously on the same board");
return false;
}
Mat fundamentalMatrix, essentialMatrix;
Mat cameraMatrix = distortedIntrinsics.getCameraMatrix();
Mat dstCameraMatrix = dst.getDistortedIntrinsics().getCameraMatrix();
// uses CALIB_FIX_INTRINSIC by default
stereoCalibrate(objectPoints,
imagePoints, dst.imagePoints,
cameraMatrix, distCoeffs,
dstCameraMatrix, dst.distCoeffs,
distortedIntrinsics.getImageSize(), rotation, translation,
essentialMatrix, fundamentalMatrix);
return true;
}
float Calibration::getReprojectionError() const {
return reprojectionError;
}
float Calibration::getReprojectionError(int i) const {
return perViewErrors[i];
}
const Intrinsics& Calibration::getDistortedIntrinsics() const {
return distortedIntrinsics;
}
const Intrinsics& Calibration::getUndistortedIntrinsics() const {
return undistortedIntrinsics;
}
Mat Calibration::getDistCoeffs() const {
return distCoeffs;
}
int Calibration::size() const {
return imagePoints.size();
}
cv::Size Calibration::getPatternSize() const {
return patternSize;
}
float Calibration::getSquareSize() const {
return squareSize;
}
void Calibration::customDraw() {
for(int i = 0; i < size(); i++) {
draw(i);
}
}
void Calibration::draw(int i) const {
ofPushStyle();
ofNoFill();
ofSetColor(ofColor::red);
for(int j = 0; j < (int)imagePoints[i].size(); j++) {
ofCircle(toOf(imagePoints[i][j]), 5);
}
ofPopStyle();
}
// this won't work until undistort() is in pixel coordinates
/*
void Calibration::drawUndistortion() const {
vector<ofVec2f> src, dst;
cv::Point2i divisions(32, 24);
for(int y = 0; y < divisions.y; y++) {
for(int x = 0; x < divisions.x; x++) {
src.push_back(ofVec2f(
ofMap(x, -1, divisions.x, 0, addedImageSize.width),
ofMap(y, -1, divisions.y, 0, addedImageSize.height)));
}
}
undistort(src, dst);
ofMesh mesh;
mesh.setMode(OF_PRIMITIVE_LINES);
for(int i = 0; i < src.size(); i++) {
mesh.addVertex(src[i]);
mesh.addVertex(dst[i]);
}
mesh.draw();
}
*/
void Calibration::draw3d() const {
for(int i = 0; i < size(); i++) {
draw3d(i);
}
}
void Calibration::draw3d(int i) const {
ofPushStyle();
ofPushMatrix();
ofNoFill();
applyMatrix(makeMatrix(boardRotations[i], boardTranslations[i]));
ofSetColor(ofColor::fromHsb(255 * i / size(), 255, 255));
ofDrawBitmapString(ofToString(i), 0, 0);
for(int j = 0; j < (int)objectPoints[i].size(); j++) {
ofPushMatrix();
ofTranslate(toOf(objectPoints[i][j]));
ofCircle(0, 0, .5);
ofPopMatrix();
}
ofMesh mesh;
mesh.setMode(OF_PRIMITIVE_LINE_STRIP);
for(int j = 0; j < (int)objectPoints[i].size(); j++) {
ofVec3f cur = toOf(objectPoints[i][j]);
mesh.addVertex(cur);
}
mesh.draw();
ofPopMatrix();
ofPopStyle();
}
void Calibration::updateObjectPoints() {
vector<Point3f> points = createObjectPoints(patternSize, squareSize, patternType);
objectPoints.resize(imagePoints.size(), points);
}
void Calibration::updateReprojectionError() {
vector<Point2f> imagePoints2;
int totalPoints = 0;
double totalErr = 0;
perViewErrors.clear();
perViewErrors.resize(objectPoints.size());
for(int i = 0; i < (int)objectPoints.size(); i++) {
projectPoints(Mat(objectPoints[i]), boardRotations[i], boardTranslations[i], distortedIntrinsics.getCameraMatrix(), distCoeffs, imagePoints2);
double err = norm(Mat(imagePoints[i]), Mat(imagePoints2), CV_L2);
int n = objectPoints[i].size();
perViewErrors[i] = sqrt(err * err / n);
totalErr += err * err;
totalPoints += n;
ofLog(OF_LOG_VERBOSE, "view " + ofToString(i) + " has error of " + ofToString(perViewErrors[i]));
}
reprojectionError = sqrt(totalErr / totalPoints);
ofLog(OF_LOG_VERBOSE, "all views have error of " + ofToString(reprojectionError));
}
void Calibration::updateUndistortion() {
Mat undistortedCameraMatrix = getOptimalNewCameraMatrix(distortedIntrinsics.getCameraMatrix(), distCoeffs, distortedIntrinsics.getImageSize(), fillFrame ? 0 : 1);
initUndistortRectifyMap(distortedIntrinsics.getCameraMatrix(), distCoeffs, Mat(), undistortedCameraMatrix, distortedIntrinsics.getImageSize(), CV_16SC2, undistortMapX, undistortMapY);
undistortedIntrinsics.setup(undistortedCameraMatrix, distortedIntrinsics.getImageSize());
}
vector<Point3f> Calibration::createObjectPoints(cv::Size patternSize, float squareSize, CalibrationPattern patternType) {
vector<Point3f> corners;
switch(patternType) {
case CHESSBOARD:
case CIRCLES_GRID:
for(int i = 0; i < patternSize.height; i++)
for(int j = 0; j < patternSize.width; j++)
corners.push_back(Point3f(float(j * squareSize), float(i * squareSize), 0));
break;
case ASYMMETRIC_CIRCLES_GRID:
for(int i = 0; i < patternSize.height; i++)
for(int j = 0; j < patternSize.width; j++)
corners.push_back(Point3f(float(((2 * j) + (i % 2)) * squareSize), float(i * squareSize), 0));
break;
}
return corners;
}
}