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}