[photonlib] Simulation Visualization Update (#895)

This commit is contained in:
amquake
2023-09-19 16:10:04 -07:00
committed by GitHub
parent 9e371de1cb
commit 7f283640c4
11 changed files with 1140 additions and 335 deletions

View File

@@ -25,9 +25,13 @@
package org.photonvision.simulation;
import edu.wpi.first.cscore.CvSource;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.math.util.Units;
import edu.wpi.first.util.RuntimeLoader;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -45,6 +49,7 @@ import org.opencv.core.Size;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.photonvision.estimation.OpenCVHelp;
import org.photonvision.estimation.RotTrlTransform3d;
public class VideoSimUtil {
public static final String kLocalTagImagesPath = "./src/main/resources/images/apriltags/";
@@ -57,6 +62,10 @@ public class VideoSimUtil {
// Points corresponding to marker(black square) corners of 8x8 16h5 tag images
public static final Point[] kTag16h5MarkerPts;
// field dimensions for wireframe
private static double fieldLength = 16.54175;
private static double fieldWidth = 8.0137;
static {
try {
var loader =
@@ -166,23 +175,23 @@ public class VideoSimUtil {
* @param tagId The id of the specific tag to warp onto the destination image
* @param dstPoints Points(4) in destination image where the tag marker(black square) corners
* should be warped onto.
* @param destination The destination image to place the warped tag image onto.
* @param antialiasing If antialiasing should be performed by automatically
* supersampling/interpolating the warped image. This should be used if better stream quality
* is desired or target detection is being done on the stream, but can hurt performance.
* @see OpenCVHelp#targetCornersToMat(org.photonvision.targeting.TargetCorner...)
* @param destination The destination image to place the warped tag image onto.
*/
public static void warp16h5TagImage(
int tagId, MatOfPoint2f dstPoints, Mat destination, boolean antialiasing) {
int tagId, Point[] dstPoints, boolean antialiasing, Mat destination) {
Mat tagImage = kTag16h5Images.get(tagId);
if (tagImage == null || tagImage.empty()) return;
var tagPoints = new MatOfPoint2f(kTag16h5MarkerPts);
// points of tag image corners
var tagImageCorners = new MatOfPoint2f(getImageCorners(tagImage.size()));
var dstPointMat = new MatOfPoint2f(dstPoints);
// the rectangle describing the rectangle-of-interest(ROI)
var boundingRect = Imgproc.boundingRect(dstPoints);
var boundingRect = Imgproc.boundingRect(dstPointMat);
// find the perspective transform from the tag image to the warped destination points
Mat perspecTrf = Imgproc.getPerspectiveTransform(tagPoints, dstPoints);
Mat perspecTrf = Imgproc.getPerspectiveTransform(tagPoints, dstPointMat);
// check extreme image corners after transform to check if we need to expand bounding rect
var extremeCorners = new MatOfPoint2f();
Core.perspectiveTransform(tagImageCorners, extremeCorners, perspecTrf);
@@ -238,12 +247,12 @@ public class VideoSimUtil {
// upscale if supersampling
Mat scaledDstPts = new Mat();
if (supersampling > 1) {
Core.multiply(dstPoints, new Scalar(supersampling, supersampling), scaledDstPts);
Core.multiply(dstPointMat, new Scalar(supersampling, supersampling), scaledDstPts);
boundingRect.x *= supersampling;
boundingRect.y *= supersampling;
boundingRect.width *= supersampling;
boundingRect.height *= supersampling;
} else dstPoints.assignTo(scaledDstPts);
} else dstPointMat.assignTo(scaledDstPts);
// update transform relative to expanded, scaled bounding rect
Core.subtract(scaledDstPts, new Scalar(boundingRect.tl().x, boundingRect.tl().y), scaledDstPts);
@@ -291,6 +300,58 @@ public class VideoSimUtil {
tempROI.copyTo(destination.submat(boundingRect), tempMask);
}
/**
* Given a line thickness in a 640x480 image, try to scale to the given destination image
* resolution.
*
* @param thickness480p A hypothetical line thickness in a 640x480 image
* @param destinationImg The destination image to scale to
* @return Scaled thickness which cannot be less than 1
*/
public static double getScaledThickness(double thickness480p, Mat destinationImg) {
double scaleX = destinationImg.width() / 640.0;
double scaleY = destinationImg.height() / 480.0;
double minScale = Math.min(scaleX, scaleY);
return Math.max(thickness480p * minScale, 1.0);
}
/**
* Draw a filled ellipse in the destination image.
*
* @param dstPoints The points in the destination image representing the rectangle in which the
* ellipse is inscribed.
* @param color The color of the ellipse. This is a scalar with BGR values (0-255)
* @param destination The destination image to draw onto. The image should be in the BGR color
* space.
*/
public static void drawInscribedEllipse(Point[] dstPoints, Scalar color, Mat destination) {
// create RotatedRect from points
var rect = OpenCVHelp.getMinAreaRect(dstPoints);
// inscribe ellipse inside rectangle
Imgproc.ellipse(destination, rect, color, -1, Imgproc.LINE_AA);
}
/**
* Draw a polygon outline or filled polygon to the destination image with the given points.
*
* @param dstPoints The points in the destination image representing the polygon.
* @param thickness The thickness of the outline in pixels. If this is not positive, a filled
* polygon is drawn instead.
* @param color The color drawn. This should match the color space of the destination image.
* @param isClosed If the last point should connect to the first point in the polygon outline.
* @param destination The destination image to draw onto.
*/
public static void drawPoly(
Point[] dstPoints, int thickness, Scalar color, boolean isClosed, Mat destination) {
var dstPointsd = new MatOfPoint(dstPoints);
if (thickness > 0) {
Imgproc.polylines(
destination, List.of(dstPointsd), isClosed, color, thickness, Imgproc.LINE_AA);
} else {
Imgproc.fillPoly(destination, List.of(dstPointsd), color, Imgproc.LINE_AA);
}
}
/**
* Draws a contour around the given points and text of the id onto the destination image.
*
@@ -300,31 +361,242 @@ public class VideoSimUtil {
* @param destination The destination image to draw onto. The image should be in the BGR color
* space.
*/
public static void drawTagDetection(int id, MatOfPoint2f dstPoints, Mat destination) {
var dstPointsd = new MatOfPoint(dstPoints.toArray());
double scaleX = destination.width() / 640.0;
double scaleY = destination.height() / 480.0;
double minScale = Math.min(scaleX, scaleY);
int thickness = (int) (1 * minScale);
// for(var pt : dstPoints.toArray()) {
// Imgproc.circle(destination, pt, 4, new Scalar(255), 1, Imgproc.LINE_AA);
// }
// Imgproc.rectangle(destination, extremeRect, new Scalar(255), 1, Imgproc.LINE_AA);
// Imgproc.rectangle(destination, Imgproc.boundingRect(dstPoints), new Scalar(255), 1,
// Imgproc.LINE_AA);
Imgproc.polylines(
destination, List.of(dstPointsd), true, new Scalar(0, 0, 255), thickness, Imgproc.LINE_AA);
var textPt = Imgproc.boundingRect(dstPoints).tl();
textPt.x -= 10.0 * scaleX;
textPt.y -= 12.0 * scaleY;
public static void drawTagDetection(int id, Point[] dstPoints, Mat destination) {
double thickness = getScaledThickness(1, destination);
drawPoly(dstPoints, (int) thickness, new Scalar(0, 0, 255), true, destination);
var rect = Imgproc.boundingRect(new MatOfPoint(dstPoints));
var textPt = new Point(rect.x + rect.width, rect.y);
textPt.x += thickness;
textPt.y += thickness;
Imgproc.putText(
destination,
String.valueOf(id),
textPt,
Imgproc.FONT_HERSHEY_PLAIN,
1.5 * minScale,
new Scalar(0, 0, 255),
thickness,
1.5 * thickness,
new Scalar(0, 200, 0),
(int) thickness,
Imgproc.LINE_AA);
}
/**
* Set the field dimensions that are used for drawing the field wireframe.
*
* @param fieldLengthMeters
* @param fieldWidthMeters
*/
public static void setFieldDimensionsMeters(double fieldLengthMeters, double fieldWidthMeters) {
fieldLength = fieldLengthMeters;
fieldWidth = fieldWidthMeters;
}
/**
* The translations used to draw the field side walls and driver station walls. It is a List of
* Lists because the translations are not all connected.
*/
private static List<List<Translation3d>> getFieldWallLines() {
var list = new ArrayList<List<Translation3d>>();
final double sideHt = Units.inchesToMeters(19.5);
final double driveHt = Units.inchesToMeters(35);
final double topHt = Units.inchesToMeters(78);
// field floor
list.add(
List.of(
new Translation3d(0, 0, 0),
new Translation3d(fieldLength, 0, 0),
new Translation3d(fieldLength, fieldWidth, 0),
new Translation3d(0, fieldWidth, 0),
new Translation3d(0, 0, 0)));
// right side wall
list.add(
List.of(
new Translation3d(0, 0, 0),
new Translation3d(0, 0, sideHt),
new Translation3d(fieldLength, 0, sideHt),
new Translation3d(fieldLength, 0, 0)));
// red driverstation
list.add(
List.of(
new Translation3d(fieldLength, 0, sideHt),
new Translation3d(fieldLength, 0, topHt),
new Translation3d(fieldLength, fieldWidth, topHt),
new Translation3d(fieldLength, fieldWidth, sideHt)));
list.add(
List.of(
new Translation3d(fieldLength, 0, driveHt),
new Translation3d(fieldLength, fieldWidth, driveHt)));
// left side wall
list.add(
List.of(
new Translation3d(0, fieldWidth, 0),
new Translation3d(0, fieldWidth, sideHt),
new Translation3d(fieldLength, fieldWidth, sideHt),
new Translation3d(fieldLength, fieldWidth, 0)));
// blue driverstation
list.add(
List.of(
new Translation3d(0, 0, sideHt),
new Translation3d(0, 0, topHt),
new Translation3d(0, fieldWidth, topHt),
new Translation3d(0, fieldWidth, sideHt)));
list.add(List.of(new Translation3d(0, 0, driveHt), new Translation3d(0, fieldWidth, driveHt)));
return list;
}
/**
* The translations used to draw the field floor subdivisions (not the floor outline). It is a
* List of Lists because the translations are not all connected.
*
* @param subdivisions How many "subdivisions" along the width/length of the floor. E.g. 3
* subdivisions would mean 2 lines along the length and 2 lines along the width creating a 3x3
* "grid".
*/
private static List<List<Translation3d>> getFieldFloorLines(int subdivisions) {
var list = new ArrayList<List<Translation3d>>();
final double subLength = fieldLength / subdivisions;
final double subWidth = fieldWidth / subdivisions;
// field floor subdivisions
for (int i = 0; i < subdivisions; i++) {
list.add(
List.of(
new Translation3d(0, subWidth * (i + 1), 0),
new Translation3d(fieldLength, subWidth * (i + 1), 0)));
list.add(
List.of(
new Translation3d(subLength * (i + 1), 0, 0),
new Translation3d(subLength * (i + 1), fieldWidth, 0)));
}
return list;
}
/**
* Convert 3D lines represented by the given series of translations into a polygon(s) in the
* camera's image.
*
* @param camRt The change in basis from world coordinates to camera coordinates. See {@link
* RotTrlTransform3d#makeRelativeTo(Pose3d)}.
* @param prop The simulated camera's properties.
* @param trls A sequential series of translations defining the polygon to be drawn.
* @param resolution Resolution as a fraction(0 - 1) of the video frame's diagonal length in
* pixels. Line segments will be subdivided if they exceed this resolution.
* @param isClosed If the final translation should also draw a line to the first translation.
* @param destination The destination image that is being drawn to.
* @return A list of polygons(which are an array of points)
*/
public static List<Point[]> polyFrom3dLines(
RotTrlTransform3d camRt,
SimCameraProperties prop,
List<Translation3d> trls,
double resolution,
boolean isClosed,
Mat destination) {
resolution = Math.hypot(destination.size().height, destination.size().width) * resolution;
List<Translation3d> pts = new ArrayList<>(trls);
if (isClosed) pts.add(pts.get(0));
List<Point[]> polyPointList = new ArrayList<>();
for (int i = 0; i < pts.size() - 1; i++) {
var pta = pts.get(i);
var ptb = pts.get(i + 1);
// check if line is inside camera fulcrum
var inter = prop.getVisibleLine(camRt, pta, ptb);
if (inter.getSecond() == null) continue;
// cull line to the inside of the camera fulcrum
double inter1 = inter.getFirst().doubleValue();
double inter2 = inter.getSecond().doubleValue();
var baseDelta = ptb.minus(pta);
var old_pta = pta;
if (inter1 > 0) pta = old_pta.plus(baseDelta.times(inter1));
if (inter2 < 1) ptb = old_pta.plus(baseDelta.times(inter2));
baseDelta = ptb.minus(pta);
// project points into 2d
var poly = new ArrayList<Point>();
poly.addAll(
Arrays.asList(
OpenCVHelp.projectPoints(
prop.getIntrinsics(), prop.getDistCoeffs(), camRt, List.of(pta, ptb))));
var pxa = poly.get(0);
var pxb = poly.get(1);
// subdivide projected line based on desired resolution
double pxDist = Math.hypot(pxb.x - pxa.x, pxb.y - pxa.y);
int subdivisions = (int) (pxDist / resolution);
var subDelta = baseDelta.div(subdivisions + 1);
var subPts = new ArrayList<Translation3d>();
for (int j = 0; j < subdivisions; j++) {
subPts.add(pta.plus(subDelta.times(j + 1)));
}
if (subPts.size() > 0) {
poly.addAll(
1,
Arrays.asList(
OpenCVHelp.projectPoints(
prop.getIntrinsics(), prop.getDistCoeffs(), camRt, subPts)));
}
polyPointList.add(poly.toArray(Point[]::new));
}
return polyPointList;
}
/**
* Draw a wireframe of the field to the given image.
*
* @param camRt The change in basis from world coordinates to camera coordinates. See {@link
* RotTrlTransform3d#makeRelativeTo(Pose3d)}.
* @param prop The simulated camera's properties.
* @param resolution Resolution as a fraction(0 - 1) of the video frame's diagonal length in
* pixels. Line segments will be subdivided if they exceed this resolution.
* @param wallThickness Thickness of the lines used for drawing the field walls in pixels. This is
* scaled by {@link #getScaledThickness(double, Mat)}.
* @param wallColor Color of the lines used for drawing the field walls.
* @param floorSubdivisions A NxN "grid" is created from the floor where this parameter is N,
* which defines the floor lines.
* @param floorThickness Thickness of the lines used for drawing the field floor grid in pixels.
* This is scaled by {@link #getScaledThickness(double, Mat)}.
* @param floorColor Color of the lines used for drawing the field floor grid.
* @param destination The destination image to draw to.
*/
public static void drawFieldWireframe(
RotTrlTransform3d camRt,
SimCameraProperties prop,
double resolution,
double wallThickness,
Scalar wallColor,
int floorSubdivisions,
double floorThickness,
Scalar floorColor,
Mat destination) {
for (var trls : getFieldFloorLines(floorSubdivisions)) {
var polys = VideoSimUtil.polyFrom3dLines(camRt, prop, trls, resolution, false, destination);
for (var poly : polys) {
drawPoly(
poly,
(int) Math.round(getScaledThickness(floorThickness, destination)),
floorColor,
false,
destination);
}
}
for (var trls : getFieldWallLines()) {
var polys = VideoSimUtil.polyFrom3dLines(camRt, prop, trls, resolution, false, destination);
for (var poly : polys) {
drawPoly(
poly,
(int) Math.round(getScaledThickness(wallThickness, destination)),
wallColor,
false,
destination);
}
}
}
}