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.calibration;
019
020import com.fasterxml.jackson.annotation.JsonAlias;
021import com.fasterxml.jackson.annotation.JsonCreator;
022import com.fasterxml.jackson.annotation.JsonIgnore;
023import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
024import com.fasterxml.jackson.annotation.JsonProperty;
025import java.util.Arrays;
026import java.util.List;
027import org.opencv.core.Mat;
028import org.opencv.core.MatOfDouble;
029import org.opencv.core.Size;
030import org.photonvision.vision.opencv.ImageRotationMode;
031import org.photonvision.vision.opencv.Releasable;
032
033@JsonIgnoreProperties(ignoreUnknown = true)
034public class CameraCalibrationCoefficients implements Releasable {
035    @JsonProperty("resolution")
036    public final Size unrotatedImageSize;
037
038    @JsonProperty("cameraIntrinsics")
039    public final JsonMatOfDouble cameraIntrinsics;
040
041    @JsonProperty("distCoeffs")
042    @JsonAlias({"distCoeffs", "distCoeffs"})
043    public final JsonMatOfDouble distCoeffs;
044
045    @JsonProperty("observations")
046    public final List<BoardObservation> observations;
047
048    @JsonProperty("calobjectWarp")
049    public final double[] calobjectWarp;
050
051    @JsonProperty("calobjectSize")
052    public final Size calobjectSize;
053
054    @JsonProperty("calobjectSpacing")
055    public final double calobjectSpacing;
056
057    @JsonProperty("lensmodel")
058    public final CameraLensModel lensmodel;
059
060    /**
061     * Contains all camera calibration data for a particular resolution of a camera. Designed for use
062     * with standard opencv camera calibration matrices. For details on the layout of camera
063     * intrinsics/distortion matrices, see:
064     * https://docs.opencv.org/4.x/d9/d0c/group__calib3d.html#ga3207604e4b1a1758aa66acb6ed5aa65d
065     *
066     * @param resolution The resolution this applies to. We don't assume camera binning or try
067     *     rescaling calibration
068     * @param cameraIntrinsics Camera intrinsics parameters matrix, in the standard opencv form.
069     * @param distCoeffs Camera distortion coefficients array. Variable length depending on order of
070     *     distortion model
071     * @param calobjectWarp Board deformation parameters, for calibrators that can estimate that. See:
072     *     https://mrcal.secretsauce.net/formulation.html#board-deformation
073     * @param observations List of snapshots used to construct this calibration
074     * @param calobjectSize Dimensions of the object used to calibrate, in # of squares in
075     *     width/height
076     * @param calobjectSpacing Spacing between adjacent squares, in meters
077     */
078    @JsonCreator
079    public CameraCalibrationCoefficients(
080            @JsonProperty("resolution") Size resolution,
081            @JsonProperty("cameraIntrinsics") JsonMatOfDouble cameraIntrinsics,
082            @JsonProperty("distCoeffs") JsonMatOfDouble distCoeffs,
083            @JsonProperty("calobjectWarp") double[] calobjectWarp,
084            @JsonProperty("observations") List<BoardObservation> observations,
085            @JsonProperty("calobjectSize") Size calobjectSize,
086            @JsonProperty("calobjectSpacing") double calobjectSpacing,
087            @JsonProperty("lensmodel") CameraLensModel lensmodel) {
088        this.unrotatedImageSize = resolution;
089        this.cameraIntrinsics = cameraIntrinsics;
090        this.distCoeffs = distCoeffs;
091        this.calobjectWarp = calobjectWarp;
092        this.calobjectSize = calobjectSize;
093        this.calobjectSpacing = calobjectSpacing;
094        this.lensmodel = lensmodel;
095
096        // Legacy migration just to make sure that observations is at worst empty and never null
097        if (observations == null) {
098            observations = List.of();
099        }
100        this.observations = observations;
101    }
102
103    public CameraCalibrationCoefficients rotateCoefficients(ImageRotationMode rotation) {
104        if (rotation == ImageRotationMode.DEG_0) {
105            return this;
106        }
107        Mat rotatedIntrinsics = getCameraIntrinsicsMat().clone();
108        Mat rotatedDistCoeffs = getDistCoeffsMat().clone();
109        double cx = getCameraIntrinsicsMat().get(0, 2)[0];
110        double cy = getCameraIntrinsicsMat().get(1, 2)[0];
111        double fx = getCameraIntrinsicsMat().get(0, 0)[0];
112        double fy = getCameraIntrinsicsMat().get(1, 1)[0];
113
114        // only adjust p1 and p2 the rest are radial distortion coefficients
115
116        double p1 = getDistCoeffsMat().get(0, 2)[0];
117        double p2 = getDistCoeffsMat().get(0, 3)[0];
118
119        Size rotatedImageSize = null;
120
121        // A bunch of horrifying opaque rotation black magic. See image-rotation.md for more details.
122        switch (rotation) {
123            case DEG_0:
124                break;
125            case DEG_270_CCW:
126                // FX
127                rotatedIntrinsics.put(0, 0, fy);
128                // FY
129                rotatedIntrinsics.put(1, 1, fx);
130
131                // CX
132                rotatedIntrinsics.put(0, 2, unrotatedImageSize.height - cy);
133                // CY
134                rotatedIntrinsics.put(1, 2, cx);
135
136                // P1
137                rotatedDistCoeffs.put(0, 2, p2);
138                // P2
139                rotatedDistCoeffs.put(0, 3, -p1);
140
141                // The rotated image size is the same as the unrotated image size, but the width and height
142                // are flipped
143                rotatedImageSize = new Size(unrotatedImageSize.height, unrotatedImageSize.width);
144                break;
145            case DEG_180_CCW:
146                // CX
147                rotatedIntrinsics.put(0, 2, unrotatedImageSize.width - cx);
148                // CY
149                rotatedIntrinsics.put(1, 2, unrotatedImageSize.height - cy);
150
151                // P1
152                rotatedDistCoeffs.put(0, 2, -p1);
153                // P2
154                rotatedDistCoeffs.put(0, 3, -p2);
155
156                // The rotated image size is the same as the unrotated image size
157                rotatedImageSize = unrotatedImageSize;
158                break;
159            case DEG_90_CCW:
160                // FX
161                rotatedIntrinsics.put(0, 0, fy);
162                // FY
163                rotatedIntrinsics.put(1, 1, fx);
164
165                // CX
166                rotatedIntrinsics.put(0, 2, cy);
167                // CY
168                rotatedIntrinsics.put(1, 2, unrotatedImageSize.width - cx);
169
170                // P1
171                rotatedDistCoeffs.put(0, 2, -p2);
172                // P2
173                rotatedDistCoeffs.put(0, 3, p1);
174
175                // The rotated image size is the same as the unrotated image size, but the width and height
176                // are flipped
177                rotatedImageSize = new Size(unrotatedImageSize.height, unrotatedImageSize.width);
178                break;
179        }
180
181        JsonMatOfDouble newIntrinsics = JsonMatOfDouble.fromMat(rotatedIntrinsics);
182
183        JsonMatOfDouble newDistCoeffs = JsonMatOfDouble.fromMat(rotatedDistCoeffs);
184
185        rotatedIntrinsics.release();
186        rotatedDistCoeffs.release();
187
188        return new CameraCalibrationCoefficients(
189                rotatedImageSize,
190                newIntrinsics,
191                newDistCoeffs,
192                calobjectWarp,
193                observations,
194                calobjectSize,
195                calobjectSpacing,
196                lensmodel);
197    }
198
199    @JsonIgnore
200    public Mat getCameraIntrinsicsMat() {
201        return cameraIntrinsics.getAsMatOfDouble();
202    }
203
204    @JsonIgnore
205    public MatOfDouble getDistCoeffsMat() {
206        return distCoeffs.getAsMatOfDouble();
207    }
208
209    @JsonIgnore
210    public double[] getIntrinsicsArr() {
211        return cameraIntrinsics.data;
212    }
213
214    @JsonIgnore
215    public double[] getDistCoeffsArr() {
216        return distCoeffs.data;
217    }
218
219    @JsonIgnore
220    public List<BoardObservation> getPerViewErrors() {
221        return observations;
222    }
223
224    @Override
225    public void release() {
226        cameraIntrinsics.release();
227        distCoeffs.release();
228    }
229
230    @Override
231    public String toString() {
232        return "CameraCalibrationCoefficients [resolution="
233                + unrotatedImageSize
234                + ", cameraIntrinsics="
235                + cameraIntrinsics
236                + ", distCoeffs="
237                + distCoeffs
238                + ", observationslen="
239                + observations.size()
240                + ", calobjectWarp="
241                + Arrays.toString(calobjectWarp)
242                + "]";
243    }
244
245    public UICameraCalibrationCoefficients cloneWithoutObservations() {
246        return new UICameraCalibrationCoefficients(
247                unrotatedImageSize,
248                cameraIntrinsics,
249                distCoeffs,
250                calobjectWarp,
251                observations,
252                calobjectSize,
253                calobjectSpacing,
254                lensmodel);
255    }
256}