mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-20 00:51:41 +00:00
[photonlib] Simulation Visualization Update (#895)
This commit is contained in:
@@ -28,7 +28,6 @@ import edu.wpi.first.math.Matrix;
|
||||
import edu.wpi.first.math.Nat;
|
||||
import edu.wpi.first.math.Num;
|
||||
import edu.wpi.first.math.Vector;
|
||||
import edu.wpi.first.math.geometry.CoordinateSystem;
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Rotation3d;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
@@ -56,6 +55,9 @@ import org.opencv.imgproc.Imgproc;
|
||||
import org.photonvision.targeting.TargetCorner;
|
||||
|
||||
public final class OpenCVHelp {
|
||||
private static RotTrlTransform3d NWU_TO_EDN;
|
||||
private static RotTrlTransform3d EDN_TO_NWU;
|
||||
|
||||
static {
|
||||
try {
|
||||
var loader =
|
||||
@@ -65,15 +67,21 @@ public final class OpenCVHelp {
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load native libraries!", e);
|
||||
}
|
||||
|
||||
NWU_TO_EDN =
|
||||
new RotTrlTransform3d(
|
||||
new Rotation3d(Matrix.mat(Nat.N3(), Nat.N3()).fill(0, -1, 0, 0, 0, -1, 1, 0, 0)),
|
||||
new Translation3d());
|
||||
EDN_TO_NWU =
|
||||
new RotTrlTransform3d(
|
||||
new Rotation3d(Matrix.mat(Nat.N3(), Nat.N3()).fill(0, 0, 1, -1, 0, 0, 0, -1, 0)),
|
||||
new Translation3d());
|
||||
}
|
||||
|
||||
public static MatOfDouble matrixToMat(SimpleMatrix matrix) {
|
||||
public static Mat matrixToMat(SimpleMatrix matrix) {
|
||||
var mat = new Mat(matrix.numRows(), matrix.numCols(), CvType.CV_64F);
|
||||
mat.put(0, 0, matrix.getDDRM().getData());
|
||||
var wrappedMat = new MatOfDouble();
|
||||
mat.convertTo(wrappedMat, CvType.CV_64F);
|
||||
mat.release();
|
||||
return wrappedMat;
|
||||
return mat;
|
||||
}
|
||||
|
||||
public static Matrix<Num, Num> matToMatrix(Mat mat) {
|
||||
@@ -93,8 +101,7 @@ public final class OpenCVHelp {
|
||||
public static MatOfPoint3f translationToTvec(Translation3d... translations) {
|
||||
Point3[] points = new Point3[translations.length];
|
||||
for (int i = 0; i < translations.length; i++) {
|
||||
var trl =
|
||||
CoordinateSystem.convert(translations[i], CoordinateSystem.NWU(), CoordinateSystem.EDN());
|
||||
var trl = translationNWUtoEDN(translations[i]);
|
||||
points[i] = new Point3(trl.getX(), trl.getY(), trl.getZ());
|
||||
}
|
||||
return new MatOfPoint3f(points);
|
||||
@@ -112,10 +119,7 @@ public final class OpenCVHelp {
|
||||
tvecInput.convertTo(wrapped, CvType.CV_32F);
|
||||
wrapped.get(0, 0, data);
|
||||
wrapped.release();
|
||||
return CoordinateSystem.convert(
|
||||
new Translation3d(data[0], data[1], data[2]),
|
||||
CoordinateSystem.EDN(),
|
||||
CoordinateSystem.NWU());
|
||||
return translationEDNtoNWU(new Translation3d(data[0], data[1], data[2]));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,42 +154,46 @@ public final class OpenCVHelp {
|
||||
return rotationEDNtoNWU(new Rotation3d(axis.div(axis.norm()), axis.norm()));
|
||||
}
|
||||
|
||||
public static TargetCorner averageCorner(List<TargetCorner> corners) {
|
||||
if (corners == null || corners.size() == 0) return null;
|
||||
|
||||
var pointMat = targetCornersToMat(corners);
|
||||
public static Point avgPoint(Point[] points) {
|
||||
if (points == null || points.length == 0) return null;
|
||||
var pointMat = new MatOfPoint2f(points);
|
||||
Core.reduce(pointMat, pointMat, 0, Core.REDUCE_AVG);
|
||||
var avgPt = matToTargetCorners(pointMat)[0];
|
||||
var avgPt = pointMat.toArray()[0];
|
||||
pointMat.release();
|
||||
return avgPt;
|
||||
}
|
||||
|
||||
public static MatOfPoint2f targetCornersToMat(List<TargetCorner> corners) {
|
||||
return targetCornersToMat(corners.toArray(TargetCorner[]::new));
|
||||
public static Point[] cornersToPoints(List<TargetCorner> corners) {
|
||||
var points = new Point[corners.size()];
|
||||
for (int i = 0; i < corners.size(); i++) {
|
||||
var corn = corners.get(i);
|
||||
points[i] = new Point(corn.x, corn.y);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
public static MatOfPoint2f targetCornersToMat(TargetCorner... corners) {
|
||||
public static Point[] cornersToPoints(TargetCorner... corners) {
|
||||
var points = new Point[corners.length];
|
||||
for (int i = 0; i < corners.length; i++) {
|
||||
points[i] = new Point(corners[i].x, corners[i].y);
|
||||
}
|
||||
return new MatOfPoint2f(points);
|
||||
return points;
|
||||
}
|
||||
|
||||
public static TargetCorner[] pointsToTargetCorners(Point... points) {
|
||||
var corners = new TargetCorner[points.length];
|
||||
public static List<TargetCorner> pointsToCorners(Point... points) {
|
||||
var corners = new ArrayList<TargetCorner>(points.length);
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
corners[i] = new TargetCorner(points[i].x, points[i].y);
|
||||
corners.add(new TargetCorner(points[i].x, points[i].y));
|
||||
}
|
||||
return corners;
|
||||
}
|
||||
|
||||
public static TargetCorner[] matToTargetCorners(MatOfPoint2f matInput) {
|
||||
var corners = new TargetCorner[(int) matInput.total()];
|
||||
public static List<TargetCorner> pointsToCorners(MatOfPoint2f matInput) {
|
||||
var corners = new ArrayList<TargetCorner>();
|
||||
float[] data = new float[(int) matInput.total() * matInput.channels()];
|
||||
matInput.get(0, 0, data);
|
||||
for (int i = 0; i < corners.length; i++) {
|
||||
corners[i] = new TargetCorner(data[0 + 2 * i], data[1 + 2 * i]);
|
||||
for (int i = 0; i < (int) matInput.total(); i++) {
|
||||
corners.add(new TargetCorner(data[0 + 2 * i], data[1 + 2 * i]));
|
||||
}
|
||||
return corners;
|
||||
}
|
||||
@@ -221,24 +229,39 @@ public final class OpenCVHelp {
|
||||
return reordered;
|
||||
}
|
||||
|
||||
// TODO: RotTrlTransform3d removal awaiting Rotation3d performance improvements
|
||||
/**
|
||||
* Convert a rotation from EDN to NWU. For example, if you have a rotation X,Y,Z {1, 0, 0} in EDN,
|
||||
* this would be XYZ {0, -1, 0} in NWU.
|
||||
* Convert a rotation delta from EDN to NWU. For example, if you have a rotation X,Y,Z {1, 0, 0}
|
||||
* in EDN, this would be {0, -1, 0} in NWU.
|
||||
*/
|
||||
private static Rotation3d rotationEDNtoNWU(Rotation3d rot) {
|
||||
return CoordinateSystem.convert(
|
||||
new Rotation3d(), CoordinateSystem.NWU(), CoordinateSystem.EDN())
|
||||
.plus(CoordinateSystem.convert(rot, CoordinateSystem.EDN(), CoordinateSystem.NWU()));
|
||||
return new RotTrlTransform3d(EDN_TO_NWU.apply(rot), new Translation3d())
|
||||
.apply(EDN_TO_NWU.inverse().getRotation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a rotation from EDN to NWU. For example, if you have a rotation X,Y,Z {1, 0, 0} in EDN,
|
||||
* this would be XYZ {0, -1, 0} in NWU.
|
||||
* Convert a rotation delta from NWU to EDN. For example, if you have a rotation X,Y,Z {1, 0, 0}
|
||||
* in NWU, this would be {0, 0, 1} in EDN.
|
||||
*/
|
||||
private static Rotation3d rotationNWUtoEDN(Rotation3d rot) {
|
||||
return CoordinateSystem.convert(
|
||||
new Rotation3d(), CoordinateSystem.EDN(), CoordinateSystem.NWU())
|
||||
.plus(CoordinateSystem.convert(rot, CoordinateSystem.NWU(), CoordinateSystem.EDN()));
|
||||
return new RotTrlTransform3d(NWU_TO_EDN.apply(rot), new Translation3d())
|
||||
.apply(NWU_TO_EDN.inverse().getRotation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a translation from EDN to NWU. For example, if you have a translation X,Y,Z {1, 0, 0}
|
||||
* in EDN, this would be {0, -1, 0} in NWU.
|
||||
*/
|
||||
private static Translation3d translationEDNtoNWU(Translation3d trl) {
|
||||
return EDN_TO_NWU.apply(trl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a translation from NWU to EDN. For example, if you have a translation X,Y,Z {1, 0, 0}
|
||||
* in NWU, this would be {0, 0, 1} in EDN.
|
||||
*/
|
||||
private static Translation3d translationNWUtoEDN(Translation3d trl) {
|
||||
return NWU_TO_EDN.apply(trl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,29 +270,27 @@ public final class OpenCVHelp {
|
||||
*
|
||||
* @param cameraMatrix the camera intrinsics matrix in standard opencv form
|
||||
* @param distCoeffs the camera distortion matrix in standard opencv form
|
||||
* @param camPose The current camera pose in the 3d world
|
||||
* @param camRt The change in basis from world coordinates to camera coordinates. See {@link
|
||||
* RotTrlTransform3d#makeRelativeTo(Pose3d)}.
|
||||
* @param objectTranslations The 3d points to be projected
|
||||
* @return The 2d points in pixels which correspond to the image of the 3d points on the camera
|
||||
* @return The 2d points in pixels which correspond to the camera's image of the 3d points
|
||||
*/
|
||||
public static List<TargetCorner> projectPoints(
|
||||
public static Point[] projectPoints(
|
||||
Matrix<N3, N3> cameraMatrix,
|
||||
Matrix<N5, N1> distCoeffs,
|
||||
Pose3d camPose,
|
||||
RotTrlTransform3d camRt,
|
||||
List<Translation3d> objectTranslations) {
|
||||
// translate to opencv classes
|
||||
var objectPoints = translationToTvec(objectTranslations.toArray(new Translation3d[0]));
|
||||
// opencv rvec/tvec describe a change in basis from world to camera
|
||||
var basisChange = RotTrlTransform3d.makeRelativeTo(camPose);
|
||||
var rvec = rotationToRvec(basisChange.getRotation());
|
||||
var tvec = translationToTvec(basisChange.getTranslation());
|
||||
var rvec = rotationToRvec(camRt.getRotation());
|
||||
var tvec = translationToTvec(camRt.getTranslation());
|
||||
var cameraMatrixMat = matrixToMat(cameraMatrix.getStorage());
|
||||
var distCoeffsMat = matrixToMat(distCoeffs.getStorage());
|
||||
var distCoeffsMat = new MatOfDouble(matrixToMat(distCoeffs.getStorage()));
|
||||
var imagePoints = new MatOfPoint2f();
|
||||
// project to 2d
|
||||
Calib3d.projectPoints(objectPoints, rvec, tvec, cameraMatrixMat, distCoeffsMat, imagePoints);
|
||||
|
||||
// turn 2d point Mat into TargetCorners
|
||||
var corners = matToTargetCorners(imagePoints);
|
||||
var points = imagePoints.toArray();
|
||||
|
||||
// release our Mats from native memory
|
||||
objectPoints.release();
|
||||
@@ -279,37 +300,37 @@ public final class OpenCVHelp {
|
||||
distCoeffsMat.release();
|
||||
imagePoints.release();
|
||||
|
||||
return Arrays.asList(corners);
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Undistort 2d image points using a given camera's intrinsics and distortion.
|
||||
*
|
||||
* <p>2d image points from projectPoints(CameraProperties, Pose3d, List) projectPoints will
|
||||
* naturally be distorted, so this operation is important if the image points need to be directly
|
||||
* used (e.g. 2d yaw/pitch).
|
||||
* <p>2d image points from {@link #projectPoints(Matrix, Matrix, RotTrlTransform3d, List)
|
||||
* projectPoints()} will naturally be distorted, so this operation is important if the image
|
||||
* points need to be directly used (e.g. 2d yaw/pitch).
|
||||
*
|
||||
* @param cameraMatrix the camera intrinsics matrix in standard opencv form
|
||||
* @param distCoeffs the camera distortion matrix in standard opencv form
|
||||
* @param corners The distorted image points
|
||||
* @param cameraMatrix The camera intrinsics matrix in standard opencv form
|
||||
* @param distCoeffs The camera distortion matrix in standard opencv form
|
||||
* @param points The distorted image points
|
||||
* @return The undistorted image points
|
||||
*/
|
||||
public static List<TargetCorner> undistortPoints(
|
||||
Matrix<N3, N3> cameraMatrix, Matrix<N5, N1> distCoeffs, List<TargetCorner> corners) {
|
||||
var points_in = targetCornersToMat(corners);
|
||||
var points_out = new MatOfPoint2f();
|
||||
public static Point[] undistortPoints(
|
||||
Matrix<N3, N3> cameraMatrix, Matrix<N5, N1> distCoeffs, Point[] points) {
|
||||
var distMat = new MatOfPoint2f(points);
|
||||
var undistMat = new MatOfPoint2f();
|
||||
var cameraMatrixMat = matrixToMat(cameraMatrix.getStorage());
|
||||
var distCoeffsMat = matrixToMat(distCoeffs.getStorage());
|
||||
|
||||
Calib3d.undistortImagePoints(points_in, points_out, cameraMatrixMat, distCoeffsMat);
|
||||
var corners_out = matToTargetCorners(points_out);
|
||||
Calib3d.undistortImagePoints(distMat, undistMat, cameraMatrixMat, distCoeffsMat);
|
||||
var undistPoints = undistMat.toArray();
|
||||
|
||||
points_in.release();
|
||||
points_out.release();
|
||||
distMat.release();
|
||||
undistMat.release();
|
||||
cameraMatrixMat.release();
|
||||
distCoeffsMat.release();
|
||||
|
||||
return Arrays.asList(corners_out);
|
||||
return undistPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -318,58 +339,52 @@ public final class OpenCVHelp {
|
||||
* <p>Note that rectangle size and position are stored with ints and do not have sub-pixel
|
||||
* accuracy.
|
||||
*
|
||||
* @param corners The corners/points to be bounded
|
||||
* @return Rectangle bounding the given corners
|
||||
* @param points The points to be bounded
|
||||
* @return Rectangle bounding the given points
|
||||
*/
|
||||
public static Rect getBoundingRect(List<TargetCorner> corners) {
|
||||
var corn = targetCornersToMat(corners);
|
||||
var rect = Imgproc.boundingRect(corn);
|
||||
corn.release();
|
||||
public static Rect getBoundingRect(Point[] points) {
|
||||
var pointMat = new MatOfPoint2f(points);
|
||||
var rect = Imgproc.boundingRect(pointMat);
|
||||
pointMat.release();
|
||||
return rect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the rotated rectangle with minimum area which bounds this contour.
|
||||
*
|
||||
* <p>Note that rectangle size and position are stored with doubles and have sub-pixel accuracy.
|
||||
* <p>Note that rectangle size and position are stored with floats and have sub-pixel accuracy.
|
||||
*
|
||||
* @param corners The corners/points to be bounded
|
||||
* @return Rotated rectangle bounding the given corners
|
||||
* @param points The points to be bounded
|
||||
* @return Rotated rectangle bounding the given points
|
||||
*/
|
||||
public static RotatedRect getMinAreaRect(List<TargetCorner> corners) {
|
||||
var corn = targetCornersToMat(corners);
|
||||
var rect = Imgproc.minAreaRect(corn);
|
||||
corn.release();
|
||||
public static RotatedRect getMinAreaRect(Point[] points) {
|
||||
var pointMat = new MatOfPoint2f(points);
|
||||
var rect = Imgproc.minAreaRect(pointMat);
|
||||
pointMat.release();
|
||||
return rect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the area in pixels of this target's contour. It's important to note that this may be
|
||||
* different from the area of the bounding rectangle around the contour.
|
||||
* Gets the convex hull contour (the outline) of a list of points.
|
||||
*
|
||||
* @param corners The corners defining this contour
|
||||
* @return Area in pixels (units of corner x/y)
|
||||
* @param points The input contour
|
||||
* @return The subset of points defining the convex hull. Note that these use ints and not floats.
|
||||
*/
|
||||
public static double getContourAreaPx(List<TargetCorner> corners) {
|
||||
var temp = targetCornersToMat(corners);
|
||||
var corn = new MatOfPoint(temp.toArray());
|
||||
temp.release();
|
||||
|
||||
public static Point[] getConvexHull(Point[] points) {
|
||||
var pointMat = new MatOfPoint(points);
|
||||
// outputHull gives us indices (of corn) that make a convex hull contour
|
||||
var outputHull = new MatOfInt();
|
||||
Imgproc.convexHull(corn, outputHull);
|
||||
|
||||
Imgproc.convexHull(pointMat, outputHull);
|
||||
|
||||
int[] indices = outputHull.toArray();
|
||||
outputHull.release();
|
||||
var tempPoints = corn.toArray();
|
||||
var points = tempPoints.clone();
|
||||
pointMat.release();
|
||||
var convexPoints = new Point[indices.length];
|
||||
for (int i = 0; i < indices.length; i++) {
|
||||
points[i] = tempPoints[indices[i]];
|
||||
convexPoints[i] = points[indices[i]];
|
||||
}
|
||||
corn.fromArray(points);
|
||||
// calculate area of the (convex hull) contour
|
||||
double area = Imgproc.contourArea(corn);
|
||||
corn.release();
|
||||
return area;
|
||||
return convexPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -396,8 +411,8 @@ public final class OpenCVHelp {
|
||||
* <li>Point 3: [0, -squareLength / 2, -squareLength / 2]
|
||||
* </ul>
|
||||
*
|
||||
* @param imageCorners The projection of these 3d object points into the 2d camera image. The
|
||||
* order should match the given object point translations.
|
||||
* @param imagePoints The projection of these 3d object points into the 2d camera image. The order
|
||||
* should match the given object point translations.
|
||||
* @return The resulting transformation that maps the camera pose to the target pose and the
|
||||
* ambiguity if an alternate solution is available.
|
||||
*/
|
||||
@@ -405,10 +420,10 @@ public final class OpenCVHelp {
|
||||
Matrix<N3, N3> cameraMatrix,
|
||||
Matrix<N5, N1> distCoeffs,
|
||||
List<Translation3d> modelTrls,
|
||||
List<TargetCorner> imageCorners) {
|
||||
Point[] imagePoints) {
|
||||
// solvepnp inputs
|
||||
MatOfPoint3f objectPoints = new MatOfPoint3f();
|
||||
MatOfPoint2f imagePoints = new MatOfPoint2f();
|
||||
MatOfPoint3f objectMat = new MatOfPoint3f();
|
||||
MatOfPoint2f imageMat = new MatOfPoint2f();
|
||||
MatOfDouble cameraMatrixMat = new MatOfDouble();
|
||||
MatOfDouble distCoeffsMat = new MatOfDouble();
|
||||
var rvecs = new ArrayList<Mat>();
|
||||
@@ -419,10 +434,10 @@ public final class OpenCVHelp {
|
||||
try {
|
||||
// IPPE_SQUARE expects our corners in a specific order
|
||||
modelTrls = reorderCircular(modelTrls, true, -1);
|
||||
imageCorners = reorderCircular(imageCorners, true, -1);
|
||||
imagePoints = reorderCircular(Arrays.asList(imagePoints), true, -1).toArray(Point[]::new);
|
||||
// translate to opencv classes
|
||||
translationToTvec(modelTrls.toArray(new Translation3d[0])).assignTo(objectPoints);
|
||||
targetCornersToMat(imageCorners).assignTo(imagePoints);
|
||||
translationToTvec(modelTrls.toArray(new Translation3d[0])).assignTo(objectMat);
|
||||
imageMat.fromArray(imagePoints);
|
||||
matrixToMat(cameraMatrix.getStorage()).assignTo(cameraMatrixMat);
|
||||
matrixToMat(distCoeffs.getStorage()).assignTo(distCoeffsMat);
|
||||
|
||||
@@ -433,8 +448,8 @@ public final class OpenCVHelp {
|
||||
for (int tries = 0; tries < 2; tries++) {
|
||||
// calc rvecs/tvecs and associated reprojection error from image points
|
||||
Calib3d.solvePnPGeneric(
|
||||
objectPoints,
|
||||
imagePoints,
|
||||
objectMat,
|
||||
imageMat,
|
||||
cameraMatrixMat,
|
||||
distCoeffsMat,
|
||||
rvecs,
|
||||
@@ -456,10 +471,10 @@ public final class OpenCVHelp {
|
||||
// check if we got a NaN result
|
||||
if (!Double.isNaN(errors[0])) break;
|
||||
else { // add noise and retry
|
||||
double[] br = imagePoints.get(0, 0);
|
||||
double[] br = imageMat.get(0, 0);
|
||||
br[0] -= 0.001;
|
||||
br[1] -= 0.001;
|
||||
imagePoints.put(0, 0, br);
|
||||
imageMat.put(0, 0, br);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,8 +492,8 @@ public final class OpenCVHelp {
|
||||
return new PNPResults();
|
||||
} finally {
|
||||
// release our Mats from native memory
|
||||
objectPoints.release();
|
||||
imagePoints.release();
|
||||
objectMat.release();
|
||||
imageMat.release();
|
||||
cameraMatrixMat.release();
|
||||
distCoeffsMat.release();
|
||||
for (var v : rvecs) v.release();
|
||||
@@ -503,8 +518,8 @@ public final class OpenCVHelp {
|
||||
* @param cameraMatrix The camera intrinsics matrix in standard opencv form
|
||||
* @param distCoeffs The camera distortion matrix in standard opencv form
|
||||
* @param objectTrls The translations of the object corners, relative to the field.
|
||||
* @param imageCorners The projection of these 3d object points into the 2d camera image. The
|
||||
* order should match the given object point translations.
|
||||
* @param imagePoints The projection of these 3d object points into the 2d camera image. The order
|
||||
* should match the given object point translations.
|
||||
* @return The resulting transformation that maps the camera pose to the target pose. If the 3d
|
||||
* model points are supplied relative to the origin, this transformation brings the camera to
|
||||
* the origin.
|
||||
@@ -513,13 +528,13 @@ public final class OpenCVHelp {
|
||||
Matrix<N3, N3> cameraMatrix,
|
||||
Matrix<N5, N1> distCoeffs,
|
||||
List<Translation3d> objectTrls,
|
||||
List<TargetCorner> imageCorners) {
|
||||
Point[] imagePoints) {
|
||||
try {
|
||||
// translate to opencv classes
|
||||
MatOfPoint3f objectPoints = translationToTvec(objectTrls.toArray(new Translation3d[0]));
|
||||
MatOfPoint2f imagePoints = targetCornersToMat(imageCorners);
|
||||
MatOfDouble cameraMatrixMat = matrixToMat(cameraMatrix.getStorage());
|
||||
MatOfDouble distCoeffsMat = matrixToMat(distCoeffs.getStorage());
|
||||
MatOfPoint3f objectMat = translationToTvec(objectTrls.toArray(new Translation3d[0]));
|
||||
MatOfPoint2f imageMat = new MatOfPoint2f(imagePoints);
|
||||
Mat cameraMatrixMat = matrixToMat(cameraMatrix.getStorage());
|
||||
Mat distCoeffsMat = matrixToMat(distCoeffs.getStorage());
|
||||
var rvecs = new ArrayList<Mat>();
|
||||
var tvecs = new ArrayList<Mat>();
|
||||
Mat rvec = Mat.zeros(3, 1, CvType.CV_32F);
|
||||
@@ -527,8 +542,8 @@ public final class OpenCVHelp {
|
||||
Mat reprojectionError = new Mat();
|
||||
// calc rvec/tvec from image points
|
||||
Calib3d.solvePnPGeneric(
|
||||
objectPoints,
|
||||
imagePoints,
|
||||
objectMat,
|
||||
imageMat,
|
||||
cameraMatrixMat,
|
||||
distCoeffsMat,
|
||||
rvecs,
|
||||
@@ -545,8 +560,8 @@ public final class OpenCVHelp {
|
||||
var best = new Transform3d(tvecToTranslation(tvecs.get(0)), rvecToRotation(rvecs.get(0)));
|
||||
|
||||
// release our Mats from native memory
|
||||
objectPoints.release();
|
||||
imagePoints.release();
|
||||
objectMat.release();
|
||||
imageMat.release();
|
||||
cameraMatrixMat.release();
|
||||
distCoeffsMat.release();
|
||||
for (var v : rvecs) v.release();
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
package org.photonvision.estimation;
|
||||
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Quaternion;
|
||||
import edu.wpi.first.math.geometry.Rotation3d;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.math.geometry.Translation3d;
|
||||
@@ -37,16 +38,49 @@ import java.util.stream.Collectors;
|
||||
public class RotTrlTransform3d {
|
||||
private final Translation3d trl;
|
||||
private final Rotation3d rot;
|
||||
// TODO: removal awaiting wpilib Rotation3d performance improvements
|
||||
private double m_w;
|
||||
private double m_x;
|
||||
private double m_y;
|
||||
private double m_z;
|
||||
|
||||
public RotTrlTransform3d() {
|
||||
this(new Rotation3d(), new Translation3d());
|
||||
/**
|
||||
* A rotation-translation transformation.
|
||||
*
|
||||
* <p>Applying this RotTrlTransform3d to poses will preserve their current origin-to-pose
|
||||
* transform as if the origin was transformed by these components instead.
|
||||
*
|
||||
* @param rot The rotation component
|
||||
* @param trl The translation component
|
||||
*/
|
||||
public RotTrlTransform3d(Rotation3d rot, Translation3d trl) {
|
||||
this.rot = rot;
|
||||
var quat = rot.getQuaternion();
|
||||
m_w = quat.getW();
|
||||
m_x = quat.getX();
|
||||
m_y = quat.getY();
|
||||
m_z = quat.getZ();
|
||||
this.trl = trl;
|
||||
}
|
||||
|
||||
public RotTrlTransform3d(Pose3d initial, Pose3d last) {
|
||||
// this.rot = last.getRotation().minus(initial.getRotation());
|
||||
// this.trl = last.getTranslation().minus(initial.getTranslation().rotateBy(rot));
|
||||
|
||||
var quat = initial.getRotation().getQuaternion();
|
||||
m_w = quat.getW();
|
||||
m_x = quat.getX();
|
||||
m_y = quat.getY();
|
||||
m_z = quat.getZ();
|
||||
this.rot = invrotate(last.getRotation());
|
||||
this.trl = last.getTranslation().minus(rotate(initial.getTranslation()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a rotation-translation transformation from a Transform3d.
|
||||
*
|
||||
* <p>Applying this transformation to poses will preserve their current origin-to-pose transform
|
||||
* as if the origin was transformed by these components.
|
||||
* <p>Applying this RotTrlTransform3d to poses will preserve their current origin-to-pose
|
||||
* transform as if the origin was transformed by trf instead.
|
||||
*
|
||||
* @param trf The origin transformation
|
||||
*/
|
||||
@@ -54,18 +88,40 @@ public class RotTrlTransform3d {
|
||||
this(trf.getRotation(), trf.getTranslation());
|
||||
}
|
||||
|
||||
/**
|
||||
* A rotation-translation transformation.
|
||||
*
|
||||
* <p>Applying this transformation to poses will preserve their current origin-to-pose transform
|
||||
* as if the origin was transformed by these components.
|
||||
*
|
||||
* @param rot The rotation component
|
||||
* @param trl The translation component
|
||||
*/
|
||||
public RotTrlTransform3d(Rotation3d rot, Translation3d trl) {
|
||||
this.rot = rot;
|
||||
this.trl = trl;
|
||||
public RotTrlTransform3d() {
|
||||
this(new Rotation3d(), new Translation3d());
|
||||
}
|
||||
|
||||
private Translation3d rotate(Translation3d otrl) {
|
||||
final var p = new Quaternion(0.0, otrl.getX(), otrl.getY(), otrl.getZ());
|
||||
final var qprime = times(times(p), new Quaternion(m_w, -m_x, -m_y, -m_z));
|
||||
return new Translation3d(qprime.getX(), qprime.getY(), qprime.getZ());
|
||||
}
|
||||
|
||||
private Translation3d invrotate(Translation3d otrl) {
|
||||
m_x = -m_x;
|
||||
m_y = -m_y;
|
||||
m_z = -m_z;
|
||||
var result = rotate(otrl);
|
||||
m_x = -m_x;
|
||||
m_y = -m_y;
|
||||
m_z = -m_z;
|
||||
return result;
|
||||
}
|
||||
|
||||
private Rotation3d rotate(Rotation3d orot) {
|
||||
return new Rotation3d(times(orot.getQuaternion()));
|
||||
}
|
||||
|
||||
private Rotation3d invrotate(Rotation3d orot) {
|
||||
m_x = -m_x;
|
||||
m_y = -m_y;
|
||||
m_z = -m_z;
|
||||
var result = rotate(orot);
|
||||
m_x = -m_x;
|
||||
m_y = -m_y;
|
||||
m_z = -m_z;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,9 +136,12 @@ public class RotTrlTransform3d {
|
||||
|
||||
/** The inverse of this transformation. Applying the inverse will "undo" this transformation. */
|
||||
public RotTrlTransform3d inverse() {
|
||||
var inverseRot = rot.unaryMinus();
|
||||
var inverseTrl = trl.rotateBy(inverseRot).unaryMinus();
|
||||
return new RotTrlTransform3d(inverseRot, inverseTrl);
|
||||
// var inverseRot = rot.unaryMinus();
|
||||
// var inverseTrl = trl.rotateBy(inverseRot).unaryMinus();
|
||||
// return new RotTrlTransform3d(inverseRot, inverseTrl);
|
||||
|
||||
var inverseTrl = invrotate(trl).unaryMinus();
|
||||
return new RotTrlTransform3d(new Rotation3d(new Quaternion(m_w, -m_x, -m_y, -m_z)), inverseTrl);
|
||||
}
|
||||
|
||||
/** This transformation as a Transform3d (as if of the origin) */
|
||||
@@ -101,19 +160,74 @@ public class RotTrlTransform3d {
|
||||
}
|
||||
|
||||
public Translation3d apply(Translation3d trl) {
|
||||
return apply(new Pose3d(trl, new Rotation3d())).getTranslation();
|
||||
// return trl.rotateBy(rot).plus(this.trl);
|
||||
return rotate(trl).plus(this.trl);
|
||||
}
|
||||
;
|
||||
|
||||
public List<Translation3d> applyTrls(List<Translation3d> trls) {
|
||||
return trls.stream().map(this::apply).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public Rotation3d apply(Rotation3d rot) {
|
||||
return rotate(rot);
|
||||
}
|
||||
|
||||
public List<Rotation3d> applyRots(List<Rotation3d> rots) {
|
||||
return rots.stream().map(this::apply).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public Pose3d apply(Pose3d pose) {
|
||||
return new Pose3d(pose.getTranslation().rotateBy(rot).plus(trl), pose.getRotation().plus(rot));
|
||||
// return new Pose3d(pose.getTranslation().rotateBy(rot).plus(trl),
|
||||
// pose.getRotation().plus(rot));
|
||||
return new Pose3d(apply(pose.getTranslation()), apply(pose.getRotation()));
|
||||
}
|
||||
|
||||
public List<Pose3d> applyPoses(List<Pose3d> poses) {
|
||||
return poses.stream().map(this::apply).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// TODO: removal awaiting wpilib Rotation3d performance improvements
|
||||
private Quaternion times(Quaternion other) {
|
||||
final double o_w = other.getW();
|
||||
final double o_x = other.getX();
|
||||
final double o_y = other.getY();
|
||||
final double o_z = other.getZ();
|
||||
return times(m_w, m_x, m_y, m_z, o_w, o_x, o_y, o_z);
|
||||
}
|
||||
|
||||
private static Quaternion times(Quaternion a, Quaternion b) {
|
||||
final double m_w = a.getW();
|
||||
final double m_x = a.getX();
|
||||
final double m_y = a.getY();
|
||||
final double m_z = a.getZ();
|
||||
final double o_w = b.getW();
|
||||
final double o_x = b.getX();
|
||||
final double o_y = b.getY();
|
||||
final double o_z = b.getZ();
|
||||
return times(m_w, m_x, m_y, m_z, o_w, o_x, o_y, o_z);
|
||||
}
|
||||
|
||||
private static Quaternion times(
|
||||
double m_w,
|
||||
double m_x,
|
||||
double m_y,
|
||||
double m_z,
|
||||
double o_w,
|
||||
double o_x,
|
||||
double o_y,
|
||||
double o_z) {
|
||||
// https://en.wikipedia.org/wiki/Quaternion#Scalar_and_vector_parts
|
||||
|
||||
// v₁ x v₂
|
||||
final double cross_x = m_y * o_z - o_y * m_z;
|
||||
final double cross_y = o_x * m_z - m_x * o_z;
|
||||
final double cross_z = m_x * o_y - o_x * m_y;
|
||||
|
||||
// v = w₁v₂ + w₂v₁ + v₁ x v₂
|
||||
final double new_x = o_x * m_w + (m_x * o_w) + cross_x;
|
||||
final double new_y = o_y * m_w + (m_y * o_w) + cross_y;
|
||||
final double new_z = o_z * m_w + (m_z * o_w) + cross_z;
|
||||
|
||||
return new Quaternion(m_w * o_w - (m_x * o_x + m_y * o_y + m_z * o_z), new_x, new_y, new_z);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
package org.photonvision.estimation;
|
||||
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Rotation2d;
|
||||
import edu.wpi.first.math.geometry.Rotation3d;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.math.geometry.Translation3d;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
import java.util.ArrayList;
|
||||
@@ -36,8 +36,8 @@ import java.util.stream.Collectors;
|
||||
/** Describes the 3d model of a target. */
|
||||
public class TargetModel {
|
||||
/**
|
||||
* Translations of this target's vertices relative to its pose. If this target is spherical, this
|
||||
* list has one translation with x == radius.
|
||||
* Translations of this target's vertices relative to its pose. Rectangular and spherical targets
|
||||
* will have four vertices. See their respective constructors for more info.
|
||||
*/
|
||||
public final List<Translation3d> vertices;
|
||||
|
||||
@@ -47,7 +47,17 @@ public class TargetModel {
|
||||
public static final TargetModel kTag16h5 =
|
||||
new TargetModel(Units.inchesToMeters(6), Units.inchesToMeters(6));
|
||||
|
||||
/** Creates a rectangular, planar target model given the width and height. */
|
||||
/**
|
||||
* Creates a rectangular, planar target model given the width and height. The model has four
|
||||
* vertices:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Point 0: [0, -width/2, -height/2]
|
||||
* <li>Point 1: [0, width/2, -height/2]
|
||||
* <li>Point 2: [0, width/2, height/2]
|
||||
* <li>Point 3: [0, -width/2, height/2]
|
||||
* </ul>
|
||||
*/
|
||||
public TargetModel(double widthMeters, double heightMeters) {
|
||||
this.vertices =
|
||||
List.of(
|
||||
@@ -61,18 +71,64 @@ public class TargetModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a spherical target model which has similar dimensions when viewed from any angle. This
|
||||
* model will only have one vertex which has x == radius.
|
||||
* Creates a cuboid target model given the length, width, height. The model has eight vertices:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Point 0: [length/2, -width/2, -height/2]
|
||||
* <li>Point 1: [length/2, width/2, -height/2]
|
||||
* <li>Point 2: [length/2, width/2, height/2]
|
||||
* <li>Point 3: [length/2, -width/2, height/2]
|
||||
* <li>Point 4: [-length/2, -width/2, height/2]
|
||||
* <li>Point 5: [-length/2, width/2, height/2]
|
||||
* <li>Point 6: [-length/2, width/2, -height/2]
|
||||
* <li>Point 7: [-length/2, -width/2, -height/2]
|
||||
* </ul>
|
||||
*/
|
||||
public TargetModel(double lengthMeters, double widthMeters, double heightMeters) {
|
||||
this(
|
||||
List.of(
|
||||
new Translation3d(lengthMeters / 2.0, -widthMeters / 2.0, -heightMeters / 2.0),
|
||||
new Translation3d(lengthMeters / 2.0, widthMeters / 2.0, -heightMeters / 2.0),
|
||||
new Translation3d(lengthMeters / 2.0, widthMeters / 2.0, heightMeters / 2.0),
|
||||
new Translation3d(lengthMeters / 2.0, -widthMeters / 2.0, heightMeters / 2.0),
|
||||
new Translation3d(-lengthMeters / 2.0, -widthMeters / 2.0, heightMeters / 2.0),
|
||||
new Translation3d(-lengthMeters / 2.0, widthMeters / 2.0, heightMeters / 2.0),
|
||||
new Translation3d(-lengthMeters / 2.0, widthMeters / 2.0, -heightMeters / 2.0),
|
||||
new Translation3d(-lengthMeters / 2.0, -widthMeters / 2.0, -heightMeters / 2.0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a spherical target model which has similar dimensions regardless of its rotation. This
|
||||
* model has four vertices:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Point 0: [0, -radius, 0]
|
||||
* <li>Point 1: [0, 0, -radius]
|
||||
* <li>Point 2: [0, radius, 0]
|
||||
* <li>Point 3: [0, 0, radius]
|
||||
* </ul>
|
||||
*
|
||||
* <i>Q: Why these vertices?</i> A: This target should be oriented to the camera every frame, much
|
||||
* like a sprite/decal, and these vertices represent the ellipse vertices (maxima). These vertices
|
||||
* are used for drawing the image of this sphere, but do not match the corners that will be
|
||||
* published by photonvision.
|
||||
*/
|
||||
public TargetModel(double diameterMeters) {
|
||||
this.vertices = List.of(new Translation3d(diameterMeters / 2.0, 0, 0));
|
||||
double radius = diameterMeters / 2.0;
|
||||
this.vertices =
|
||||
List.of(
|
||||
new Translation3d(0, -radius, 0),
|
||||
new Translation3d(0, 0, -radius),
|
||||
new Translation3d(0, radius, 0),
|
||||
new Translation3d(0, 0, radius));
|
||||
this.isPlanar = false;
|
||||
this.isSpherical = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a target model from arbitrary 3d vertices. Automatically determines if the given
|
||||
* vertices are planar(x == 0). More than 2 vertices must be given.
|
||||
* vertices are planar(x == 0). More than 2 vertices must be given. If this is a planar model, the
|
||||
* vertices should define a non-intersecting contour.
|
||||
*
|
||||
* @param vertices Translations representing the vertices of this target model relative to its
|
||||
* pose.
|
||||
@@ -95,13 +151,31 @@ public class TargetModel {
|
||||
/**
|
||||
* This target's vertices offset from its field pose.
|
||||
*
|
||||
* <p>Note: If this target is spherical, only one vertex radius meters in front of the pose is
|
||||
* returned.
|
||||
* <p>Note: If this target is spherical, use {@link #getOrientedPose(Translation3d,
|
||||
* Translation3d)} with this method.
|
||||
*/
|
||||
public List<Translation3d> getFieldVertices(Pose3d targetPose) {
|
||||
return vertices.stream()
|
||||
.map(t -> targetPose.plus(new Transform3d(t, new Rotation3d())).getTranslation())
|
||||
.collect(Collectors.toList());
|
||||
var basisChange = new RotTrlTransform3d(targetPose.getRotation(), targetPose.getTranslation());
|
||||
return vertices.stream().map(t -> basisChange.apply(t)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Pose3d with the given target translation oriented (with its relative x-axis aligned)
|
||||
* to the camera translation. This is used for spherical targets which should not have their
|
||||
* projection change regardless of their own rotation.
|
||||
*
|
||||
* @param tgtTrl This target's translation
|
||||
* @param cameraTrl Camera's translation
|
||||
* @return This target's pose oriented to the camera
|
||||
*/
|
||||
public static Pose3d getOrientedPose(Translation3d tgtTrl, Translation3d cameraTrl) {
|
||||
var relCam = cameraTrl.minus(tgtTrl);
|
||||
var orientToCam =
|
||||
new Rotation3d(
|
||||
0,
|
||||
new Rotation2d(Math.hypot(relCam.getX(), relCam.getY()), -relCam.getZ()).getRadians(),
|
||||
new Rotation2d(relCam.getX(), relCam.getY()).getRadians());
|
||||
return new Pose3d(tgtTrl, orientToCam);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -35,6 +35,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import org.opencv.core.Point;
|
||||
import org.photonvision.targeting.PhotonTrackedTarget;
|
||||
import org.photonvision.targeting.TargetCorner;
|
||||
|
||||
@@ -66,7 +67,7 @@ public class VisionEstimation {
|
||||
*
|
||||
* @param cameraMatrix The camera intrinsics matrix in standard opencv form
|
||||
* @param distCoeffs The camera distortion matrix in standard opencv form
|
||||
* @param visTags The visible tags reported by PV
|
||||
* @param visTags The visible tags reported by PV. Non-tag targets are automatically excluded.
|
||||
* @param tagLayout The known tag layout on the field
|
||||
* @return The transformation that maps the field origin to the camera pose. Ensure the {@link
|
||||
* PNPResults} are present before utilizing them.
|
||||
@@ -84,17 +85,28 @@ public class VisionEstimation {
|
||||
}
|
||||
|
||||
var corners = new ArrayList<TargetCorner>();
|
||||
for (var tag : visTags) corners.addAll(tag.getDetectedCorners());
|
||||
var knownTags = getVisibleLayoutTags(visTags, tagLayout);
|
||||
var knownTags = new ArrayList<AprilTag>();
|
||||
// ensure these are AprilTags in our layout
|
||||
for (var tgt : visTags) {
|
||||
int id = tgt.getFiducialId();
|
||||
tagLayout
|
||||
.getTagPose(id)
|
||||
.ifPresent(
|
||||
pose -> {
|
||||
knownTags.add(new AprilTag(id, pose));
|
||||
corners.addAll(tgt.getDetectedCorners());
|
||||
});
|
||||
}
|
||||
if (knownTags.size() == 0 || corners.size() == 0 || corners.size() % 4 != 0) {
|
||||
return new PNPResults();
|
||||
}
|
||||
Point[] points = OpenCVHelp.cornersToPoints(corners);
|
||||
|
||||
// single-tag pnp
|
||||
if (visTags.size() == 1) {
|
||||
var camToTag =
|
||||
OpenCVHelp.solvePNP_SQUARE(
|
||||
cameraMatrix, distCoeffs, TargetModel.kTag16h5.vertices, corners);
|
||||
cameraMatrix, distCoeffs, TargetModel.kTag16h5.vertices, points);
|
||||
if (!camToTag.isPresent) return new PNPResults();
|
||||
var bestPose = knownTags.get(0).pose.transformBy(camToTag.best.inverse());
|
||||
var altPose = new Pose3d();
|
||||
@@ -113,7 +125,7 @@ public class VisionEstimation {
|
||||
else {
|
||||
var objectTrls = new ArrayList<Translation3d>();
|
||||
for (var tag : knownTags) objectTrls.addAll(TargetModel.kTag16h5.getFieldVertices(tag.pose));
|
||||
var camToOrigin = OpenCVHelp.solvePNP_SQPNP(cameraMatrix, distCoeffs, objectTrls, corners);
|
||||
var camToOrigin = OpenCVHelp.solvePNP_SQPNP(cameraMatrix, distCoeffs, objectTrls, points);
|
||||
if (!camToOrigin.isPresent) return new PNPResults();
|
||||
return new PNPResults(
|
||||
camToOrigin.best.inverse(),
|
||||
|
||||
Reference in New Issue
Block a user