Camera Calibration using ChArUco Boards Demo

Calibration using a ChArUco board.

To capture a frame for calibration, press 'c', If input comes from video, press any key for next frame To finish capturing, press 'ESC' key and calibration starts.

Sources:

Contents

Parameters

% options
vidFile = '';                   % Use video file instead of camera as input
squaresX = 5;                   % Number of squares in X direction
squaresY = 7;                   % Number of squares in Y direction
squareLength = 60;              % Square side length (in pixels)
markerLength = 30;              % Marker side length (in pixels)
dictionaryId = '6x6_250';       % Dictionary id
refindStrategy = true;          % Apply refined strategy
showChessboardCorners = true;   % Show detected chessboard corners after calibration
calibrationFlags = {
    'UseIntrinsicGuess',false, ...
    'FixAspectRatio',false, ...     % Fix aspect ratio (fx/fy)
    'ZeroTangentDist',true, ...     % Assume zero tangential distortion
    'FixPrincipalPoint',false       % Fix the principal point at the center
};
aspectRatio = 1;                    % Fix aspect ratio (fx/fy) to this value

% FixAspectRatio in camera parameters
if calibrationFlags{4}
    camMatrix = eye(3);
    camMatrix(1,1) = aspectRatio;
    calibrationFlags = [calibrationFlags, 'CameraMatrix',camMatrix];
end

% marker detector parameters
detectorParams = struct();
if false
    %detectorParams.nMarkers = 1024;
    detectorParams.adaptiveThreshWinSizeMin = 3;
    detectorParams.adaptiveThreshWinSizeMax = 23;
    detectorParams.adaptiveThreshWinSizeStep = 10;
    detectorParams.adaptiveThreshConstant = 7;
    detectorParams.minMarkerPerimeterRate = 0.03;
    detectorParams.maxMarkerPerimeterRate = 4.0;
    detectorParams.polygonalApproxAccuracyRate = 0.05;
    detectorParams.minCornerDistanceRate = 0.05;
    detectorParams.minDistanceToBorder = 3;
    detectorParams.minMarkerDistanceRate = 0.05;
    detectorParams.cornerRefinementMethod = 'None';
    detectorParams.cornerRefinementWinSize = 5;
    detectorParams.cornerRefinementMaxIterations = 30;
    detectorParams.cornerRefinementMinAccuracy = 0.1;
    detectorParams.markerBorderBits = 1;
    detectorParams.perspectiveRemovePixelPerCell = 8;
    detectorParams.perspectiveRemoveIgnoredMarginPerCell = 0.13;
    detectorParams.maxErroneousBitsInBorderRate = 0.04;
    detectorParams.minOtsuStdDev = 5.0;
    detectorParams.errorCorrectionRate = 0.6;
end

% create charuco board
dictionary = {'Predefined', dictionaryId};
board = {squaresX, squaresY, squareLength, markerLength, dictionary};

Input source

if ~isempty(vidFile) && exist(vidFile, 'file') == 2
    vid = cv.VideoCapture(vidFile);
    waitTime = 1;     % 1 sec
else
    vid = createVideoCapture([], 'charuco');
    waitTime = 0.01;  % 10 msec
end
if ~vid.isOpened(), error('failed to initialize VideoCapture'); end

Collect

% collect data from each frame
allCorners = {};
allIds = {};
allImgs = {};
imgSize = [];
hImg = []; hFig = [];
while true
    % grab frame
    img = vid.read();
    if isempty(img), break; end

    % detect markers
    [corners, ids, rejected] = cv.detectMarkers(img, dictionary, ...
        'DetectorParameters',detectorParams);

    % refined strategy to detect more markers
    if refindStrategy
        [corners, ids, rejected] = cv.refineDetectedMarkers(img, ...
            ['CharucoBoard',board], corners, ids, rejected);
    end

    % interpolate charuco corners
    if ~isempty(ids)
        [charucoCorners, charucoIds] = cv.interpolateCornersCharuco(...
            corners, ids, img, board);
    end

    % draw results
    out = img;
    if ~isempty(ids)
        out = cv.drawDetectedMarkers(out, corners);  % 'IDs',ids
        if ~isempty(charucoCorners)
            out = cv.drawDetectedCornersCharuco(out, charucoCorners, ...
                'IDs',charucoIds);
        end
    end
    out = cv.putText(out, ['Press "c" to add current frame. ', ...
        '"ESC" to finish and calibrate'], [10 20], ...
        'FontScale',0.5, 'Color',[255 0 0], 'Thickness',2);

    if isempty(hImg)
        hImg = imshow(out);
        hFig = ancestor(hImg, 'figure');
        set(hFig, 'KeyPressFcn',@(o,e) setappdata(o, 'key',e.Key));
        setappdata(hFig, 'key','');
    elseif ishghandle(hImg)
        set(hImg, 'CData',out);
    else
        break;
    end
    drawnow; pause(waitTime);

    % collect frame
    switch getappdata(hFig, 'key')
        case {'space', 'return', 'c'}
            if ~isempty(ids)
                fprintf('Frame captured at %s\n', datestr(now()));
                allCorners{end+1} = corners;
                allIds{end+1} = ids;
                allImgs{end+1} = img;
                imgSize = size(img);
            else
                disp('frame skipped, no corners detected!');
            end
        case {'escape', 'q'}
            disp('Finished collecting frames.');
            break;
        case 'p'
            pause(5);
    end
    setappdata(hFig, 'key','');
end
vid.release();
Frame captured at 03-Dec-2017 20:03:08
Frame captured at 03-Dec-2017 20:03:09
Frame captured at 03-Dec-2017 20:03:11
Frame captured at 03-Dec-2017 20:03:13
Frame captured at 03-Dec-2017 20:03:16
Frame captured at 03-Dec-2017 20:03:17

Calibration

disp('Calibrating...')

% calibrate camera using aruco markers
if isempty([allIds{:}]), error('Not enough captures for calibration'); end
[camMatrix, distCoeffs, arucoRepErr] = ...
    cv.calibrateCameraAruco([allCorners{:}], [allIds{:}], ...
        cellfun(@numel, allCorners), ['CharucoBoard',board], ...
        imgSize([2 1]), calibrationFlags{:});

% prepare data for charuco calibration
allCharucoCorners = cell(size(allCorners));
allCharucoIds = cell(size(allCorners));
for i=1:numel(allCorners)
    % interpolate using camera parameters
    [allCharucoCorners{i}, allCharucoIds{i}] = cv.interpolateCornersCharuco(...
        allCorners{i}, allIds{i}, allImgs{i}, board, ...
        'CameraMatrix',camMatrix, 'DistCoeffs',distCoeffs);
    if isempty(allCharucoIds{i})
        warning('interpolateCornersCharuco found no corners');
    end
end

% drop images where it failed to interpolate charuco corners
idx = cellfun(@isempty, allCharucoIds);
allCharucoIds(idx) = [];
allCharucoCorners(idx) = [];
allImgs(idx) = [];

% calibrate camera using charuco
if numel(allCharucoCorners) < 4, error('Not enough corners for calibration'); end
[camMatrix, distCoeffs, repError, rvecs, tvecs] = ...
    cv.calibrateCameraCharuco(allCharucoCorners, allCharucoIds, board, ...
        imgSize([2 1]), calibrationFlags{:});

% calibration results
fprintf('Calibration Time: %s\n', datestr(now()));
fprintf('Image Width: %d, Image Height: %d\n', imgSize(2), imgSize(1));
disp('Flags:'); cellfun(@disp,calibrationFlags);
if calibrationFlags{4}, fprintf('Aspect Ratio: %f\n', aspectRatio); end
disp('Camera Matrix:'); disp(camMatrix)
disp('Distortion Coefficients:'); disp(distCoeffs)
fprintf('Reprojection Error: Charuco = %f, Aruco = %f\n', repError, arucoRepErr);
save camera_parameters.mat -mat camMatrix distCoeffs

% show interpolated charuco corners for debugging
if showChessboardCorners
    axisLength = 0.5 * min(squaresX, squaresY) * squareLength;
    outImgs = {};
    for i=1:numel(allImgs)
        if ~isempty(allIds{i}) && ~isempty(allCharucoCorners)
            img = cv.drawDetectedCornersCharuco(...
                allImgs{i}, allCharucoCorners{i}, 'IDs',allCharucoIds{i});
            img = cv.drawAxis(img, camMatrix, distCoeffs, ...
                rvecs{i}, tvecs{i}, axisLength);
            outImgs{end+1} = img;
        end
    end
    if ~mexopencv.isOctave() && mexopencv.require('images')
        %HACK: IMPLAY not implemented in Octave
        implay(cat(4, outImgs{:}), 1);
    end
end
Calibrating...
Calibration Time: 03-Dec-2017 20:03:19
Image Width: 512, Image Height: 512
Flags:
UseIntrinsicGuess
   0
FixAspectRatio
   0
ZeroTangentDist
   1
FixPrincipalPoint
   0
Camera Matrix:
  486.6273         0  256.6522
         0  484.4509  229.9744
         0         0    1.0000
Distortion Coefficients:
   -0.0264   -1.6408         0         0   10.3417
Reprojection Error: Charuco = 0.149381, Aruco = 0.726062