001/* 002 * Copyright (C) Photon Vision. 003 * 004 * This program is free software: you can redistribute it and/or modify 005 * it under the terms of the GNU General Public License as published by 006 * the Free Software Foundation, either version 3 of the License, or 007 * (at your option) any later version. 008 * 009 * This program is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 012 * GNU General Public License for more details. 013 * 014 * You should have received a copy of the GNU General Public License 015 * along with this program. If not, see <https://www.gnu.org/licenses/>. 016 */ 017 018package org.photonvision.vision.pipe.impl; 019 020import java.nio.file.Path; 021import java.nio.file.Paths; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.List; 025import java.util.stream.Collectors; 026import org.apache.commons.io.FileUtils; 027import org.opencv.calib3d.Calib3d; 028import org.opencv.core.*; 029import org.opencv.imgcodecs.Imgcodecs; 030import org.photonvision.common.logging.LogGroup; 031import org.photonvision.common.logging.Logger; 032import org.photonvision.common.util.math.MathUtils; 033import org.photonvision.mrcal.MrCalJNI; 034import org.photonvision.mrcal.MrCalJNI.MrCalResult; 035import org.photonvision.mrcal.MrCalJNILoader; 036import org.photonvision.vision.calibration.BoardObservation; 037import org.photonvision.vision.calibration.CameraCalibrationCoefficients; 038import org.photonvision.vision.calibration.CameraLensModel; 039import org.photonvision.vision.calibration.JsonMatOfDouble; 040import org.photonvision.vision.frame.FrameStaticProperties; 041import org.photonvision.vision.pipe.CVPipe; 042import org.photonvision.vision.pipe.impl.FindBoardCornersPipe.FindBoardCornersPipeResult; 043 044public class Calibrate3dPipe 045 extends CVPipe< 046 Calibrate3dPipe.CalibrationInput, 047 CameraCalibrationCoefficients, 048 Calibrate3dPipe.CalibratePipeParams> { 049 public static class CalibrationInput { 050 final List<FindBoardCornersPipe.FindBoardCornersPipeResult> observations; 051 final FrameStaticProperties imageProps; 052 final Path imageSavePath; 053 054 public CalibrationInput( 055 List<FindBoardCornersPipeResult> observations, 056 FrameStaticProperties imageProps, 057 Path imageSavePath) { 058 this.observations = observations; 059 this.imageProps = imageProps; 060 this.imageSavePath = imageSavePath; 061 } 062 } 063 064 // For logging 065 private static final Logger logger = new Logger(Calibrate3dPipe.class, LogGroup.General); 066 067 // The Standard deviation of the estimated parameters 068 private final Mat stdDeviationsIntrinsics = new Mat(); 069 private final Mat stdDeviationsExtrinsics = new Mat(); 070 071 // Contains the re projection error of each snapshot by re projecting the 072 // corners we found and 073 // finding the Euclidean distance between the actual corners. 074 private final Mat perViewErrors = new Mat(); 075 076 /** 077 * Runs the process for the pipe. 078 * 079 * @param in Input for pipe processing. In the format (Input image, object points, image points) 080 * @return Result of processing. 081 */ 082 @Override 083 protected CameraCalibrationCoefficients process(CalibrationInput in) { 084 var filteredIn = 085 in.observations.stream() 086 .filter( 087 it -> 088 it != null 089 && it.imagePoints != null 090 && it.objectPoints != null 091 && it.size != null) 092 .collect(Collectors.toList()); 093 094 CameraCalibrationCoefficients ret; 095 var start = System.nanoTime(); 096 097 if (MrCalJNILoader.getInstance().isLoaded() && params.useMrCal) { 098 logger.debug("Calibrating with mrcal!"); 099 ret = 100 calibrateMrcal( 101 filteredIn, 102 in.imageProps.horizontalFocalLength, 103 in.imageProps.verticalFocalLength, 104 in.imageSavePath); 105 } else { 106 logger.debug("Calibrating with opencv!"); 107 ret = 108 calibrateOpenCV( 109 filteredIn, 110 in.imageProps.horizontalFocalLength, 111 in.imageProps.verticalFocalLength, 112 in.imageSavePath); 113 } 114 var dt = System.nanoTime() - start; 115 116 if (ret != null) 117 logger.info( 118 "CALIBRATION SUCCESS for res " 119 + in.observations.get(0).size 120 + " in " 121 + dt / 1e6 122 + "ms! camMatrix: \n" 123 + Arrays.toString(ret.cameraIntrinsics.data) 124 + "\ndistortionCoeffs:\n" 125 + Arrays.toString(ret.distCoeffs.data) 126 + "\n"); 127 else logger.info("Calibration failed! Review log for more details"); 128 129 return ret; 130 } 131 132 protected CameraCalibrationCoefficients calibrateOpenCV( 133 List<FindBoardCornersPipe.FindBoardCornersPipeResult> in, 134 double fxGuess, 135 double fyGuess, 136 Path imageSavePath) { 137 List<MatOfPoint3f> objPointsIn = 138 in.stream().map(it -> it.objectPoints).collect(Collectors.toList()); 139 List<MatOfPoint2f> imgPointsIn = 140 in.stream().map(it -> it.imagePoints).collect(Collectors.toList()); 141 List<MatOfFloat> levelsArr = in.stream().map(it -> it.levels).collect(Collectors.toList()); 142 143 if (objPointsIn.size() != imgPointsIn.size() || objPointsIn.size() != levelsArr.size()) { 144 logger.error("objpts.size != imgpts.size"); 145 return null; 146 } 147 148 // And delete rows depending on the level -- otherwise, level has no impact for opencv 149 List<Mat> objPoints = new ArrayList<>(); 150 List<Mat> imgPoints = new ArrayList<>(); 151 for (int i = 0; i < objPointsIn.size(); i++) { 152 MatOfPoint3f objPtsOut = new MatOfPoint3f(); 153 MatOfPoint2f imgPtsOut = new MatOfPoint2f(); 154 155 deleteIgnoredPoints( 156 objPointsIn.get(i), imgPointsIn.get(i), levelsArr.get(i), objPtsOut, imgPtsOut); 157 158 objPoints.add(objPtsOut); 159 imgPoints.add(imgPtsOut); 160 } 161 162 Mat cameraMatrix = new Mat(3, 3, CvType.CV_64F); 163 MatOfDouble distortionCoefficients = new MatOfDouble(); 164 List<Mat> rvecs = new ArrayList<>(); 165 List<Mat> tvecs = new ArrayList<>(); 166 167 // initial camera matrix guess 168 double cx = (in.get(0).size.width / 2.0) - 0.5; 169 double cy = (in.get(0).size.width / 2.0) - 0.5; 170 cameraMatrix.put(0, 0, new double[] {fxGuess, 0, cx, 0, fyGuess, cy, 0, 0, 1}); 171 172 try { 173 // FindBoardCorners pipe outputs all the image points, object points, and frames 174 // to calculate 175 // imageSize from, other parameters are output Mats 176 177 Calib3d.calibrateCameraExtended( 178 objPoints, 179 imgPoints, 180 new Size(in.get(0).size.width, in.get(0).size.height), 181 cameraMatrix, 182 distortionCoefficients, 183 rvecs, 184 tvecs, 185 stdDeviationsIntrinsics, 186 stdDeviationsExtrinsics, 187 perViewErrors, 188 Calib3d.CALIB_USE_LU + Calib3d.CALIB_USE_INTRINSIC_GUESS); 189 } catch (Exception e) { 190 logger.error("Calibration failed!", e); 191 e.printStackTrace(); 192 return null; 193 } 194 195 JsonMatOfDouble cameraMatrixMat = JsonMatOfDouble.fromMat(cameraMatrix); 196 JsonMatOfDouble distortionCoefficientsMat = JsonMatOfDouble.fromMat(distortionCoefficients); 197 198 var observations = 199 createObservations( 200 in, cameraMatrix, distortionCoefficients, rvecs, tvecs, null, imageSavePath); 201 202 cameraMatrix.release(); 203 distortionCoefficients.release(); 204 rvecs.forEach(Mat::release); 205 tvecs.forEach(Mat::release); 206 objPoints.forEach(Mat::release); 207 imgPoints.forEach(Mat::release); 208 209 return new CameraCalibrationCoefficients( 210 in.get(0).size, 211 cameraMatrixMat, 212 distortionCoefficientsMat, 213 new double[0], 214 observations, 215 new Size(params.boardWidth, params.boardHeight), 216 params.squareSize, 217 CameraLensModel.LENSMODEL_OPENCV); 218 } 219 220 protected CameraCalibrationCoefficients calibrateMrcal( 221 List<FindBoardCornersPipe.FindBoardCornersPipeResult> in, 222 double fxGuess, 223 double fyGuess, 224 Path imageSavePath) { 225 List<MatOfPoint2f> corner_locations = 226 in.stream().map(it -> it.imagePoints).map(MatOfPoint2f::new).collect(Collectors.toList()); 227 228 List<MatOfFloat> levels = 229 in.stream().map(it -> it.levels).map(MatOfFloat::new).collect(Collectors.toList()); 230 231 int imageWidth = (int) in.get(0).size.width; 232 int imageHeight = (int) in.get(0).size.height; 233 234 MrCalResult result = 235 MrCalJNI.calibrateCamera( 236 corner_locations, 237 levels, 238 params.boardWidth, 239 params.boardHeight, 240 params.squareSize, 241 imageWidth, 242 imageHeight, 243 (fxGuess + fyGuess) / 2.0); 244 245 levels.forEach(MatOfFloat::release); 246 corner_locations.forEach(MatOfPoint2f::release); 247 248 // intrinsics are fx fy cx cy from mrcal 249 JsonMatOfDouble cameraMatrixMat = 250 new JsonMatOfDouble( 251 3, 252 3, 253 CvType.CV_64FC1, 254 new double[] { 255 // fx 0 cx 256 result.intrinsics[0], 257 0, 258 result.intrinsics[2], 259 // 0 fy cy 260 0, 261 result.intrinsics[1], 262 result.intrinsics[3], 263 // 0 0 1 264 0, 265 0, 266 1 267 }); 268 JsonMatOfDouble distortionCoefficientsMat = 269 new JsonMatOfDouble(1, 8, CvType.CV_64FC1, Arrays.copyOfRange(result.intrinsics, 4, 12)); 270 271 // Calculate optimized board poses manually. We get this for free from mrcal 272 // too, but that's not JNIed (yet) 273 List<Mat> rvecs = new ArrayList<>(); 274 List<Mat> tvecs = new ArrayList<>(); 275 276 for (var o : in) { 277 var rvec = new Mat(); 278 var tvec = new Mat(); 279 280 // If the calibration points contain points that are negative then we need to exclude them, 281 // they are considered points that we dont want to use in calibration/solvepnp. These points 282 // are required prior to this to allow mrcal to work. 283 Point3[] oPoints = o.objectPoints.toArray(); 284 Point[] iPoints = o.imagePoints.toArray(); 285 286 List<Point3> outputOPoints = new ArrayList<Point3>(); 287 List<Point> outputIPoints = new ArrayList<Point>(); 288 289 for (int i = 0; i < iPoints.length; i++) { 290 if (iPoints[i].x >= 0 && iPoints[i].y >= 0) { 291 outputIPoints.add(iPoints[i]); 292 } 293 } 294 for (int i = 0; i < oPoints.length; i++) { 295 if (oPoints[i].x >= 0 && oPoints[i].y >= 0 && oPoints[i].z >= 0) { 296 outputOPoints.add(oPoints[i]); 297 } 298 } 299 300 o.objectPoints.fromList(outputOPoints); 301 o.imagePoints.fromList(outputIPoints); 302 303 Calib3d.solvePnP( 304 o.objectPoints, 305 o.imagePoints, 306 cameraMatrixMat.getAsMatOfDouble(), 307 distortionCoefficientsMat.getAsMatOfDouble(), 308 rvec, 309 tvec); 310 rvecs.add(rvec); 311 tvecs.add(tvec); 312 } 313 314 List<BoardObservation> observations = 315 createObservations( 316 in, 317 cameraMatrixMat.getAsMatOfDouble(), 318 distortionCoefficientsMat.getAsMatOfDouble(), 319 rvecs, 320 tvecs, 321 new double[] {result.warp_x, result.warp_y}, 322 imageSavePath); 323 324 rvecs.forEach(Mat::release); 325 tvecs.forEach(Mat::release); 326 327 return new CameraCalibrationCoefficients( 328 in.get(0).size, 329 cameraMatrixMat, 330 distortionCoefficientsMat, 331 new double[] {result.warp_x, result.warp_y}, 332 observations, 333 new Size(params.boardWidth, params.boardHeight), 334 params.squareSize, 335 CameraLensModel.LENSMODEL_OPENCV); 336 } 337 338 private List<BoardObservation> createObservations( 339 List<FindBoardCornersPipe.FindBoardCornersPipeResult> in, 340 Mat cameraMatrix_, 341 MatOfDouble distortionCoefficients_, 342 List<Mat> rvecs, 343 List<Mat> tvecs, 344 double[] calobject_warp, 345 Path imageSavePath) { 346 List<Mat> objPoints = in.stream().map(it -> it.objectPoints).collect(Collectors.toList()); 347 List<Mat> imgPts = in.stream().map(it -> it.imagePoints).collect(Collectors.toList()); 348 349 // Clear the calibration image folder of any old images before we save the new ones. 350 351 try { 352 FileUtils.cleanDirectory(imageSavePath.toFile()); 353 } catch (Exception e) { 354 logger.error("Failed to clean calibration image directory", e); 355 } 356 357 // For each observation, calc reprojection error 358 Mat jac_temp = new Mat(); 359 List<BoardObservation> observations = new ArrayList<>(); 360 for (int i = 0; i < objPoints.size(); i++) { 361 MatOfPoint3f i_objPtsNative = new MatOfPoint3f(); 362 objPoints.get(i).copyTo(i_objPtsNative); 363 var i_objPts = i_objPtsNative.toList(); 364 var i_imgPts = ((MatOfPoint2f) imgPts.get(i)).toList(); 365 366 // Apply warp, if set 367 if (calobject_warp != null && calobject_warp.length == 2) { 368 // mrcal warp model! 369 // The chessboard spans [-1, 1] on the x and y axies. We then let 370 // z=k_x(1-x^2)+k_y(1-y^2) 371 372 double xmin = 0; 373 double ymin = 0; 374 double xmax = params.boardWidth * params.squareSize; 375 double ymax = params.boardHeight * params.squareSize; 376 double k_x = calobject_warp[0]; 377 double k_y = calobject_warp[1]; 378 379 // Convert to list, remap z, and back to cv::Mat 380 var list = i_objPtsNative.toArray(); 381 for (var pt : list) { 382 double x_norm = MathUtils.map(pt.x, xmin, xmax, -1, 1); 383 double y_norm = MathUtils.map(pt.y, ymin, ymax, -1, 1); 384 pt.z = k_x * (1 - x_norm * x_norm) + k_y * (1 - y_norm * y_norm); 385 } 386 i_objPtsNative.fromArray(list); 387 } 388 389 var img_pts_reprojected = new MatOfPoint2f(); 390 try { 391 Calib3d.projectPoints( 392 i_objPtsNative, 393 rvecs.get(i), 394 tvecs.get(i), 395 cameraMatrix_, 396 distortionCoefficients_, 397 img_pts_reprojected, 398 jac_temp, 399 0.0); 400 } catch (Exception e) { 401 e.printStackTrace(); 402 continue; 403 } 404 var img_pts_reprojected_list = img_pts_reprojected.toList(); 405 406 var reprojectionError = new ArrayList<Point>(); 407 for (int j = 0; j < img_pts_reprojected_list.size(); j++) { 408 // error = (measured - expected) 409 var measured = img_pts_reprojected_list.get(j); 410 var expected = i_imgPts.get(j); 411 var error = new Point(measured.x - expected.x, measured.y - expected.y); 412 reprojectionError.add(error); 413 } 414 415 var camToBoard = MathUtils.opencvRTtoPose3d(rvecs.get(i), tvecs.get(i)); 416 417 var inputImage = in.get(i).inputImage; 418 Path image_path = null; 419 String snapshotName = "img" + i + ".png"; 420 if (inputImage != null) { 421 image_path = Paths.get(imageSavePath.toString(), snapshotName); 422 Imgcodecs.imwrite(image_path.toString(), inputImage); 423 } 424 425 observations.add( 426 new BoardObservation( 427 i_objPts, i_imgPts, reprojectionError, camToBoard, true, snapshotName, image_path)); 428 } 429 jac_temp.release(); 430 431 return observations; 432 } 433 434 /** Delete all rows of mats where level is < 0. Useful for opencv */ 435 private void deleteIgnoredPoints( 436 MatOfPoint3f objPtsMatIn, 437 MatOfPoint2f imgPtsMatIn, 438 MatOfFloat levelsMat, 439 MatOfPoint3f objPtsMatOut, 440 MatOfPoint2f imgPtsMatOut) { 441 var levels = levelsMat.toArray(); 442 var objPtsIn = objPtsMatIn.toArray(); 443 var imgPtsIn = imgPtsMatIn.toArray(); 444 445 var objPtsOut = new ArrayList<Point3>(); 446 var imgPtsOut = new ArrayList<Point>(); 447 448 for (int i = 0; i < levels.length; i++) { 449 if (levels[i] >= 0) { 450 // point survives 451 objPtsOut.add(objPtsIn[i]); 452 imgPtsOut.add(imgPtsIn[i]); 453 } 454 } 455 456 objPtsMatOut.fromList(objPtsOut); 457 imgPtsMatOut.fromList(imgPtsOut); 458 } 459 460 public static class CalibratePipeParams { 461 // Size (in # of corners) of the calibration object 462 public int boardHeight; 463 public int boardWidth; 464 // And size of each square 465 public double squareSize; 466 467 public boolean useMrCal; 468 469 public CalibratePipeParams( 470 int boardHeightSquares, int boardWidthSquares, double squareSize, boolean usemrcal) { 471 this.boardHeight = boardHeightSquares - 1; 472 this.boardWidth = boardWidthSquares - 1; 473 this.squareSize = squareSize; 474 this.useMrCal = usemrcal; 475 } 476 } 477}