2023-06-18 15:54:12 -07:00
|
|
|
/*
|
|
|
|
|
* MIT License
|
|
|
|
|
*
|
|
|
|
|
* Copyright (c) PhotonVision
|
|
|
|
|
*
|
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
|
|
|
* in the Software without restriction, including without limitation the rights
|
|
|
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
|
|
|
* furnished to do so, subject to the following conditions:
|
|
|
|
|
*
|
|
|
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
|
|
|
* copies or substantial portions of the Software.
|
|
|
|
|
*
|
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
|
* SOFTWARE.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
package org.photonvision.simulation;
|
|
|
|
|
|
2023-10-25 18:35:06 -07:00
|
|
|
import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
|
|
|
|
import edu.wpi.first.apriltag.AprilTagFields;
|
2023-06-18 15:54:12 -07:00
|
|
|
import edu.wpi.first.cameraserver.CameraServer;
|
|
|
|
|
import edu.wpi.first.cscore.CvSource;
|
|
|
|
|
import edu.wpi.first.cscore.VideoSource.ConnectionStrategy;
|
|
|
|
|
import edu.wpi.first.math.MathUtil;
|
|
|
|
|
import edu.wpi.first.math.Pair;
|
|
|
|
|
import edu.wpi.first.math.geometry.Pose3d;
|
2024-09-22 22:27:13 -04:00
|
|
|
import edu.wpi.first.math.geometry.Transform3d;
|
2024-01-02 10:09:32 -05:00
|
|
|
import edu.wpi.first.util.PixelFormat;
|
2023-06-18 15:54:12 -07:00
|
|
|
import edu.wpi.first.util.WPIUtilJNI;
|
2024-05-10 14:04:34 -04:00
|
|
|
import edu.wpi.first.wpilibj.RobotController;
|
2023-06-18 15:54:12 -07:00
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Optional;
|
2023-10-25 18:35:06 -07:00
|
|
|
import java.util.stream.Collectors;
|
2023-06-18 15:54:12 -07:00
|
|
|
import org.opencv.core.CvType;
|
|
|
|
|
import org.opencv.core.Mat;
|
|
|
|
|
import org.opencv.core.Point;
|
2023-09-19 16:10:04 -07:00
|
|
|
import org.opencv.core.RotatedRect;
|
|
|
|
|
import org.opencv.core.Scalar;
|
2023-06-18 15:54:12 -07:00
|
|
|
import org.opencv.core.Size;
|
|
|
|
|
import org.opencv.imgproc.Imgproc;
|
|
|
|
|
import org.photonvision.PhotonCamera;
|
|
|
|
|
import org.photonvision.PhotonTargetSortMode;
|
|
|
|
|
import org.photonvision.common.networktables.NTTopicSet;
|
2023-10-25 18:19:48 -07:00
|
|
|
import org.photonvision.estimation.CameraTargetRelation;
|
2023-06-18 15:54:12 -07:00
|
|
|
import org.photonvision.estimation.OpenCVHelp;
|
2023-09-19 16:10:04 -07:00
|
|
|
import org.photonvision.estimation.RotTrlTransform3d;
|
|
|
|
|
import org.photonvision.estimation.TargetModel;
|
2023-10-25 18:35:06 -07:00
|
|
|
import org.photonvision.estimation.VisionEstimation;
|
2023-11-15 18:28:26 -05:00
|
|
|
import org.photonvision.targeting.MultiTargetPNPResult;
|
2023-06-18 15:54:12 -07:00
|
|
|
import org.photonvision.targeting.PhotonPipelineResult;
|
|
|
|
|
import org.photonvision.targeting.PhotonTrackedTarget;
|
2024-08-31 13:44:19 -04:00
|
|
|
import org.photonvision.targeting.PnpResult;
|
2023-06-18 15:54:12 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A handle for simulating {@link PhotonCamera} values. Processing simulated targets through this
|
|
|
|
|
* class will change the associated PhotonCamera's results.
|
|
|
|
|
*/
|
|
|
|
|
@SuppressWarnings("unused")
|
|
|
|
|
public class PhotonCameraSim implements AutoCloseable {
|
|
|
|
|
private final PhotonCamera cam;
|
|
|
|
|
|
2024-11-01 20:50:21 -07:00
|
|
|
protected NTTopicSet ts = new NTTopicSet();
|
2024-10-31 08:27:19 -07:00
|
|
|
private long heartbeatCounter = 1;
|
2023-06-18 15:54:12 -07:00
|
|
|
|
|
|
|
|
/** This simulated camera's {@link SimCameraProperties} */
|
|
|
|
|
public final SimCameraProperties prop;
|
|
|
|
|
|
|
|
|
|
private long nextNTEntryTime = WPIUtilJNI.now();
|
|
|
|
|
|
|
|
|
|
private double maxSightRangeMeters = Double.MAX_VALUE;
|
2023-07-23 18:32:36 -07:00
|
|
|
private static final double kDefaultMinAreaPx = 100;
|
2023-06-18 15:54:12 -07:00
|
|
|
private double minTargetAreaPercent;
|
|
|
|
|
private PhotonTargetSortMode sortMode = PhotonTargetSortMode.Largest;
|
|
|
|
|
|
2023-11-15 18:28:26 -05:00
|
|
|
private final AprilTagFieldLayout tagLayout =
|
2024-08-04 14:23:46 -04:00
|
|
|
AprilTagFieldLayout.loadField(AprilTagFields.kDefaultField);
|
2023-10-25 18:35:06 -07:00
|
|
|
|
2023-06-18 15:54:12 -07:00
|
|
|
// video stream simulation
|
|
|
|
|
private final CvSource videoSimRaw;
|
|
|
|
|
private final Mat videoSimFrameRaw = new Mat();
|
|
|
|
|
private boolean videoSimRawEnabled = true;
|
2023-09-19 16:10:04 -07:00
|
|
|
private boolean videoSimWireframeEnabled = false;
|
|
|
|
|
private double videoSimWireframeResolution = 0.1;
|
2023-06-18 15:54:12 -07:00
|
|
|
private final CvSource videoSimProcessed;
|
|
|
|
|
private final Mat videoSimFrameProcessed = new Mat();
|
|
|
|
|
private boolean videoSimProcEnabled = true;
|
|
|
|
|
|
|
|
|
|
static {
|
2023-11-05 18:13:00 -05:00
|
|
|
OpenCVHelp.forceLoadOpenCV();
|
2023-06-18 15:54:12 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void close() {
|
|
|
|
|
videoSimRaw.close();
|
|
|
|
|
videoSimFrameRaw.release();
|
|
|
|
|
videoSimProcessed.close();
|
|
|
|
|
videoSimFrameProcessed.release();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructs a handle for simulating {@link PhotonCamera} values. Processing simulated targets
|
|
|
|
|
* through this class will change the associated PhotonCamera's results.
|
|
|
|
|
*
|
|
|
|
|
* <p><b>This constructor's camera has a 90 deg FOV with no simulated lag!</b>
|
|
|
|
|
*
|
|
|
|
|
* <p>By default, the minimum target area is 100 pixels and there is no maximum sight range.
|
|
|
|
|
*
|
|
|
|
|
* @param camera The camera to be simulated
|
|
|
|
|
*/
|
|
|
|
|
public PhotonCameraSim(PhotonCamera camera) {
|
|
|
|
|
this(camera, SimCameraProperties.PERFECT_90DEG());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructs a handle for simulating {@link PhotonCamera} values. Processing simulated targets
|
|
|
|
|
* through this class will change the associated PhotonCamera's results.
|
|
|
|
|
*
|
|
|
|
|
* <p>By default, the minimum target area is 100 pixels and there is no maximum sight range.
|
|
|
|
|
*
|
|
|
|
|
* @param camera The camera to be simulated
|
|
|
|
|
* @param prop Properties of this camera such as FOV and FPS
|
|
|
|
|
*/
|
|
|
|
|
public PhotonCameraSim(PhotonCamera camera, SimCameraProperties prop) {
|
|
|
|
|
this.cam = camera;
|
|
|
|
|
this.prop = prop;
|
|
|
|
|
setMinTargetAreaPixels(kDefaultMinAreaPx);
|
|
|
|
|
|
|
|
|
|
videoSimRaw =
|
|
|
|
|
CameraServer.putVideo(camera.getName() + "-raw", prop.getResWidth(), prop.getResHeight());
|
|
|
|
|
videoSimRaw.setPixelFormat(PixelFormat.kGray);
|
|
|
|
|
videoSimProcessed =
|
|
|
|
|
CameraServer.putVideo(
|
|
|
|
|
camera.getName() + "-processed", prop.getResWidth(), prop.getResHeight());
|
|
|
|
|
|
|
|
|
|
ts.removeEntries();
|
|
|
|
|
ts.subTable = camera.getCameraTable();
|
|
|
|
|
ts.updateEntries();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructs a handle for simulating {@link PhotonCamera} values. Processing simulated targets
|
|
|
|
|
* through this class will change the associated PhotonCamera's results.
|
|
|
|
|
*
|
|
|
|
|
* @param camera The camera to be simulated
|
|
|
|
|
* @param prop Properties of this camera such as FOV and FPS
|
|
|
|
|
* @param minTargetAreaPercent The minimum percentage(0 - 100) a detected target must take up of
|
|
|
|
|
* the camera's image to be processed. Match this with your contour filtering settings in the
|
|
|
|
|
* PhotonVision GUI.
|
|
|
|
|
* @param maxSightRangeMeters Maximum distance at which the target is illuminated to your camera.
|
|
|
|
|
* Note that minimum target area of the image is separate from this.
|
|
|
|
|
*/
|
|
|
|
|
public PhotonCameraSim(
|
|
|
|
|
PhotonCamera camera,
|
|
|
|
|
SimCameraProperties prop,
|
|
|
|
|
double minTargetAreaPercent,
|
|
|
|
|
double maxSightRangeMeters) {
|
|
|
|
|
this(camera, prop);
|
|
|
|
|
this.minTargetAreaPercent = minTargetAreaPercent;
|
|
|
|
|
this.maxSightRangeMeters = maxSightRangeMeters;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PhotonCamera getCamera() {
|
|
|
|
|
return cam;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public double getMinTargetAreaPercent() {
|
|
|
|
|
return minTargetAreaPercent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public double getMinTargetAreaPixels() {
|
|
|
|
|
return minTargetAreaPercent / 100.0 * prop.getResArea();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public double getMaxSightRangeMeters() {
|
|
|
|
|
return maxSightRangeMeters;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PhotonTargetSortMode getTargetSortMode() {
|
|
|
|
|
return sortMode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public CvSource getVideoSimRaw() {
|
|
|
|
|
return videoSimRaw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Mat getVideoSimFrameRaw() {
|
|
|
|
|
return videoSimFrameRaw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determines if this target's pose should be visible to the camera without considering its
|
|
|
|
|
* projected image points. Does not account for image area.
|
|
|
|
|
*
|
|
|
|
|
* @param camPose Camera's 3d pose
|
|
|
|
|
* @param target Vision target containing pose and shape
|
|
|
|
|
* @return If this vision target can be seen before image projection.
|
|
|
|
|
*/
|
|
|
|
|
public boolean canSeeTargetPose(Pose3d camPose, VisionTargetSim target) {
|
2023-10-25 18:19:48 -07:00
|
|
|
var rel = new CameraTargetRelation(camPose, target.getPose());
|
2023-09-19 16:10:04 -07:00
|
|
|
|
2023-06-18 15:54:12 -07:00
|
|
|
return (
|
|
|
|
|
// target translation is outside of camera's FOV
|
2023-10-25 18:19:48 -07:00
|
|
|
(Math.abs(rel.camToTargYaw.getDegrees()) < prop.getHorizFOV().getDegrees() / 2)
|
|
|
|
|
&& (Math.abs(rel.camToTargPitch.getDegrees()) < prop.getVertFOV().getDegrees() / 2)
|
2023-06-18 15:54:12 -07:00
|
|
|
&& (!target.getModel().isPlanar
|
2023-10-25 18:19:48 -07:00
|
|
|
|| Math.abs(rel.targToCamAngle.getDegrees())
|
2023-06-18 15:54:12 -07:00
|
|
|
< 90) // camera is behind planar target and it should be occluded
|
2023-10-25 18:19:48 -07:00
|
|
|
&& (rel.camToTarg.getTranslation().getNorm() <= maxSightRangeMeters)); // target is too far
|
2023-06-18 15:54:12 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2023-09-19 16:10:04 -07:00
|
|
|
* Determines if all target points are inside the camera's image.
|
2023-06-18 15:54:12 -07:00
|
|
|
*
|
2023-09-19 16:10:04 -07:00
|
|
|
* @param points The target's 2d image points
|
2023-06-18 15:54:12 -07:00
|
|
|
*/
|
2023-09-19 16:10:04 -07:00
|
|
|
public boolean canSeeCorners(Point[] points) {
|
|
|
|
|
for (var point : points) {
|
|
|
|
|
if (MathUtil.clamp(point.x, 0, prop.getResWidth()) != point.x
|
|
|
|
|
|| MathUtil.clamp(point.y, 0, prop.getResHeight()) != point.y) {
|
|
|
|
|
return false; // point is outside of resolution
|
2023-06-18 15:54:12 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determine if this camera should process a new frame based on performance metrics and the time
|
|
|
|
|
* since the last update. This returns an Optional which is either empty if no update should occur
|
|
|
|
|
* or a Long of the timestamp in microseconds of when the frame which should be received by NT. If
|
|
|
|
|
* a timestamp is returned, the last frame update time becomes that timestamp.
|
|
|
|
|
*
|
|
|
|
|
* @return Optional long which is empty while blocked or the NT entry timestamp in microseconds if
|
|
|
|
|
* ready
|
|
|
|
|
*/
|
|
|
|
|
public Optional<Long> consumeNextEntryTime() {
|
|
|
|
|
// check if this camera is ready for another frame update
|
|
|
|
|
long now = WPIUtilJNI.now();
|
|
|
|
|
long timestamp = -1;
|
|
|
|
|
int iter = 0;
|
|
|
|
|
// prepare next latest update
|
|
|
|
|
while (now >= nextNTEntryTime) {
|
|
|
|
|
timestamp = nextNTEntryTime;
|
|
|
|
|
long frameTime = (long) (prop.estMsUntilNextFrame() * 1e3);
|
|
|
|
|
nextNTEntryTime += frameTime;
|
|
|
|
|
|
|
|
|
|
// if frame time is very small, avoid blocking
|
|
|
|
|
if (iter++ > 50) {
|
|
|
|
|
timestamp = now;
|
|
|
|
|
nextNTEntryTime = now + frameTime;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// return the timestamp of the latest update
|
|
|
|
|
if (timestamp >= 0) return Optional.of(timestamp);
|
|
|
|
|
// or this camera isn't ready to process yet
|
|
|
|
|
else return Optional.empty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The minimum percentage(0 - 100) a detected target must take up of the camera's image to be
|
|
|
|
|
* processed.
|
|
|
|
|
*/
|
|
|
|
|
public void setMinTargetAreaPercent(double areaPercent) {
|
|
|
|
|
this.minTargetAreaPercent = areaPercent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The minimum number of pixels a detected target must take up in the camera's image to be
|
|
|
|
|
* processed.
|
|
|
|
|
*/
|
|
|
|
|
public void setMinTargetAreaPixels(double areaPx) {
|
|
|
|
|
this.minTargetAreaPercent = areaPx / prop.getResArea() * 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Maximum distance at which the target is illuminated to your camera. Note that minimum target
|
|
|
|
|
* area of the image is separate from this.
|
|
|
|
|
*/
|
|
|
|
|
public void setMaxSightRange(double rangeMeters) {
|
|
|
|
|
this.maxSightRangeMeters = rangeMeters;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Defines the order the targets are sorted in the pipeline result. */
|
|
|
|
|
public void setTargetSortMode(PhotonTargetSortMode sortMode) {
|
|
|
|
|
if (sortMode != null) this.sortMode = sortMode;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-19 16:10:04 -07:00
|
|
|
/**
|
|
|
|
|
* Sets whether the raw video stream simulation is enabled.
|
|
|
|
|
*
|
|
|
|
|
* <p>Note: This may increase loop times.
|
|
|
|
|
*/
|
2023-06-18 15:54:12 -07:00
|
|
|
public void enableRawStream(boolean enabled) {
|
|
|
|
|
videoSimRawEnabled = enabled;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-19 16:10:04 -07:00
|
|
|
/**
|
|
|
|
|
* Sets whether a wireframe of the field is drawn to the raw video stream.
|
|
|
|
|
*
|
|
|
|
|
* <p>Note: This will dramatically increase loop times.
|
|
|
|
|
*/
|
|
|
|
|
public void enableDrawWireframe(boolean enabled) {
|
|
|
|
|
videoSimWireframeEnabled = enabled;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sets the resolution of the drawn wireframe if enabled. Drawn line segments will be subdivided
|
|
|
|
|
* into smaller segments based on a threshold set by the resolution.
|
|
|
|
|
*
|
|
|
|
|
* @param resolution Resolution as a fraction(0 - 1) of the video frame's diagonal length in
|
|
|
|
|
* pixels
|
|
|
|
|
*/
|
|
|
|
|
public void setWireframeResolution(double resolution) {
|
|
|
|
|
videoSimWireframeResolution = resolution;
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-18 15:54:12 -07:00
|
|
|
/** Sets whether the processed video stream simulation is enabled. */
|
|
|
|
|
public void enableProcessedStream(boolean enabled) {
|
|
|
|
|
videoSimProcEnabled = enabled;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PhotonPipelineResult process(
|
|
|
|
|
double latencyMillis, Pose3d cameraPose, List<VisionTargetSim> targets) {
|
|
|
|
|
// sort targets by distance to camera
|
|
|
|
|
targets = new ArrayList<>(targets);
|
|
|
|
|
targets.sort(
|
|
|
|
|
(t1, t2) -> {
|
|
|
|
|
double dist1 = t1.getPose().getTranslation().getDistance(cameraPose.getTranslation());
|
|
|
|
|
double dist2 = t2.getPose().getTranslation().getDistance(cameraPose.getTranslation());
|
|
|
|
|
if (dist1 == dist2) return 0;
|
|
|
|
|
return dist1 < dist2 ? 1 : -1;
|
|
|
|
|
});
|
2023-09-19 16:10:04 -07:00
|
|
|
// all targets visible before noise
|
|
|
|
|
var visibleTgts = new ArrayList<Pair<VisionTargetSim, Point[]>>();
|
|
|
|
|
// all targets actually detected by camera (after noise)
|
2023-06-18 15:54:12 -07:00
|
|
|
var detectableTgts = new ArrayList<PhotonTrackedTarget>();
|
2023-09-19 16:10:04 -07:00
|
|
|
// basis change from world coordinates to camera coordinates
|
|
|
|
|
var camRt = RotTrlTransform3d.makeRelativeTo(cameraPose);
|
2023-06-18 15:54:12 -07:00
|
|
|
|
|
|
|
|
// reset our frame
|
|
|
|
|
VideoSimUtil.updateVideoProp(videoSimRaw, prop);
|
|
|
|
|
VideoSimUtil.updateVideoProp(videoSimProcessed, prop);
|
|
|
|
|
Size videoFrameSize = new Size(prop.getResWidth(), prop.getResHeight());
|
|
|
|
|
Mat.zeros(videoFrameSize, CvType.CV_8UC1).assignTo(videoSimFrameRaw);
|
|
|
|
|
|
|
|
|
|
for (var tgt : targets) {
|
|
|
|
|
// pose isn't visible, skip to next
|
|
|
|
|
if (!canSeeTargetPose(cameraPose, tgt)) continue;
|
|
|
|
|
|
|
|
|
|
// find target's 3d corner points
|
|
|
|
|
var fieldCorners = tgt.getFieldVertices();
|
2023-09-19 16:10:04 -07:00
|
|
|
if (tgt.getModel().isSpherical) { // target is spherical
|
|
|
|
|
var model = tgt.getModel();
|
|
|
|
|
// orient the model to the camera (like a sprite/decal) so it appears similar regardless of
|
|
|
|
|
// view
|
|
|
|
|
fieldCorners =
|
|
|
|
|
model.getFieldVertices(
|
|
|
|
|
TargetModel.getOrientedPose(
|
|
|
|
|
tgt.getPose().getTranslation(), cameraPose.getTranslation()));
|
|
|
|
|
}
|
2023-06-18 15:54:12 -07:00
|
|
|
// project 3d target points into 2d image points
|
2023-09-19 16:10:04 -07:00
|
|
|
var imagePoints =
|
|
|
|
|
OpenCVHelp.projectPoints(prop.getIntrinsics(), prop.getDistCoeffs(), camRt, fieldCorners);
|
|
|
|
|
// spherical targets need a rotated rectangle of their midpoints for visualization
|
|
|
|
|
if (tgt.getModel().isSpherical) {
|
|
|
|
|
var center = OpenCVHelp.avgPoint(imagePoints);
|
|
|
|
|
int l = 0, t, b, r = 0;
|
|
|
|
|
// reference point (left side midpoint)
|
|
|
|
|
for (int i = 1; i < 4; i++) {
|
|
|
|
|
if (imagePoints[i].x < imagePoints[l].x) l = i;
|
|
|
|
|
}
|
|
|
|
|
var lc = imagePoints[l];
|
|
|
|
|
// determine top, right, bottom midpoints
|
|
|
|
|
double[] angles = new double[4];
|
|
|
|
|
t = (l + 1) % 4;
|
|
|
|
|
b = (l + 1) % 4;
|
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
|
|
|
if (i == l) continue;
|
|
|
|
|
var ic = imagePoints[i];
|
|
|
|
|
angles[i] = Math.atan2(lc.y - ic.y, ic.x - lc.x);
|
|
|
|
|
if (angles[i] >= angles[t]) t = i;
|
|
|
|
|
if (angles[i] <= angles[b]) b = i;
|
|
|
|
|
}
|
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
|
|
|
if (i != t && i != l && i != b) r = i;
|
|
|
|
|
}
|
|
|
|
|
// create RotatedRect from midpoints
|
|
|
|
|
var rect =
|
|
|
|
|
new RotatedRect(
|
|
|
|
|
new Point(center.x, center.y),
|
|
|
|
|
new Size(imagePoints[r].x - lc.x, imagePoints[b].y - imagePoints[t].y),
|
|
|
|
|
Math.toDegrees(-angles[r]));
|
|
|
|
|
// set target corners to rect corners
|
|
|
|
|
Point[] points = new Point[4];
|
|
|
|
|
rect.points(points);
|
|
|
|
|
imagePoints = points;
|
2023-06-18 15:54:12 -07:00
|
|
|
}
|
2023-09-19 16:10:04 -07:00
|
|
|
// save visible targets for raw video stream simulation
|
|
|
|
|
visibleTgts.add(new Pair<>(tgt, imagePoints));
|
2023-06-18 15:54:12 -07:00
|
|
|
// estimate pixel noise
|
2023-09-19 16:10:04 -07:00
|
|
|
var noisyTargetCorners = prop.estPixelNoise(imagePoints);
|
|
|
|
|
// find the minimum area rectangle of target corners
|
|
|
|
|
var minAreaRect = OpenCVHelp.getMinAreaRect(noisyTargetCorners);
|
|
|
|
|
Point[] minAreaRectPts = new Point[4];
|
|
|
|
|
minAreaRect.points(minAreaRectPts);
|
2023-06-18 15:54:12 -07:00
|
|
|
// find the (naive) 2d yaw/pitch
|
2023-09-19 16:10:04 -07:00
|
|
|
var centerPt = minAreaRect.center;
|
|
|
|
|
var centerRot = prop.getPixelRot(centerPt);
|
2023-06-18 15:54:12 -07:00
|
|
|
// find contour area
|
|
|
|
|
double areaPercent = prop.getContourAreaPercent(noisyTargetCorners);
|
|
|
|
|
|
|
|
|
|
// projected target can't be detected, skip to next
|
|
|
|
|
if (!(canSeeCorners(noisyTargetCorners) && areaPercent >= minTargetAreaPercent)) continue;
|
|
|
|
|
|
2024-08-31 13:44:19 -04:00
|
|
|
var pnpSim = new PnpResult();
|
2023-06-18 15:54:12 -07:00
|
|
|
if (tgt.fiducialID >= 0 && tgt.getFieldVertices().size() == 4) { // single AprilTag solvePNP
|
|
|
|
|
pnpSim =
|
2024-08-31 13:44:19 -04:00
|
|
|
OpenCVHelp.solvePNP_SQPNP(
|
|
|
|
|
prop.getIntrinsics(),
|
|
|
|
|
prop.getDistCoeffs(),
|
|
|
|
|
tgt.getModel().vertices,
|
|
|
|
|
noisyTargetCorners)
|
|
|
|
|
.get();
|
2023-06-18 15:54:12 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
detectableTgts.add(
|
|
|
|
|
new PhotonTrackedTarget(
|
2024-02-18 17:59:54 -08:00
|
|
|
-Math.toDegrees(centerRot.getZ()),
|
2023-06-18 15:54:12 -07:00
|
|
|
-Math.toDegrees(centerRot.getY()),
|
|
|
|
|
areaPercent,
|
|
|
|
|
Math.toDegrees(centerRot.getX()),
|
|
|
|
|
tgt.fiducialID,
|
2024-05-10 14:52:16 -04:00
|
|
|
-1,
|
|
|
|
|
-1,
|
2023-06-18 15:54:12 -07:00
|
|
|
pnpSim.best,
|
|
|
|
|
pnpSim.alt,
|
|
|
|
|
pnpSim.ambiguity,
|
2023-09-19 16:10:04 -07:00
|
|
|
OpenCVHelp.pointsToCorners(minAreaRectPts),
|
|
|
|
|
OpenCVHelp.pointsToCorners(noisyTargetCorners)));
|
2023-06-18 15:54:12 -07:00
|
|
|
}
|
|
|
|
|
// render visible tags to raw video frame
|
|
|
|
|
if (videoSimRawEnabled) {
|
2023-09-19 16:10:04 -07:00
|
|
|
// draw field wireframe
|
|
|
|
|
if (videoSimWireframeEnabled) {
|
|
|
|
|
VideoSimUtil.drawFieldWireframe(
|
|
|
|
|
camRt,
|
|
|
|
|
prop,
|
|
|
|
|
videoSimWireframeResolution,
|
|
|
|
|
1.5,
|
|
|
|
|
new Scalar(80),
|
|
|
|
|
6,
|
|
|
|
|
1,
|
|
|
|
|
new Scalar(30),
|
|
|
|
|
videoSimFrameRaw);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// draw targets
|
|
|
|
|
for (var pair : visibleTgts) {
|
|
|
|
|
var tgt = pair.getFirst();
|
|
|
|
|
var corn = pair.getSecond();
|
|
|
|
|
|
|
|
|
|
if (tgt.fiducialID >= 0) { // apriltags
|
2024-01-02 10:09:32 -05:00
|
|
|
VideoSimUtil.warp36h11TagImage(tgt.fiducialID, corn, true, videoSimFrameRaw);
|
2023-09-19 16:10:04 -07:00
|
|
|
} else if (!tgt.getModel().isSpherical) { // non-spherical targets
|
|
|
|
|
var contour = corn;
|
|
|
|
|
if (!tgt.getModel()
|
|
|
|
|
.isPlanar) { // visualization cant handle non-convex projections of 3d models
|
|
|
|
|
contour = OpenCVHelp.getConvexHull(contour);
|
|
|
|
|
}
|
|
|
|
|
VideoSimUtil.drawPoly(contour, -1, new Scalar(255), true, videoSimFrameRaw);
|
|
|
|
|
} else { // spherical targets
|
|
|
|
|
VideoSimUtil.drawInscribedEllipse(corn, new Scalar(255), videoSimFrameRaw);
|
|
|
|
|
}
|
2023-06-18 15:54:12 -07:00
|
|
|
}
|
|
|
|
|
videoSimRaw.putFrame(videoSimFrameRaw);
|
|
|
|
|
} else videoSimRaw.setConnectionStrategy(ConnectionStrategy.kForceClose);
|
2023-09-19 16:10:04 -07:00
|
|
|
// draw/annotate target detection outline on processed view
|
2023-06-18 15:54:12 -07:00
|
|
|
if (videoSimProcEnabled) {
|
|
|
|
|
Imgproc.cvtColor(videoSimFrameRaw, videoSimFrameProcessed, Imgproc.COLOR_GRAY2BGR);
|
2023-09-19 16:10:04 -07:00
|
|
|
Imgproc.drawMarker( // crosshair
|
|
|
|
|
videoSimFrameProcessed,
|
|
|
|
|
new Point(prop.getResWidth() / 2.0, prop.getResHeight() / 2.0),
|
|
|
|
|
new Scalar(0, 255, 0),
|
|
|
|
|
Imgproc.MARKER_CROSS,
|
|
|
|
|
(int) VideoSimUtil.getScaledThickness(15, videoSimFrameProcessed),
|
|
|
|
|
(int) VideoSimUtil.getScaledThickness(1, videoSimFrameProcessed),
|
|
|
|
|
Imgproc.LINE_AA);
|
2023-06-18 15:54:12 -07:00
|
|
|
for (var tgt : detectableTgts) {
|
2023-09-19 16:10:04 -07:00
|
|
|
if (tgt.getFiducialId() >= 0) { // apriltags
|
2023-06-18 15:54:12 -07:00
|
|
|
VideoSimUtil.drawTagDetection(
|
|
|
|
|
tgt.getFiducialId(),
|
2023-09-19 16:10:04 -07:00
|
|
|
OpenCVHelp.cornersToPoints(tgt.getDetectedCorners()),
|
|
|
|
|
videoSimFrameProcessed);
|
|
|
|
|
} else { // other targets
|
|
|
|
|
// bounding rectangle
|
|
|
|
|
Imgproc.rectangle(
|
|
|
|
|
videoSimFrameProcessed,
|
|
|
|
|
OpenCVHelp.getBoundingRect(OpenCVHelp.cornersToPoints(tgt.getDetectedCorners())),
|
|
|
|
|
new Scalar(0, 0, 255),
|
|
|
|
|
(int) VideoSimUtil.getScaledThickness(1, videoSimFrameProcessed),
|
|
|
|
|
Imgproc.LINE_AA);
|
|
|
|
|
|
|
|
|
|
VideoSimUtil.drawPoly(
|
|
|
|
|
OpenCVHelp.cornersToPoints(tgt.getMinAreaRectCorners()),
|
|
|
|
|
(int) VideoSimUtil.getScaledThickness(1, videoSimFrameProcessed),
|
|
|
|
|
new Scalar(255, 30, 30),
|
|
|
|
|
true,
|
2023-06-18 15:54:12 -07:00
|
|
|
videoSimFrameProcessed);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
videoSimProcessed.putFrame(videoSimFrameProcessed);
|
|
|
|
|
} else videoSimProcessed.setConnectionStrategy(ConnectionStrategy.kForceClose);
|
|
|
|
|
|
2023-10-25 18:35:06 -07:00
|
|
|
// calculate multitag results
|
2024-08-31 13:44:19 -04:00
|
|
|
Optional<MultiTargetPNPResult> multitagResult = Optional.empty();
|
2023-10-25 18:35:06 -07:00
|
|
|
// TODO: Implement ATFL subscribing in backend
|
|
|
|
|
// var tagLayout = cam.getAprilTagFieldLayout();
|
|
|
|
|
var visibleLayoutTags = VisionEstimation.getVisibleLayoutTags(detectableTgts, tagLayout);
|
|
|
|
|
if (visibleLayoutTags.size() > 1) {
|
2024-08-31 13:44:19 -04:00
|
|
|
List<Short> usedIDs =
|
|
|
|
|
visibleLayoutTags.stream().map(t -> (short) t.ID).sorted().collect(Collectors.toList());
|
2023-11-15 18:28:26 -05:00
|
|
|
var pnpResult =
|
2023-10-25 18:35:06 -07:00
|
|
|
VisionEstimation.estimateCamPosePNP(
|
2023-10-29 23:02:16 -04:00
|
|
|
prop.getIntrinsics(),
|
|
|
|
|
prop.getDistCoeffs(),
|
|
|
|
|
detectableTgts,
|
|
|
|
|
tagLayout,
|
2024-01-02 10:09:32 -05:00
|
|
|
TargetModel.kAprilTag36h11);
|
2024-08-31 13:44:19 -04:00
|
|
|
|
|
|
|
|
if (pnpResult.isPresent()) {
|
|
|
|
|
multitagResult = Optional.of(new MultiTargetPNPResult(pnpResult.get(), usedIDs));
|
|
|
|
|
}
|
2023-10-25 18:35:06 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sort target order
|
2023-06-18 15:54:12 -07:00
|
|
|
if (sortMode != null) {
|
|
|
|
|
detectableTgts.sort(sortMode.getComparator());
|
|
|
|
|
}
|
2023-10-25 18:35:06 -07:00
|
|
|
|
|
|
|
|
// put this simulated data to NT
|
2024-05-10 14:04:34 -04:00
|
|
|
var now = RobotController.getFPGATime();
|
|
|
|
|
var ret =
|
|
|
|
|
new PhotonPipelineResult(
|
|
|
|
|
heartbeatCounter,
|
|
|
|
|
now - (long) (latencyMillis * 1000),
|
|
|
|
|
now,
|
2024-10-31 08:27:19 -07:00
|
|
|
// Pretend like we heard a pong recently
|
|
|
|
|
1000L + (long) ((Math.random() - 0.5) * 50),
|
2024-05-10 14:04:34 -04:00
|
|
|
detectableTgts,
|
|
|
|
|
multitagResult);
|
|
|
|
|
return ret;
|
2023-06-18 15:54:12 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Simulate one processed frame of vision data, putting one result to NT at current timestamp.
|
|
|
|
|
* Image capture time is assumed be (current time - latency).
|
|
|
|
|
*
|
|
|
|
|
* @param result The pipeline result to submit
|
|
|
|
|
*/
|
|
|
|
|
public void submitProcessedFrame(PhotonPipelineResult result) {
|
|
|
|
|
submitProcessedFrame(result, WPIUtilJNI.now());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Simulate one processed frame of vision data, putting one result to NT. Image capture timestamp
|
|
|
|
|
* overrides {@link PhotonPipelineResult#getTimestampSeconds() getTimestampSeconds()} for more
|
|
|
|
|
* precise latency simulation.
|
|
|
|
|
*
|
|
|
|
|
* @param result The pipeline result to submit
|
|
|
|
|
* @param receiveTimestamp The (sim) timestamp when this result was read by NT in microseconds
|
|
|
|
|
*/
|
|
|
|
|
public void submitProcessedFrame(PhotonPipelineResult result, long receiveTimestamp) {
|
2024-08-31 13:44:19 -04:00
|
|
|
ts.latencyMillisEntry.set(result.metadata.getLatencyMillis(), receiveTimestamp);
|
2023-06-18 15:54:12 -07:00
|
|
|
|
2024-08-31 13:44:19 -04:00
|
|
|
// Results are now dynamically sized, so let's guess 1024 bytes is big enough
|
|
|
|
|
ts.resultPublisher.set(result, 1024);
|
2023-06-18 15:54:12 -07:00
|
|
|
|
|
|
|
|
boolean hasTargets = result.hasTargets();
|
|
|
|
|
ts.hasTargetEntry.set(hasTargets, receiveTimestamp);
|
|
|
|
|
if (!hasTargets) {
|
|
|
|
|
ts.targetPitchEntry.set(0.0, receiveTimestamp);
|
|
|
|
|
ts.targetYawEntry.set(0.0, receiveTimestamp);
|
|
|
|
|
ts.targetAreaEntry.set(0.0, receiveTimestamp);
|
2024-09-22 22:27:13 -04:00
|
|
|
ts.targetPoseEntry.set(new Transform3d(), receiveTimestamp);
|
2023-06-18 15:54:12 -07:00
|
|
|
ts.targetSkewEntry.set(0.0, receiveTimestamp);
|
|
|
|
|
} else {
|
|
|
|
|
var bestTarget = result.getBestTarget();
|
|
|
|
|
|
|
|
|
|
ts.targetPitchEntry.set(bestTarget.getPitch(), receiveTimestamp);
|
|
|
|
|
ts.targetYawEntry.set(bestTarget.getYaw(), receiveTimestamp);
|
|
|
|
|
ts.targetAreaEntry.set(bestTarget.getArea(), receiveTimestamp);
|
|
|
|
|
ts.targetSkewEntry.set(bestTarget.getSkew(), receiveTimestamp);
|
|
|
|
|
|
|
|
|
|
var transform = bestTarget.getBestCameraToTarget();
|
2024-09-22 22:27:13 -04:00
|
|
|
ts.targetPoseEntry.set(transform, receiveTimestamp);
|
2023-06-18 15:54:12 -07:00
|
|
|
}
|
|
|
|
|
|
2023-07-23 18:32:36 -07:00
|
|
|
ts.cameraIntrinsicsPublisher.set(prop.getIntrinsics().getData(), receiveTimestamp);
|
|
|
|
|
ts.cameraDistortionPublisher.set(prop.getDistCoeffs().getData(), receiveTimestamp);
|
2024-10-31 08:27:19 -07:00
|
|
|
|
|
|
|
|
ts.heartbeatPublisher.set(heartbeatCounter, receiveTimestamp);
|
|
|
|
|
heartbeatCounter += 1;
|
2023-06-18 15:54:12 -07:00
|
|
|
}
|
|
|
|
|
}
|