Calibrer une caméra avec OpenCV et ArUco

Calibrer une ou plusieurs caméras avec OpenCV se fait traditionnellement en prenant plusieurs images d’une mire damier avec des poses variées. Les coins sont détectés automatiquement et la correspondance entre les positions dans l’image et sur la mire servent à calculer les paramètres intrinsèques de la caméra.

Cependant, cette manière peut poser quelques problèmes dans certains cas. Un des problèmes que l’on peut rencontrer si deux caméras doivent observer la même mire, est qu’elles peuvent décider d’une orientation différente selon le point de vue. La mire peut aussi être positionnée de manière à ce qu’un morceau de la mire ne soit pas détecté. Les points sont alors mal détectés et leur ordre est inconnu.

Ces deux problèmes sont réglés avec ArUco, car les marqueurs sont détectés d’abord et servent à prépositionner les coins du damier. Ainsi, une détection partielle est possible. Les coins sont identifiés, ils ne peuvent alors plus être interprétés différemment.

ArUco est dans les modules de contribution d’OpenCV, il faut donc compiler ce dernier avec ces modules. J’ai déjà écrit un article sur le sujet ici :

https://vivienmetayer.com/compiler-opencv-4-4-avec-cuda-et-les-modules-de-contribution/

Une fois la lib utilisable, nous pouvons générer une mire de calibration comme ceci :

int main()
{
	// Create Aruco dictionary and checkboard
	const Ptr<aruco::Dictionary> dictionary = getPredefinedDictionary(aruco::DICT_5X5_50);
	Mat boardImage;
 
	// draw board
	Ptr<aruco::CharucoBoard> charucoBoard = aruco::CharucoBoard::create(7, 7, 0.02f, 0.01f, dictionary);
	charucoBoard->draw(Size(900, 900), boardImage, 100);
	imwrite("board.png", boardImage);
}

Une fois la mire générée et imprimée, elle est détectée de la manière suivante :

int detect_charuco(const String& filename)
{
	Mat image = imread(filename);
 
	// Create Aruco dictionary and checkboard
	const Ptr<aruco::Dictionary> dictionary = getPredefinedDictionary(aruco::DICT_5X5_50);
	const Ptr<aruco::CharucoBoard> charucoBoard = aruco::CharucoBoard::create(7, 7, 0.02f, 0.01f, dictionary);
 
	// Detect aruco markers
	vector<vector<Point2f>> markerCorners, rejectedCorners;
	vector<int> markerIds;
	const Ptr<aruco::DetectorParameters> params = aruco::DetectorParameters::create();
	params->cornerRefinementMethod = aruco::CORNER_REFINE_CONTOUR;
	params->cornerRefinementWinSize = 31;
	aruco::detectMarkers(image, dictionary, markerCorners, markerIds, params, rejectedCorners);
 
	if (markerIds.empty())
		return -1;
 
	vector<cv::Point2f> charucoCorners;
	vector<int> charucoIds;
	aruco::interpolateCornersCharuco(markerCorners, markerIds, image, charucoBoard, charucoCorners, charucoIds);
	aruco::drawDetectedCornersCharuco(image, charucoCorners, charucoIds);
 
	imshow("charuco", image);
 
	return 0;
}

Les coins de la mire sont numérotés et elle peut être partiellement détectée. Le procédé de calibration est plus robuste, ce qui peut être utile dans des conditions difficiles.

Pour la calibration de la caméra, il faut prendre plusieurs images de la mire dans des poses différentes. Dans mon cas, j’ai pris une trentaine d’images et j’ai calibré avec le code suivant

vector<Point3f> detectedCharucoPoints(vector<Point3f> charucoPoints, vector<int> charucoIds)
{
	vector<Point3f> detectedObjectPoints(charucoIds.size());
	for (size_t i = 0; i < charucoIds.size(); i++)	{
		detectedObjectPoints[i] = charucoPoints[charucoIds[i]];
	}
	return detectedObjectPoints;
}
 
int calibrate(const String& folder) // 25.8 mm square size
{
	// Detect charuco boards
	vector<vector<Point2f>> imagePoints;
	vector<vector<Point3f>> objectPoints;
	vector<Point3f> charucoPoints = BuildObjectPoints(6, 6, 25.8f);
	for (const auto& file : fs::directory_iterator(folder)) {
		if (file.path().extension() != ".jpg")
			continue;
		vector<Point2f> corners;
		vector<int> charucoIds;
		detect_charuco(file.path().string(), corners, charucoIds);
		if (corners.size() > 6) {
			imagePoints.push_back(corners);
			objectPoints.push_back(detectedCharucoPoints(charucoPoints, charucoIds));
		}
	}
 
	// Calibrate camera
	const int imageWidth = 1920, imageHeight = 1080;
	Mat cameraMatrix = Mat::eye(3, 3, CV_64F);
	Mat distCoeffs = Mat::zeros(8, 1, CV_64F);
	Mat rvecs, tvecs;
	double rms = calibrateCamera(objectPoints, imagePoints, Size(imageWidth, imageHeight), cameraMatrix, distCoeffs, rvecs, tvecs,
		CALIB_FIX_ASPECT_RATIO | CALIB_FIX_PRINCIPAL_POINT | CALIB_FIX_K4 | CALIB_FIX_K5 | CALIB_FIX_K6,
		TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 30, 2e-15));
	double numPixelsDiagonal = sqrt(imageWidth * imageWidth + imageHeight * imageHeight);
	double fov = atan2(numPixelsDiagonal / 2, cameraMatrix.at<double>(0));
 
	cout << "camera matrix : " << endl << cameraMatrix << endl;
	cout << "rms : " << rms << endl;
	cout << "diagonal FOV : " << 2 * fov * 180.0 / CV_PI << endl;
	
	return 0;
}

Avec ceci, j’ai pu calibrer ma Razer Kiyo et obtenir un FOV diagonal de 80.9°, valeur assez proche de celle donnée par le constructeur : 81.6°.

Voilà donc comment utiliser les marqueurs ArUco pour aider à la calibration. Pour seulement quelques lignes de code supplémentaires, il a été possible de faire des acquisitions plus rapidement. En effet, il n’y a plus besoin de se vérifier que toutes les mires sont vues entièrement. De plus, en cas de calibration avec deux caméras, le problème de l’orientation de la mire ne se pose plus. À ne pas oublier la prochaine fois que vous voudrez calibrer une caméra avec OpenCV !