From a2b4f9a5365b181e10d1429ac2723c4a1abcdb75 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 4 Mar 2024 23:27:11 -0500 Subject: [PATCH 1/3] Bump mrcal to include solver fixes --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d1b9cbf706..116360088b 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ ext { photonGlDriverLibVersion = "dev-v2023.1.0-9-g75fc678" rknnVersion = "dev-v2024.0.0-64-gc0836a6" frcYear = "2024" - mrcalVersion = "dev-v2024.0.0-7-gc976aaa"; + mrcalVersion = "dev-v2024.0.0-18-gb903a09"; pubVersion = versionString From c9a52cced6322e1bc0d3efa87f56be1006463f55 Mon Sep 17 00:00:00 2001 From: Matt Morley Date: Wed, 6 Mar 2024 00:15:14 -0500 Subject: [PATCH 2/3] Pipe in corners used --- build.gradle | 2 +- .../vision/calibration/BoardObservation.java | 21 +++++-- .../vision/pipe/impl/Calibrate3dPipe.java | 58 ++++++++++++------- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/build.gradle b/build.gradle index 116360088b..bfa31315fb 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ ext { photonGlDriverLibVersion = "dev-v2023.1.0-9-g75fc678" rknnVersion = "dev-v2024.0.0-64-gc0836a6" frcYear = "2024" - mrcalVersion = "dev-v2024.0.0-18-gb903a09"; + mrcalVersion = "dev-v2024.0.0-29-g1536857"; pubVersion = versionString diff --git a/photon-core/src/main/java/org/photonvision/vision/calibration/BoardObservation.java b/photon-core/src/main/java/org/photonvision/vision/calibration/BoardObservation.java index 8bf41c112e..d45a443724 100644 --- a/photon-core/src/main/java/org/photonvision/vision/calibration/BoardObservation.java +++ b/photon-core/src/main/java/org/photonvision/vision/calibration/BoardObservation.java @@ -18,12 +18,15 @@ package org.photonvision.vision.calibration; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import edu.wpi.first.math.geometry.Pose3d; +import java.util.Arrays; import java.util.List; import org.opencv.core.Point; import org.opencv.core.Point3; +@JsonIgnoreProperties(ignoreUnknown = true) public final class BoardObservation implements Cloneable { // Expected feature 3d location in the camera frame @JsonProperty("locationInObjectSpace") @@ -42,8 +45,8 @@ public final class BoardObservation implements Cloneable { public Pose3d optimisedCameraToObject; // If we should use this observation when re-calculating camera calibration - @JsonProperty("includeObservationInCalibration") - public boolean includeObservationInCalibration; + @JsonProperty("cornersUsed") + public boolean[] cornersUsed; @JsonProperty("snapshotName") public String snapshotName; @@ -57,16 +60,22 @@ public BoardObservation( @JsonProperty("locationInImageSpace") List locationInImageSpace, @JsonProperty("reprojectionErrors") List reprojectionErrors, @JsonProperty("optimisedCameraToObject") Pose3d optimisedCameraToObject, - @JsonProperty("includeObservationInCalibration") boolean includeObservationInCalibration, + @JsonProperty("cornersUsed") boolean[] cornersUsed, @JsonProperty("snapshotName") String snapshotName, @JsonProperty("snapshotData") JsonImageMat snapshotData) { this.locationInObjectSpace = locationInObjectSpace; this.locationInImageSpace = locationInImageSpace; this.reprojectionErrors = reprojectionErrors; this.optimisedCameraToObject = optimisedCameraToObject; - this.includeObservationInCalibration = includeObservationInCalibration; + this.cornersUsed = cornersUsed; this.snapshotName = snapshotName; this.snapshotData = snapshotData; + + // legacy migration -- we assume all points are inliers + if (cornersUsed == null) { + cornersUsed = new boolean[locationInObjectSpace.size()]; + Arrays.fill(cornersUsed, true); + } } @Override @@ -79,8 +88,8 @@ public String toString() { + reprojectionErrors + ", optimisedCameraToObject=" + optimisedCameraToObject - + ", includeObservationInCalibration=" - + includeObservationInCalibration + + ", cornersUsed=" + + cornersUsed + ", snapshotName=" + snapshotName + ", snapshotData=" diff --git a/photon-core/src/main/java/org/photonvision/vision/pipe/impl/Calibrate3dPipe.java b/photon-core/src/main/java/org/photonvision/vision/pipe/impl/Calibrate3dPipe.java index 1f9b32020b..dd71a69abe 100644 --- a/photon-core/src/main/java/org/photonvision/vision/pipe/impl/Calibrate3dPipe.java +++ b/photon-core/src/main/java/org/photonvision/vision/pipe/impl/Calibrate3dPipe.java @@ -144,8 +144,19 @@ protected CameraCalibrationCoefficients calibrateOpenCV( JsonMatOfDouble cameraMatrixMat = JsonMatOfDouble.fromMat(cameraMatrix); JsonMatOfDouble distortionCoefficientsMat = JsonMatOfDouble.fromMat(distortionCoefficients); + // Opencv is lame, so we can only assume all points are inliers + var inliners = + objPoints.stream() + .map( + it -> { + var array = new boolean[it.rows() * it.cols()]; + Arrays.fill(array, true); + return array; + }) + .collect(Collectors.toList()); + var observations = - createObservations(in, cameraMatrix, distortionCoefficients, rvecs, tvecs, null); + createObservations(in, cameraMatrix, distortionCoefficients, rvecs, tvecs, inliners, null); cameraMatrix.release(); distortionCoefficients.release(); @@ -205,20 +216,17 @@ protected CameraCalibrationCoefficients calibrateMrcal( JsonMatOfDouble distortionCoefficientsMat = new JsonMatOfDouble(1, 8, CvType.CV_64FC1, Arrays.copyOfRange(result.intrinsics, 4, 12)); - // Calculate optimized board poses manually. We get this for free from mrcal too, but that's not - // JNIed (yet) + // Pull optimised camera to board poses out from the JNI List rvecs = new ArrayList<>(); List tvecs = new ArrayList<>(); - for (var o : in) { - var rvec = new Mat(); - var tvec = new Mat(); - Calib3d.solvePnP( - o.objectPoints, - o.imagePoints, - cameraMatrixMat.getAsMat(), - distortionCoefficientsMat.getAsMatOfDouble(), - rvec, - tvec); + for (var o : result.optimizedPoses) { + var rvec = new MatOfDouble(); + var tvec = new MatOfDouble(); + + rvec.fromArray(o.getRotation().getAxis().times(o.getRotation().getAngle()).getData()); + tvec.fromArray( + o.getTranslation().getX(), o.getTranslation().getY(), o.getTranslation().getZ()); + rvecs.add(rvec); tvecs.add(tvec); } @@ -230,6 +238,7 @@ protected CameraCalibrationCoefficients calibrateMrcal( distortionCoefficientsMat.getAsMatOfDouble(), rvecs, tvecs, + result.cornersUsed, new double[] {result.warp_x, result.warp_y}); rvecs.forEach(Mat::release); @@ -252,6 +261,7 @@ private List createObservations( MatOfDouble distortionCoefficients_, List rvecs, List tvecs, + List cornersUsed, double[] calobject_warp) { List objPoints = in.stream().map(it -> it.objectPoints).collect(Collectors.toList()); List imgPts = in.stream().map(it -> it.imagePoints).collect(Collectors.toList()); @@ -259,11 +269,11 @@ private List createObservations( // For each observation, calc reprojection error Mat jac_temp = new Mat(); List observations = new ArrayList<>(); - for (int i = 0; i < objPoints.size(); i++) { + for (int snapshotId = 0; snapshotId < objPoints.size(); snapshotId++) { MatOfPoint3f i_objPtsNative = new MatOfPoint3f(); - objPoints.get(i).copyTo(i_objPtsNative); + objPoints.get(snapshotId).copyTo(i_objPtsNative); var i_objPts = i_objPtsNative.toList(); - var i_imgPts = ((MatOfPoint2f) imgPts.get(i)).toList(); + var i_imgPts = ((MatOfPoint2f) imgPts.get(snapshotId)).toList(); // Apply warp, if set if (calobject_warp != null && calobject_warp.length == 2) { @@ -291,8 +301,8 @@ private List createObservations( try { Calib3d.projectPoints( i_objPtsNative, - rvecs.get(i), - tvecs.get(i), + rvecs.get(snapshotId), + tvecs.get(snapshotId), cameraMatrix_, distortionCoefficients_, img_pts_reprojected, @@ -313,16 +323,22 @@ private List createObservations( reprojectionError.add(error); } - var camToBoard = MathUtils.opencvRTtoPose3d(rvecs.get(i), tvecs.get(i)); + var camToBoard = MathUtils.opencvRTtoPose3d(rvecs.get(snapshotId), tvecs.get(snapshotId)); JsonImageMat image = null; - var inputImage = in.get(i).inputImage; + var inputImage = in.get(snapshotId).inputImage; if (inputImage != null) { image = new JsonImageMat(inputImage); } observations.add( new BoardObservation( - i_objPts, i_imgPts, reprojectionError, camToBoard, true, "img" + i + ".png", image)); + i_objPts, + i_imgPts, + reprojectionError, + camToBoard, + cornersUsed.get(snapshotId), + "img" + snapshotId + ".png", + image)); } jac_temp.release(); From 2ab8a121e3cc54c1192ede3b3d0cdd90cb3f7a46 Mon Sep 17 00:00:00 2001 From: Matt Morley Date: Wed, 6 Mar 2024 01:00:58 -0500 Subject: [PATCH 3/3] Show outliers in board observation --- .../cameras/CameraCalibrationInfoCard.vue | 5 +-- photon-client/src/types/SettingTypes.ts | 4 +-- .../vision/calibration/BoardObservation.java | 32 ++++++++++++++++++- .../photonvision/server/RequestHandler.java | 10 +++--- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/photon-client/src/components/cameras/CameraCalibrationInfoCard.vue b/photon-client/src/components/cameras/CameraCalibrationInfoCard.vue index 52f5eb93c2..c5937ab442 100644 --- a/photon-client/src/components/cameras/CameraCalibrationInfoCard.vue +++ b/photon-client/src/components/cameras/CameraCalibrationInfoCard.vue @@ -228,10 +228,7 @@ const calibrationImageURL = (index: number) => Board warp, X/Y {{ - useCameraSettingsStore() - .getCalibrationCoeffs(props.videoFormat.resolution) - ?.calobjectWarp?.map((it) => (it * 1000).toFixed(2) + " mm") - .join(" / ") + currentCalibrationCoeffs?.calobjectWarp?.map((it) => (it * 1000).toFixed(2) + " mm").join(" / ") }} diff --git a/photon-client/src/types/SettingTypes.ts b/photon-client/src/types/SettingTypes.ts index 0531bb8b44..6f54718eaa 100644 --- a/photon-client/src/types/SettingTypes.ts +++ b/photon-client/src/types/SettingTypes.ts @@ -129,7 +129,7 @@ export interface BoardObservation { locationInImageSpace: CvPoint[]; reprojectionErrors: CvPoint[]; optimisedCameraToObject: Pose3d; - includeObservationInCalibration: boolean; + cornersUsed: boolean[]; snapshotName: string; snapshotData: JsonImageMat; } @@ -256,7 +256,7 @@ export const PlaceholderCameraSettings: CameraSettings = { { x: 2, y: 1 }, { x: 3, y: 1 } ], - includeObservationInCalibration: false, + cornersUsed: [true, true, true, false, false, true], snapshotName: "img0.png", snapshotData: { rows: 480, cols: 640, type: CvType.CV_8U, data: "" } } diff --git a/photon-core/src/main/java/org/photonvision/vision/calibration/BoardObservation.java b/photon-core/src/main/java/org/photonvision/vision/calibration/BoardObservation.java index d45a443724..8e2b56e695 100644 --- a/photon-core/src/main/java/org/photonvision/vision/calibration/BoardObservation.java +++ b/photon-core/src/main/java/org/photonvision/vision/calibration/BoardObservation.java @@ -18,13 +18,19 @@ package org.photonvision.vision.calibration; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import edu.wpi.first.math.geometry.Pose3d; +import java.awt.Color; import java.util.Arrays; import java.util.List; +import org.opencv.core.Mat; import org.opencv.core.Point; import org.opencv.core.Point3; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; +import org.photonvision.common.util.ColorHelper; @JsonIgnoreProperties(ignoreUnknown = true) public final class BoardObservation implements Cloneable { @@ -67,7 +73,6 @@ public BoardObservation( this.locationInImageSpace = locationInImageSpace; this.reprojectionErrors = reprojectionErrors; this.optimisedCameraToObject = optimisedCameraToObject; - this.cornersUsed = cornersUsed; this.snapshotName = snapshotName; this.snapshotData = snapshotData; @@ -76,6 +81,7 @@ public BoardObservation( cornersUsed = new boolean[locationInObjectSpace.size()]; Arrays.fill(cornersUsed, true); } + this.cornersUsed = cornersUsed; } @Override @@ -106,4 +112,28 @@ public BoardObservation clone() { return null; } } + + @JsonIgnore + public Mat getAnnotatedImage() { + var image = snapshotData.getAsMat().clone(); + var diag = Math.hypot(image.width(), image.height()); + int thickness = (int) Math.max(diag * 1.0 / 600.0, 1); + int r = (int) Math.max(diag * 4.0 / 500.0, 3); + var r2 = r / Math.sqrt(2); + for (int i = 0; i < this.locationInImageSpace.size(); i++) { + Scalar color; + if (cornersUsed[i]) { + color = ColorHelper.colorToScalar(Color.green); + } else { + color = ColorHelper.colorToScalar(Color.red); + } + var c = locationInImageSpace.get(i); + Imgproc.circle(image, c, r, color, thickness); + Imgproc.line( + image, new Point(c.x - r2, c.y - r2), new Point(c.x + r2, c.y + r2), color, thickness); + Imgproc.line( + image, new Point(c.x + r2, c.y - r2), new Point(c.x - r2, c.y + r2), color, thickness); + } + return image; + } } diff --git a/photon-server/src/main/java/org/photonvision/server/RequestHandler.java b/photon-server/src/main/java/org/photonvision/server/RequestHandler.java index 0ea9f05d79..71bb82b3c6 100644 --- a/photon-server/src/main/java/org/photonvision/server/RequestHandler.java +++ b/photon-server/src/main/java/org/photonvision/server/RequestHandler.java @@ -609,16 +609,16 @@ public static void onCalibrationSnapshotRequest(Context ctx) { return; } + var mat = calList.observations.get(observationIdx).getAnnotatedImage(); + // encode as jpeg to save even more space. reduces size of a 1280p image from 300k to 25k var jpegBytes = new MatOfByte(); - Imgcodecs.imencode( - ".jpg", - calList.observations.get(observationIdx).snapshotData.getAsMat(), - jpegBytes, - new MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, 60)); + Imgcodecs.imencode(".jpg", mat, jpegBytes, new MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, 60)); ctx.result(jpegBytes.toArray()); + jpegBytes.release(); + mat.release(); ctx.status(200); }