mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-19 00:41:41 +00:00
Update C++ Simulation to match Java (#1026)
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
|
||||
#include <frc/Errors.h>
|
||||
#include <frc/Timer.h>
|
||||
#include <opencv2/core.hpp>
|
||||
#include <opencv2/core/mat.hpp>
|
||||
|
||||
#include "PhotonVersion.h"
|
||||
@@ -128,8 +129,13 @@ LEDMode PhotonCamera::GetLEDMode() const {
|
||||
std::optional<cv::Mat> PhotonCamera::GetCameraMatrix() {
|
||||
auto camCoeffs = cameraIntrinsicsSubscriber.Get();
|
||||
if (camCoeffs.size() == 9) {
|
||||
// clone should deal with ownership concerns? not sure
|
||||
return cv::Mat(3, 3, CV_64FC1, camCoeffs.data()).clone();
|
||||
cv::Mat retVal(3, 3, CV_64FC1);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
retVal.at<double>(i, j) = camCoeffs[(j * 3) + i];
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -145,8 +151,11 @@ const std::string_view PhotonCamera::GetCameraName() const {
|
||||
std::optional<cv::Mat> PhotonCamera::GetDistCoeffs() {
|
||||
auto distCoeffs = cameraDistortionSubscriber.Get();
|
||||
if (distCoeffs.size() == 5) {
|
||||
// clone should deal with ownership concerns? not sure
|
||||
return cv::Mat(5, 1, CV_64FC1, distCoeffs.data()).clone();
|
||||
cv::Mat retVal(5, 1, CV_64FC1);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
retVal.at<double>(i, 0) = distCoeffs[i];
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -39,11 +39,7 @@
|
||||
#include <units/time.h>
|
||||
#include <wpi/deprecated.h>
|
||||
|
||||
#include "photon/dataflow/structures/Packet.h"
|
||||
#include "photon/targeting/MultiTargetPNPResult.h"
|
||||
#include "photon/targeting/PNPResult.h"
|
||||
#include "photon/targeting/PhotonPipelineResult.h"
|
||||
#include "photon/targeting/PhotonTrackedTarget.h"
|
||||
#include "photon/targeting//PhotonPipelineResult.h"
|
||||
|
||||
namespace cv {
|
||||
class Mat;
|
||||
@@ -172,6 +168,8 @@ class PhotonCamera {
|
||||
PhotonCamera::VERSION_CHECK_ENABLED = enabled;
|
||||
}
|
||||
|
||||
std::shared_ptr<nt::NetworkTable> GetCameraTable() const { return rootTable; }
|
||||
|
||||
// For use in tests
|
||||
bool test = false;
|
||||
PhotonPipelineResult testResult;
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cameraserver/CameraServer.h>
|
||||
#include <photon/PhotonCamera.h>
|
||||
#include <photon/PhotonTargetSortMode.h>
|
||||
#include <photon/estimation/CameraTargetRelation.h>
|
||||
#include <photon/estimation/VisionEstimation.h>
|
||||
#include <photon/networktables/NTTopicSet.h>
|
||||
#include <photon/simulation/SimCameraProperties.h>
|
||||
#include <photon/simulation/VideoSimUtil.h>
|
||||
#include <photon/simulation/VisionTargetSim.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <frc/apriltag/AprilTagFieldLayout.h>
|
||||
#include <frc/apriltag/AprilTagFields.h>
|
||||
#include <units/math.h>
|
||||
#include <wpi/timestamp.h>
|
||||
|
||||
namespace photon {
|
||||
class PhotonCameraSim {
|
||||
public:
|
||||
explicit PhotonCameraSim(PhotonCamera* camera)
|
||||
: PhotonCameraSim(camera, photon::SimCameraProperties::PERFECT_90DEG()) {}
|
||||
PhotonCameraSim(PhotonCamera* camera, const SimCameraProperties& props)
|
||||
: prop(props), cam(camera) {
|
||||
SetMinTargetAreaPixels(kDefaultMinAreaPx);
|
||||
videoSimRaw = frc::CameraServer::PutVideo(
|
||||
std::string{camera->GetCameraName()} + "-raw", prop.GetResWidth(),
|
||||
prop.GetResHeight());
|
||||
videoSimRaw.SetPixelFormat(cs::VideoMode::PixelFormat::kGray);
|
||||
videoSimProcessed = frc::CameraServer::PutVideo(
|
||||
std::string{camera->GetCameraName()} + "-processed", prop.GetResWidth(),
|
||||
prop.GetResHeight());
|
||||
ts.subTable = cam->GetCameraTable();
|
||||
ts.UpdateEntries();
|
||||
}
|
||||
PhotonCameraSim(PhotonCamera* camera, const SimCameraProperties& props,
|
||||
double minTargetAreaPercent, units::meter_t maxSightRange)
|
||||
: PhotonCameraSim(camera, props) {
|
||||
this->minTargetAreaPercent = minTargetAreaPercent;
|
||||
this->maxSightRange = maxSightRange;
|
||||
}
|
||||
PhotonCamera* GetCamera() { return cam; }
|
||||
double GetMinTargetAreaPercent() { return minTargetAreaPercent; }
|
||||
double GetMinTargetAreaPixels() {
|
||||
return minTargetAreaPercent / 100.0 * prop.GetResArea();
|
||||
}
|
||||
units::meter_t GetMaxSightRange() { return maxSightRange; }
|
||||
const cs::CvSource& GetVideoSimRaw() { return videoSimRaw; }
|
||||
const cv::Mat& GetVideoSimFrameRaw() { return videoSimFrameRaw; }
|
||||
bool CanSeeTargetPose(const frc::Pose3d& camPose,
|
||||
const VisionTargetSim& target) {
|
||||
CameraTargetRelation rel{camPose, target.GetPose()};
|
||||
return ((units::math::abs(rel.camToTargYaw.Degrees()) <
|
||||
prop.GetHorizFOV().Degrees() / 2.0) &&
|
||||
(units::math::abs(rel.camToTargPitch.Degrees()) <
|
||||
prop.GetVertFOV().Degrees() / 2.0) &&
|
||||
(!target.GetModel().GetIsPlanar() ||
|
||||
units::math::abs(rel.targToCamAngle.Degrees()) < 90_deg) &&
|
||||
(rel.camToTarg.Translation().Norm() <= maxSightRange));
|
||||
}
|
||||
bool CanSeeCorner(const std::vector<cv::Point2f>& points) {
|
||||
for (const auto& pt : points) {
|
||||
if (std::clamp<float>(pt.x, 0, prop.GetResWidth()) != pt.x ||
|
||||
std::clamp<float>(pt.y, 0, prop.GetResHeight()) != pt.y) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::optional<uint64_t> ConsumeNextEntryTime() {
|
||||
uint64_t now = wpi::Now();
|
||||
uint64_t timestamp{};
|
||||
int iter = 0;
|
||||
while (now >= nextNTEntryTime) {
|
||||
timestamp = nextNTEntryTime;
|
||||
uint64_t frameTime = prop.EstSecUntilNextFrame()
|
||||
.convert<units::microseconds>()
|
||||
.to<uint64_t>();
|
||||
nextNTEntryTime += frameTime;
|
||||
|
||||
if (iter++ > 50) {
|
||||
timestamp = now;
|
||||
nextNTEntryTime = now + frameTime;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (timestamp != 0) {
|
||||
return timestamp;
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
void SetMinTargetAreaPercent(double areaPercent) {
|
||||
minTargetAreaPercent = areaPercent;
|
||||
}
|
||||
void SetMinTargetAreaPixels(double areaPx) {
|
||||
minTargetAreaPercent = areaPx / prop.GetResArea() * 100;
|
||||
}
|
||||
void SetMaxSightRange(units::meter_t range) { maxSightRange = range; }
|
||||
void EnableRawStream(bool enabled) { videoSimRawEnabled = enabled; }
|
||||
void EnableDrawWireframe(bool enabled) { videoSimWireframeEnabled = enabled; }
|
||||
void SetWireframeResolution(double resolution) {
|
||||
videoSimWireframeResolution = resolution;
|
||||
}
|
||||
void EnabledProcessedStream(double enabled) { videoSimProcEnabled = enabled; }
|
||||
PhotonPipelineResult Process(units::second_t latency,
|
||||
const frc::Pose3d& cameraPose,
|
||||
std::vector<VisionTargetSim> targets) {
|
||||
std::sort(
|
||||
targets.begin(), targets.end(),
|
||||
[cameraPose](const VisionTargetSim& t1, const VisionTargetSim& t2) {
|
||||
units::meter_t dist1 =
|
||||
t1.GetPose().Translation().Distance(cameraPose.Translation());
|
||||
units::meter_t dist2 =
|
||||
t2.GetPose().Translation().Distance(cameraPose.Translation());
|
||||
return dist1 > dist2;
|
||||
});
|
||||
|
||||
std::vector<std::pair<VisionTargetSim, std::vector<cv::Point2f>>>
|
||||
visibleTgts{};
|
||||
std::vector<PhotonTrackedTarget> detectableTgts{};
|
||||
RotTrlTransform3d camRt = RotTrlTransform3d::MakeRelativeTo(cameraPose);
|
||||
|
||||
VideoSimUtil::UpdateVideoProp(videoSimRaw, prop);
|
||||
VideoSimUtil::UpdateVideoProp(videoSimProcessed, prop);
|
||||
cv::Size videoFrameSize{prop.GetResWidth(), prop.GetResHeight()};
|
||||
cv::Mat blankFrame = cv::Mat::zeros(videoFrameSize, CV_8UC1);
|
||||
blankFrame.assignTo(videoSimFrameRaw);
|
||||
|
||||
for (const auto& tgt : targets) {
|
||||
if (!CanSeeTargetPose(cameraPose, tgt)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<frc::Translation3d> fieldCorners = tgt.GetFieldVertices();
|
||||
if (tgt.GetModel().GetIsSpherical()) {
|
||||
TargetModel model = tgt.GetModel();
|
||||
fieldCorners = model.GetFieldVertices(TargetModel::GetOrientedPose(
|
||||
tgt.GetPose().Translation(), cameraPose.Translation()));
|
||||
}
|
||||
|
||||
std::vector<cv::Point2f> imagePoints = OpenCVHelp::ProjectPoints(
|
||||
prop.GetIntrinsics(), prop.GetDistCoeffs(), camRt, fieldCorners);
|
||||
if (tgt.GetModel().GetIsSpherical()) {
|
||||
cv::Point2d center = OpenCVHelp::AvgPoint(imagePoints);
|
||||
int l = 0;
|
||||
int t = 0;
|
||||
int b = 0;
|
||||
int r = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (imagePoints[i].x < imagePoints[l].x) {
|
||||
l = i;
|
||||
}
|
||||
}
|
||||
cv::Point2d lc = imagePoints[l];
|
||||
std::array<double, 4> angles{};
|
||||
t = (l + 1) % 4;
|
||||
b = (l + 1) % 4;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (i == l) {
|
||||
continue;
|
||||
}
|
||||
cv::Point2d ic = imagePoints[i];
|
||||
angles[i] = std::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;
|
||||
}
|
||||
}
|
||||
cv::RotatedRect rect{
|
||||
cv::Point2d{center.x, center.y},
|
||||
cv::Size2d{imagePoints[r].x - lc.x,
|
||||
imagePoints[b].y - imagePoints[t].y},
|
||||
units::radian_t{-angles[r]}.convert<units::degrees>().to<float>()};
|
||||
std::vector<cv::Point2f> points{};
|
||||
rect.points(points);
|
||||
|
||||
// Can't find an easier way to convert from Point2f to Point2d
|
||||
imagePoints.clear();
|
||||
std::transform(points.begin(), points.end(),
|
||||
std::back_inserter(imagePoints),
|
||||
[](const cv::Point2f& p) { return (cv::Point2d)p; });
|
||||
}
|
||||
|
||||
visibleTgts.emplace_back(std::make_pair(tgt, imagePoints));
|
||||
std::vector<cv::Point2f> noisyTargetCorners =
|
||||
prop.EstPixelNoise(imagePoints);
|
||||
cv::RotatedRect minAreaRect =
|
||||
OpenCVHelp::GetMinAreaRect(noisyTargetCorners);
|
||||
std::vector<cv::Point2f> minAreaRectPts;
|
||||
minAreaRectPts.reserve(4);
|
||||
minAreaRect.points(minAreaRectPts);
|
||||
cv::Point2d centerPt = minAreaRect.center;
|
||||
frc::Rotation3d centerRot = prop.GetPixelRot(centerPt);
|
||||
double areaPercent = prop.GetContourAreaPercent(noisyTargetCorners);
|
||||
|
||||
if (!(CanSeeCorner(noisyTargetCorners) &&
|
||||
areaPercent >= minTargetAreaPercent)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PNPResult pnpSim{};
|
||||
if (tgt.fiducialId >= 0 && tgt.GetFieldVertices().size() == 4) {
|
||||
pnpSim = OpenCVHelp::SolvePNP_Square(
|
||||
prop.GetIntrinsics(), prop.GetDistCoeffs(),
|
||||
tgt.GetModel().GetVertices(), noisyTargetCorners);
|
||||
}
|
||||
|
||||
std::vector<std::pair<float, float>> tempCorners =
|
||||
OpenCVHelp::PointsToCorners(minAreaRectPts);
|
||||
wpi::SmallVector<std::pair<double, double>, 4> smallVec;
|
||||
|
||||
for (const auto& corner : tempCorners) {
|
||||
smallVec.emplace_back(
|
||||
std::make_pair(static_cast<double>(corner.first),
|
||||
static_cast<double>(corner.second)));
|
||||
}
|
||||
|
||||
std::vector<std::pair<float, float>> cornersFloat =
|
||||
OpenCVHelp::PointsToCorners(noisyTargetCorners);
|
||||
|
||||
std::vector<std::pair<double, double>> cornersDouble{cornersFloat.begin(),
|
||||
cornersFloat.end()};
|
||||
detectableTgts.emplace_back(PhotonTrackedTarget{
|
||||
centerRot.Z().convert<units::degrees>().to<double>(),
|
||||
-centerRot.Y().convert<units::degrees>().to<double>(), areaPercent,
|
||||
centerRot.X().convert<units::degrees>().to<double>(), tgt.fiducialId,
|
||||
pnpSim.best, pnpSim.alt, pnpSim.ambiguity, smallVec, cornersDouble});
|
||||
}
|
||||
|
||||
if (videoSimRawEnabled) {
|
||||
if (videoSimWireframeEnabled) {
|
||||
VideoSimUtil::DrawFieldWireFrame(
|
||||
camRt, prop, videoSimWireframeResolution, 1.5, cv::Scalar{80}, 6, 1,
|
||||
cv::Scalar{30}, videoSimFrameRaw);
|
||||
}
|
||||
|
||||
for (const auto& pair : visibleTgts) {
|
||||
VisionTargetSim tgt = pair.first;
|
||||
std::vector<cv::Point2f> corners = pair.second;
|
||||
|
||||
if (tgt.fiducialId > 0) {
|
||||
VideoSimUtil::Warp165h5TagImage(tgt.fiducialId, corners, true,
|
||||
videoSimFrameRaw);
|
||||
} else if (!tgt.GetModel().GetIsSpherical()) {
|
||||
std::vector<cv::Point2f> contour = corners;
|
||||
if (!tgt.GetModel().GetIsPlanar()) {
|
||||
contour = OpenCVHelp::GetConvexHull(contour);
|
||||
}
|
||||
VideoSimUtil::DrawPoly(contour, -1, cv::Scalar{255}, true,
|
||||
videoSimFrameRaw);
|
||||
} else {
|
||||
VideoSimUtil::DrawInscribedEllipse(corners, cv::Scalar{255},
|
||||
videoSimFrameRaw);
|
||||
}
|
||||
}
|
||||
videoSimRaw.PutFrame(videoSimFrameRaw);
|
||||
} else {
|
||||
videoSimRaw.SetConnectionStrategy(
|
||||
cs::VideoSource::ConnectionStrategy::kConnectionForceClose);
|
||||
}
|
||||
|
||||
if (videoSimProcEnabled) {
|
||||
cv::cvtColor(videoSimFrameRaw, videoSimFrameProcessed,
|
||||
cv::COLOR_GRAY2BGR);
|
||||
cv::drawMarker(
|
||||
videoSimFrameProcessed,
|
||||
cv::Point2d{prop.GetResWidth() / 2.0, prop.GetResHeight() / 2.0},
|
||||
cv::Scalar{0, 255, 0}, cv::MARKER_CROSS,
|
||||
static_cast<int>(
|
||||
VideoSimUtil::GetScaledThickness(15, videoSimFrameProcessed)),
|
||||
static_cast<int>(
|
||||
VideoSimUtil::GetScaledThickness(1, videoSimFrameProcessed)),
|
||||
cv::LINE_AA);
|
||||
for (const auto& tgt : detectableTgts) {
|
||||
auto detectedCornersDouble = tgt.GetDetectedCorners();
|
||||
std::vector<std::pair<float, float>> detectedCornerFloat{
|
||||
detectedCornersDouble.begin(), detectedCornersDouble.end()};
|
||||
if (tgt.GetFiducialId() >= 0) {
|
||||
VideoSimUtil::DrawTagDetection(
|
||||
tgt.GetFiducialId(),
|
||||
OpenCVHelp::CornersToPoints(detectedCornerFloat),
|
||||
videoSimFrameProcessed);
|
||||
} else {
|
||||
cv::rectangle(videoSimFrameProcessed,
|
||||
OpenCVHelp::GetBoundingRect(
|
||||
OpenCVHelp::CornersToPoints(detectedCornerFloat)),
|
||||
cv::Scalar{0, 0, 255},
|
||||
static_cast<int>(VideoSimUtil::GetScaledThickness(
|
||||
1, videoSimFrameProcessed)),
|
||||
cv::LINE_AA);
|
||||
|
||||
wpi::SmallVector<std::pair<double, double>, 4> smallVec =
|
||||
tgt.GetMinAreaRectCorners();
|
||||
|
||||
std::vector<std::pair<float, float>> cornersCopy{};
|
||||
cornersCopy.reserve(4);
|
||||
|
||||
for (const auto& corner : smallVec) {
|
||||
cornersCopy.emplace_back(
|
||||
std::make_pair(static_cast<float>(corner.first),
|
||||
static_cast<float>(corner.second)));
|
||||
}
|
||||
|
||||
VideoSimUtil::DrawPoly(
|
||||
OpenCVHelp::CornersToPoints(cornersCopy),
|
||||
static_cast<int>(
|
||||
VideoSimUtil::GetScaledThickness(1, videoSimFrameProcessed)),
|
||||
cv::Scalar{255, 30, 30}, true, videoSimFrameProcessed);
|
||||
}
|
||||
}
|
||||
videoSimProcessed.PutFrame(videoSimFrameProcessed);
|
||||
} else {
|
||||
videoSimProcessed.SetConnectionStrategy(
|
||||
cs::VideoSource::ConnectionStrategy::kConnectionForceClose);
|
||||
}
|
||||
|
||||
MultiTargetPNPResult multiTagResults{};
|
||||
|
||||
std::vector<frc::AprilTag> visibleLayoutTags =
|
||||
VisionEstimation::GetVisibleLayoutTags(detectableTgts, tagLayout);
|
||||
if (visibleLayoutTags.size() > 1) {
|
||||
wpi::SmallVector<int16_t, 32> usedIds{};
|
||||
std::transform(visibleLayoutTags.begin(), visibleLayoutTags.end(),
|
||||
usedIds.begin(),
|
||||
[](const frc::AprilTag& tag) { return tag.ID; });
|
||||
std::sort(usedIds.begin(), usedIds.end());
|
||||
PNPResult pnpResult = VisionEstimation::EstimateCamPosePNP(
|
||||
prop.GetIntrinsics(), prop.GetDistCoeffs(), detectableTgts, tagLayout,
|
||||
kAprilTag16h5);
|
||||
multiTagResults = MultiTargetPNPResult{pnpResult, usedIds};
|
||||
}
|
||||
|
||||
return PhotonPipelineResult{latency, detectableTgts, multiTagResults};
|
||||
}
|
||||
void SubmitProcessedFrame(const PhotonPipelineResult& result) {
|
||||
SubmitProcessedFrame(result, wpi::Now());
|
||||
}
|
||||
void SubmitProcessedFrame(const PhotonPipelineResult& result,
|
||||
uint64_t recieveTimestamp) {
|
||||
ts.latencyMillisEntry.Set(
|
||||
result.GetLatency().convert<units::milliseconds>().to<double>(),
|
||||
recieveTimestamp);
|
||||
|
||||
Packet newPacket{};
|
||||
newPacket << result;
|
||||
ts.rawBytesEntry.Set(newPacket.GetData(), recieveTimestamp);
|
||||
|
||||
bool hasTargets = result.HasTargets();
|
||||
ts.hasTargetEntry.Set(hasTargets, recieveTimestamp);
|
||||
if (!hasTargets) {
|
||||
ts.targetPitchEntry.Set(0.0, recieveTimestamp);
|
||||
ts.targetYawEntry.Set(0.0, recieveTimestamp);
|
||||
ts.targetAreaEntry.Set(0.0, recieveTimestamp);
|
||||
std::array<double, 3> poseData{0.0, 0.0, 0.0};
|
||||
ts.targetPoseEntry.Set(poseData, recieveTimestamp);
|
||||
ts.targetSkewEntry.Set(0.0, recieveTimestamp);
|
||||
} else {
|
||||
PhotonTrackedTarget bestTarget = result.GetBestTarget();
|
||||
|
||||
ts.targetPitchEntry.Set(bestTarget.GetPitch(), recieveTimestamp);
|
||||
ts.targetYawEntry.Set(bestTarget.GetYaw(), recieveTimestamp);
|
||||
ts.targetAreaEntry.Set(bestTarget.GetArea(), recieveTimestamp);
|
||||
ts.targetSkewEntry.Set(bestTarget.GetSkew(), recieveTimestamp);
|
||||
|
||||
frc::Transform3d transform = bestTarget.GetBestCameraToTarget();
|
||||
std::array<double, 4> poseData{
|
||||
transform.X().to<double>(), transform.Y().to<double>(),
|
||||
transform.Rotation().ToRotation2d().Degrees().to<double>()};
|
||||
ts.targetPoseEntry.Set(poseData, recieveTimestamp);
|
||||
}
|
||||
|
||||
auto intrinsics = prop.GetIntrinsics();
|
||||
std::vector<double> intrinsicsView{intrinsics.data(),
|
||||
intrinsics.data() + intrinsics.size()};
|
||||
ts.cameraIntrinsicsPublisher.Set(intrinsicsView, recieveTimestamp);
|
||||
|
||||
auto distortion = prop.GetDistCoeffs();
|
||||
std::vector<double> distortionView{distortion.data(),
|
||||
distortion.data() + distortion.size()};
|
||||
ts.cameraDistortionPublisher.Set(distortionView, recieveTimestamp);
|
||||
|
||||
ts.heartbeatPublisher.Set(heartbeatCounter++, recieveTimestamp);
|
||||
}
|
||||
SimCameraProperties prop;
|
||||
|
||||
private:
|
||||
PhotonCamera* cam;
|
||||
|
||||
NTTopicSet ts{};
|
||||
uint64_t heartbeatCounter{0};
|
||||
|
||||
uint64_t nextNTEntryTime{wpi::Now()};
|
||||
|
||||
units::meter_t maxSightRange{std::numeric_limits<double>::max()};
|
||||
static constexpr double kDefaultMinAreaPx{100};
|
||||
double minTargetAreaPercent;
|
||||
|
||||
frc::AprilTagFieldLayout tagLayout{
|
||||
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2023ChargedUp)};
|
||||
|
||||
cs::CvSource videoSimRaw;
|
||||
cv::Mat videoSimFrameRaw{};
|
||||
bool videoSimRawEnabled{true};
|
||||
bool videoSimWireframeEnabled{false};
|
||||
double videoSimWireframeResolution{0.1};
|
||||
cs::CvSource videoSimProcessed;
|
||||
cv::Mat videoSimFrameProcessed{};
|
||||
bool videoSimProcEnabled{true};
|
||||
};
|
||||
} // namespace photon
|
||||
@@ -0,0 +1,475 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <photon/estimation/OpenCVHelp.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <frc/Errors.h>
|
||||
#include <frc/MathUtil.h>
|
||||
#include <frc/geometry/Rotation2d.h>
|
||||
#include <frc/geometry/Translation3d.h>
|
||||
#include <units/frequency.h>
|
||||
#include <units/time.h>
|
||||
|
||||
namespace photon {
|
||||
class SimCameraProperties {
|
||||
public:
|
||||
SimCameraProperties() { SetCalibration(960, 720, frc::Rotation2d{90_deg}); }
|
||||
SimCameraProperties(std::string path, int width, int height) {}
|
||||
void SetCalibration(int width, int height, frc::Rotation2d fovDiag) {
|
||||
if (fovDiag.Degrees() < 1_deg || fovDiag.Degrees() > 179_deg) {
|
||||
fovDiag = frc::Rotation2d{
|
||||
std::clamp<units::degree_t>(fovDiag.Degrees(), 1_deg, 179_deg)};
|
||||
FRC_ReportError(
|
||||
frc::err::Error,
|
||||
"Requested invalid FOV! Clamping between (1, 179) degrees...");
|
||||
}
|
||||
double resDiag = std::sqrt(width * width + height * height);
|
||||
double diagRatio = units::math::tan(fovDiag.Radians() / 2.0);
|
||||
frc::Rotation2d fovWidth{
|
||||
units::radian_t{std::atan(diagRatio * (width / resDiag)) * 2}};
|
||||
frc::Rotation2d fovHeight{
|
||||
units::radian_t{std::atan(diagRatio * (height / resDiag)) * 2}};
|
||||
|
||||
Eigen::Matrix<double, 5, 1> newDistCoeffs;
|
||||
newDistCoeffs << 0, 0, 0, 0, 0;
|
||||
|
||||
double cx = width / 2.0 - 0.5;
|
||||
double cy = height / 2.0 - 0.5;
|
||||
|
||||
double fx = cx / units::math::tan(fovWidth.Radians() / 2.0);
|
||||
double fy = cy / units::math::tan(fovHeight.Radians() / 2.0);
|
||||
|
||||
Eigen::Matrix<double, 3, 3> newCamIntrinsics;
|
||||
newCamIntrinsics << fx, 0.0, cx, 0.0, fy, cy, 0.0, 0.0, 1.0;
|
||||
SetCalibration(width, height, newCamIntrinsics, newDistCoeffs);
|
||||
}
|
||||
|
||||
void SetCalibration(int width, int height,
|
||||
const Eigen::Matrix<double, 3, 3>& newCamIntrinsics,
|
||||
const Eigen::Matrix<double, 5, 1>& newDistCoeffs) {
|
||||
resWidth = width;
|
||||
resHeight = height;
|
||||
camIntrinsics = newCamIntrinsics;
|
||||
distCoeffs = newDistCoeffs;
|
||||
|
||||
std::array<frc::Translation3d, 4> p{
|
||||
frc::Translation3d{
|
||||
1_m,
|
||||
frc::Rotation3d{0_rad, 0_rad,
|
||||
(GetPixelYaw(0) + frc::Rotation2d{units::radian_t{
|
||||
-std::numbers::pi / 2.0}})
|
||||
.Radians()}},
|
||||
frc::Translation3d{
|
||||
1_m, frc::Rotation3d{0_rad, 0_rad,
|
||||
(GetPixelYaw(width) +
|
||||
frc::Rotation2d{
|
||||
units::radian_t{std::numbers::pi / 2.0}})
|
||||
.Radians()}},
|
||||
frc::Translation3d{
|
||||
1_m, frc::Rotation3d{0_rad,
|
||||
(GetPixelPitch(0) +
|
||||
frc::Rotation2d{
|
||||
units::radian_t{std::numbers::pi / 2.0}})
|
||||
.Radians(),
|
||||
0_rad}},
|
||||
frc::Translation3d{
|
||||
1_m, frc::Rotation3d{0_rad,
|
||||
(GetPixelPitch(height) +
|
||||
frc::Rotation2d{
|
||||
units::radian_t{-std::numbers::pi / 2.0}})
|
||||
.Radians(),
|
||||
0_rad}},
|
||||
};
|
||||
viewplanes.clear();
|
||||
for (size_t i = 0; i < p.size(); i++) {
|
||||
viewplanes.emplace_back(Eigen::Matrix<double, 3, 1>{
|
||||
p[i].X().to<double>(), p[i].Y().to<double>(), p[i].Z().to<double>()});
|
||||
}
|
||||
}
|
||||
|
||||
void SetCalibError(double newAvgErrorPx, double newErrorStdDevPx) {
|
||||
avgErrorPx = newAvgErrorPx;
|
||||
errorStdDevPx = newErrorStdDevPx;
|
||||
}
|
||||
|
||||
void SetFPS(units::hertz_t fps) {
|
||||
frameSpeed = units::math::max(1 / fps, exposureTime);
|
||||
}
|
||||
|
||||
void SetExposureTime(units::second_t newExposureTime) {
|
||||
exposureTime = newExposureTime;
|
||||
frameSpeed = units::math::max(frameSpeed, exposureTime);
|
||||
}
|
||||
|
||||
void SetAvgLatency(units::second_t newAvgLatency) {
|
||||
avgLatency = newAvgLatency;
|
||||
}
|
||||
|
||||
void SetLatencyStdDev(units::second_t newLatencyStdDev) {
|
||||
latencyStdDev = newLatencyStdDev;
|
||||
}
|
||||
|
||||
int GetResWidth() const { return resWidth; }
|
||||
|
||||
int GetResHeight() const { return resHeight; }
|
||||
|
||||
int GetResArea() const { return resWidth * resHeight; }
|
||||
|
||||
double GetAspectRatio() const {
|
||||
return static_cast<double>(resWidth) / static_cast<double>(resHeight);
|
||||
}
|
||||
|
||||
Eigen::Matrix<double, 3, 3> GetIntrinsics() const { return camIntrinsics; }
|
||||
|
||||
Eigen::Matrix<double, 5, 1> GetDistCoeffs() const { return distCoeffs; }
|
||||
|
||||
units::hertz_t GetFPS() const { return 1 / frameSpeed; }
|
||||
|
||||
units::second_t GetFrameSpeed() const { return frameSpeed; }
|
||||
|
||||
units::second_t GetExposureTime() const { return exposureTime; }
|
||||
|
||||
units::second_t GetAverageLatency() const { return avgLatency; }
|
||||
|
||||
units::second_t GetLatencyStdDev() const { return latencyStdDev; }
|
||||
|
||||
double GetContourAreaPercent(const std::vector<cv::Point2f>& points) {
|
||||
return cv::contourArea(photon::OpenCVHelp::GetConvexHull(points)) /
|
||||
GetResArea() * 100;
|
||||
}
|
||||
|
||||
frc::Rotation2d GetPixelYaw(double pixelX) const {
|
||||
double fx = camIntrinsics(0, 0);
|
||||
double cx = camIntrinsics(0, 2);
|
||||
double xOffset = cx - pixelX;
|
||||
return frc::Rotation2d{fx, xOffset};
|
||||
}
|
||||
|
||||
frc::Rotation2d GetPixelPitch(double pixelY) const {
|
||||
double fy = camIntrinsics(1, 1);
|
||||
double cy = camIntrinsics(1, 2);
|
||||
double yOffset = cy - pixelY;
|
||||
return frc::Rotation2d{fy, -yOffset};
|
||||
}
|
||||
|
||||
frc::Rotation3d GetPixelRot(const cv::Point2d& point) const {
|
||||
return frc::Rotation3d{0_rad, GetPixelPitch(point.y).Radians(),
|
||||
GetPixelYaw(point.x).Radians()};
|
||||
}
|
||||
|
||||
frc::Rotation3d GetCorrectedPixelRot(const cv::Point2d& point) const {
|
||||
double fx = camIntrinsics(0, 0);
|
||||
double cx = camIntrinsics(0, 2);
|
||||
double xOffset = cx - point.x;
|
||||
|
||||
double fy = camIntrinsics(1, 1);
|
||||
double cy = camIntrinsics(1, 2);
|
||||
double yOffset = cy - point.y;
|
||||
|
||||
frc::Rotation2d yaw{fx, xOffset};
|
||||
frc::Rotation2d pitch{fy / std::cos(std::atan(xOffset / fx)), -yOffset};
|
||||
return frc::Rotation3d{0_rad, pitch.Radians(), yaw.Radians()};
|
||||
}
|
||||
|
||||
frc::Rotation2d GetHorizFOV() const {
|
||||
frc::Rotation2d left = GetPixelYaw(0);
|
||||
frc::Rotation2d right = GetPixelYaw(resWidth);
|
||||
return left - right;
|
||||
}
|
||||
|
||||
frc::Rotation2d GetVertFOV() const {
|
||||
frc::Rotation2d above = GetPixelPitch(0);
|
||||
frc::Rotation2d below = GetPixelPitch(resHeight);
|
||||
return below - above;
|
||||
}
|
||||
|
||||
frc::Rotation2d GetDiagFOV() const {
|
||||
return frc::Rotation2d{
|
||||
units::math::hypot(GetHorizFOV().Radians(), GetVertFOV().Radians())};
|
||||
}
|
||||
|
||||
std::pair<std::optional<double>, std::optional<double>> GetVisibleLine(
|
||||
const RotTrlTransform3d& camRt, const frc::Translation3d& a,
|
||||
const frc::Translation3d& b) const {
|
||||
frc::Translation3d relA = camRt.Apply(a);
|
||||
frc::Translation3d relB = camRt.Apply(b);
|
||||
|
||||
if (relA.X() <= 0_m && relB.X() <= 0_m) {
|
||||
return {std::nullopt, std::nullopt};
|
||||
}
|
||||
|
||||
Eigen::Matrix<double, 3, 1> av{relA.X().to<double>(), relA.Y().to<double>(),
|
||||
relA.Z().to<double>()};
|
||||
Eigen::Matrix<double, 3, 1> bv{relB.X().to<double>(), relB.Y().to<double>(),
|
||||
relB.Z().to<double>()};
|
||||
Eigen::Matrix<double, 3, 1> abv = bv - av;
|
||||
|
||||
bool aVisible = true;
|
||||
bool bVisible = true;
|
||||
for (size_t i = 0; i < viewplanes.size(); i++) {
|
||||
Eigen::Matrix<double, 3, 1> normal = viewplanes[i];
|
||||
double aVisibility = av.dot(normal);
|
||||
if (aVisibility < 0) {
|
||||
aVisible = false;
|
||||
}
|
||||
double bVisibility = bv.dot(normal);
|
||||
if (bVisibility < 0) {
|
||||
bVisible = false;
|
||||
}
|
||||
if (aVisibility <= 0 && bVisibility <= 0) {
|
||||
return {std::nullopt, std::nullopt};
|
||||
}
|
||||
}
|
||||
|
||||
if (aVisible && bVisible) {
|
||||
return {0, 1};
|
||||
}
|
||||
|
||||
std::array<double, 4> intersections{std::nan(""), std::nan(""),
|
||||
std::nan(""), std::nan("")};
|
||||
std::vector<std::optional<Eigen::Matrix<double, 3, 1>>> ipts{
|
||||
{std::nullopt, std::nullopt, std::nullopt, std::nullopt}};
|
||||
|
||||
for (size_t i = 0; i < viewplanes.size(); i++) {
|
||||
Eigen::Matrix<double, 3, 1> normal = viewplanes[i];
|
||||
Eigen::Matrix<double, 3, 1> a_projn{};
|
||||
a_projn = (av.dot(normal) / normal.dot(normal)) * normal;
|
||||
|
||||
if (std::abs(abv.dot(normal)) < 1e-5) {
|
||||
continue;
|
||||
}
|
||||
intersections[i] = a_projn.dot(a_projn) / -(abv.dot(a_projn));
|
||||
|
||||
Eigen::Matrix<double, 3, 1> apv{};
|
||||
apv = intersections[i] * abv;
|
||||
Eigen::Matrix<double, 3, 1> intersectpt{};
|
||||
intersectpt = av + apv;
|
||||
ipts[i] = intersectpt;
|
||||
|
||||
for (size_t j = 1; j < viewplanes.size(); j++) {
|
||||
int oi = (i + j) % viewplanes.size();
|
||||
Eigen::Matrix<double, 3, 1> onormal = viewplanes[oi];
|
||||
if (intersectpt.dot(onormal) < 0) {
|
||||
intersections[i] = std::nan("");
|
||||
ipts[i] = std::nullopt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ipts[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = i - 1; j >= 0; j--) {
|
||||
std::optional<Eigen::Matrix<double, 3, 1>> oipt = ipts[j];
|
||||
if (!oipt) {
|
||||
continue;
|
||||
}
|
||||
Eigen::Matrix<double, 3, 1> diff{};
|
||||
diff = oipt.value() - intersectpt;
|
||||
if (diff.cwiseAbs().maxCoeff() < 1e-4) {
|
||||
intersections[i] = std::nan("");
|
||||
ipts[i] = std::nullopt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double inter1 = std::nan("");
|
||||
double inter2 = std::nan("");
|
||||
for (double inter : intersections) {
|
||||
if (!std::isnan(inter)) {
|
||||
if (std::isnan(inter1)) {
|
||||
inter1 = inter;
|
||||
} else {
|
||||
inter2 = inter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!std::isnan(inter2)) {
|
||||
double max = std::max(inter1, inter2);
|
||||
double min = std::min(inter1, inter2);
|
||||
if (aVisible) {
|
||||
min = 0;
|
||||
}
|
||||
if (bVisible) {
|
||||
max = 1;
|
||||
}
|
||||
return {min, max};
|
||||
} else if (!std::isnan(inter1)) {
|
||||
if (aVisible) {
|
||||
return {0, inter1};
|
||||
}
|
||||
if (bVisible) {
|
||||
return {inter1, 1};
|
||||
}
|
||||
return {inter1, std::nullopt};
|
||||
} else {
|
||||
return {std::nullopt, std::nullopt};
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<cv::Point2f> EstPixelNoise(
|
||||
const std::vector<cv::Point2f>& points) {
|
||||
if (avgErrorPx == 0 && errorStdDevPx == 0) {
|
||||
return points;
|
||||
}
|
||||
|
||||
std::vector<cv::Point2f> noisyPts;
|
||||
noisyPts.reserve(points.size());
|
||||
for (size_t i = 0; i < points.size(); i++) {
|
||||
cv::Point2f p = points[i];
|
||||
float error = avgErrorPx + gaussian(generator) * errorStdDevPx;
|
||||
float errorAngle =
|
||||
uniform(generator) * 2 * std::numbers::pi - std::numbers::pi;
|
||||
noisyPts.emplace_back(cv::Point2f{p.x + error * std::cos(errorAngle),
|
||||
p.y + error * std::sin(errorAngle)});
|
||||
}
|
||||
return noisyPts;
|
||||
}
|
||||
|
||||
units::second_t EstLatency() {
|
||||
return units::math::max(avgLatency + gaussian(generator) * latencyStdDev,
|
||||
0_s);
|
||||
}
|
||||
|
||||
units::second_t EstSecUntilNextFrame() {
|
||||
return frameSpeed + units::math::max(0_s, EstLatency() - frameSpeed);
|
||||
}
|
||||
|
||||
static SimCameraProperties PERFECT_90DEG() { return SimCameraProperties{}; }
|
||||
|
||||
static SimCameraProperties PI4_LIFECAM_320_240() {
|
||||
SimCameraProperties prop{};
|
||||
prop.SetCalibration(
|
||||
320, 240,
|
||||
(Eigen::MatrixXd(3, 3) << 328.2733242048587, 0.0, 164.8190261141906,
|
||||
0.0, 318.0609794305216, 123.8633838438093, 0.0, 0.0, 1.0)
|
||||
.finished(),
|
||||
Eigen::Matrix<double, 5, 1>{
|
||||
0.09957946553445934, -0.9166265114485799, 0.0019519890627236526,
|
||||
-0.0036071725380870333, 1.5627234622420942});
|
||||
prop.SetCalibError(0.21, 0.0124);
|
||||
prop.SetFPS(30_Hz);
|
||||
prop.SetAvgLatency(30_ms);
|
||||
prop.SetLatencyStdDev(10_ms);
|
||||
return prop;
|
||||
}
|
||||
|
||||
static SimCameraProperties PI4_LIFECAM_640_480() {
|
||||
SimCameraProperties prop{};
|
||||
prop.SetCalibration(
|
||||
640, 480,
|
||||
(Eigen::MatrixXd(3, 3) << 669.1428078983059, 0.0, 322.53377249329213,
|
||||
0.0, 646.9843137061716, 241.26567383784163, 0.0, 0.0, 1.0)
|
||||
.finished(),
|
||||
Eigen::Matrix<double, 5, 1>{
|
||||
0.12788470750464645, -1.2350335805796528, 0.0024990767286192732,
|
||||
-0.0026958287600230705, 2.2951386729115537});
|
||||
prop.SetCalibError(0.26, 0.046);
|
||||
prop.SetFPS(15_Hz);
|
||||
prop.SetAvgLatency(65_ms);
|
||||
prop.SetLatencyStdDev(15_ms);
|
||||
return prop;
|
||||
}
|
||||
|
||||
static SimCameraProperties LL2_640_480() {
|
||||
SimCameraProperties prop{};
|
||||
prop.SetCalibration(
|
||||
640, 480,
|
||||
(Eigen::MatrixXd(3, 3) << 511.22843367007755, 0.0, 323.62049380211096,
|
||||
0.0, 514.5452336723849, 261.8827920543568, 0.0, 0.0, 1.0)
|
||||
.finished(),
|
||||
Eigen::Matrix<double, 5, 1>{0.1917469998873756, -0.5142936883324216,
|
||||
0.012461562046896614, 0.0014084973492408186,
|
||||
0.35160648971214437});
|
||||
prop.SetCalibError(0.25, 0.05);
|
||||
prop.SetFPS(15_Hz);
|
||||
prop.SetAvgLatency(35_ms);
|
||||
prop.SetLatencyStdDev(8_ms);
|
||||
return prop;
|
||||
}
|
||||
|
||||
static SimCameraProperties LL2_960_720() {
|
||||
SimCameraProperties prop{};
|
||||
prop.SetCalibration(
|
||||
960, 720,
|
||||
(Eigen::MatrixXd(3, 3) << 769.6873145148892, 0.0, 486.1096609458122,
|
||||
0.0, 773.8164483705323, 384.66071662358354, 0.0, 0.0, 1.0)
|
||||
.finished(),
|
||||
Eigen::Matrix<double, 5, 1>{0.189462064814501, -0.49903003669627627,
|
||||
0.007468423590519429, 0.002496885298683693,
|
||||
0.3443122090208624});
|
||||
prop.SetCalibError(0.35, 0.10);
|
||||
prop.SetFPS(10_Hz);
|
||||
prop.SetAvgLatency(50_ms);
|
||||
prop.SetLatencyStdDev(15_ms);
|
||||
return prop;
|
||||
}
|
||||
|
||||
static SimCameraProperties LL2_1280_720() {
|
||||
SimCameraProperties prop{};
|
||||
prop.SetCalibration(
|
||||
1280, 720,
|
||||
(Eigen::MatrixXd(3, 3) << 1011.3749416937393, 0.0, 645.4955139388737,
|
||||
0.0, 1008.5391755084075, 508.32877656020196, 0.0, 0.0, 1.0)
|
||||
.finished(),
|
||||
Eigen::Matrix<double, 5, 1>{0.13730101577061535, -0.2904345656989261,
|
||||
8.32475714507539E-4, -3.694397782014239E-4,
|
||||
0.09487962227027584});
|
||||
prop.SetCalibError(0.37, 0.06);
|
||||
prop.SetFPS(7_Hz);
|
||||
prop.SetAvgLatency(60_ms);
|
||||
prop.SetLatencyStdDev(20_ms);
|
||||
return prop;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mt19937 generator{std::random_device{}()};
|
||||
std::normal_distribution<float> gaussian{0.0, 1.0};
|
||||
std::uniform_real_distribution<float> uniform{0.0, 1.0};
|
||||
|
||||
int resWidth;
|
||||
int resHeight;
|
||||
Eigen::Matrix<double, 3, 3> camIntrinsics;
|
||||
Eigen::Matrix<double, 5, 1> distCoeffs;
|
||||
double avgErrorPx;
|
||||
double errorStdDevPx;
|
||||
units::second_t frameSpeed{0};
|
||||
units::second_t exposureTime{0};
|
||||
units::second_t avgLatency{0};
|
||||
units::second_t latencyStdDev{0};
|
||||
std::vector<Eigen::Matrix<double, 3, 1>> viewplanes{};
|
||||
};
|
||||
} // namespace photon
|
||||
@@ -0,0 +1,432 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cscore_cv.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <frc/apriltag/AprilTag.h>
|
||||
#include <opencv2/core.hpp>
|
||||
#include <opencv2/imgcodecs.hpp>
|
||||
#include <opencv2/objdetect.hpp>
|
||||
#include <units/length.h>
|
||||
|
||||
#include "SimCameraProperties.h"
|
||||
#include "photon/estimation/RotTrlTransform3d.h"
|
||||
|
||||
namespace mathutil {
|
||||
template <typename T>
|
||||
int sgn(T val) {
|
||||
return (T(0) < val) - (val < T(0));
|
||||
}
|
||||
} // namespace mathutil
|
||||
|
||||
namespace photon {
|
||||
namespace VideoSimUtil {
|
||||
static constexpr int kNumTags16h5 = 30;
|
||||
|
||||
static constexpr units::meter_t fieldLength{16.54175_m};
|
||||
static constexpr units::meter_t fieldWidth{8.0137_m};
|
||||
|
||||
static cv::Mat Get16h5TagImage(int id) {
|
||||
wpi::RawFrame frame = frc::AprilTag::Generate16h5AprilTagImage(id);
|
||||
cv::Mat markerImage{frame.height, frame.width, CV_8UC1, frame.data};
|
||||
cv::Mat markerClone = markerImage.colRange(0, frame.dataLength).clone();
|
||||
return markerClone;
|
||||
}
|
||||
|
||||
static std::unordered_map<int, cv::Mat> LoadAprilTagImages() {
|
||||
std::unordered_map<int, cv::Mat> retVal{};
|
||||
for (int i = 0; i < kNumTags16h5; i++) {
|
||||
cv::Mat tagImage = Get16h5TagImage(i);
|
||||
retVal[i] = tagImage;
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
static std::vector<cv::Point2f> GetImageCorners(const cv::Size& size) {
|
||||
std::vector<cv::Point2f> retVal{};
|
||||
retVal.emplace_back(cv::Point2f{-0.5f, -0.5f});
|
||||
retVal.emplace_back(cv::Point2f{size.width - 0.5f, -0.5f});
|
||||
retVal.emplace_back(cv::Point2f{size.width - 0.5f, size.height - 0.5f});
|
||||
retVal.emplace_back(cv::Point2f{-0.5f, size.height - 0.5f});
|
||||
return retVal;
|
||||
}
|
||||
|
||||
static std::vector<cv::Point2f> Get16h5MarkerPts(int scale) {
|
||||
cv::Rect2f roi16h5{cv::Point2f{1, 1}, cv::Point2f{6, 6}};
|
||||
roi16h5.x *= scale;
|
||||
roi16h5.y *= scale;
|
||||
roi16h5.width *= scale;
|
||||
roi16h5.height *= scale;
|
||||
std::vector<cv::Point2f> pts = GetImageCorners(roi16h5.size());
|
||||
for (size_t i = 0; i < pts.size(); i++) {
|
||||
cv::Point2f pt = pts[i];
|
||||
pts[i] = cv::Point2f{roi16h5.tl().x + pt.x, roi16h5.tl().y + pt.y};
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
static std::vector<cv::Point2f> Get16h5MarkerPts() {
|
||||
return Get16h5MarkerPts(1);
|
||||
}
|
||||
|
||||
static const std::unordered_map<int, cv::Mat> kTag16h5Images =
|
||||
LoadAprilTagImages();
|
||||
static const std::vector<cv::Point2f> kTag16h5MarkPts = Get16h5MarkerPts();
|
||||
|
||||
[[maybe_unused]] static void UpdateVideoProp(cs::CvSource& video,
|
||||
const SimCameraProperties& prop) {
|
||||
video.SetResolution(prop.GetResWidth(), prop.GetResHeight());
|
||||
video.SetFPS(prop.GetFPS().to<int>());
|
||||
}
|
||||
|
||||
[[maybe_unused]] static void Warp165h5TagImage(
|
||||
int tagId, const std::vector<cv::Point2f>& dstPoints, bool antialiasing,
|
||||
cv::Mat& destination) {
|
||||
if (!kTag16h5Images.contains(tagId)) {
|
||||
return;
|
||||
}
|
||||
cv::Mat tagImage = kTag16h5Images.at(tagId);
|
||||
std::vector<cv::Point2f> tagPoints{kTag16h5MarkPts};
|
||||
std::vector<cv::Point2f> tagImageCorners{GetImageCorners(tagImage.size())};
|
||||
std::vector<cv::Point2f> dstPointMat = dstPoints;
|
||||
cv::Rect boundingRect = cv::boundingRect(dstPointMat);
|
||||
cv::Mat perspecTrf = cv::getPerspectiveTransform(tagPoints, dstPointMat);
|
||||
std::vector<cv::Point2f> extremeCorners{};
|
||||
cv::perspectiveTransform(tagImageCorners, extremeCorners, perspecTrf);
|
||||
boundingRect = cv::boundingRect(extremeCorners);
|
||||
|
||||
double warpedContourArea = cv::contourArea(extremeCorners);
|
||||
double warpedTagUpscale =
|
||||
std::sqrt(warpedContourArea) / std::sqrt(tagImage.size().area());
|
||||
int warpStrat = cv::INTER_NEAREST;
|
||||
|
||||
int supersampling = 6;
|
||||
supersampling = static_cast<int>(std::ceil(supersampling / warpedTagUpscale));
|
||||
supersampling = std::max(std::min(supersampling, 8), 1);
|
||||
|
||||
cv::Mat scaledTagImage{};
|
||||
if (warpedTagUpscale > 2.0) {
|
||||
warpStrat = cv::INTER_LINEAR;
|
||||
int scaleFactor = static_cast<int>(warpedTagUpscale / 3.0) + 2;
|
||||
scaleFactor = std::max(std::min(scaleFactor, 40), 1);
|
||||
scaleFactor *= supersampling;
|
||||
cv::resize(tagImage, scaledTagImage, cv::Size{}, scaleFactor, scaleFactor,
|
||||
cv::INTER_NEAREST);
|
||||
tagPoints = Get16h5MarkerPts(scaleFactor);
|
||||
} else {
|
||||
scaledTagImage = tagImage;
|
||||
}
|
||||
|
||||
boundingRect.x -= 1;
|
||||
boundingRect.y -= 1;
|
||||
boundingRect.width += 2;
|
||||
boundingRect.height += 2;
|
||||
if (boundingRect.x < 0) {
|
||||
boundingRect.width += boundingRect.x;
|
||||
boundingRect.x = 0;
|
||||
}
|
||||
if (boundingRect.y < 0) {
|
||||
boundingRect.height += boundingRect.y;
|
||||
boundingRect.y = 0;
|
||||
}
|
||||
boundingRect.width =
|
||||
std::min(destination.size().width - boundingRect.x, boundingRect.width);
|
||||
boundingRect.height =
|
||||
std::min(destination.size().height - boundingRect.y, boundingRect.height);
|
||||
if (boundingRect.width <= 0 || boundingRect.height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<cv::Point2f> scaledDstPts{};
|
||||
if (supersampling > 1) {
|
||||
cv::multiply(dstPointMat,
|
||||
cv::Scalar{static_cast<double>(supersampling),
|
||||
static_cast<double>(supersampling)},
|
||||
scaledDstPts);
|
||||
boundingRect.x *= supersampling;
|
||||
boundingRect.y *= supersampling;
|
||||
boundingRect.width *= supersampling;
|
||||
boundingRect.height *= supersampling;
|
||||
} else {
|
||||
scaledDstPts = dstPointMat;
|
||||
}
|
||||
|
||||
cv::subtract(scaledDstPts,
|
||||
cv::Scalar{static_cast<double>(boundingRect.tl().x),
|
||||
static_cast<double>(boundingRect.tl().y)},
|
||||
scaledDstPts);
|
||||
perspecTrf = cv::getPerspectiveTransform(tagPoints, scaledDstPts);
|
||||
|
||||
cv::Mat tempRoi{};
|
||||
cv::warpPerspective(scaledTagImage, tempRoi, perspecTrf, boundingRect.size(),
|
||||
warpStrat);
|
||||
|
||||
if (supersampling > 1) {
|
||||
boundingRect.x /= supersampling;
|
||||
boundingRect.y /= supersampling;
|
||||
boundingRect.width /= supersampling;
|
||||
boundingRect.height /= supersampling;
|
||||
cv::resize(tempRoi, tempRoi, boundingRect.size(), 0, 0, cv::INTER_AREA);
|
||||
}
|
||||
|
||||
cv::Mat tempMask{cv::Mat::zeros(tempRoi.size(), CV_8UC1)};
|
||||
cv::subtract(extremeCorners,
|
||||
cv::Scalar{static_cast<float>(boundingRect.tl().x),
|
||||
static_cast<float>(boundingRect.tl().y)},
|
||||
extremeCorners);
|
||||
cv::Point2f tempCenter{};
|
||||
tempCenter.x =
|
||||
std::accumulate(extremeCorners.begin(), extremeCorners.end(), 0.0,
|
||||
[extremeCorners](float acc, const cv::Point2f& p2) {
|
||||
return acc + p2.x / extremeCorners.size();
|
||||
});
|
||||
tempCenter.y =
|
||||
std::accumulate(extremeCorners.begin(), extremeCorners.end(), 0.0,
|
||||
[extremeCorners](float acc, const cv::Point2f& p2) {
|
||||
return acc + p2.y / extremeCorners.size();
|
||||
});
|
||||
|
||||
for (auto& corner : extremeCorners) {
|
||||
float xDiff = corner.x - tempCenter.x;
|
||||
float yDiff = corner.y - tempCenter.y;
|
||||
xDiff += 1 * mathutil::sgn(xDiff);
|
||||
yDiff += 1 * mathutil::sgn(yDiff);
|
||||
corner = cv::Point2f{tempCenter.x + xDiff, tempCenter.y + yDiff};
|
||||
}
|
||||
|
||||
std::vector<cv::Point> extremeCornerInt{extremeCorners.begin(),
|
||||
extremeCorners.end()};
|
||||
cv::fillConvexPoly(tempMask, extremeCornerInt, cv::Scalar{255});
|
||||
|
||||
cv::copyTo(tempRoi, destination(boundingRect), tempMask);
|
||||
}
|
||||
|
||||
static double GetScaledThickness(double thickness480p,
|
||||
const cv::Mat& destinationMat) {
|
||||
double scaleX = destinationMat.size().width / 640.0;
|
||||
double scaleY = destinationMat.size().height / 480.0;
|
||||
double minScale = std::min(scaleX, scaleY);
|
||||
return std::max(thickness480p * minScale, 1.0);
|
||||
}
|
||||
|
||||
[[maybe_unused]] static void DrawInscribedEllipse(
|
||||
const std::vector<cv::Point2f>& dstPoints, const cv::Scalar& color,
|
||||
cv::Mat& destination) {
|
||||
cv::RotatedRect rect = OpenCVHelp::GetMinAreaRect(dstPoints);
|
||||
cv::ellipse(destination, rect, color, -1, cv::LINE_AA);
|
||||
}
|
||||
|
||||
static void DrawPoly(const std::vector<cv::Point2f>& dstPoints, int thickness,
|
||||
const cv::Scalar& color, bool isClosed,
|
||||
cv::Mat& destination) {
|
||||
std::vector<cv::Point> intDstPoints{dstPoints.begin(), dstPoints.end()};
|
||||
std::vector<std::vector<cv::Point>> listOfListOfPoints;
|
||||
listOfListOfPoints.emplace_back(intDstPoints);
|
||||
if (thickness > 0) {
|
||||
cv::polylines(destination, listOfListOfPoints, isClosed, color, thickness,
|
||||
cv::LINE_AA);
|
||||
} else {
|
||||
cv::fillPoly(destination, listOfListOfPoints, color, cv::LINE_AA);
|
||||
}
|
||||
}
|
||||
|
||||
[[maybe_unused]] static void DrawTagDetection(
|
||||
int id, const std::vector<cv::Point2f>& dstPoints, cv::Mat& destination) {
|
||||
double thickness = GetScaledThickness(1, destination);
|
||||
DrawPoly(dstPoints, static_cast<int>(thickness), cv::Scalar{0, 0, 255}, true,
|
||||
destination);
|
||||
cv::Rect2d rect{cv::boundingRect(dstPoints)};
|
||||
cv::Point2d textPt{rect.x + rect.width, rect.y};
|
||||
textPt.x += thickness;
|
||||
textPt.y += thickness;
|
||||
cv::putText(destination, std::to_string(id), textPt, cv::FONT_HERSHEY_PLAIN,
|
||||
1.5 * thickness, cv::Scalar{0, 200, 0},
|
||||
static_cast<int>(thickness), cv::LINE_AA);
|
||||
}
|
||||
|
||||
static std::vector<std::vector<frc::Translation3d>> GetFieldWallLines() {
|
||||
std::vector<std::vector<frc::Translation3d>> list;
|
||||
|
||||
const units::meter_t sideHt = 19.5_in;
|
||||
const units::meter_t driveHt = 35_in;
|
||||
const units::meter_t topHt = 78_in;
|
||||
|
||||
// field floor
|
||||
list.emplace_back(std::vector<frc::Translation3d>{
|
||||
frc::Translation3d{0_m, 0_m, 0_m},
|
||||
frc::Translation3d{fieldLength, 0_m, 0_m},
|
||||
frc::Translation3d{fieldLength, fieldWidth, 0_m},
|
||||
frc::Translation3d{0_m, fieldWidth, 0_m},
|
||||
frc::Translation3d{0_m, 0_m, 0_m}});
|
||||
|
||||
// right side wall
|
||||
list.emplace_back(std::vector<frc::Translation3d>{
|
||||
frc::Translation3d{0_m, 0_m, 0_m}, frc::Translation3d{0_m, 0_m, sideHt},
|
||||
frc::Translation3d{fieldLength, 0_m, sideHt},
|
||||
frc::Translation3d{fieldLength, 0_m, 0_m}});
|
||||
|
||||
// red driverstation
|
||||
list.emplace_back(std::vector<frc::Translation3d>{
|
||||
frc::Translation3d{fieldLength, 0_m, sideHt},
|
||||
frc::Translation3d{fieldLength, 0_m, topHt},
|
||||
frc::Translation3d{fieldLength, fieldWidth, topHt},
|
||||
frc::Translation3d{fieldLength, fieldWidth, sideHt},
|
||||
});
|
||||
list.emplace_back(std::vector<frc::Translation3d>{
|
||||
frc::Translation3d{fieldLength, 0_m, driveHt},
|
||||
frc::Translation3d{fieldLength, fieldWidth, driveHt}});
|
||||
|
||||
// left side wall
|
||||
list.emplace_back(std::vector<frc::Translation3d>{
|
||||
frc::Translation3d{0_m, fieldWidth, 0_m},
|
||||
frc::Translation3d{0_m, fieldWidth, sideHt},
|
||||
frc::Translation3d{fieldLength, fieldWidth, sideHt},
|
||||
frc::Translation3d{fieldLength, fieldWidth, 0_m}});
|
||||
|
||||
// blue driverstation
|
||||
list.emplace_back(std::vector<frc::Translation3d>{
|
||||
frc::Translation3d{0_m, 0_m, sideHt},
|
||||
frc::Translation3d{0_m, 0_m, topHt},
|
||||
frc::Translation3d{0_m, fieldWidth, topHt},
|
||||
frc::Translation3d{0_m, fieldWidth, sideHt},
|
||||
});
|
||||
list.emplace_back(std::vector<frc::Translation3d>{
|
||||
frc::Translation3d{0_m, 0_m, driveHt},
|
||||
frc::Translation3d{0_m, fieldWidth, driveHt}});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
static std::vector<std::vector<frc::Translation3d>> GetFieldFloorLines(
|
||||
int subdivisions) {
|
||||
std::vector<std::vector<frc::Translation3d>> list;
|
||||
const units::meter_t subLength = fieldLength / subdivisions;
|
||||
const units::meter_t subWidth = fieldWidth / subdivisions;
|
||||
|
||||
for (int i = 0; i < subdivisions; i++) {
|
||||
list.emplace_back(std::vector<frc::Translation3d>{
|
||||
frc::Translation3d{0_m, subWidth * (i + 1), 0_m},
|
||||
frc::Translation3d{fieldLength, subWidth * (i + 1), 0_m}});
|
||||
list.emplace_back(std::vector<frc::Translation3d>{
|
||||
frc::Translation3d{subLength * (i + 1), 0_m, 0_m},
|
||||
frc::Translation3d{subLength * (i + 1), fieldWidth, 0_m}});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
static std::vector<std::vector<cv::Point2f>> PolyFrom3dLines(
|
||||
const RotTrlTransform3d& camRt, const SimCameraProperties& prop,
|
||||
const std::vector<frc::Translation3d>& trls, double resolution,
|
||||
bool isClosed, cv::Mat& destination) {
|
||||
resolution = std::hypot(destination.size().height, destination.size().width) *
|
||||
resolution;
|
||||
std::vector<frc::Translation3d> pts{trls};
|
||||
if (isClosed) {
|
||||
pts.emplace_back(pts[0]);
|
||||
}
|
||||
std::vector<std::vector<cv::Point2f>> polyPointList{};
|
||||
|
||||
for (size_t i = 0; i < pts.size() - 1; i++) {
|
||||
frc::Translation3d pta = pts[i];
|
||||
frc::Translation3d ptb = pts[i + 1];
|
||||
|
||||
std::pair<std::optional<double>, std::optional<double>> inter =
|
||||
prop.GetVisibleLine(camRt, pta, ptb);
|
||||
if (!inter.second) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double inter1 = inter.first.value();
|
||||
double inter2 = inter.second.value();
|
||||
frc::Translation3d baseDelta = ptb - pta;
|
||||
frc::Translation3d old_pta = pta;
|
||||
if (inter1 > 0) {
|
||||
pta = old_pta + baseDelta * inter1;
|
||||
}
|
||||
if (inter2 < 1) {
|
||||
ptb = old_pta + baseDelta * inter2;
|
||||
}
|
||||
baseDelta = ptb - pta;
|
||||
|
||||
std::vector<cv::Point2f> poly = OpenCVHelp::ProjectPoints(
|
||||
prop.GetIntrinsics(), prop.GetDistCoeffs(), camRt, {pta, ptb});
|
||||
cv::Point2d pxa = poly[0];
|
||||
cv::Point2d pxb = poly[1];
|
||||
|
||||
double pxDist = std::hypot(pxb.x - pxa.x, pxb.y - pxa.y);
|
||||
int subdivisions = static_cast<int>(pxDist / resolution);
|
||||
frc::Translation3d subDelta = baseDelta / (subdivisions + 1);
|
||||
std::vector<frc::Translation3d> subPts{};
|
||||
for (int j = 0; j < subdivisions; j++) {
|
||||
subPts.emplace_back(pta + (subDelta * (j + 1)));
|
||||
}
|
||||
if (subPts.size() > 0) {
|
||||
std::vector<cv::Point2f> toAdd = OpenCVHelp::ProjectPoints(
|
||||
prop.GetIntrinsics(), prop.GetDistCoeffs(), camRt, subPts);
|
||||
poly.insert(poly.begin() + 1, toAdd.begin(), toAdd.end());
|
||||
}
|
||||
|
||||
polyPointList.emplace_back(poly);
|
||||
}
|
||||
|
||||
return polyPointList;
|
||||
}
|
||||
|
||||
[[maybe_unused]] static void DrawFieldWireFrame(
|
||||
const RotTrlTransform3d& camRt, const SimCameraProperties& prop,
|
||||
double resolution, double wallThickness, const cv::Scalar& wallColor,
|
||||
int floorSubdivisions, double floorThickness, const cv::Scalar& floorColor,
|
||||
cv::Mat& destination) {
|
||||
for (const auto& trls : GetFieldFloorLines(floorSubdivisions)) {
|
||||
auto polys =
|
||||
PolyFrom3dLines(camRt, prop, trls, resolution, false, destination);
|
||||
for (const auto& poly : polys) {
|
||||
DrawPoly(poly,
|
||||
static_cast<int>(
|
||||
std::round(GetScaledThickness(floorThickness, destination))),
|
||||
floorColor, false, destination);
|
||||
}
|
||||
}
|
||||
for (const auto& trls : GetFieldWallLines()) {
|
||||
auto polys =
|
||||
PolyFrom3dLines(camRt, prop, trls, resolution, false, destination);
|
||||
for (const auto& poly : polys) {
|
||||
DrawPoly(poly,
|
||||
static_cast<int>(
|
||||
std::round(GetScaledThickness(wallThickness, destination))),
|
||||
wallColor, false, destination);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace VideoSimUtil
|
||||
} // namespace photon
|
||||
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <frc/Timer.h>
|
||||
#include <frc/interpolation/TimeInterpolatableBuffer.h>
|
||||
#include <frc/smartdashboard/Field2d.h>
|
||||
#include <frc/smartdashboard/FieldObject2d.h>
|
||||
#include <frc/smartdashboard/SmartDashboard.h>
|
||||
|
||||
#include "photon/simulation/PhotonCameraSim.h"
|
||||
|
||||
namespace photon {
|
||||
class VisionSystemSim {
|
||||
public:
|
||||
explicit VisionSystemSim(std::string visionSystemName) {
|
||||
std::string tableName = "VisionSystemSim-" + visionSystemName;
|
||||
frc::SmartDashboard::PutData(tableName + "/Sim Field", &dbgField);
|
||||
}
|
||||
std::optional<PhotonCameraSim*> GetCameraSim(std::string name) {
|
||||
auto it = camSimMap.find(name);
|
||||
if (it != camSimMap.end()) {
|
||||
return std::make_optional(it->second);
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
std::vector<PhotonCameraSim*> GetCameraSims() {
|
||||
std::vector<PhotonCameraSim*> retVal;
|
||||
for (auto const& cam : camSimMap) {
|
||||
retVal.emplace_back(cam.second);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
void AddCamera(PhotonCameraSim* cameraSim,
|
||||
const frc::Transform3d& robotToCamera) {
|
||||
auto found =
|
||||
camSimMap.find(std::string{cameraSim->GetCamera()->GetCameraName()});
|
||||
if (found == camSimMap.end()) {
|
||||
camSimMap[std::string{cameraSim->GetCamera()->GetCameraName()}] =
|
||||
cameraSim;
|
||||
camTrfMap.insert(std::make_pair(
|
||||
std::move(cameraSim),
|
||||
frc::TimeInterpolatableBuffer<frc::Pose3d>{bufferLength}));
|
||||
camTrfMap.at(cameraSim).AddSample(frc::Timer::GetFPGATimestamp(),
|
||||
frc::Pose3d{} + robotToCamera);
|
||||
}
|
||||
}
|
||||
void ClearCameras() {
|
||||
camSimMap.clear();
|
||||
camTrfMap.clear();
|
||||
}
|
||||
bool RemoveCamera(PhotonCameraSim* cameraSim) {
|
||||
int numOfElementsRemoved =
|
||||
camSimMap.erase(std::string{cameraSim->GetCamera()->GetCameraName()});
|
||||
if (numOfElementsRemoved == 1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::optional<frc::Transform3d> GetRobotToCamera(PhotonCameraSim* cameraSim) {
|
||||
return GetRobotToCamera(cameraSim, frc::Timer::GetFPGATimestamp());
|
||||
}
|
||||
std::optional<frc::Transform3d> GetRobotToCamera(PhotonCameraSim* cameraSim,
|
||||
units::second_t time) {
|
||||
if (camTrfMap.find(cameraSim) != camTrfMap.end()) {
|
||||
frc::TimeInterpolatableBuffer<frc::Pose3d> trfBuffer =
|
||||
camTrfMap.at(cameraSim);
|
||||
std::optional<frc::Pose3d> sample = trfBuffer.Sample(time);
|
||||
if (!sample) {
|
||||
return std::nullopt;
|
||||
} else {
|
||||
return std::make_optional(
|
||||
frc::Transform3d{frc::Pose3d{}, sample.value_or(frc::Pose3d{})});
|
||||
}
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
std::optional<frc::Pose3d> GetCameraPose(PhotonCameraSim* cameraSim) {
|
||||
return GetCameraPose(cameraSim, frc::Timer::GetFPGATimestamp());
|
||||
}
|
||||
std::optional<frc::Pose3d> GetCameraPose(PhotonCameraSim* cameraSim,
|
||||
units::second_t time) {
|
||||
auto robotToCamera = GetRobotToCamera(cameraSim, time);
|
||||
if (!robotToCamera) {
|
||||
return std::nullopt;
|
||||
} else {
|
||||
return std::make_optional(GetRobotPose(time) + robotToCamera.value());
|
||||
}
|
||||
}
|
||||
bool AdjustCamera(PhotonCameraSim* cameraSim,
|
||||
const frc::Transform3d& robotToCamera) {
|
||||
if (camTrfMap.find(cameraSim) != camTrfMap.end()) {
|
||||
camTrfMap.at(cameraSim).AddSample(frc::Timer::GetFPGATimestamp(),
|
||||
frc::Pose3d{} + robotToCamera);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void ResetCameraTransforms() {
|
||||
for (const auto& pair : camTrfMap) {
|
||||
ResetCameraTransforms(pair.first);
|
||||
}
|
||||
}
|
||||
bool ResetCameraTransforms(PhotonCameraSim* cameraSim) {
|
||||
units::second_t now = frc::Timer::GetFPGATimestamp();
|
||||
if (camTrfMap.find(cameraSim) != camTrfMap.end()) {
|
||||
auto trfBuffer = camTrfMap.at(cameraSim);
|
||||
frc::Transform3d lastTrf{frc::Pose3d{},
|
||||
trfBuffer.Sample(now).value_or(frc::Pose3d{})};
|
||||
trfBuffer.Clear();
|
||||
AdjustCamera(cameraSim, lastTrf);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::vector<VisionTargetSim> GetVisionTargets() {
|
||||
std::vector<VisionTargetSim> all{};
|
||||
for (const auto& entry : targetSets) {
|
||||
for (const auto& target : entry.second) {
|
||||
all.emplace_back(target);
|
||||
}
|
||||
}
|
||||
return all;
|
||||
}
|
||||
std::vector<VisionTargetSim> GetVisionTargets(std::string type) {
|
||||
return targetSets[type];
|
||||
}
|
||||
void AddVisionTargets(const std::vector<VisionTargetSim>& targets) {
|
||||
AddVisionTargets("targets", targets);
|
||||
}
|
||||
void AddVisionTargets(std::string type,
|
||||
const std::vector<VisionTargetSim>& targets) {
|
||||
if (!targetSets.contains(type)) {
|
||||
targetSets[type] = std::vector<VisionTargetSim>{};
|
||||
}
|
||||
for (const auto& tgt : targets) {
|
||||
targetSets[type].emplace_back(tgt);
|
||||
}
|
||||
}
|
||||
void AddAprilTags(const frc::AprilTagFieldLayout& layout) {
|
||||
std::vector<VisionTargetSim> targets;
|
||||
for (const frc::AprilTag& tag : layout.GetTags()) {
|
||||
targets.emplace_back(VisionTargetSim{layout.GetTagPose(tag.ID).value(),
|
||||
photon::kAprilTag16h5, tag.ID});
|
||||
}
|
||||
AddVisionTargets("apriltag", targets);
|
||||
}
|
||||
void ClearVisionTargets() { targetSets.clear(); }
|
||||
void ClearAprilTags() { RemoveVisionTargets("apriltag"); }
|
||||
void RemoveVisionTargets(std::string type) { targetSets.erase(type); }
|
||||
std::vector<VisionTargetSim> RemoveVisionTargets(
|
||||
const std::vector<VisionTargetSim>& targets) {
|
||||
std::vector<VisionTargetSim> removedList;
|
||||
for (auto& entry : targetSets) {
|
||||
for (auto target : entry.second) {
|
||||
auto it = std::find(targets.begin(), targets.end(), target);
|
||||
if (it != targets.end()) {
|
||||
removedList.emplace_back(target);
|
||||
entry.second.erase(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
return removedList;
|
||||
}
|
||||
frc::Pose3d GetRobotPose() {
|
||||
return GetRobotPose(frc::Timer::GetFPGATimestamp());
|
||||
}
|
||||
frc::Pose3d GetRobotPose(units::second_t timestamp) {
|
||||
return robotPoseBuffer.Sample(timestamp).value_or(frc::Pose3d{});
|
||||
}
|
||||
void ResetRobotPose(const frc::Pose2d& robotPose) {
|
||||
ResetRobotPose(frc::Pose3d{robotPose});
|
||||
}
|
||||
void ResetRobotPose(const frc::Pose3d& robotPose) {
|
||||
robotPoseBuffer.Clear();
|
||||
robotPoseBuffer.AddSample(frc::Timer::GetFPGATimestamp(), robotPose);
|
||||
}
|
||||
frc::Field2d& GetDebugField() { return dbgField; }
|
||||
void Update(const frc::Pose2d& robotPose) { Update(frc::Pose3d{robotPose}); }
|
||||
void Update(const frc::Pose3d& robotPose) {
|
||||
for (auto& set : targetSets) {
|
||||
std::vector<frc::Pose2d> posesToAdd{};
|
||||
for (auto& target : set.second) {
|
||||
posesToAdd.emplace_back(target.GetPose().ToPose2d());
|
||||
}
|
||||
dbgField.GetObject(set.first)->SetPoses(posesToAdd);
|
||||
}
|
||||
|
||||
units::second_t now = frc::Timer::GetFPGATimestamp();
|
||||
robotPoseBuffer.AddSample(now, robotPose);
|
||||
dbgField.SetRobotPose(robotPose.ToPose2d());
|
||||
|
||||
std::vector<VisionTargetSim> allTargets{};
|
||||
for (const auto& set : targetSets) {
|
||||
for (const auto& target : set.second) {
|
||||
allTargets.emplace_back(target);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<frc::Pose2d> visTgtPoses2d{};
|
||||
std::vector<frc::Pose2d> cameraPoses2d{};
|
||||
bool processed{false};
|
||||
for (const auto& entry : camSimMap) {
|
||||
auto camSim = entry.second;
|
||||
auto optTimestamp = camSim->ConsumeNextEntryTime();
|
||||
if (!optTimestamp) {
|
||||
continue;
|
||||
} else {
|
||||
processed = true;
|
||||
}
|
||||
uint64_t timestampNt = optTimestamp.value();
|
||||
units::second_t latency = camSim->prop.EstLatency();
|
||||
units::second_t timestampCapture =
|
||||
units::microsecond_t{static_cast<double>(timestampNt)} - latency;
|
||||
|
||||
frc::Pose3d lateRobotPose = GetRobotPose(timestampCapture);
|
||||
frc::Pose3d lateCameraPose =
|
||||
lateRobotPose + GetRobotToCamera(camSim, timestampCapture).value();
|
||||
cameraPoses2d.push_back(lateCameraPose.ToPose2d());
|
||||
|
||||
auto camResult = camSim->Process(latency, lateCameraPose, allTargets);
|
||||
camSim->SubmitProcessedFrame(camResult, timestampNt);
|
||||
for (const auto& target : camResult.GetTargets()) {
|
||||
auto trf = target.GetBestCameraToTarget();
|
||||
if (trf == kEmptyTrf) {
|
||||
continue;
|
||||
}
|
||||
visTgtPoses2d.push_back(lateCameraPose.TransformBy(trf).ToPose2d());
|
||||
}
|
||||
}
|
||||
if (processed) {
|
||||
dbgField.GetObject("visibleTargetPoses")->SetPoses(visTgtPoses2d);
|
||||
}
|
||||
if (cameraPoses2d.size() != 0) {
|
||||
dbgField.GetObject("cameras")->SetPoses(cameraPoses2d);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, PhotonCameraSim*> camSimMap{};
|
||||
static constexpr units::second_t bufferLength{1.5_s};
|
||||
std::unordered_map<PhotonCameraSim*,
|
||||
frc::TimeInterpolatableBuffer<frc::Pose3d>>
|
||||
camTrfMap;
|
||||
frc::TimeInterpolatableBuffer<frc::Pose3d> robotPoseBuffer{bufferLength};
|
||||
std::unordered_map<std::string, std::vector<VisionTargetSim>> targetSets{};
|
||||
frc::Field2d dbgField{};
|
||||
const frc::Transform3d kEmptyTrf{};
|
||||
};
|
||||
} // namespace photon
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <frc/geometry/Pose3d.h>
|
||||
|
||||
#include "photon/estimation/TargetModel.h"
|
||||
|
||||
namespace photon {
|
||||
class VisionTargetSim {
|
||||
public:
|
||||
VisionTargetSim(const frc::Pose3d& pose, const TargetModel& model)
|
||||
: fiducialId(-1), pose(pose), model(model) {}
|
||||
VisionTargetSim(const frc::Pose3d& pose, const TargetModel& model, int id)
|
||||
: fiducialId(id), pose(pose), model(model) {}
|
||||
void SetPose(const frc::Pose3d& newPose) { pose = newPose; }
|
||||
void SetModel(const TargetModel& newModel) { model = newModel; }
|
||||
frc::Pose3d GetPose() const { return pose; }
|
||||
TargetModel GetModel() const { return model; }
|
||||
std::vector<frc::Translation3d> GetFieldVertices() const {
|
||||
return model.GetFieldVertices(pose);
|
||||
}
|
||||
int fiducialId;
|
||||
|
||||
bool operator<(const VisionTargetSim& right) const {
|
||||
return pose.Translation().Norm() < right.pose.Translation().Norm();
|
||||
}
|
||||
|
||||
bool operator==(const VisionTargetSim& other) const {
|
||||
return units::math::abs(pose.Translation().X() -
|
||||
other.GetPose().Translation().X()) < 1_in &&
|
||||
units::math::abs(pose.Translation().Y() -
|
||||
other.GetPose().Translation().Y()) < 1_in &&
|
||||
units::math::abs(pose.Translation().Z() -
|
||||
other.GetPose().Translation().Z()) < 1_in &&
|
||||
units::math::abs(pose.Rotation().X() -
|
||||
other.GetPose().Rotation().X()) < 1_deg &&
|
||||
units::math::abs(pose.Rotation().Y() -
|
||||
other.GetPose().Rotation().Y()) < 1_deg &&
|
||||
units::math::abs(pose.Rotation().Z() -
|
||||
other.GetPose().Rotation().Z()) < 1_deg &&
|
||||
model.GetIsPlanar() == other.GetModel().GetIsPlanar();
|
||||
}
|
||||
|
||||
private:
|
||||
frc::Pose3d pose;
|
||||
TargetModel model;
|
||||
};
|
||||
} // namespace photon
|
||||
461
photon-lib/src/test/native/cpp/VisionSystemSimTest.cpp
Normal file
461
photon-lib/src/test/native/cpp/VisionSystemSimTest.cpp
Normal file
@@ -0,0 +1,461 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "photon/PhotonUtils.h"
|
||||
#include "photon/simulation/VisionSystemSim.h"
|
||||
|
||||
class VisionSystemSimTest : public ::testing::Test {
|
||||
void SetUp() override {
|
||||
nt::NetworkTableInstance::GetDefault().StartServer();
|
||||
photon::PhotonCamera::SetVersionCheckEnabled(false);
|
||||
}
|
||||
|
||||
void TearDown() override {}
|
||||
};
|
||||
|
||||
class VisionSystemSimTestWithParamsTest
|
||||
: public VisionSystemSimTest,
|
||||
public testing::WithParamInterface<units::degree_t> {};
|
||||
class VisionSystemSimTestDistanceParamsTest
|
||||
: public VisionSystemSimTest,
|
||||
public testing::WithParamInterface<
|
||||
std::tuple<units::foot_t, units::degree_t, units::foot_t>> {};
|
||||
|
||||
TEST_F(VisionSystemSimTest, TestVisibilityCupidShuffle) {
|
||||
frc::Pose3d targetPose{
|
||||
frc::Translation3d{15.98_m, 0_m, 2_m},
|
||||
frc::Rotation3d{0_rad, 0_rad, units::radian_t{std::numbers::pi}}};
|
||||
|
||||
photon::VisionSystemSim visionSysSim{"Test"};
|
||||
photon::PhotonCamera camera{"camera"};
|
||||
photon::PhotonCameraSim cameraSim{&camera};
|
||||
visionSysSim.AddCamera(&cameraSim, frc::Transform3d{});
|
||||
cameraSim.prop.SetCalibration(640, 480, frc::Rotation2d{80_deg});
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPose, photon::TargetModel{1.0_m, 1.0_m}, 3}});
|
||||
|
||||
// To the right, to the right
|
||||
frc::Pose2d robotPose{frc::Translation2d{5_m, 0_m}, frc::Rotation2d{-70_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_FALSE(camera.GetLatestResult().HasTargets());
|
||||
|
||||
// To the right, to the right
|
||||
robotPose =
|
||||
frc::Pose2d{frc::Translation2d{5_m, 0_m}, frc::Rotation2d{-95_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_FALSE(camera.GetLatestResult().HasTargets());
|
||||
|
||||
// To the left, to the left
|
||||
robotPose =
|
||||
frc::Pose2d{frc::Translation2d{5_m, 0_m}, frc::Rotation2d{90_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_FALSE(camera.GetLatestResult().HasTargets());
|
||||
|
||||
// To the left, to the left
|
||||
robotPose =
|
||||
frc::Pose2d{frc::Translation2d{5_m, 0_m}, frc::Rotation2d{65_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_FALSE(camera.GetLatestResult().HasTargets());
|
||||
|
||||
// Now kick, now kick
|
||||
robotPose = frc::Pose2d{frc::Translation2d{2_m, 0_m}, frc::Rotation2d{5_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_TRUE(camera.GetLatestResult().HasTargets());
|
||||
|
||||
// Now kick, now kick
|
||||
robotPose =
|
||||
frc::Pose2d{frc::Translation2d{2_m, 0_m}, frc::Rotation2d{-5_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_TRUE(camera.GetLatestResult().HasTargets());
|
||||
|
||||
// Now walk it by yourself
|
||||
robotPose =
|
||||
frc::Pose2d{frc::Translation2d{2_m, 0_m}, frc::Rotation2d{-179_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_FALSE(camera.GetLatestResult().HasTargets());
|
||||
|
||||
// Now walk it by yourself
|
||||
visionSysSim.AdjustCamera(
|
||||
&cameraSim,
|
||||
frc::Transform3d{
|
||||
frc::Translation3d{},
|
||||
frc::Rotation3d{0_deg, 0_deg, units::radian_t{std::numbers::pi}}});
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_TRUE(camera.GetLatestResult().HasTargets());
|
||||
}
|
||||
|
||||
TEST_F(VisionSystemSimTest, TestNotVisibleVert1) {
|
||||
frc::Pose3d targetPose{
|
||||
frc::Translation3d{15.98_m, 0_m, 1_m},
|
||||
frc::Rotation3d{0_rad, 0_rad, units::radian_t{std::numbers::pi}}};
|
||||
|
||||
photon::VisionSystemSim visionSysSim{"Test"};
|
||||
photon::PhotonCamera camera{"camera"};
|
||||
photon::PhotonCameraSim cameraSim{&camera};
|
||||
visionSysSim.AddCamera(&cameraSim, frc::Transform3d{});
|
||||
cameraSim.prop.SetCalibration(640, 480, frc::Rotation2d{80_deg});
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPose, photon::TargetModel{3.0_m, 3.0_m}, 3}});
|
||||
|
||||
frc::Pose2d robotPose{frc::Translation2d{5_m, 0_m}, frc::Rotation2d{5_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_TRUE(camera.GetLatestResult().HasTargets());
|
||||
|
||||
visionSysSim.AdjustCamera(
|
||||
&cameraSim,
|
||||
frc::Transform3d{
|
||||
frc::Translation3d{0_m, 0_m, 5000_m},
|
||||
frc::Rotation3d{0_deg, 0_deg, units::radian_t{std::numbers::pi}}});
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_FALSE(camera.GetLatestResult().HasTargets());
|
||||
}
|
||||
|
||||
TEST_F(VisionSystemSimTest, TestNotVisibleVert2) {
|
||||
frc::Pose3d targetPose{
|
||||
frc::Translation3d{15.98_m, 0_m, 2_m},
|
||||
frc::Rotation3d{0_rad, 0_rad, units::radian_t{std::numbers::pi}}};
|
||||
|
||||
frc::Transform3d robotToCamera{
|
||||
frc::Translation3d{0_m, 0_m, 1_m},
|
||||
frc::Rotation3d{0_rad, units::radian_t{-std::numbers::pi / 4}, 0_rad}};
|
||||
|
||||
photon::VisionSystemSim visionSysSim{"Test"};
|
||||
photon::PhotonCamera camera{"camera"};
|
||||
photon::PhotonCameraSim cameraSim{&camera};
|
||||
visionSysSim.AddCamera(&cameraSim, robotToCamera);
|
||||
cameraSim.prop.SetCalibration(1234, 1234, frc::Rotation2d{80_deg});
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPose, photon::TargetModel{0.5_m, 0.5_m}, 1736}});
|
||||
|
||||
frc::Pose2d robotPose{frc::Translation2d{13.98_m, 0_m},
|
||||
frc::Rotation2d{5_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_TRUE(camera.GetLatestResult().HasTargets());
|
||||
|
||||
robotPose = frc::Pose2d{frc::Translation2d{0_m, 0_m}, frc::Rotation2d{5_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_FALSE(camera.GetLatestResult().HasTargets());
|
||||
}
|
||||
|
||||
TEST_F(VisionSystemSimTest, TestNotVisibleTargetSize) {
|
||||
frc::Pose3d targetPose{
|
||||
frc::Translation3d{15.98_m, 0_m, 1_m},
|
||||
frc::Rotation3d{0_rad, 0_rad, units::radian_t{std::numbers::pi}}};
|
||||
|
||||
photon::VisionSystemSim visionSysSim{"Test"};
|
||||
photon::PhotonCamera camera{"camera"};
|
||||
photon::PhotonCameraSim cameraSim{&camera};
|
||||
visionSysSim.AddCamera(&cameraSim, frc::Transform3d{});
|
||||
cameraSim.prop.SetCalibration(640, 480, frc::Rotation2d{80_deg});
|
||||
cameraSim.SetMinTargetAreaPixels(20.0);
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPose, photon::TargetModel{0.1_m, 0.1_m}, 24}});
|
||||
|
||||
frc::Pose2d robotPose{frc::Translation2d{12_m, 0_m}, frc::Rotation2d{5_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_TRUE(camera.GetLatestResult().HasTargets());
|
||||
|
||||
robotPose = frc::Pose2d{frc::Translation2d{0_m, 0_m}, frc::Rotation2d{5_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_FALSE(camera.GetLatestResult().HasTargets());
|
||||
}
|
||||
|
||||
TEST_F(VisionSystemSimTest, TestNotVisibleTooFarLeds) {
|
||||
frc::Pose3d targetPose{
|
||||
frc::Translation3d{15.98_m, 0_m, 1_m},
|
||||
frc::Rotation3d{0_rad, 0_rad, units::radian_t{std::numbers::pi}}};
|
||||
|
||||
photon::VisionSystemSim visionSysSim{"Test"};
|
||||
photon::PhotonCamera camera{"camera"};
|
||||
photon::PhotonCameraSim cameraSim{&camera};
|
||||
visionSysSim.AddCamera(&cameraSim, frc::Transform3d{});
|
||||
cameraSim.prop.SetCalibration(640, 480, frc::Rotation2d{80_deg});
|
||||
cameraSim.SetMinTargetAreaPixels(1.0);
|
||||
cameraSim.SetMaxSightRange(10_m);
|
||||
visionSysSim.AddVisionTargets(
|
||||
{photon::VisionTargetSim{targetPose, photon::TargetModel{1_m, 1_m}, 25}});
|
||||
|
||||
frc::Pose2d robotPose{frc::Translation2d{10_m, 0_m}, frc::Rotation2d{5_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_TRUE(camera.GetLatestResult().HasTargets());
|
||||
|
||||
robotPose = frc::Pose2d{frc::Translation2d{0_m, 0_m}, frc::Rotation2d{5_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_FALSE(camera.GetLatestResult().HasTargets());
|
||||
}
|
||||
|
||||
TEST_P(VisionSystemSimTestWithParamsTest, YawAngles) {
|
||||
const frc::Pose3d targetPose{
|
||||
{15.98_m, 0_m, 0_m},
|
||||
frc::Rotation3d{0_deg, 0_deg, units::radian_t{3 * std::numbers::pi / 4}}};
|
||||
frc::Pose2d robotPose{{10_m, 0_m}, frc::Rotation2d{GetParam() * -1.0}};
|
||||
photon::VisionSystemSim visionSysSim{"Test"};
|
||||
photon::PhotonCamera camera{"camera"};
|
||||
photon::PhotonCameraSim cameraSim{&camera};
|
||||
visionSysSim.AddCamera(&cameraSim, frc::Transform3d{});
|
||||
cameraSim.prop.SetCalibration(640, 480, frc::Rotation2d{80_deg});
|
||||
cameraSim.SetMinTargetAreaPixels(0.0);
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPose, photon::TargetModel{0.5_m, 0.5_m}, 3}});
|
||||
|
||||
robotPose = frc::Pose2d{frc::Translation2d{10_m, 0_m},
|
||||
frc::Rotation2d{-1 * GetParam()}};
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_TRUE(camera.GetLatestResult().HasTargets());
|
||||
ASSERT_NEAR(GetParam().to<double>(),
|
||||
camera.GetLatestResult().GetBestTarget().GetYaw(), 0.25);
|
||||
}
|
||||
|
||||
TEST_P(VisionSystemSimTestWithParamsTest, PitchAngles) {
|
||||
const frc::Pose3d targetPose{
|
||||
{15.98_m, 0_m, 0_m},
|
||||
frc::Rotation3d{0_deg, 0_deg, units::radian_t{3 * std::numbers::pi / 4}}};
|
||||
frc::Pose2d robotPose{{10_m, 0_m}, frc::Rotation2d{GetParam() * -1.0}};
|
||||
photon::VisionSystemSim visionSysSim{"Test"};
|
||||
photon::PhotonCamera camera{"camera"};
|
||||
photon::PhotonCameraSim cameraSim{&camera};
|
||||
visionSysSim.AddCamera(&cameraSim, frc::Transform3d{});
|
||||
cameraSim.prop.SetCalibration(640, 480, frc::Rotation2d{120_deg});
|
||||
cameraSim.SetMinTargetAreaPixels(0.0);
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPose, photon::TargetModel{0.5_m, 0.5_m}, 3}});
|
||||
|
||||
robotPose = frc::Pose2d{frc::Translation2d{10_m, 0_m},
|
||||
frc::Rotation2d{-1 * GetParam()}};
|
||||
visionSysSim.AdjustCamera(
|
||||
&cameraSim,
|
||||
frc::Transform3d{
|
||||
frc::Translation3d{},
|
||||
frc::Rotation3d{0_rad, units::degree_t{GetParam()}, 0_rad}});
|
||||
visionSysSim.Update(robotPose);
|
||||
ASSERT_TRUE(camera.GetLatestResult().HasTargets());
|
||||
ASSERT_NEAR(GetParam().to<double>(),
|
||||
camera.GetLatestResult().GetBestTarget().GetPitch(), 0.25);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(AnglesTests, VisionSystemSimTestWithParamsTest,
|
||||
testing::Values(-10_deg, -5_deg, -0_deg, -1_deg,
|
||||
-2_deg, 5_deg, 7_deg, 10.23_deg));
|
||||
|
||||
TEST_P(VisionSystemSimTestDistanceParamsTest, DistanceCalc) {
|
||||
units::foot_t distParam;
|
||||
units::degree_t pitchParam;
|
||||
units::foot_t heightParam;
|
||||
std::tie(distParam, pitchParam, heightParam) = GetParam();
|
||||
|
||||
const frc::Pose3d targetPose{
|
||||
{15.98_m, 0_m, 1_m},
|
||||
frc::Rotation3d{0_deg, 0_deg, units::radian_t{std::numbers::pi * 0.98}}};
|
||||
frc::Pose3d robotPose{{15.98_m - distParam, 0_m, 0_m}, frc::Rotation3d{}};
|
||||
frc::Transform3d robotToCamera{frc::Translation3d{0_m, 0_m, heightParam},
|
||||
frc::Rotation3d{0_deg, pitchParam, 0_deg}};
|
||||
photon::VisionSystemSim visionSysSim{
|
||||
"absurdlylongnamewhichshouldneveractuallyhappenbuteehwelltestitanywaysoho"
|
||||
"wsyourdaygoingihopegoodhaveagreatrestofyourlife"};
|
||||
photon::PhotonCamera camera{"camera"};
|
||||
photon::PhotonCameraSim cameraSim{&camera};
|
||||
visionSysSim.AddCamera(&cameraSim, frc::Transform3d{});
|
||||
cameraSim.prop.SetCalibration(640, 480, frc::Rotation2d{160_deg});
|
||||
cameraSim.SetMinTargetAreaPixels(0.0);
|
||||
visionSysSim.AdjustCamera(&cameraSim, robotToCamera);
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPose, photon::TargetModel{0.5_m, 0.5_m}, 0}});
|
||||
visionSysSim.Update(robotPose);
|
||||
|
||||
photon::PhotonPipelineResult res = camera.GetLatestResult();
|
||||
ASSERT_TRUE(res.HasTargets());
|
||||
photon::PhotonTrackedTarget target = res.GetBestTarget();
|
||||
|
||||
ASSERT_NEAR(0.0, target.GetYaw(), 0.5);
|
||||
|
||||
units::meter_t dist = photon::PhotonUtils::CalculateDistanceToTarget(
|
||||
robotToCamera.Z(), targetPose.Z(), -pitchParam,
|
||||
units::degree_t{target.GetPitch()});
|
||||
ASSERT_NEAR(dist.to<double>(),
|
||||
distParam.convert<units::meters>().to<double>(), 0.25);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
DistanceParamsTests, VisionSystemSimTestDistanceParamsTest,
|
||||
testing::Values(std::make_tuple(5_ft, -15.98_deg, 0_ft),
|
||||
std::make_tuple(6_ft, -15.98_deg, 1_ft),
|
||||
std::make_tuple(10_ft, -15.98_deg, 0_ft),
|
||||
std::make_tuple(15_ft, -15.98_deg, 2_ft),
|
||||
std::make_tuple(19.95_ft, -15.98_deg, 0_ft),
|
||||
std::make_tuple(20_ft, -15.98_deg, 0_ft),
|
||||
std::make_tuple(5_ft, -42_deg, 1_ft),
|
||||
std::make_tuple(6_ft, -42_deg, 0_ft),
|
||||
std::make_tuple(10_ft, -42_deg, 2_ft),
|
||||
std::make_tuple(15_ft, -42_deg, 0.5_ft),
|
||||
std::make_tuple(19.42_ft, -15.98_deg, 0_ft),
|
||||
std::make_tuple(20_ft, -42_deg, 0_ft),
|
||||
std::make_tuple(5_ft, -55_deg, 2_ft),
|
||||
std::make_tuple(6_ft, -55_deg, 0_ft),
|
||||
std::make_tuple(10_ft, -54_deg, 2.2_ft),
|
||||
std::make_tuple(15_ft, -53_deg, 0_ft),
|
||||
std::make_tuple(19.52_ft, -15.98_deg, 1.1_ft)));
|
||||
|
||||
TEST_F(VisionSystemSimTest, TestMultipleTargets) {
|
||||
frc::Pose3d targetPoseL{
|
||||
frc::Translation3d{15.98_m, 2_m, 0_m},
|
||||
frc::Rotation3d{0_rad, 0_rad, units::radian_t{std::numbers::pi}}};
|
||||
frc::Pose3d targetPoseC{
|
||||
frc::Translation3d{15.98_m, 0_m, 0_m},
|
||||
frc::Rotation3d{0_rad, 0_rad, units::radian_t{std::numbers::pi}}};
|
||||
frc::Pose3d targetPoseR{
|
||||
frc::Translation3d{15.98_m, -2_m, 0_m},
|
||||
frc::Rotation3d{0_rad, 0_rad, units::radian_t{std::numbers::pi}}};
|
||||
|
||||
photon::VisionSystemSim visionSysSim{"Test"};
|
||||
photon::PhotonCamera camera{"camera"};
|
||||
photon::PhotonCameraSim cameraSim{&camera};
|
||||
visionSysSim.AddCamera(&cameraSim, frc::Transform3d{});
|
||||
cameraSim.prop.SetCalibration(640, 480, frc::Rotation2d{80_deg});
|
||||
cameraSim.SetMinTargetAreaPixels(20.0);
|
||||
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPoseL.TransformBy(frc::Transform3d{
|
||||
frc::Translation3d{0_m, 0_m, 0_m}, frc::Rotation3d{}}),
|
||||
photon::kAprilTag16h5, 1}});
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPoseC.TransformBy(frc::Transform3d{
|
||||
frc::Translation3d{0_m, 0_m, 0_m}, frc::Rotation3d{}}),
|
||||
photon::kAprilTag16h5, 2}});
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPoseR.TransformBy(frc::Transform3d{
|
||||
frc::Translation3d{0_m, 0_m, 0_m}, frc::Rotation3d{}}),
|
||||
photon::kAprilTag16h5, 3}});
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPoseL.TransformBy(frc::Transform3d{
|
||||
frc::Translation3d{0_m, 0_m, 1_m}, frc::Rotation3d{}}),
|
||||
photon::kAprilTag16h5, 4}});
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPoseC.TransformBy(frc::Transform3d{
|
||||
frc::Translation3d{0_m, 0_m, 1_m}, frc::Rotation3d{}}),
|
||||
photon::kAprilTag16h5, 5}});
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPoseR.TransformBy(frc::Transform3d{
|
||||
frc::Translation3d{0_m, 0_m, 1_m}, frc::Rotation3d{}}),
|
||||
photon::kAprilTag16h5, 6}});
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPoseL.TransformBy(frc::Transform3d{
|
||||
frc::Translation3d{0_m, 0_m, 0.5_m}, frc::Rotation3d{}}),
|
||||
photon::kAprilTag16h5, 7}});
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPoseC.TransformBy(frc::Transform3d{
|
||||
frc::Translation3d{0_m, 0_m, 0.5_m}, frc::Rotation3d{}}),
|
||||
photon::kAprilTag16h5, 8}});
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPoseL.TransformBy(frc::Transform3d{
|
||||
frc::Translation3d{0_m, 0_m, 0.75_m}, frc::Rotation3d{}}),
|
||||
photon::kAprilTag16h5, 9}});
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPoseR.TransformBy(frc::Transform3d{
|
||||
frc::Translation3d{0_m, 0_m, 0.75_m}, frc::Rotation3d{}}),
|
||||
photon::kAprilTag16h5, 10}});
|
||||
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
|
||||
targetPoseL.TransformBy(frc::Transform3d{
|
||||
frc::Translation3d{0_m, 0_m, 0.25_m}, frc::Rotation3d{}}),
|
||||
photon::kAprilTag16h5, 11}});
|
||||
|
||||
frc::Pose2d robotPose{frc::Translation2d{6_m, 0_m}, frc::Rotation2d{.25_deg}};
|
||||
visionSysSim.Update(robotPose);
|
||||
photon::PhotonPipelineResult res = camera.GetLatestResult();
|
||||
ASSERT_TRUE(res.HasTargets());
|
||||
std::span<const photon::PhotonTrackedTarget> tgtList = res.GetTargets();
|
||||
ASSERT_EQ(static_cast<size_t>(11), tgtList.size());
|
||||
}
|
||||
|
||||
TEST_F(VisionSystemSimTest, TestPoseEstimation) {
|
||||
photon::VisionSystemSim visionSysSim{"Test"};
|
||||
photon::PhotonCamera camera{"camera"};
|
||||
photon::PhotonCameraSim cameraSim{&camera};
|
||||
visionSysSim.AddCamera(&cameraSim, frc::Transform3d{});
|
||||
cameraSim.prop.SetCalibration(640, 480, frc::Rotation2d{90_deg});
|
||||
cameraSim.SetMinTargetAreaPixels(20.0);
|
||||
|
||||
std::vector<frc::AprilTag> tagList;
|
||||
tagList.emplace_back(frc::AprilTag{
|
||||
0, frc::Pose3d{12_m, 3_m, 1_m,
|
||||
frc::Rotation3d{0_rad, 0_rad,
|
||||
units::radian_t{std::numbers::pi}}}});
|
||||
tagList.emplace_back(frc::AprilTag{
|
||||
1, frc::Pose3d{12_m, 1_m, -1_m,
|
||||
frc::Rotation3d{0_rad, 0_rad,
|
||||
units::radian_t{std::numbers::pi}}}});
|
||||
tagList.emplace_back(frc::AprilTag{
|
||||
2, frc::Pose3d{11_m, 0_m, 2_m,
|
||||
frc::Rotation3d{0_rad, 0_rad,
|
||||
units::radian_t{std::numbers::pi}}}});
|
||||
units::meter_t fieldLength{54};
|
||||
units::meter_t fieldWidth{27};
|
||||
frc::AprilTagFieldLayout layout{tagList, fieldLength, fieldWidth};
|
||||
frc::Pose2d robotPose{frc::Translation2d{5_m, 1_m}, frc::Rotation2d{5_deg}};
|
||||
visionSysSim.AddVisionTargets(
|
||||
{photon::VisionTargetSim{tagList[0].pose, photon::kAprilTag16h5, 0}});
|
||||
visionSysSim.Update(robotPose);
|
||||
|
||||
Eigen::Matrix<double, 3, 3> camEigen;
|
||||
cv::cv2eigen(camera.GetCameraMatrix().value(), camEigen);
|
||||
|
||||
Eigen::Matrix<double, 5, 1> distEigen;
|
||||
cv::cv2eigen(camera.GetDistCoeffs().value(), distEigen);
|
||||
|
||||
auto camResults = camera.GetLatestResult();
|
||||
auto targetSpan = camResults.GetTargets();
|
||||
std::vector<photon::PhotonTrackedTarget> targets;
|
||||
for (photon::PhotonTrackedTarget tar : targetSpan) {
|
||||
targets.push_back(tar);
|
||||
}
|
||||
photon::PNPResult results = photon::VisionEstimation::EstimateCamPosePNP(
|
||||
camEigen, distEigen, targets, layout, photon::kAprilTag16h5);
|
||||
frc::Pose3d pose = frc::Pose3d{} + results.best;
|
||||
ASSERT_NEAR(5, pose.X().to<double>(), 0.01);
|
||||
ASSERT_NEAR(1, pose.Y().to<double>(), 0.01);
|
||||
ASSERT_NEAR(0, pose.Z().to<double>(), 0.01);
|
||||
ASSERT_NEAR(units::degree_t{5}.convert<units::radians>().to<double>(),
|
||||
pose.Rotation().Z().to<double>(), 0.01);
|
||||
|
||||
visionSysSim.AddVisionTargets(
|
||||
{photon::VisionTargetSim{tagList[1].pose, photon::kAprilTag16h5, 1}});
|
||||
visionSysSim.AddVisionTargets(
|
||||
{photon::VisionTargetSim{tagList[2].pose, photon::kAprilTag16h5, 2}});
|
||||
visionSysSim.Update(robotPose);
|
||||
|
||||
auto camResults2 = camera.GetLatestResult();
|
||||
auto targetSpan2 = camResults2.GetTargets();
|
||||
std::vector<photon::PhotonTrackedTarget> targets2;
|
||||
for (photon::PhotonTrackedTarget tar : targetSpan2) {
|
||||
targets2.push_back(tar);
|
||||
}
|
||||
photon::PNPResult results2 = photon::VisionEstimation::EstimateCamPosePNP(
|
||||
camEigen, distEigen, targets2, layout, photon::kAprilTag16h5);
|
||||
frc::Pose3d pose2 = frc::Pose3d{} + results2.best;
|
||||
ASSERT_NEAR(5, pose2.X().to<double>(), 0.01);
|
||||
ASSERT_NEAR(1, pose2.Y().to<double>(), 0.01);
|
||||
ASSERT_NEAR(0, pose2.Z().to<double>(), 0.01);
|
||||
ASSERT_NEAR(units::degree_t{5}.convert<units::radians>().to<double>(),
|
||||
pose2.Rotation().Z().to<double>(), 0.01);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <frc/geometry/Pose3d.h>
|
||||
|
||||
namespace photon {
|
||||
class CameraTargetRelation {
|
||||
public:
|
||||
const frc::Pose3d camPose;
|
||||
const frc::Transform3d camToTarg;
|
||||
const units::meter_t camToTargDist;
|
||||
const units::meter_t camToTargDistXY;
|
||||
const frc::Rotation2d camToTargYaw;
|
||||
const frc::Rotation2d camToTargPitch;
|
||||
|
||||
const frc::Rotation2d camToTargAngle;
|
||||
|
||||
const frc::Transform3d targToCam;
|
||||
const frc::Rotation2d targToCamYaw;
|
||||
const frc::Rotation2d targToCamPitch;
|
||||
|
||||
const frc::Rotation2d targToCamAngle;
|
||||
|
||||
CameraTargetRelation(const frc::Pose3d& cameraPose,
|
||||
const frc::Pose3d& targetPose)
|
||||
: camPose(cameraPose),
|
||||
camToTarg(frc::Transform3d{cameraPose, targetPose}),
|
||||
camToTargDist(camToTarg.Translation().Norm()),
|
||||
camToTargDistXY(units::math::hypot(camToTarg.Translation().X(),
|
||||
camToTarg.Translation().Y())),
|
||||
camToTargYaw(frc::Rotation2d{camToTarg.X().to<double>(),
|
||||
camToTarg.Y().to<double>()}),
|
||||
camToTargPitch(frc::Rotation2d{camToTargDistXY.to<double>(),
|
||||
-camToTarg.Z().to<double>()}),
|
||||
camToTargAngle(frc::Rotation2d{units::math::hypot(
|
||||
camToTargYaw.Radians(), camToTargPitch.Radians())}),
|
||||
targToCam(frc::Transform3d{targetPose, cameraPose}),
|
||||
targToCamYaw(frc::Rotation2d{targToCam.X().to<double>(),
|
||||
targToCam.Y().to<double>()}),
|
||||
targToCamPitch(frc::Rotation2d{camToTargDistXY.to<double>(),
|
||||
-targToCam.Z().to<double>()}),
|
||||
targToCamAngle(frc::Rotation2d{units::math::hypot(
|
||||
targToCamYaw.Radians(), targToCamPitch.Radians())}) {}
|
||||
};
|
||||
} // namespace photon
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <opencv2/calib3d.hpp>
|
||||
#include <opencv2/core.hpp>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
|
||||
#include "RotTrlTransform3d.h"
|
||||
|
||||
#define OPENCV_DISABLE_EIGEN_TENSOR_SUPPORT
|
||||
#include <opencv2/core/eigen.hpp>
|
||||
#include "photon/targeting/PNPResult.h"
|
||||
#include "photon/targeting/MultiTargetPNPResult.h"
|
||||
|
||||
namespace photon {
|
||||
namespace OpenCVHelp {
|
||||
|
||||
static frc::Rotation3d NWU_TO_EDN{
|
||||
(Eigen::Matrix3d() << 0, -1, 0, 0, 0, -1, 1, 0, 0).finished()};
|
||||
static frc::Rotation3d EDN_TO_NWU{
|
||||
(Eigen::Matrix3d() << 0, 0, 1, -1, 0, 0, 0, -1, 0).finished()};
|
||||
|
||||
static std::vector<cv::Point2f> GetConvexHull(
|
||||
const std::vector<cv::Point2f>& points) {
|
||||
std::vector<int> outputHull{};
|
||||
cv::convexHull(points, outputHull);
|
||||
std::vector<cv::Point2f> convexPoints;
|
||||
for (size_t i = 0; i < outputHull.size(); i++) {
|
||||
convexPoints.push_back(points[outputHull[i]]);
|
||||
}
|
||||
return convexPoints;
|
||||
}
|
||||
|
||||
static cv::RotatedRect GetMinAreaRect(const std::vector<cv::Point2f>& points) {
|
||||
return cv::minAreaRect(points);
|
||||
}
|
||||
|
||||
static frc::Translation3d TranslationNWUtoEDN(const frc::Translation3d& trl) {
|
||||
return trl.RotateBy(NWU_TO_EDN);
|
||||
}
|
||||
|
||||
static frc::Rotation3d RotationNWUtoEDN(const frc::Rotation3d& rot) {
|
||||
return -NWU_TO_EDN + (rot + NWU_TO_EDN);
|
||||
}
|
||||
|
||||
static std::vector<cv::Point3f> TranslationToTVec(
|
||||
const std::vector<frc::Translation3d>& translations) {
|
||||
std::vector<cv::Point3f> retVal;
|
||||
retVal.reserve(translations.size());
|
||||
for (size_t i = 0; i < translations.size(); i++) {
|
||||
frc::Translation3d trl = TranslationNWUtoEDN(translations[i]);
|
||||
retVal.emplace_back(cv::Point3f{trl.X().to<float>(), trl.Y().to<float>(),
|
||||
trl.Z().to<float>()});
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
static std::vector<cv::Point3f> RotationToRVec(
|
||||
const frc::Rotation3d& rotation) {
|
||||
std::vector<cv::Point3f> retVal{};
|
||||
frc::Rotation3d rot = RotationNWUtoEDN(rotation);
|
||||
retVal.emplace_back(cv::Point3d{
|
||||
rot.GetQuaternion().ToRotationVector()(0),
|
||||
rot.GetQuaternion().ToRotationVector()(1),
|
||||
rot.GetQuaternion().ToRotationVector()(2),
|
||||
});
|
||||
return retVal;
|
||||
}
|
||||
|
||||
[[maybe_unused]] static cv::Point2f AvgPoint(std::vector<cv::Point2f> points) {
|
||||
if (points.size() == 0) {
|
||||
return cv::Point2f{};
|
||||
}
|
||||
cv::reduce(points, points, 0, cv::REDUCE_AVG);
|
||||
return points[0];
|
||||
}
|
||||
|
||||
[[maybe_unused]] static std::vector<std::pair<float, float>> PointsToCorners(
|
||||
const std::vector<cv::Point2f>& points) {
|
||||
std::vector<std::pair<float, float>> retVal;
|
||||
retVal.reserve(points.size());
|
||||
for (size_t i = 0; i < points.size(); i++) {
|
||||
retVal.emplace_back(std::make_pair(points[i].x, points[i].y));
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
[[maybe_unused]] static std::vector<cv::Point2f> CornersToPoints(
|
||||
const std::vector<std::pair<float, float>>& corners) {
|
||||
std::vector<cv::Point2f> retVal;
|
||||
retVal.reserve(corners.size());
|
||||
for (size_t i = 0; i < corners.size(); i++) {
|
||||
retVal.emplace_back(cv::Point2f{corners[i].first, corners[i].second});
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
[[maybe_unused]] static cv::Rect GetBoundingRect(
|
||||
const std::vector<cv::Point2f>& points) {
|
||||
return cv::boundingRect(points);
|
||||
}
|
||||
|
||||
static std::vector<cv::Point2f> ProjectPoints(
|
||||
const Eigen::Matrix<double, 3, 3>& cameraMatrix,
|
||||
const Eigen::Matrix<double, 5, 1>& distCoeffs,
|
||||
const RotTrlTransform3d& camRt,
|
||||
const std::vector<frc::Translation3d>& objectTranslations) {
|
||||
std::vector<cv::Point3f> objectPoints = TranslationToTVec(objectTranslations);
|
||||
std::vector<cv::Point3f> rvec = RotationToRVec(camRt.GetRotation());
|
||||
std::vector<cv::Point3f> tvec = TranslationToTVec({camRt.GetTranslation()});
|
||||
cv::Mat cameraMat(cameraMatrix.rows(), cameraMatrix.cols(), CV_64F);
|
||||
cv::eigen2cv(cameraMatrix, cameraMat);
|
||||
cv::Mat distCoeffsMat(distCoeffs.rows(), distCoeffs.cols(), CV_64F);
|
||||
cv::eigen2cv(distCoeffs, distCoeffsMat);
|
||||
std::vector<cv::Point2f> imagePoints{};
|
||||
cv::projectPoints(objectPoints, rvec, tvec, cameraMat, distCoeffsMat,
|
||||
imagePoints);
|
||||
return imagePoints;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static std::vector<T> ReorderCircular(const std::vector<T> elements,
|
||||
bool backwards, int shiftStart) {
|
||||
size_t size = elements.size();
|
||||
int dir = backwards ? -1 : 1;
|
||||
std::vector<T> reordered{elements};
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
int index = (i * dir + shiftStart * dir) % size;
|
||||
if (index < 0) {
|
||||
index = size + index;
|
||||
}
|
||||
reordered[i] = elements[index];
|
||||
}
|
||||
return reordered;
|
||||
}
|
||||
|
||||
static frc::Translation3d TranslationEDNToNWU(const frc::Translation3d& trl) {
|
||||
return trl.RotateBy(EDN_TO_NWU);
|
||||
}
|
||||
|
||||
static frc::Rotation3d RotationEDNToNWU(const frc::Rotation3d& rot) {
|
||||
return -EDN_TO_NWU + (rot + EDN_TO_NWU);
|
||||
}
|
||||
|
||||
static frc::Translation3d TVecToTranslation(const cv::Mat& tvecInput) {
|
||||
cv::Vec3f data{};
|
||||
cv::Mat wrapped{tvecInput.rows, tvecInput.cols, CV_32F};
|
||||
tvecInput.convertTo(wrapped, CV_32F);
|
||||
data = wrapped.at<cv::Vec3f>(cv::Point{0, 0});
|
||||
return TranslationEDNToNWU(frc::Translation3d{units::meter_t{data[0]},
|
||||
units::meter_t{data[1]},
|
||||
units::meter_t{data[2]}});
|
||||
}
|
||||
|
||||
static frc::Rotation3d RVecToRotation(const cv::Mat& rvecInput) {
|
||||
cv::Vec3f data{};
|
||||
cv::Mat wrapped{rvecInput.rows, rvecInput.cols, CV_32F};
|
||||
rvecInput.convertTo(wrapped, CV_32F);
|
||||
data = wrapped.at<cv::Vec3f>(cv::Point{0, 0});
|
||||
return RotationEDNToNWU(frc::Rotation3d{units::radian_t{data[0]},
|
||||
units::radian_t{data[1]},
|
||||
units::radian_t{data[2]}});
|
||||
}
|
||||
|
||||
[[maybe_unused]] static photon::PNPResult SolvePNP_Square(
|
||||
const Eigen::Matrix<double, 3, 3>& cameraMatrix,
|
||||
const Eigen::Matrix<double, 5, 1>& distCoeffs,
|
||||
std::vector<frc::Translation3d> modelTrls,
|
||||
std::vector<cv::Point2f> imagePoints) {
|
||||
modelTrls = ReorderCircular(modelTrls, true, -1);
|
||||
imagePoints = ReorderCircular(imagePoints, true, -1);
|
||||
std::vector<cv::Point3f> objectMat = TranslationToTVec(modelTrls);
|
||||
std::vector<cv::Mat> rvecs;
|
||||
std::vector<cv::Mat> tvecs;
|
||||
cv::Mat rvec = cv::Mat::zeros(3, 1, CV_32F);
|
||||
cv::Mat tvec = cv::Mat::zeros(3, 1, CV_32F);
|
||||
cv::Mat reprojectionError = cv::Mat::zeros(2, 1, CV_32F);
|
||||
|
||||
cv::Mat cameraMat(cameraMatrix.rows(), cameraMatrix.cols(), CV_32F);
|
||||
cv::eigen2cv(cameraMatrix, cameraMat);
|
||||
cv::Mat distCoeffsMat(distCoeffs.rows(), distCoeffs.cols(), CV_32F);
|
||||
cv::eigen2cv(distCoeffs, distCoeffsMat);
|
||||
|
||||
cv::Vec2d errors{};
|
||||
frc::Transform3d best{};
|
||||
std::optional<frc::Transform3d> alt{std::nullopt};
|
||||
|
||||
for (int tries = 0; tries < 2; tries++) {
|
||||
cv::solvePnPGeneric(objectMat, imagePoints, cameraMat, distCoeffsMat, rvecs,
|
||||
tvecs, false, cv::SOLVEPNP_IPPE_SQUARE, rvec, tvec,
|
||||
reprojectionError);
|
||||
|
||||
errors = reprojectionError.at<cv::Vec2f>(cv::Point{0, 0});
|
||||
best = frc::Transform3d{TVecToTranslation(tvecs.at(0)),
|
||||
RVecToRotation(rvecs[0])};
|
||||
|
||||
if (tvecs.size() > 1) {
|
||||
alt = frc::Transform3d{TVecToTranslation(tvecs.at(1)),
|
||||
RVecToRotation(rvecs[1])};
|
||||
}
|
||||
|
||||
if (!std::isnan(errors[0])) {
|
||||
break;
|
||||
} else {
|
||||
cv::Point2f pt = imagePoints[0];
|
||||
pt.x -= 0.001f;
|
||||
pt.y -= 0.001f;
|
||||
imagePoints[0] = pt;
|
||||
}
|
||||
}
|
||||
|
||||
if (std::isnan(errors[0])) {
|
||||
fmt::print("SolvePNP_Square failed!\n");
|
||||
}
|
||||
if (alt) {
|
||||
photon::PNPResult result;
|
||||
result.best = best;
|
||||
result.alt = alt.value();
|
||||
result.ambiguity = errors[0] / errors[1];
|
||||
result.bestReprojErr = errors[0];
|
||||
result.altReprojErr = errors[1];
|
||||
result.isPresent = true;
|
||||
return result;
|
||||
} else {
|
||||
photon::PNPResult result;
|
||||
result.best = best;
|
||||
result.bestReprojErr = errors[0];
|
||||
result.isPresent = true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
[[maybe_unused]] static photon::PNPResult SolvePNP_SQPNP(
|
||||
const Eigen::Matrix<double, 3, 3>& cameraMatrix,
|
||||
const Eigen::Matrix<double, 5, 1>& distCoeffs,
|
||||
std::vector<frc::Translation3d> modelTrls,
|
||||
std::vector<cv::Point2f> imagePoints) {
|
||||
std::vector<cv::Point3f> objectMat = TranslationToTVec(modelTrls);
|
||||
std::vector<cv::Mat> rvecs{};
|
||||
std::vector<cv::Mat> tvecs{};
|
||||
cv::Mat rvec = cv::Mat::zeros(3, 1, CV_32F);
|
||||
cv::Mat tvec = cv::Mat::zeros(3, 1, CV_32F);
|
||||
cv::Mat reprojectionError = cv::Mat::zeros(2, 1, CV_32F);
|
||||
|
||||
cv::Mat cameraMat(cameraMatrix.rows(), cameraMatrix.cols(), CV_64F);
|
||||
cv::eigen2cv(cameraMatrix, cameraMat);
|
||||
cv::Mat distCoeffsMat(distCoeffs.rows(), distCoeffs.cols(), CV_64F);
|
||||
cv::eigen2cv(distCoeffs, distCoeffsMat);
|
||||
|
||||
float error = 0;
|
||||
frc::Transform3d best{};
|
||||
|
||||
cv::solvePnPGeneric(objectMat, imagePoints, cameraMat, distCoeffsMat, rvecs,
|
||||
tvecs, false, cv::SOLVEPNP_SQPNP, rvec, tvec,
|
||||
reprojectionError);
|
||||
|
||||
error = reprojectionError.at<float>(cv::Point{0, 0});
|
||||
best = frc::Transform3d{TVecToTranslation(tvecs.at(0)),
|
||||
RVecToRotation(rvecs[0])};
|
||||
|
||||
if (std::isnan(error)) {
|
||||
fmt::print("SolvePNP_Square failed!\n");
|
||||
}
|
||||
photon::PNPResult result;
|
||||
result.best = best;
|
||||
result.bestReprojErr = error;
|
||||
result.isPresent = true;
|
||||
return result;
|
||||
}
|
||||
} // namespace OpenCVHelp
|
||||
} // namespace photon
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <frc/geometry/Pose3d.h>
|
||||
#include <frc/geometry/Rotation3d.h>
|
||||
#include <frc/geometry/Translation3d.h>
|
||||
|
||||
namespace photon {
|
||||
class RotTrlTransform3d {
|
||||
public:
|
||||
RotTrlTransform3d(const frc::Rotation3d& rot, const frc::Translation3d& trl)
|
||||
: trl(trl), rot(rot) {}
|
||||
RotTrlTransform3d(const frc::Pose3d& initial, const frc::Pose3d& last)
|
||||
: trl(last.Translation() - initial.Translation().RotateBy(rot)),
|
||||
rot(last.Rotation() - initial.Rotation()) {}
|
||||
explicit RotTrlTransform3d(const frc::Transform3d& trf)
|
||||
: RotTrlTransform3d(trf.Rotation(), trf.Translation()) {}
|
||||
RotTrlTransform3d()
|
||||
: RotTrlTransform3d(frc::Rotation3d{}, frc::Translation3d{}) {}
|
||||
|
||||
static RotTrlTransform3d MakeRelativeTo(const frc::Pose3d& pose) {
|
||||
return RotTrlTransform3d{pose.Rotation(), pose.Translation()}.Inverse();
|
||||
}
|
||||
|
||||
RotTrlTransform3d Inverse() const {
|
||||
frc::Rotation3d invRot = -rot;
|
||||
frc::Translation3d invTrl = -(trl.RotateBy(invRot));
|
||||
return RotTrlTransform3d{invRot, invTrl};
|
||||
}
|
||||
|
||||
frc::Transform3d GetTransform() const { return frc::Transform3d{trl, rot}; }
|
||||
|
||||
frc::Translation3d GetTranslation() const { return trl; }
|
||||
|
||||
frc::Rotation3d GetRotation() const { return rot; }
|
||||
|
||||
frc::Translation3d Apply(const frc::Translation3d& trlToApply) const {
|
||||
return trlToApply.RotateBy(rot) + trl;
|
||||
}
|
||||
|
||||
std::vector<frc::Translation3d> ApplyTrls(
|
||||
const std::vector<frc::Translation3d>& trls) const {
|
||||
std::vector<frc::Translation3d> retVal;
|
||||
retVal.reserve(trls.size());
|
||||
for (const auto& currentTrl : trls) {
|
||||
retVal.emplace_back(Apply(currentTrl));
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
frc::Rotation3d Apply(const frc::Rotation3d& rotToApply) const {
|
||||
return rotToApply + rot;
|
||||
}
|
||||
|
||||
std::vector<frc::Rotation3d> ApplyTrls(
|
||||
const std::vector<frc::Rotation3d>& rots) const {
|
||||
std::vector<frc::Rotation3d> retVal;
|
||||
retVal.reserve(rots.size());
|
||||
for (const auto& currentRot : rots) {
|
||||
retVal.emplace_back(Apply(currentRot));
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
frc::Pose3d Apply(const frc::Pose3d& poseToApply) const {
|
||||
return frc::Pose3d{Apply(poseToApply.Translation()),
|
||||
Apply(poseToApply.Rotation())};
|
||||
}
|
||||
|
||||
std::vector<frc::Pose3d> ApplyPoses(
|
||||
const std::vector<frc::Pose3d>& poses) const {
|
||||
std::vector<frc::Pose3d> retVal;
|
||||
retVal.reserve(poses.size());
|
||||
for (const auto& currentPose : poses) {
|
||||
retVal.emplace_back(Apply(currentPose));
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
private:
|
||||
const frc::Translation3d trl;
|
||||
const frc::Rotation3d rot;
|
||||
};
|
||||
} // namespace photon
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <frc/geometry/Pose3d.h>
|
||||
#include <frc/geometry/Translation3d.h>
|
||||
|
||||
#include "RotTrlTransform3d.h"
|
||||
|
||||
namespace photon {
|
||||
class TargetModel {
|
||||
public:
|
||||
TargetModel(units::meter_t width, units::meter_t height)
|
||||
: vertices({frc::Translation3d{0_m, -width / 2.0, -height / 2.0},
|
||||
frc::Translation3d{0_m, width / 2.0, -height / 2.0},
|
||||
frc::Translation3d{0_m, width / 2.0, height / 2.0},
|
||||
frc::Translation3d{0_m, -width / 2.0, height / 2.0}}),
|
||||
isPlanar(true),
|
||||
isSpherical(false) {}
|
||||
|
||||
TargetModel(units::meter_t length, units::meter_t width,
|
||||
units::meter_t height)
|
||||
: TargetModel({
|
||||
frc::Translation3d{length / 2.0, -width / 2.0, -height / 2.0},
|
||||
frc::Translation3d{length / 2.0, width / 2.0, -height / 2.0},
|
||||
frc::Translation3d{length / 2.0, width / 2.0, height / 2.0},
|
||||
frc::Translation3d{length / 2.0, -width / 2.0, height / 2.0},
|
||||
frc::Translation3d{-length / 2.0, -width / 2.0, height / 2.0},
|
||||
frc::Translation3d{-length / 2.0, width / 2.0, height / 2.0},
|
||||
frc::Translation3d{-length / 2.0, width / 2.0, -height / 2.0},
|
||||
frc::Translation3d{-length / 2.0, -width / 2.0, -height / 2.0},
|
||||
}) {}
|
||||
|
||||
explicit TargetModel(units::meter_t diameter)
|
||||
: vertices({
|
||||
frc::Translation3d{0_m, -diameter / 2.0, 0_m},
|
||||
frc::Translation3d{0_m, 0_m, -diameter / 2.0},
|
||||
frc::Translation3d{0_m, diameter / 2.0, 0_m},
|
||||
frc::Translation3d{0_m, 0_m, diameter / 2.0},
|
||||
}),
|
||||
isPlanar(false),
|
||||
isSpherical(true) {}
|
||||
|
||||
explicit TargetModel(const std::vector<frc::Translation3d>& verts)
|
||||
: isSpherical(false) {
|
||||
if (verts.size() <= 2) {
|
||||
vertices = std::vector<frc::Translation3d>();
|
||||
isPlanar = false;
|
||||
} else {
|
||||
bool cornersPlanar = true;
|
||||
for (const auto& corner : verts) {
|
||||
if (corner.X() != 0_m) {
|
||||
cornersPlanar = false;
|
||||
}
|
||||
}
|
||||
isPlanar = cornersPlanar;
|
||||
}
|
||||
vertices = verts;
|
||||
}
|
||||
|
||||
std::vector<frc::Translation3d> GetFieldVertices(
|
||||
const frc::Pose3d& targetPose) const {
|
||||
RotTrlTransform3d basisChange{targetPose.Rotation(),
|
||||
targetPose.Translation()};
|
||||
std::vector<frc::Translation3d> retVal;
|
||||
retVal.reserve(vertices.size());
|
||||
for (const auto& vert : vertices) {
|
||||
retVal.emplace_back(basisChange.Apply(vert));
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
static frc::Pose3d GetOrientedPose(const frc::Translation3d& tgtTrl,
|
||||
const frc::Translation3d& cameraTrl) {
|
||||
frc::Translation3d relCam = cameraTrl - tgtTrl;
|
||||
frc::Rotation3d orientToCam = frc::Rotation3d{
|
||||
0_rad,
|
||||
frc::Rotation2d{units::math::hypot(relCam.X(), relCam.Y()).to<double>(),
|
||||
-relCam.Z().to<double>()}
|
||||
.Radians(),
|
||||
frc::Rotation2d{relCam.X().to<double>(), relCam.Y().to<double>()}
|
||||
.Radians()};
|
||||
return frc::Pose3d{tgtTrl, orientToCam};
|
||||
}
|
||||
|
||||
std::vector<frc::Translation3d> GetVertices() const { return vertices; }
|
||||
bool GetIsPlanar() const { return isPlanar; }
|
||||
bool GetIsSpherical() const { return isSpherical; }
|
||||
|
||||
private:
|
||||
std::vector<frc::Translation3d> vertices;
|
||||
bool isPlanar;
|
||||
bool isSpherical;
|
||||
};
|
||||
|
||||
static const TargetModel kAprilTag16h5{6_in, 6_in};
|
||||
static const TargetModel kAprilTag36h11{6.5_in, 6.5_in};
|
||||
} // namespace photon
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <frc/apriltag/AprilTag.h>
|
||||
#include <frc/apriltag/AprilTagFieldLayout.h>
|
||||
|
||||
#include "OpenCVHelp.h"
|
||||
#include "TargetModel.h"
|
||||
#include "photon/targeting/MultiTargetPNPResult.h"
|
||||
#include "photon/targeting/PhotonTrackedTarget.h"
|
||||
|
||||
namespace photon {
|
||||
namespace VisionEstimation {
|
||||
|
||||
static std::vector<frc::AprilTag> GetVisibleLayoutTags(
|
||||
const std::vector<PhotonTrackedTarget>& visTags,
|
||||
const frc::AprilTagFieldLayout& layout) {
|
||||
std::vector<frc::AprilTag> retVal{};
|
||||
for (const auto& tag : visTags) {
|
||||
int id = tag.GetFiducialId();
|
||||
auto maybePose = layout.GetTagPose(id);
|
||||
if (maybePose) {
|
||||
retVal.emplace_back(frc::AprilTag{id, maybePose.value()});
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
static PNPResult EstimateCamPosePNP(
|
||||
const Eigen::Matrix<double, 3, 3>& cameraMatrix,
|
||||
const Eigen::Matrix<double, 5, 1>& distCoeffs,
|
||||
const std::vector<PhotonTrackedTarget>& visTags,
|
||||
const frc::AprilTagFieldLayout& layout, const TargetModel& tagModel) {
|
||||
if (visTags.size() == 0) {
|
||||
return PNPResult();
|
||||
}
|
||||
|
||||
std::vector<std::pair<float, float>> corners{};
|
||||
std::vector<frc::AprilTag> knownTags{};
|
||||
|
||||
for (const auto& tgt : visTags) {
|
||||
int id = tgt.GetFiducialId();
|
||||
auto maybePose = layout.GetTagPose(id);
|
||||
if (maybePose) {
|
||||
knownTags.emplace_back(frc::AprilTag{id, maybePose.value()});
|
||||
auto currentCorners = tgt.GetDetectedCorners();
|
||||
corners.insert(corners.end(), currentCorners.begin(),
|
||||
currentCorners.end());
|
||||
}
|
||||
}
|
||||
if (knownTags.size() == 0 || corners.size() == 0 || corners.size() % 4 != 0) {
|
||||
return PNPResult{};
|
||||
}
|
||||
|
||||
std::vector<cv::Point2f> points = OpenCVHelp::CornersToPoints(corners);
|
||||
|
||||
if (knownTags.size() == 1) {
|
||||
PNPResult camToTag = OpenCVHelp::SolvePNP_Square(
|
||||
cameraMatrix, distCoeffs, tagModel.GetVertices(), points);
|
||||
if (!camToTag.isPresent) {
|
||||
return PNPResult{};
|
||||
}
|
||||
frc::Pose3d bestPose =
|
||||
knownTags[0].pose.TransformBy(camToTag.best.Inverse());
|
||||
frc::Pose3d altPose{};
|
||||
if (camToTag.ambiguity != 0) {
|
||||
altPose = knownTags[0].pose.TransformBy(camToTag.alt.Inverse());
|
||||
}
|
||||
frc::Pose3d o{};
|
||||
PNPResult result{};
|
||||
result.best = frc::Transform3d{o, bestPose};
|
||||
result.alt = frc::Transform3d{o, altPose};
|
||||
result.ambiguity = camToTag.ambiguity;
|
||||
result.bestReprojErr = camToTag.bestReprojErr;
|
||||
result.altReprojErr = camToTag.altReprojErr;
|
||||
return result;
|
||||
} else {
|
||||
std::vector<frc::Translation3d> objectTrls{};
|
||||
for (const auto& tag : knownTags) {
|
||||
auto verts = tagModel.GetFieldVertices(tag.pose);
|
||||
objectTrls.insert(objectTrls.end(), verts.begin(), verts.end());
|
||||
}
|
||||
PNPResult camToOrigin = OpenCVHelp::SolvePNP_SQPNP(cameraMatrix, distCoeffs,
|
||||
objectTrls, points);
|
||||
if (!camToOrigin.isPresent) {
|
||||
return PNPResult{};
|
||||
} else {
|
||||
PNPResult result{};
|
||||
result.best = camToOrigin.best.Inverse(),
|
||||
result.alt = camToOrigin.alt.Inverse(),
|
||||
result.ambiguity = camToOrigin.ambiguity;
|
||||
result.bestReprojErr = camToOrigin.bestReprojErr;
|
||||
result.altReprojErr = camToOrigin.altReprojErr;
|
||||
result.isPresent = true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace VisionEstimation
|
||||
} // namespace photon
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <networktables/BooleanTopic.h>
|
||||
#include <networktables/DoubleArrayTopic.h>
|
||||
#include <networktables/DoubleTopic.h>
|
||||
#include <networktables/IntegerTopic.h>
|
||||
#include <networktables/NetworkTable.h>
|
||||
#include <networktables/RawTopic.h>
|
||||
|
||||
namespace photon {
|
||||
class NTTopicSet {
|
||||
public:
|
||||
std::shared_ptr<nt::NetworkTable> subTable;
|
||||
nt::RawPublisher rawBytesEntry;
|
||||
|
||||
nt::IntegerPublisher pipelineIndexPublisher;
|
||||
nt::IntegerSubscriber pipelineIndexRequestSub;
|
||||
|
||||
nt::BooleanTopic driverModeEntry;
|
||||
nt::BooleanPublisher driverModePublisher;
|
||||
nt::BooleanSubscriber driverModeSubscriber;
|
||||
|
||||
nt::DoublePublisher latencyMillisEntry;
|
||||
nt::BooleanPublisher hasTargetEntry;
|
||||
nt::DoublePublisher targetPitchEntry;
|
||||
nt::DoublePublisher targetYawEntry;
|
||||
nt::DoublePublisher targetAreaEntry;
|
||||
nt::DoubleArrayPublisher targetPoseEntry;
|
||||
nt::DoublePublisher targetSkewEntry;
|
||||
|
||||
nt::DoublePublisher bestTargetPosX;
|
||||
nt::DoublePublisher bestTargetPosY;
|
||||
|
||||
nt::IntegerTopic heartbeatTopic;
|
||||
nt::IntegerPublisher heartbeatPublisher;
|
||||
|
||||
nt::DoubleArrayPublisher cameraIntrinsicsPublisher;
|
||||
nt::DoubleArrayPublisher cameraDistortionPublisher;
|
||||
|
||||
void UpdateEntries() {
|
||||
nt::PubSubOptions options;
|
||||
options.periodic = 0.01;
|
||||
options.sendAll = true;
|
||||
rawBytesEntry =
|
||||
subTable->GetRawTopic("rawBytes").Publish("rawBytes", options);
|
||||
|
||||
pipelineIndexPublisher =
|
||||
subTable->GetIntegerTopic("pipelineIndexState").Publish();
|
||||
pipelineIndexRequestSub =
|
||||
subTable->GetIntegerTopic("pipelineIndexRequest").Subscribe(0);
|
||||
|
||||
driverModePublisher = subTable->GetBooleanTopic("driverMode").Publish();
|
||||
driverModeSubscriber =
|
||||
subTable->GetBooleanTopic("driverModeRequest").Subscribe(0);
|
||||
|
||||
driverModeSubscriber.GetTopic().Publish().SetDefault(false);
|
||||
|
||||
latencyMillisEntry = subTable->GetDoubleTopic("latencyMillis").Publish();
|
||||
hasTargetEntry = subTable->GetBooleanTopic("hasTargets").Publish();
|
||||
|
||||
targetPitchEntry = subTable->GetDoubleTopic("targetPitch").Publish();
|
||||
targetAreaEntry = subTable->GetDoubleTopic("targetArea").Publish();
|
||||
targetYawEntry = subTable->GetDoubleTopic("targetYaw").Publish();
|
||||
targetPoseEntry = subTable->GetDoubleArrayTopic("targetPose").Publish();
|
||||
targetSkewEntry = subTable->GetDoubleTopic("targetSkew").Publish();
|
||||
|
||||
bestTargetPosX = subTable->GetDoubleTopic("targetPixelsX").Publish();
|
||||
bestTargetPosY = subTable->GetDoubleTopic("targetPixelsY").Publish();
|
||||
|
||||
heartbeatTopic = subTable->GetIntegerTopic("heartbeat");
|
||||
heartbeatPublisher = heartbeatTopic.Publish();
|
||||
|
||||
cameraIntrinsicsPublisher =
|
||||
subTable->GetDoubleArrayTopic("cameraIntrinsics").Publish();
|
||||
cameraDistortionPublisher =
|
||||
subTable->GetDoubleArrayTopic("cameraDistortion").Publish();
|
||||
}
|
||||
|
||||
private:
|
||||
};
|
||||
} // namespace photon
|
||||
@@ -27,15 +27,15 @@ class PNPResult {
|
||||
public:
|
||||
// This could be wrapped in an std::optional, but chose to do it this way to
|
||||
// mirror Java
|
||||
bool isPresent;
|
||||
bool isPresent{false};
|
||||
|
||||
frc::Transform3d best;
|
||||
double bestReprojErr;
|
||||
frc::Transform3d best{};
|
||||
double bestReprojErr{0};
|
||||
|
||||
frc::Transform3d alt;
|
||||
double altReprojErr;
|
||||
frc::Transform3d alt{};
|
||||
double altReprojErr{0};
|
||||
|
||||
double ambiguity;
|
||||
double ambiguity{0};
|
||||
|
||||
bool operator==(const PNPResult& other) const;
|
||||
|
||||
|
||||
1
photonlib-cpp-examples/swervedriveposeestsim/.gitignore
vendored
Normal file
1
photonlib-cpp-examples/swervedriveposeestsim/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
vendordeps
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"enableCppIntellisense": true,
|
||||
"currentLanguage": "cpp",
|
||||
"projectYear": "2023",
|
||||
"teamNumber": 5
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
Copyright (c) 2009-2021 FIRST and other WPILib contributors
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of FIRST, WPILib, nor the names of other WPILib
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
121
photonlib-cpp-examples/swervedriveposeestsim/build.gradle
Normal file
121
photonlib-cpp-examples/swervedriveposeestsim/build.gradle
Normal file
@@ -0,0 +1,121 @@
|
||||
plugins {
|
||||
id "cpp"
|
||||
id "google-test-test-suite"
|
||||
id "edu.wpi.first.GradleRIO"
|
||||
|
||||
id "com.dorongold.task-tree" version "2.1.0"
|
||||
}
|
||||
|
||||
wpi.maven.useLocal = false
|
||||
wpi.maven.useDevelopment = true
|
||||
wpi.versions.wpilibVersion = '2024.1.1-beta-3-53-g31cd015'
|
||||
wpi.versions.wpimathVersion = '2024.1.1-beta-3-53-g31cd015'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/../shared/examples_common.gradle"
|
||||
|
||||
// Define my targets (RoboRIO) and artifacts (deployable files)
|
||||
// This is added by GradleRIO's backing project DeployUtils.
|
||||
deploy {
|
||||
targets {
|
||||
roborio(getTargetTypeClass('RoboRIO')) {
|
||||
// Team number is loaded either from the .wpilib/wpilib_preferences.json
|
||||
// or from command line. If not found an exception will be thrown.
|
||||
// You can use getTeamOrDefault(team) instead of getTeamNumber if you
|
||||
// want to store a team number in this file.
|
||||
team = project.frc.getTeamOrDefault(5940)
|
||||
debug = project.frc.getDebugOrDefault(false)
|
||||
|
||||
artifacts {
|
||||
// First part is artifact name, 2nd is artifact type
|
||||
// getTargetTypeClass is a shortcut to get the class type using a string
|
||||
|
||||
frcCpp(getArtifactTypeClass('FRCNativeArtifact')) {
|
||||
}
|
||||
|
||||
// Static files artifact
|
||||
frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) {
|
||||
files = project.fileTree('src/main/deploy')
|
||||
directory = '/home/lvuser/deploy'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def deployArtifact = deploy.targets.roborio.artifacts.frcCpp
|
||||
|
||||
// Set this to true to enable desktop support.
|
||||
def includeDesktopSupport = true
|
||||
|
||||
// Set to true to run simulation in debug mode
|
||||
wpi.cpp.debugSimulation = false
|
||||
|
||||
// Default enable simgui
|
||||
wpi.sim.addGui().defaultEnabled = true
|
||||
// Enable DS but not by default
|
||||
wpi.sim.addDriverstation()
|
||||
|
||||
model {
|
||||
components {
|
||||
frcUserProgram(NativeExecutableSpec) {
|
||||
// We don't need to build for roborio -- if we do, we need to install
|
||||
// a roborio toolchain every time we build in CI
|
||||
// Ideally, we'd be able to set the roborio toolchain as optional, but
|
||||
// I can't figure out how to set that environment variable from build.gradle
|
||||
// (see https://github.com/wpilibsuite/native-utils/blob/2917c69fb5094e36d499c465f047dab81c68446c/ToolchainPlugin/src/main/java/edu/wpi/first/toolchain/ToolchainGraphBuildService.java#L71)
|
||||
// for now, commented out
|
||||
|
||||
// targetPlatform wpi.platforms.roborio
|
||||
|
||||
if (includeDesktopSupport) {
|
||||
targetPlatform wpi.platforms.desktop
|
||||
}
|
||||
|
||||
sources.cpp {
|
||||
source {
|
||||
srcDir 'src/main/cpp'
|
||||
include '**/*.cpp', '**/*.cc'
|
||||
}
|
||||
exportedHeaders {
|
||||
srcDir 'src/main/include'
|
||||
}
|
||||
}
|
||||
|
||||
// Set deploy task to deploy this component
|
||||
deployArtifact.component = it
|
||||
|
||||
// Enable run tasks for this component
|
||||
wpi.cpp.enableExternalTasks(it)
|
||||
|
||||
// Enable simulation for this component
|
||||
wpi.sim.enable(it)
|
||||
// Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries.
|
||||
wpi.cpp.vendor.cpp(it)
|
||||
wpi.cpp.deps.wpilib(it)
|
||||
}
|
||||
}
|
||||
testSuites {
|
||||
frcUserProgramTest(GoogleTestTestSuiteSpec) {
|
||||
testing $.components.frcUserProgram
|
||||
|
||||
sources.cpp {
|
||||
source {
|
||||
srcDir 'src/test/cpp'
|
||||
include '**/*.cpp'
|
||||
}
|
||||
}
|
||||
|
||||
// Enable run tasks for this component
|
||||
wpi.cpp.enableExternalTasks(it)
|
||||
|
||||
wpi.cpp.vendor.cpp(it)
|
||||
wpi.cpp.deps.wpilib(it)
|
||||
wpi.cpp.deps.googleTest(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
241
photonlib-cpp-examples/swervedriveposeestsim/gradlew
vendored
Normal file
241
photonlib-cpp-examples/swervedriveposeestsim/gradlew
vendored
Normal file
@@ -0,0 +1,241 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
91
photonlib-cpp-examples/swervedriveposeestsim/gradlew.bat
vendored
Normal file
91
photonlib-cpp-examples/swervedriveposeestsim/gradlew.bat
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
30
photonlib-cpp-examples/swervedriveposeestsim/settings.gradle
Normal file
30
photonlib-cpp-examples/swervedriveposeestsim/settings.gradle
Normal file
@@ -0,0 +1,30 @@
|
||||
import org.gradle.internal.os.OperatingSystem
|
||||
|
||||
rootProject.name = 'aimattarget'
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
gradlePluginPortal()
|
||||
String frcYear = '2024'
|
||||
File frcHome
|
||||
if (OperatingSystem.current().isWindows()) {
|
||||
String publicFolder = System.getenv('PUBLIC')
|
||||
if (publicFolder == null) {
|
||||
publicFolder = "C:\\Users\\Public"
|
||||
}
|
||||
def homeRoot = new File(publicFolder, "wpilib")
|
||||
frcHome = new File(homeRoot, frcYear)
|
||||
} else {
|
||||
def userFolder = System.getProperty("user.home")
|
||||
def homeRoot = new File(userFolder, "wpilib")
|
||||
frcHome = new File(homeRoot, frcYear)
|
||||
}
|
||||
def frcHomeMaven = new File(frcHome, 'maven')
|
||||
maven {
|
||||
name 'frcHome'
|
||||
url frcHomeMaven
|
||||
}
|
||||
}
|
||||
}
|
||||
98
photonlib-cpp-examples/swervedriveposeestsim/simgui-ds.json
Normal file
98
photonlib-cpp-examples/swervedriveposeestsim/simgui-ds.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"keyboardJoysticks": [
|
||||
{
|
||||
"axisConfig": [
|
||||
{
|
||||
"decKey": 65,
|
||||
"incKey": 68
|
||||
},
|
||||
{
|
||||
"decKey": 87,
|
||||
"incKey": 83
|
||||
},
|
||||
{
|
||||
"decKey": 69,
|
||||
"decayRate": 0.0,
|
||||
"incKey": 82,
|
||||
"keyRate": 0.009999999776482582
|
||||
}
|
||||
],
|
||||
"axisCount": 3,
|
||||
"buttonCount": 4,
|
||||
"buttonKeys": [
|
||||
90,
|
||||
88,
|
||||
67,
|
||||
86
|
||||
],
|
||||
"povConfig": [
|
||||
{
|
||||
"key0": 328,
|
||||
"key135": 323,
|
||||
"key180": 322,
|
||||
"key225": 321,
|
||||
"key270": 324,
|
||||
"key315": 327,
|
||||
"key45": 329,
|
||||
"key90": 326
|
||||
}
|
||||
],
|
||||
"povCount": 1
|
||||
},
|
||||
{
|
||||
"axisConfig": [
|
||||
{
|
||||
"decKey": 74,
|
||||
"incKey": 76
|
||||
},
|
||||
{
|
||||
"decKey": 73,
|
||||
"incKey": 75
|
||||
}
|
||||
],
|
||||
"axisCount": 2,
|
||||
"buttonCount": 4,
|
||||
"buttonKeys": [
|
||||
77,
|
||||
44,
|
||||
46,
|
||||
47
|
||||
],
|
||||
"povCount": 0
|
||||
},
|
||||
{
|
||||
"axisConfig": [
|
||||
{
|
||||
"decKey": 263,
|
||||
"incKey": 262
|
||||
},
|
||||
{
|
||||
"decKey": 265,
|
||||
"incKey": 264
|
||||
}
|
||||
],
|
||||
"axisCount": 2,
|
||||
"buttonCount": 6,
|
||||
"buttonKeys": [
|
||||
260,
|
||||
268,
|
||||
266,
|
||||
261,
|
||||
269,
|
||||
267
|
||||
],
|
||||
"povCount": 0
|
||||
},
|
||||
{
|
||||
"axisCount": 0,
|
||||
"buttonCount": 0,
|
||||
"povCount": 0
|
||||
}
|
||||
],
|
||||
"robotJoysticks": [
|
||||
{
|
||||
"guid": "78696e70757401000000000000000000",
|
||||
"useGamepad": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"Docking": {
|
||||
"Data": []
|
||||
},
|
||||
"MainWindow": {
|
||||
"GLOBAL": {
|
||||
"fps": "120",
|
||||
"height": "910",
|
||||
"maximized": "0",
|
||||
"style": "0",
|
||||
"userScale": "2",
|
||||
"width": "1550",
|
||||
"xpos": "-1602",
|
||||
"ypos": "79"
|
||||
}
|
||||
},
|
||||
"Window": {
|
||||
"###/SmartDashboard/VisionSystemSim-main/Sim Field": {
|
||||
"Collapsed": "0",
|
||||
"Pos": "199,200",
|
||||
"Size": "1342,628"
|
||||
},
|
||||
"###FMS": {
|
||||
"Collapsed": "0",
|
||||
"Pos": "5,540",
|
||||
"Size": "283,146"
|
||||
},
|
||||
"###Joysticks": {
|
||||
"Collapsed": "0",
|
||||
"Pos": "359,95",
|
||||
"Size": "796,240"
|
||||
},
|
||||
"###NetworkTables": {
|
||||
"Collapsed": "0",
|
||||
"Pos": "865,52",
|
||||
"Size": "830,620"
|
||||
},
|
||||
"###Other Devices": {
|
||||
"Collapsed": "0",
|
||||
"Pos": "1025,20",
|
||||
"Size": "250,695"
|
||||
},
|
||||
"###System Joysticks": {
|
||||
"Collapsed": "0",
|
||||
"Pos": "5,350",
|
||||
"Size": "192,218"
|
||||
},
|
||||
"###Timing": {
|
||||
"Collapsed": "0",
|
||||
"Pos": "5,150",
|
||||
"Size": "135,127"
|
||||
},
|
||||
"Debug##Default": {
|
||||
"Collapsed": "0",
|
||||
"Pos": "60,60",
|
||||
"Size": "400,400"
|
||||
},
|
||||
"Robot State": {
|
||||
"Collapsed": "0",
|
||||
"Pos": "5,20",
|
||||
"Size": "92,99"
|
||||
}
|
||||
}
|
||||
}
|
||||
57
photonlib-cpp-examples/swervedriveposeestsim/simgui.json
Normal file
57
photonlib-cpp-examples/swervedriveposeestsim/simgui.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"NTProvider": {
|
||||
"types": {
|
||||
"/FMSInfo": "FMSInfo",
|
||||
"/SmartDashboard/VisionSystemSim-main/Sim Field": "Field2d"
|
||||
},
|
||||
"windows": {
|
||||
"/SmartDashboard/VisionSystemSim-main/Sim Field": {
|
||||
"EstimatedRobotModules": {
|
||||
"arrows": false,
|
||||
"image": "swerve_module.png",
|
||||
"length": 0.30000001192092896,
|
||||
"width": 0.30000001192092896
|
||||
},
|
||||
"apriltag": {
|
||||
"image": "tag-green.png",
|
||||
"length": 0.6000000238418579,
|
||||
"width": 0.6000000238418579
|
||||
},
|
||||
"bottom": 544,
|
||||
"builtin": "2023 Charged Up",
|
||||
"cameras": {
|
||||
"arrowSize": 19,
|
||||
"arrowWeight": 1.0,
|
||||
"style": "Hidden"
|
||||
},
|
||||
"height": 8.013679504394531,
|
||||
"left": 46,
|
||||
"right": 1088,
|
||||
"top": 36,
|
||||
"visibleTargetPoses": {
|
||||
"image": "tag-blue.png"
|
||||
},
|
||||
"width": 16.541748046875,
|
||||
"window": {
|
||||
"visible": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"NetworkTables": {
|
||||
"transitory": {
|
||||
"SmartDashboard": {
|
||||
"Drive": {
|
||||
"open": true
|
||||
},
|
||||
"open": true
|
||||
},
|
||||
"photonvision": {
|
||||
"open": true,
|
||||
"photonvision": {
|
||||
"open": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "Robot.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <frc/simulation/BatterySim.h>
|
||||
#include <frc/simulation/RoboRioSim.h>
|
||||
|
||||
void Robot::RobotInit() {}
|
||||
|
||||
void Robot::RobotPeriodic() {
|
||||
drivetrain.Periodic();
|
||||
|
||||
auto visionEst = vision.GetEstimatedGlobalPose();
|
||||
if (visionEst.has_value()) {
|
||||
auto est = visionEst.value();
|
||||
auto estPose = est.estimatedPose.ToPose2d();
|
||||
auto estStdDevs = vision.GetEstimationStdDevs(estPose);
|
||||
drivetrain.AddVisionMeasurement(est.estimatedPose.ToPose2d(), est.timestamp,
|
||||
estStdDevs);
|
||||
}
|
||||
|
||||
drivetrain.Log();
|
||||
}
|
||||
|
||||
void Robot::DisabledInit() {}
|
||||
|
||||
void Robot::DisabledPeriodic() { drivetrain.Stop(); }
|
||||
|
||||
void Robot::DisabledExit() {}
|
||||
|
||||
void Robot::AutonomousInit() {
|
||||
autoTimer.Restart();
|
||||
frc::Pose2d pose{1_m, 1_m, frc::Rotation2d{}};
|
||||
drivetrain.ResetPose(pose, true);
|
||||
}
|
||||
|
||||
void Robot::AutonomousPeriodic() {
|
||||
if (autoTimer.Get() < 10_s) {
|
||||
drivetrain.Drive(0.5_mps, 0.5_mps, 0.5_rad_per_s, false);
|
||||
} else {
|
||||
autoTimer.Stop();
|
||||
drivetrain.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
void Robot::AutonomousExit() {}
|
||||
|
||||
void Robot::TeleopInit() {}
|
||||
|
||||
void Robot::TeleopPeriodic() {
|
||||
double forward = -controller.GetLeftY() * kDriveSpeed;
|
||||
if (std::abs(forward) < 0.1) {
|
||||
forward = 0;
|
||||
}
|
||||
forward = forwardLimiter.Calculate(forward);
|
||||
|
||||
double strafe = -controller.GetLeftX() * kDriveSpeed;
|
||||
if (std::abs(strafe) < 0.1) {
|
||||
strafe = 0;
|
||||
}
|
||||
strafe = strafeLimiter.Calculate(strafe);
|
||||
|
||||
double turn = -controller.GetRightX() * kDriveSpeed;
|
||||
if (std::abs(turn) < 0.1) {
|
||||
turn = 0;
|
||||
}
|
||||
turn = turnLimiter.Calculate(turn);
|
||||
|
||||
drivetrain.Drive(forward * constants::Swerve::kMaxLinearSpeed,
|
||||
strafe * constants::Swerve::kMaxLinearSpeed,
|
||||
turn * constants::Swerve::kMaxAngularSpeed, true);
|
||||
}
|
||||
|
||||
void Robot::TeleopExit() {}
|
||||
|
||||
void Robot::TestInit() {}
|
||||
|
||||
void Robot::TestPeriodic() {}
|
||||
|
||||
void Robot::TestExit() {}
|
||||
|
||||
void Robot::SimulationPeriodic() {
|
||||
drivetrain.SimulationPeriodic();
|
||||
vision.SimPeriodic(drivetrain.GetSimPose());
|
||||
|
||||
frc::Field2d& debugField = vision.GetSimDebugField();
|
||||
debugField.GetObject("EstimatedRobot")->SetPose(drivetrain.GetPose());
|
||||
debugField.GetObject("EstimatedRobotModules")
|
||||
->SetPoses(drivetrain.GetModulePoses());
|
||||
|
||||
units::ampere_t totalCurrent = drivetrain.GetCurrentDraw();
|
||||
units::volt_t loadedBattVolts =
|
||||
frc::sim::BatterySim::Calculate({totalCurrent});
|
||||
frc::sim::RoboRioSim::SetVInVoltage(loadedBattVolts);
|
||||
}
|
||||
|
||||
#ifndef RUNNING_FRC_TESTS
|
||||
int main() { return frc::StartRobot<Robot>(); }
|
||||
#endif
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "subsystems/SwerveDrive.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <frc/TimedRobot.h>
|
||||
#include <frc/smartdashboard/SmartDashboard.h>
|
||||
|
||||
SwerveDrive::SwerveDrive()
|
||||
: poseEstimator(kinematics, GetGyroYaw(), GetModulePositions(),
|
||||
frc::Pose2d{}, {0.1, 0.1, 0.1}, {1.0, 1.0, 1.0}),
|
||||
gyroSim(gyro),
|
||||
swerveDriveSim(constants::Swerve::kDriveFF, frc::DCMotor::Falcon500(1),
|
||||
constants::Swerve::kDriveGearRatio,
|
||||
constants::Swerve::kWheelDiameter / 2,
|
||||
constants::Swerve::kSteerFF, frc::DCMotor::Falcon500(1),
|
||||
constants::Swerve::kSteerGearRatio, kinematics) {}
|
||||
|
||||
void SwerveDrive::Periodic() {
|
||||
for (auto& currentModule : swerveMods) {
|
||||
currentModule.Periodic();
|
||||
}
|
||||
|
||||
poseEstimator.Update(GetGyroYaw(), GetModulePositions());
|
||||
}
|
||||
|
||||
void SwerveDrive::Drive(units::meters_per_second_t vx,
|
||||
units::meters_per_second_t vy,
|
||||
units::radians_per_second_t omega, bool openLoop) {
|
||||
frc::ChassisSpeeds newChassisSpeeds =
|
||||
frc::ChassisSpeeds::FromFieldRelativeSpeeds(vx, vy, omega, GetHeading());
|
||||
SetChassisSpeeds(newChassisSpeeds, openLoop, false);
|
||||
}
|
||||
|
||||
void SwerveDrive::SetChassisSpeeds(const frc::ChassisSpeeds& newChassisSpeeds,
|
||||
bool openLoop, bool steerInPlace) {
|
||||
SetModuleStates(kinematics.ToSwerveModuleStates(newChassisSpeeds), openLoop,
|
||||
steerInPlace);
|
||||
this->targetChassisSpeeds = newChassisSpeeds;
|
||||
}
|
||||
|
||||
void SwerveDrive::SetModuleStates(
|
||||
const std::array<frc::SwerveModuleState, 4>& desiredStates, bool openLoop,
|
||||
bool steerInPlace) {
|
||||
std::array<frc::SwerveModuleState, 4> desaturatedStates = desiredStates;
|
||||
frc::SwerveDriveKinematics<4>::DesaturateWheelSpeeds(
|
||||
static_cast<wpi::array<frc::SwerveModuleState, 4>*>(&desaturatedStates),
|
||||
constants::Swerve::kMaxLinearSpeed);
|
||||
for (int i = 0; i < swerveMods.size(); i++) {
|
||||
swerveMods[i].SetDesiredState(desaturatedStates[i], openLoop, steerInPlace);
|
||||
}
|
||||
}
|
||||
|
||||
void SwerveDrive::Stop() { Drive(0_mps, 0_mps, 0_rad_per_s, true); }
|
||||
|
||||
void SwerveDrive::AddVisionMeasurement(const frc::Pose2d& visionMeasurement,
|
||||
units::second_t timestamp) {
|
||||
poseEstimator.AddVisionMeasurement(visionMeasurement, timestamp);
|
||||
}
|
||||
|
||||
void SwerveDrive::AddVisionMeasurement(const frc::Pose2d& visionMeasurement,
|
||||
units::second_t timestamp,
|
||||
const Eigen::Vector3d& stdDevs) {
|
||||
wpi::array<double, 3> newStdDevs{stdDevs(0), stdDevs(1), stdDevs(2)};
|
||||
poseEstimator.AddVisionMeasurement(visionMeasurement, timestamp, newStdDevs);
|
||||
}
|
||||
|
||||
void SwerveDrive::ResetPose(const frc::Pose2d& pose, bool resetSimPose) {
|
||||
if (resetSimPose) {
|
||||
swerveDriveSim.Reset(pose, false);
|
||||
for (int i = 0; i < swerveMods.size(); i++) {
|
||||
swerveMods[i].SimulationUpdate(0_m, 0_mps, 0_A, 0_rad, 0_rad_per_s, 0_A);
|
||||
}
|
||||
gyroSim.SetAngle(-pose.Rotation().Degrees());
|
||||
gyroSim.SetRate(0_rad_per_s);
|
||||
}
|
||||
|
||||
poseEstimator.ResetPosition(GetGyroYaw(), GetModulePositions(), pose);
|
||||
}
|
||||
|
||||
frc::Pose2d SwerveDrive::GetPose() const {
|
||||
return poseEstimator.GetEstimatedPosition();
|
||||
}
|
||||
|
||||
frc::Rotation2d SwerveDrive::GetHeading() const { return GetPose().Rotation(); }
|
||||
|
||||
frc::Rotation2d SwerveDrive::GetGyroYaw() const { return gyro.GetRotation2d(); }
|
||||
|
||||
frc::ChassisSpeeds SwerveDrive::GetChassisSpeeds() const {
|
||||
return kinematics.ToChassisSpeeds(GetModuleStates());
|
||||
}
|
||||
|
||||
std::array<frc::SwerveModuleState, 4> SwerveDrive::GetModuleStates() const {
|
||||
std::array<frc::SwerveModuleState, 4> moduleStates;
|
||||
moduleStates[0] = swerveMods[0].GetState();
|
||||
moduleStates[1] = swerveMods[1].GetState();
|
||||
moduleStates[2] = swerveMods[2].GetState();
|
||||
moduleStates[3] = swerveMods[3].GetState();
|
||||
return moduleStates;
|
||||
}
|
||||
|
||||
std::array<frc::SwerveModulePosition, 4> SwerveDrive::GetModulePositions()
|
||||
const {
|
||||
std::array<frc::SwerveModulePosition, 4> modulePositions;
|
||||
modulePositions[0] = swerveMods[0].GetPosition();
|
||||
modulePositions[1] = swerveMods[1].GetPosition();
|
||||
modulePositions[2] = swerveMods[2].GetPosition();
|
||||
modulePositions[3] = swerveMods[3].GetPosition();
|
||||
return modulePositions;
|
||||
}
|
||||
|
||||
std::array<frc::Pose2d, 4> SwerveDrive::GetModulePoses() const {
|
||||
std::array<frc::Pose2d, 4> modulePoses;
|
||||
for (int i = 0; i < swerveMods.size(); i++) {
|
||||
const SwerveModule& module = swerveMods[i];
|
||||
modulePoses[i] = GetPose().TransformBy(frc::Transform2d{
|
||||
module.GetModuleConstants().centerOffset, module.GetAbsoluteHeading()});
|
||||
}
|
||||
return modulePoses;
|
||||
}
|
||||
|
||||
void SwerveDrive::Log() {
|
||||
std::string table = "Drive/";
|
||||
frc::Pose2d pose = GetPose();
|
||||
frc::SmartDashboard::PutNumber(table + "X", pose.X().to<double>());
|
||||
frc::SmartDashboard::PutNumber(table + "Y", pose.Y().to<double>());
|
||||
frc::SmartDashboard::PutNumber(table + "Heading",
|
||||
pose.Rotation().Degrees().to<double>());
|
||||
frc::ChassisSpeeds chassisSpeeds = GetChassisSpeeds();
|
||||
frc::SmartDashboard::PutNumber(table + "VX", chassisSpeeds.vx.to<double>());
|
||||
frc::SmartDashboard::PutNumber(table + "VY", chassisSpeeds.vy.to<double>());
|
||||
frc::SmartDashboard::PutNumber(
|
||||
table + "Omega Degrees",
|
||||
chassisSpeeds.omega.convert<units::degrees_per_second>().to<double>());
|
||||
frc::SmartDashboard::PutNumber(table + "Target VX",
|
||||
targetChassisSpeeds.vx.to<double>());
|
||||
frc::SmartDashboard::PutNumber(table + "Target VY",
|
||||
targetChassisSpeeds.vy.to<double>());
|
||||
frc::SmartDashboard::PutNumber(
|
||||
table + "Target Omega Degrees",
|
||||
targetChassisSpeeds.omega.convert<units::degrees_per_second>()
|
||||
.to<double>());
|
||||
|
||||
for (auto& module : swerveMods) {
|
||||
module.Log();
|
||||
}
|
||||
}
|
||||
|
||||
void SwerveDrive::SimulationPeriodic() {
|
||||
std::array<units::volt_t, 4> driveInputs;
|
||||
std::array<units::volt_t, 4> steerInputs;
|
||||
for (int i = 0; i < swerveMods.size(); i++) {
|
||||
driveInputs[i] = swerveMods[i].GetDriveVoltage();
|
||||
steerInputs[i] = swerveMods[i].GetSteerVoltage();
|
||||
}
|
||||
swerveDriveSim.SetDriveInputs(driveInputs);
|
||||
swerveDriveSim.SetSteerInputs(steerInputs);
|
||||
|
||||
swerveDriveSim.Update(frc::TimedRobot::kDefaultPeriod);
|
||||
|
||||
auto driveStates = swerveDriveSim.GetDriveStates();
|
||||
auto steerStates = swerveDriveSim.GetSteerStates();
|
||||
totalCurrentDraw = 0_A;
|
||||
std::array<units::ampere_t, 4> driveCurrents =
|
||||
swerveDriveSim.GetDriveCurrentDraw();
|
||||
for (const auto& current : driveCurrents) {
|
||||
totalCurrentDraw += current;
|
||||
}
|
||||
std::array<units::ampere_t, 4> steerCurrents =
|
||||
swerveDriveSim.GetSteerCurrentDraw();
|
||||
for (const auto& current : steerCurrents) {
|
||||
totalCurrentDraw += current;
|
||||
}
|
||||
for (int i = 0; i < swerveMods.size(); i++) {
|
||||
units::meter_t drivePos{driveStates[i](0, 0)};
|
||||
units::meters_per_second_t driveRate{driveStates[i](1, 0)};
|
||||
units::radian_t steerPos{steerStates[i](0, 0)};
|
||||
units::radians_per_second_t steerRate{steerStates[i](1, 0)};
|
||||
swerveMods[i].SimulationUpdate(drivePos, driveRate, driveCurrents[i],
|
||||
steerPos, steerRate, steerCurrents[i]);
|
||||
}
|
||||
gyroSim.SetRate(-swerveDriveSim.GetOmega());
|
||||
gyroSim.SetAngle(-swerveDriveSim.GetPose().Rotation().Degrees());
|
||||
}
|
||||
|
||||
frc::Pose2d SwerveDrive::GetSimPose() const { return swerveDriveSim.GetPose(); }
|
||||
|
||||
units::ampere_t SwerveDrive::GetCurrentDraw() const { return totalCurrentDraw; }
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "subsystems/SwerveDriveSim.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <frc/RobotController.h>
|
||||
#include <frc/system/Discretization.h>
|
||||
|
||||
template <typename T>
|
||||
int sgn(T val) {
|
||||
return (T(0) < val) - (val < T(0));
|
||||
}
|
||||
|
||||
SwerveDriveSim::SwerveDriveSim(
|
||||
const frc::SimpleMotorFeedforward<units::meters>& driveFF,
|
||||
const frc::DCMotor& driveMotor, double driveGearing,
|
||||
units::meter_t driveWheelRadius,
|
||||
const frc::SimpleMotorFeedforward<units::radians>& steerFF,
|
||||
const frc::DCMotor& steerMotor, double steerGearing,
|
||||
const frc::SwerveDriveKinematics<numModules>& kinematics)
|
||||
: SwerveDriveSim(
|
||||
frc::LinearSystem<2, 1, 2>{
|
||||
(Eigen::MatrixXd(2, 2) << 0.0, 1.0, 0.0,
|
||||
-driveFF.kV.to<double>() / driveFF.kA.to<double>())
|
||||
.finished(),
|
||||
Eigen::Matrix<double, 2, 1>{0.0, 1.0 / driveFF.kA.to<double>()},
|
||||
(Eigen::MatrixXd(2, 2) << 1.0, 0.0, 0.0, 1.0).finished(),
|
||||
Eigen::Matrix<double, 2, 1>{0.0, 0.0}},
|
||||
driveFF.kS, driveMotor, driveGearing, driveWheelRadius,
|
||||
frc::LinearSystem<2, 1, 2>{
|
||||
(Eigen::MatrixXd(2, 2) << 0.0, 1.0, 0.0,
|
||||
-steerFF.kV.to<double>() / steerFF.kA.to<double>())
|
||||
.finished(),
|
||||
Eigen::Matrix<double, 2, 1>{0.0, 1.0 / steerFF.kA.to<double>()},
|
||||
(Eigen::MatrixXd(2, 2) << 1.0, 0.0, 0.0, 1.0).finished(),
|
||||
Eigen::Matrix<double, 2, 1>{0.0, 0.0}},
|
||||
steerFF.kS, steerMotor, steerGearing, kinematics) {}
|
||||
|
||||
SwerveDriveSim::SwerveDriveSim(
|
||||
const frc::LinearSystem<2, 1, 2>& drivePlant, units::volt_t driveKs,
|
||||
const frc::DCMotor& driveMotor, double driveGearing,
|
||||
units::meter_t driveWheelRadius,
|
||||
const frc::LinearSystem<2, 1, 2>& steerPlant, units::volt_t steerKs,
|
||||
const frc::DCMotor& steerMotor, double steerGearing,
|
||||
const frc::SwerveDriveKinematics<numModules>& kinematics)
|
||||
: drivePlant(drivePlant),
|
||||
driveKs(driveKs),
|
||||
driveMotor(driveMotor),
|
||||
driveGearing(driveGearing),
|
||||
driveWheelRadius(driveWheelRadius),
|
||||
steerPlant(steerPlant),
|
||||
steerKs(steerKs),
|
||||
steerMotor(steerMotor),
|
||||
steerGearing(steerGearing),
|
||||
kinematics(kinematics) {}
|
||||
|
||||
void SwerveDriveSim::SetDriveInputs(
|
||||
const std::array<units::volt_t, numModules>& inputs) {
|
||||
units::volt_t battVoltage = frc::RobotController::GetBatteryVoltage();
|
||||
for (int i = 0; i < driveInputs.size(); i++) {
|
||||
units::volt_t input = inputs[i];
|
||||
driveInputs[i] = std::clamp(input, -battVoltage, battVoltage);
|
||||
}
|
||||
}
|
||||
|
||||
void SwerveDriveSim::SetSteerInputs(
|
||||
const std::array<units::volt_t, numModules>& inputs) {
|
||||
units::volt_t battVoltage = frc::RobotController::GetBatteryVoltage();
|
||||
for (int i = 0; i < steerInputs.size(); i++) {
|
||||
units::volt_t input = inputs[i];
|
||||
steerInputs[i] = std::clamp(input, -battVoltage, battVoltage);
|
||||
}
|
||||
}
|
||||
|
||||
Eigen::Matrix<double, 2, 1> SwerveDriveSim::CalculateX(
|
||||
const Eigen::Matrix<double, 2, 2>& discA,
|
||||
const Eigen::Matrix<double, 2, 1>& discB,
|
||||
const Eigen::Matrix<double, 2, 1>& x, units::volt_t input,
|
||||
units::volt_t kS) {
|
||||
auto Ax = discA * x;
|
||||
double nextStateVel = Ax(1, 0);
|
||||
double inputToStop = nextStateVel / -discB(1, 0);
|
||||
double ksSystemEffect =
|
||||
std::clamp(inputToStop, -kS.to<double>(), kS.to<double>());
|
||||
|
||||
nextStateVel += discB(1, 0) * ksSystemEffect;
|
||||
inputToStop = nextStateVel / -discB(1, 0);
|
||||
double signToStop = sgn(inputToStop);
|
||||
double inputSign = sgn(input.to<double>());
|
||||
double ksInputEffect = 0;
|
||||
|
||||
if (std::abs(ksSystemEffect) < kS.to<double>()) {
|
||||
double absInput = std::abs(input.to<double>());
|
||||
ksInputEffect =
|
||||
-std::clamp(kS.to<double>() * inputSign, -absInput, absInput);
|
||||
} else if ((input.to<double>() * signToStop) > (inputToStop * signToStop)) {
|
||||
double absInput = std::abs(input.to<double>() - inputToStop);
|
||||
ksInputEffect =
|
||||
-std::clamp(kS.to<double>() * inputSign, -absInput, absInput);
|
||||
}
|
||||
|
||||
auto sF = Eigen::Matrix<double, 1, 1>{input.to<double>() + ksSystemEffect +
|
||||
ksInputEffect};
|
||||
auto Bu = discB * sF;
|
||||
auto retVal = Ax + Bu;
|
||||
return retVal;
|
||||
}
|
||||
|
||||
void SwerveDriveSim::Update(units::second_t dt) {
|
||||
Eigen::Matrix<double, 2, 2> driveDiscA;
|
||||
Eigen::Matrix<double, 2, 1> driveDiscB;
|
||||
frc::DiscretizeAB<2, 1>(drivePlant.A(), drivePlant.B(), dt, &driveDiscA,
|
||||
&driveDiscB);
|
||||
|
||||
Eigen::Matrix<double, 2, 2> steerDiscA;
|
||||
Eigen::Matrix<double, 2, 1> steerDiscB;
|
||||
frc::DiscretizeAB<2, 1>(steerPlant.A(), steerPlant.B(), dt, &steerDiscA,
|
||||
&steerDiscB);
|
||||
|
||||
std::array<frc::SwerveModulePosition, 4> moduleDeltas;
|
||||
|
||||
for (int i = 0; i < numModules; i++) {
|
||||
double prevDriveStatePos = driveStates[i](0, 0);
|
||||
driveStates[i] = CalculateX(driveDiscA, driveDiscB, driveStates[i],
|
||||
driveInputs[i], driveKs);
|
||||
double currentDriveStatePos = driveStates[i](0, 0);
|
||||
steerStates[i] = CalculateX(steerDiscA, steerDiscB, steerStates[i],
|
||||
steerInputs[i], steerKs);
|
||||
double currentSteerStatePos = steerStates[i](0, 0);
|
||||
moduleDeltas[i] = frc::SwerveModulePosition{
|
||||
units::meter_t{currentDriveStatePos - prevDriveStatePos},
|
||||
frc::Rotation2d{units::radian_t{currentSteerStatePos}}};
|
||||
}
|
||||
|
||||
frc::Twist2d twist = kinematics.ToTwist2d(moduleDeltas);
|
||||
pose = pose.Exp(twist);
|
||||
omega = twist.dtheta / dt;
|
||||
}
|
||||
|
||||
void SwerveDriveSim::Reset(const frc::Pose2d& pose, bool preserveMotion) {
|
||||
this->pose = pose;
|
||||
if (!preserveMotion) {
|
||||
for (int i = 0; i < numModules; i++) {
|
||||
driveStates[i] = Eigen::Matrix<double, 2, 1>{0, 0};
|
||||
steerStates[i] = Eigen::Matrix<double, 2, 1>{0, 0};
|
||||
}
|
||||
omega = 0_rad_per_s;
|
||||
}
|
||||
}
|
||||
|
||||
void SwerveDriveSim::Reset(const frc::Pose2d& pose,
|
||||
const std::array<Eigen::Matrix<double, 2, 1>,
|
||||
numModules>& moduleDriveStates,
|
||||
const std::array<Eigen::Matrix<double, 2, 1>,
|
||||
numModules>& moduleSteerStates) {
|
||||
this->pose = pose;
|
||||
driveStates = moduleDriveStates;
|
||||
steerStates = moduleSteerStates;
|
||||
omega = kinematics.ToChassisSpeeds(GetModuleStates()).omega;
|
||||
}
|
||||
|
||||
frc::Pose2d SwerveDriveSim::GetPose() const { return pose; }
|
||||
|
||||
std::array<frc::SwerveModulePosition, numModules>
|
||||
SwerveDriveSim::GetModulePositions() const {
|
||||
std::array<frc::SwerveModulePosition, numModules> positions;
|
||||
for (int i = 0; i < numModules; i++) {
|
||||
positions[i] = frc::SwerveModulePosition{
|
||||
units::meter_t{driveStates[i](0, 0)},
|
||||
frc::Rotation2d{units::radian_t{steerStates[i](0, 0)}}};
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
std::array<frc::SwerveModulePosition, numModules>
|
||||
SwerveDriveSim::GetNoisyModulePositions(units::meter_t driveStdDev,
|
||||
units::radian_t steerStdDev) {
|
||||
std::array<frc::SwerveModulePosition, numModules> positions;
|
||||
for (int i = 0; i < numModules; i++) {
|
||||
positions[i] = frc::SwerveModulePosition{
|
||||
units::meter_t{driveStates[i](0, 0)} +
|
||||
randDist(generator) * driveStdDev,
|
||||
frc::Rotation2d{units::radian_t{steerStates[i](0, 0)} +
|
||||
randDist(generator) * steerStdDev}};
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
std::array<frc::SwerveModuleState, numModules>
|
||||
SwerveDriveSim::GetModuleStates() {
|
||||
std::array<frc::SwerveModuleState, numModules> states;
|
||||
for (int i = 0; i < numModules; i++) {
|
||||
states[i] = frc::SwerveModuleState{
|
||||
units::meters_per_second_t{driveStates[i](1, 0)},
|
||||
frc::Rotation2d{units::radian_t{steerStates[i](0, 0)}}};
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
std::array<Eigen::Matrix<double, 2, 1>, numModules>
|
||||
SwerveDriveSim::GetDriveStates() const {
|
||||
return driveStates;
|
||||
}
|
||||
|
||||
std::array<Eigen::Matrix<double, 2, 1>, numModules>
|
||||
SwerveDriveSim::GetSteerStates() const {
|
||||
return steerStates;
|
||||
}
|
||||
|
||||
units::radians_per_second_t SwerveDriveSim::GetOmega() const { return omega; }
|
||||
|
||||
units::ampere_t SwerveDriveSim::GetCurrentDraw(
|
||||
const frc::DCMotor& motor, units::radians_per_second_t velocity,
|
||||
units::volt_t inputVolts, units::volt_t batteryVolts) const {
|
||||
units::volt_t effVolts = inputVolts - velocity / motor.Kv;
|
||||
if (inputVolts >= 0_V) {
|
||||
effVolts = std::clamp(effVolts, 0_V, inputVolts);
|
||||
} else {
|
||||
effVolts = std::clamp(effVolts, inputVolts, 0_V);
|
||||
}
|
||||
auto retVal = (inputVolts / batteryVolts) * (effVolts / motor.R);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
std::array<units::ampere_t, numModules> SwerveDriveSim::GetDriveCurrentDraw()
|
||||
const {
|
||||
std::array<units::ampere_t, numModules> currents;
|
||||
for (int i = 0; i < numModules; i++) {
|
||||
units::radians_per_second_t speed =
|
||||
units::radians_per_second_t{driveStates[i](1, 0)} * driveGearing /
|
||||
driveWheelRadius.to<double>();
|
||||
currents[i] = GetCurrentDraw(driveMotor, speed, driveInputs[i],
|
||||
frc::RobotController::GetBatteryVoltage());
|
||||
}
|
||||
return currents;
|
||||
}
|
||||
|
||||
std::array<units::ampere_t, numModules> SwerveDriveSim::GetSteerCurrentDraw()
|
||||
const {
|
||||
std::array<units::ampere_t, numModules> currents;
|
||||
for (int i = 0; i < numModules; i++) {
|
||||
units::radians_per_second_t speed =
|
||||
units::radians_per_second_t{steerStates[i](1, 0) * steerGearing};
|
||||
// TODO: If uncommented we get huge current values.. Not sure how to fix
|
||||
// atm. :(
|
||||
currents[i] = 20_A;
|
||||
// currents[i] = GetCurrentDraw(steerMotor, speed, steerInputs[i],
|
||||
// frc::RobotController::GetBatteryVoltage());
|
||||
}
|
||||
return currents;
|
||||
}
|
||||
|
||||
units::ampere_t SwerveDriveSim::GetTotalCurrentDraw() const {
|
||||
units::ampere_t total{0};
|
||||
for (const auto& val : GetDriveCurrentDraw()) {
|
||||
total += val;
|
||||
}
|
||||
for (const auto& val : GetSteerCurrentDraw()) {
|
||||
total += val;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "subsystems/SwerveModule.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <frc/MathUtil.h>
|
||||
#include <frc/RobotController.h>
|
||||
#include <frc/smartdashboard/SmartDashboard.h>
|
||||
|
||||
SwerveModule::SwerveModule(const constants::Swerve::ModuleConstants& consts)
|
||||
: moduleConstants(consts),
|
||||
driveMotor(frc::PWMSparkMax{moduleConstants.driveMotorId}),
|
||||
driveEncoder(frc::Encoder{moduleConstants.driveEncoderA,
|
||||
moduleConstants.driveEncoderB}),
|
||||
steerMotor(frc::PWMSparkMax{moduleConstants.steerMotorId}),
|
||||
steerEncoder(frc::Encoder{moduleConstants.steerEncoderA,
|
||||
moduleConstants.steerEncoderB}),
|
||||
driveEncoderSim(driveEncoder),
|
||||
steerEncoderSim(steerEncoder) {
|
||||
driveEncoder.SetDistancePerPulse(
|
||||
constants::Swerve::kDriveDistPerPulse.to<double>());
|
||||
steerEncoder.SetDistancePerPulse(
|
||||
constants::Swerve::kSteerRadPerPulse.to<double>());
|
||||
steerPIDController.EnableContinuousInput(-std::numbers::pi, std::numbers::pi);
|
||||
}
|
||||
|
||||
void SwerveModule::Periodic() {
|
||||
units::volt_t steerPID = units::volt_t{
|
||||
steerPIDController.Calculate(GetAbsoluteHeading().Radians().to<double>(),
|
||||
desiredState.angle.Radians().to<double>())};
|
||||
steerMotor.SetVoltage(steerPID);
|
||||
|
||||
units::volt_t driveFF =
|
||||
constants::Swerve::kDriveFF.Calculate(desiredState.speed);
|
||||
units::volt_t drivePID{0};
|
||||
if (!openLoop) {
|
||||
drivePID = units::volt_t{drivePIDController.Calculate(
|
||||
driveEncoder.GetRate(), desiredState.speed.to<double>())};
|
||||
}
|
||||
driveMotor.SetVoltage(driveFF + drivePID);
|
||||
}
|
||||
|
||||
void SwerveModule::SetDesiredState(const frc::SwerveModuleState& newState,
|
||||
bool shouldBeOpenLoop, bool steerInPlace) {
|
||||
frc::Rotation2d currentRotation = GetAbsoluteHeading();
|
||||
frc::SwerveModuleState optimizedState =
|
||||
frc::SwerveModuleState::Optimize(newState, currentRotation);
|
||||
desiredState = optimizedState;
|
||||
}
|
||||
|
||||
frc::Rotation2d SwerveModule::GetAbsoluteHeading() const {
|
||||
return frc::Rotation2d{units::radian_t{steerEncoder.GetDistance()}};
|
||||
}
|
||||
|
||||
frc::SwerveModuleState SwerveModule::GetState() const {
|
||||
return frc::SwerveModuleState{driveEncoder.GetRate() * 1_mps,
|
||||
GetAbsoluteHeading()};
|
||||
}
|
||||
|
||||
frc::SwerveModulePosition SwerveModule::GetPosition() const {
|
||||
return frc::SwerveModulePosition{driveEncoder.GetDistance() * 1_m,
|
||||
GetAbsoluteHeading()};
|
||||
}
|
||||
|
||||
units::volt_t SwerveModule::GetDriveVoltage() const {
|
||||
return driveMotor.Get() * frc::RobotController::GetBatteryVoltage();
|
||||
}
|
||||
|
||||
units::volt_t SwerveModule::GetSteerVoltage() const {
|
||||
return steerMotor.Get() * frc::RobotController::GetBatteryVoltage();
|
||||
}
|
||||
|
||||
units::ampere_t SwerveModule::GetDriveCurrentSim() const {
|
||||
return driveCurrentSim;
|
||||
}
|
||||
|
||||
units::ampere_t SwerveModule::GetSteerCurrentSim() const {
|
||||
return steerCurrentSim;
|
||||
}
|
||||
|
||||
constants::Swerve::ModuleConstants SwerveModule::GetModuleConstants() const {
|
||||
return moduleConstants;
|
||||
}
|
||||
|
||||
void SwerveModule::Log() {
|
||||
frc::SwerveModuleState state = GetState();
|
||||
|
||||
std::string table =
|
||||
"Module " + std::to_string(moduleConstants.moduleNum) + "/";
|
||||
frc::SmartDashboard::PutNumber(table + "Steer Degrees",
|
||||
frc::AngleModulus(state.angle.Radians())
|
||||
.convert<units::degrees>()
|
||||
.to<double>());
|
||||
frc::SmartDashboard::PutNumber(
|
||||
table + "Steer Target Degrees",
|
||||
units::radian_t{steerPIDController.GetSetpoint()}
|
||||
.convert<units::degrees>()
|
||||
.to<double>());
|
||||
frc::SmartDashboard::PutNumber(
|
||||
table + "Drive Velocity Feet",
|
||||
state.speed.convert<units::feet_per_second>().to<double>());
|
||||
frc::SmartDashboard::PutNumber(
|
||||
table + "Drive Velocity Target Feet",
|
||||
desiredState.speed.convert<units::feet_per_second>().to<double>());
|
||||
frc::SmartDashboard::PutNumber(table + "Drive Current",
|
||||
driveCurrentSim.to<double>());
|
||||
frc::SmartDashboard::PutNumber(table + "Steer Current",
|
||||
steerCurrentSim.to<double>());
|
||||
}
|
||||
|
||||
void SwerveModule::SimulationUpdate(
|
||||
units::meter_t driveEncoderDist,
|
||||
units::meters_per_second_t driveEncoderRate, units::ampere_t driveCurrent,
|
||||
units::radian_t steerEncoderDist,
|
||||
units::radians_per_second_t steerEncoderRate,
|
||||
units::ampere_t steerCurrent) {
|
||||
driveEncoderSim.SetDistance(driveEncoderDist.to<double>());
|
||||
driveEncoderSim.SetRate(driveEncoderRate.to<double>());
|
||||
driveCurrentSim = driveCurrent;
|
||||
steerEncoderSim.SetDistance(steerEncoderDist.to<double>());
|
||||
steerEncoderSim.SetRate(steerEncoderRate.to<double>());
|
||||
steerCurrentSim = steerCurrent;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
Files placed in this directory will be deployed to the RoboRIO into the
|
||||
'deploy' directory in the home folder. Use the 'frc::filesystem::GetDeployDirectory'
|
||||
function from the 'frc/Filesystem.h' header to get a proper path relative to the deploy
|
||||
directory.
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <numbers>
|
||||
|
||||
#include <frc/apriltag/AprilTagFieldLayout.h>
|
||||
#include <frc/apriltag/AprilTagFields.h>
|
||||
#include <frc/controller/SimpleMotorFeedforward.h>
|
||||
#include <frc/geometry/Transform3d.h>
|
||||
#include <frc/geometry/Translation2d.h>
|
||||
#include <units/length.h>
|
||||
|
||||
namespace constants {
|
||||
namespace Vision {
|
||||
static constexpr std::string_view kCameraName{"YOUR CAMERA NAME"};
|
||||
static const frc::Transform3d kRobotToCam{
|
||||
frc::Translation3d{0.5_m, 0.0_m, 0.5_m},
|
||||
frc::Rotation3d{0_rad, 0_rad, 0_rad}};
|
||||
static const frc::AprilTagFieldLayout kTagLayout{
|
||||
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2023ChargedUp)};
|
||||
|
||||
static const Eigen::Matrix<double, 3, 1> kSingleTagStdDevs{4, 4, 8};
|
||||
static const Eigen::Matrix<double, 3, 1> kMultiTagStdDevs{0.5, 0.5, 1};
|
||||
} // namespace Vision
|
||||
namespace Swerve {
|
||||
|
||||
static constexpr units::meter_t kTrackWidth{18.5_in};
|
||||
static constexpr units::meter_t kTrackLength{18.5_in};
|
||||
static constexpr units::meter_t kRobotWidth{25_in + 3.25_in * 2};
|
||||
static constexpr units::meter_t kRobotLength{25_in + 3.25_in * 2};
|
||||
static constexpr units::meters_per_second_t kMaxLinearSpeed{15.5_fps};
|
||||
static constexpr units::radians_per_second_t kMaxAngularSpeed{720_deg_per_s};
|
||||
static constexpr units::meter_t kWheelDiameter{4_in};
|
||||
static constexpr units::meter_t kWheelCircumference{kWheelDiameter *
|
||||
std::numbers::pi};
|
||||
|
||||
static constexpr double kDriveGearRatio = 6.75;
|
||||
static constexpr double kSteerGearRatio = 12.8;
|
||||
|
||||
static constexpr units::meter_t kDriveDistPerPulse =
|
||||
kWheelCircumference / 1024.0 / kDriveGearRatio;
|
||||
static constexpr units::radian_t kSteerRadPerPulse =
|
||||
units::radian_t{2 * std::numbers::pi} / 1024.0;
|
||||
|
||||
static constexpr double kDriveKP = 1.0;
|
||||
static constexpr double kDriveKI = 0.0;
|
||||
static constexpr double kDriveKD = 0.0;
|
||||
|
||||
static constexpr double kSteerKP = 20.0;
|
||||
static constexpr double kSteerKI = 0.0;
|
||||
static constexpr double kSteerKD = 0.25;
|
||||
|
||||
static const frc::SimpleMotorFeedforward<units::meters> kDriveFF{
|
||||
0.25_V, 2.5_V / 1_mps, 0.3_V / 1_mps_sq};
|
||||
|
||||
static const frc::SimpleMotorFeedforward<units::radians> kSteerFF{
|
||||
0.5_V, 0.25_V / 1_rad_per_s, 0.01_V / 1_rad_per_s_sq};
|
||||
|
||||
struct ModuleConstants {
|
||||
public:
|
||||
const int moduleNum;
|
||||
const int driveMotorId;
|
||||
const int driveEncoderA;
|
||||
const int driveEncoderB;
|
||||
const int steerMotorId;
|
||||
const int steerEncoderA;
|
||||
const int steerEncoderB;
|
||||
const double angleOffset;
|
||||
const frc::Translation2d centerOffset;
|
||||
|
||||
ModuleConstants(int moduleNum, int driveMotorId, int driveEncoderA,
|
||||
int driveEncoderB, int steerMotorId, int steerEncoderA,
|
||||
int steerEncoderB, double angleOffset, units::meter_t xOffset,
|
||||
units::meter_t yOffset)
|
||||
: moduleNum(moduleNum),
|
||||
driveMotorId(driveMotorId),
|
||||
driveEncoderA(driveEncoderA),
|
||||
driveEncoderB(driveEncoderB),
|
||||
steerMotorId(steerMotorId),
|
||||
steerEncoderA(steerEncoderA),
|
||||
steerEncoderB(steerEncoderB),
|
||||
angleOffset(angleOffset),
|
||||
centerOffset(frc::Translation2d{xOffset, yOffset}) {}
|
||||
};
|
||||
|
||||
static const ModuleConstants FL_CONSTANTS{
|
||||
1, 0, 0, 1, 1, 2, 3, 0, kTrackLength / 2, kTrackWidth / 2};
|
||||
static const ModuleConstants FR_CONSTANTS{
|
||||
2, 2, 4, 5, 3, 6, 7, 0, kTrackLength / 2, -kTrackWidth / 2};
|
||||
static const ModuleConstants BL_CONSTANTS{
|
||||
3, 4, 8, 9, 5, 10, 11, 0, -kTrackLength / 2, kTrackWidth / 2};
|
||||
static const ModuleConstants BR_CONSTANTS{
|
||||
4, 6, 12, 13, 7, 14, 15, 0, -kTrackLength / 2, -kTrackWidth / 2};
|
||||
} // namespace Swerve
|
||||
} // namespace constants
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <frc/TimedRobot.h>
|
||||
#include <frc/Timer.h>
|
||||
#include <frc/XboxController.h>
|
||||
#include <frc/filter/SlewRateLimiter.h>
|
||||
|
||||
#include "Vision.h"
|
||||
#include "subsystems/SwerveDrive.h"
|
||||
|
||||
class Robot : public frc::TimedRobot {
|
||||
public:
|
||||
void RobotInit() override;
|
||||
void RobotPeriodic() override;
|
||||
void DisabledInit() override;
|
||||
void DisabledPeriodic() override;
|
||||
void DisabledExit() override;
|
||||
void AutonomousInit() override;
|
||||
void AutonomousPeriodic() override;
|
||||
void AutonomousExit() override;
|
||||
void TeleopInit() override;
|
||||
void TeleopPeriodic() override;
|
||||
void TeleopExit() override;
|
||||
void TestInit() override;
|
||||
void TestPeriodic() override;
|
||||
void TestExit() override;
|
||||
void SimulationPeriodic() override;
|
||||
|
||||
private:
|
||||
SwerveDrive drivetrain{};
|
||||
Vision vision{};
|
||||
frc::XboxController controller{0};
|
||||
frc::SlewRateLimiter<units::scalar> forwardLimiter{1.0 / 0.6_s};
|
||||
frc::SlewRateLimiter<units::scalar> strafeLimiter{1.0 / 0.6_s};
|
||||
frc::SlewRateLimiter<units::scalar> turnLimiter{1.0 / 0.33_s};
|
||||
frc::Timer autoTimer{};
|
||||
double kDriveSpeed{0.6};
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <photon/PhotonCamera.h>
|
||||
#include <photon/PhotonPoseEstimator.h>
|
||||
#include <photon/estimation/VisionEstimation.h>
|
||||
#include <photon/simulation/VisionSystemSim.h>
|
||||
#include <photon/simulation/VisionTargetSim.h>
|
||||
#include <photon/targeting/PhotonPipelineResult.h>
|
||||
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
|
||||
#include <frc/apriltag/AprilTagFieldLayout.h>
|
||||
#include <frc/apriltag/AprilTagFields.h>
|
||||
|
||||
#include "Constants.h"
|
||||
|
||||
class Vision {
|
||||
public:
|
||||
Vision() {
|
||||
photonEstimator.SetMultiTagFallbackStrategy(
|
||||
photon::PoseStrategy::LOWEST_AMBIGUITY);
|
||||
|
||||
if (frc::RobotBase::IsSimulation()) {
|
||||
visionSim = std::make_unique<photon::VisionSystemSim>("main");
|
||||
|
||||
visionSim->AddAprilTags(constants::Vision::kTagLayout);
|
||||
|
||||
cameraProp = std::make_unique<photon::SimCameraProperties>();
|
||||
|
||||
cameraProp->SetCalibration(960, 720, frc::Rotation2d{90_deg});
|
||||
cameraProp->SetCalibError(.35, .10);
|
||||
cameraProp->SetFPS(15_Hz);
|
||||
cameraProp->SetAvgLatency(50_ms);
|
||||
cameraProp->SetLatencyStdDev(15_ms);
|
||||
|
||||
cameraSim = std::make_shared<photon::PhotonCameraSim>(camera.get(),
|
||||
*cameraProp.get());
|
||||
|
||||
visionSim->AddCamera(cameraSim.get(), robotToCam);
|
||||
cameraSim->EnableDrawWireframe(true);
|
||||
}
|
||||
}
|
||||
|
||||
photon::PhotonPipelineResult GetLatestResult() {
|
||||
return camera->GetLatestResult();
|
||||
}
|
||||
|
||||
std::optional<photon::EstimatedRobotPose> GetEstimatedGlobalPose() {
|
||||
auto visionEst = photonEstimator.Update();
|
||||
units::second_t latestTimestamp = camera->GetLatestResult().GetTimestamp();
|
||||
bool newResult =
|
||||
units::math::abs(latestTimestamp - lastEstTimestamp) > 1e-5_s;
|
||||
if (frc::RobotBase::IsSimulation()) {
|
||||
if (visionEst.has_value()) {
|
||||
GetSimDebugField()
|
||||
.GetObject("VisionEstimation")
|
||||
->SetPose(visionEst.value().estimatedPose.ToPose2d());
|
||||
} else {
|
||||
if (newResult) {
|
||||
GetSimDebugField().GetObject("VisionEstimation")->SetPoses({});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newResult) {
|
||||
lastEstTimestamp = latestTimestamp;
|
||||
}
|
||||
return visionEst;
|
||||
}
|
||||
|
||||
Eigen::Matrix<double, 3, 1> GetEstimationStdDevs(frc::Pose2d estimatedPose) {
|
||||
Eigen::Matrix<double, 3, 1> estStdDevs =
|
||||
constants::Vision::kSingleTagStdDevs;
|
||||
auto targets = GetLatestResult().GetTargets();
|
||||
int numTags = 0;
|
||||
units::meter_t avgDist = 0_m;
|
||||
for (const auto& tgt : targets) {
|
||||
auto tagPose =
|
||||
photonEstimator.GetFieldLayout().GetTagPose(tgt.GetFiducialId());
|
||||
if (tagPose.has_value()) {
|
||||
numTags++;
|
||||
avgDist += tagPose.value().ToPose2d().Translation().Distance(
|
||||
estimatedPose.Translation());
|
||||
}
|
||||
}
|
||||
if (numTags == 0) {
|
||||
return estStdDevs;
|
||||
}
|
||||
avgDist /= numTags;
|
||||
if (numTags > 1) {
|
||||
estStdDevs = constants::Vision::kMultiTagStdDevs;
|
||||
}
|
||||
if (numTags == 1 && avgDist > 4_m) {
|
||||
estStdDevs = (Eigen::MatrixXd(3, 1) << std::numeric_limits<double>::max(),
|
||||
std::numeric_limits<double>::max(),
|
||||
std::numeric_limits<double>::max())
|
||||
.finished();
|
||||
} else {
|
||||
estStdDevs = estStdDevs * (1 + (avgDist.value() * avgDist.value() / 30));
|
||||
}
|
||||
return estStdDevs;
|
||||
}
|
||||
|
||||
void SimPeriodic(frc::Pose2d robotSimPose) {
|
||||
visionSim->Update(robotSimPose);
|
||||
}
|
||||
|
||||
void ResetSimPose(frc::Pose2d pose) {
|
||||
if (frc::RobotBase::IsSimulation()) {
|
||||
visionSim->ResetRobotPose(pose);
|
||||
}
|
||||
}
|
||||
|
||||
frc::Field2d& GetSimDebugField() { return visionSim->GetDebugField(); }
|
||||
|
||||
private:
|
||||
frc::Transform3d robotToCam{frc::Translation3d{0.5_m, 0.5_m, 0.5_m},
|
||||
frc::Rotation3d{}};
|
||||
photon::PhotonPoseEstimator photonEstimator{
|
||||
LoadAprilTagLayoutField(frc::AprilTagField::k2023ChargedUp),
|
||||
photon::PoseStrategy::MULTI_TAG_PNP_ON_COPROCESSOR,
|
||||
photon::PhotonCamera{"photonvision"}, robotToCam};
|
||||
std::shared_ptr<photon::PhotonCamera> camera{photonEstimator.GetCamera()};
|
||||
std::unique_ptr<photon::VisionSystemSim> visionSim;
|
||||
std::unique_ptr<photon::SimCameraProperties> cameraProp;
|
||||
std::shared_ptr<photon::PhotonCameraSim> cameraSim;
|
||||
units::second_t lastEstTimestamp{0_s};
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <frc/ADXRS450_Gyro.h>
|
||||
#include <frc/SPI.h>
|
||||
#include <frc/estimator/SwerveDrivePoseEstimator.h>
|
||||
#include <frc/kinematics/SwerveDriveKinematics.h>
|
||||
#include <frc/simulation/ADXRS450_GyroSim.h>
|
||||
|
||||
#include "SwerveDriveSim.h"
|
||||
#include "SwerveModule.h"
|
||||
|
||||
class SwerveDrive {
|
||||
public:
|
||||
SwerveDrive();
|
||||
void Periodic();
|
||||
void Drive(units::meters_per_second_t vx, units::meters_per_second_t vy,
|
||||
units::radians_per_second_t omega, bool openLoop);
|
||||
void SetChassisSpeeds(const frc::ChassisSpeeds& targetChassisSpeeds,
|
||||
bool openLoop, bool steerInPlace);
|
||||
void SetModuleStates(
|
||||
const std::array<frc::SwerveModuleState, 4>& desiredStates, bool openLoop,
|
||||
bool steerInPlace);
|
||||
void Stop();
|
||||
void AddVisionMeasurement(const frc::Pose2d& visionMeasurement,
|
||||
units::second_t timestamp);
|
||||
void AddVisionMeasurement(const frc::Pose2d& visionMeasurement,
|
||||
units::second_t timestamp,
|
||||
const Eigen::Vector3d& stdDevs);
|
||||
void ResetPose(const frc::Pose2d& pose, bool resetSimPose);
|
||||
frc::Pose2d GetPose() const;
|
||||
frc::Rotation2d GetHeading() const;
|
||||
frc::Rotation2d GetGyroYaw() const;
|
||||
frc::ChassisSpeeds GetChassisSpeeds() const;
|
||||
std::array<frc::SwerveModuleState, 4> GetModuleStates() const;
|
||||
std::array<frc::SwerveModulePosition, 4> GetModulePositions() const;
|
||||
std::array<frc::Pose2d, 4> GetModulePoses() const;
|
||||
void Log();
|
||||
void SimulationPeriodic();
|
||||
frc::Pose2d GetSimPose() const;
|
||||
units::ampere_t GetCurrentDraw() const;
|
||||
|
||||
private:
|
||||
std::array<SwerveModule, 4> swerveMods{
|
||||
SwerveModule{constants::Swerve::FL_CONSTANTS},
|
||||
SwerveModule{constants::Swerve::FR_CONSTANTS},
|
||||
SwerveModule{constants::Swerve::BL_CONSTANTS},
|
||||
SwerveModule{constants::Swerve::BR_CONSTANTS}};
|
||||
frc::SwerveDriveKinematics<4> kinematics{
|
||||
swerveMods[0].GetModuleConstants().centerOffset,
|
||||
swerveMods[1].GetModuleConstants().centerOffset,
|
||||
swerveMods[2].GetModuleConstants().centerOffset,
|
||||
swerveMods[3].GetModuleConstants().centerOffset,
|
||||
};
|
||||
frc::ADXRS450_Gyro gyro{frc::SPI::Port::kOnboardCS0};
|
||||
frc::SwerveDrivePoseEstimator<4> poseEstimator;
|
||||
frc::ChassisSpeeds targetChassisSpeeds{};
|
||||
|
||||
frc::sim::ADXRS450_GyroSim gyroSim;
|
||||
SwerveDriveSim swerveDriveSim;
|
||||
units::ampere_t totalCurrentDraw{0};
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <random>
|
||||
|
||||
#include <frc/controller/SimpleMotorFeedforward.h>
|
||||
#include <frc/kinematics/SwerveDriveKinematics.h>
|
||||
#include <frc/system/LinearSystem.h>
|
||||
#include <frc/system/plant/DCMotor.h>
|
||||
#include <units/voltage.h>
|
||||
|
||||
static constexpr int numModules{4};
|
||||
|
||||
class SwerveDriveSim {
|
||||
public:
|
||||
SwerveDriveSim(const frc::SimpleMotorFeedforward<units::meters>& driveFF,
|
||||
const frc::DCMotor& driveMotor, double driveGearing,
|
||||
units::meter_t driveWheelRadius,
|
||||
const frc::SimpleMotorFeedforward<units::radians>& steerFF,
|
||||
const frc::DCMotor& steerMotor, double steerGearing,
|
||||
const frc::SwerveDriveKinematics<numModules>& kinematics);
|
||||
SwerveDriveSim(const frc::LinearSystem<2, 1, 2>& drivePlant,
|
||||
units::volt_t driveKs, const frc::DCMotor& driveMotor,
|
||||
double driveGearing, units::meter_t driveWheelRadius,
|
||||
const frc::LinearSystem<2, 1, 2>& steerPlant,
|
||||
units::volt_t steerKs, const frc::DCMotor& steerMotor,
|
||||
double steerGearing,
|
||||
const frc::SwerveDriveKinematics<numModules>& kinematics);
|
||||
void SetDriveInputs(const std::array<units::volt_t, numModules>& inputs);
|
||||
void SetSteerInputs(const std::array<units::volt_t, numModules>& inputs);
|
||||
static Eigen::Matrix<double, 2, 1> CalculateX(
|
||||
const Eigen::Matrix<double, 2, 2>& discA,
|
||||
const Eigen::Matrix<double, 2, 1>& discB,
|
||||
const Eigen::Matrix<double, 2, 1>& x, units::volt_t input,
|
||||
units::volt_t kS);
|
||||
void Update(units::second_t dt);
|
||||
void Reset(const frc::Pose2d& pose, bool preserveMotion);
|
||||
void Reset(const frc::Pose2d& pose,
|
||||
const std::array<Eigen::Matrix<double, 2, 1>, numModules>&
|
||||
moduleDriveStates,
|
||||
const std::array<Eigen::Matrix<double, 2, 1>, numModules>&
|
||||
moduleSteerStates);
|
||||
frc::Pose2d GetPose() const;
|
||||
std::array<frc::SwerveModulePosition, numModules> GetModulePositions() const;
|
||||
std::array<frc::SwerveModulePosition, numModules> GetNoisyModulePositions(
|
||||
units::meter_t driveStdDev, units::radian_t steerStdDev);
|
||||
std::array<frc::SwerveModuleState, numModules> GetModuleStates();
|
||||
std::array<Eigen::Matrix<double, 2, 1>, numModules> GetDriveStates() const;
|
||||
std::array<Eigen::Matrix<double, 2, 1>, numModules> GetSteerStates() const;
|
||||
units::radians_per_second_t GetOmega() const;
|
||||
units::ampere_t GetCurrentDraw(const frc::DCMotor& motor,
|
||||
units::radians_per_second_t velocity,
|
||||
units::volt_t inputVolts,
|
||||
units::volt_t batteryVolts) const;
|
||||
std::array<units::ampere_t, numModules> GetDriveCurrentDraw() const;
|
||||
std::array<units::ampere_t, numModules> GetSteerCurrentDraw() const;
|
||||
units::ampere_t GetTotalCurrentDraw() const;
|
||||
|
||||
private:
|
||||
std::random_device rd{};
|
||||
std::mt19937 generator{rd()};
|
||||
std::normal_distribution<double> randDist{0.0, 1.0};
|
||||
const frc::LinearSystem<2, 1, 2> drivePlant;
|
||||
const units::volt_t driveKs;
|
||||
const frc::DCMotor driveMotor;
|
||||
const double driveGearing;
|
||||
const units::meter_t driveWheelRadius;
|
||||
const frc::LinearSystem<2, 1, 2> steerPlant;
|
||||
const units::volt_t steerKs;
|
||||
const frc::DCMotor steerMotor;
|
||||
const double steerGearing;
|
||||
const frc::SwerveDriveKinematics<numModules> kinematics;
|
||||
std::array<units::volt_t, numModules> driveInputs{};
|
||||
std::array<Eigen::Matrix<double, 2, 1>, numModules> driveStates{};
|
||||
std::array<units::volt_t, numModules> steerInputs{};
|
||||
std::array<Eigen::Matrix<double, 2, 1>, numModules> steerStates{};
|
||||
frc::Pose2d pose{frc::Pose2d{}};
|
||||
units::radians_per_second_t omega{0};
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <frc/Encoder.h>
|
||||
#include <frc/controller/PIDController.h>
|
||||
#include <frc/kinematics/SwerveModulePosition.h>
|
||||
#include <frc/kinematics/SwerveModuleState.h>
|
||||
#include <frc/motorcontrol/PWMSparkMax.h>
|
||||
#include <frc/simulation/EncoderSim.h>
|
||||
#include <units/current.h>
|
||||
|
||||
#include "Constants.h"
|
||||
|
||||
class SwerveModule {
|
||||
public:
|
||||
explicit SwerveModule(const constants::Swerve::ModuleConstants& consts);
|
||||
void Periodic();
|
||||
void SetDesiredState(const frc::SwerveModuleState& newState,
|
||||
bool shouldBeOpenLoop, bool steerInPlace);
|
||||
frc::Rotation2d GetAbsoluteHeading() const;
|
||||
frc::SwerveModuleState GetState() const;
|
||||
frc::SwerveModulePosition GetPosition() const;
|
||||
units::volt_t GetDriveVoltage() const;
|
||||
units::volt_t GetSteerVoltage() const;
|
||||
units::ampere_t GetDriveCurrentSim() const;
|
||||
units::ampere_t GetSteerCurrentSim() const;
|
||||
constants::Swerve::ModuleConstants GetModuleConstants() const;
|
||||
void Log();
|
||||
void SimulationUpdate(units::meter_t driveEncoderDist,
|
||||
units::meters_per_second_t driveEncoderRate,
|
||||
units::ampere_t driveCurrent,
|
||||
units::radian_t steerEncoderDist,
|
||||
units::radians_per_second_t steerEncoderRate,
|
||||
units::ampere_t steerCurrent);
|
||||
|
||||
private:
|
||||
const constants::Swerve::ModuleConstants moduleConstants;
|
||||
|
||||
frc::PWMSparkMax driveMotor;
|
||||
frc::Encoder driveEncoder;
|
||||
frc::PWMSparkMax steerMotor;
|
||||
frc::Encoder steerEncoder;
|
||||
|
||||
frc::SwerveModuleState desiredState{};
|
||||
bool openLoop{false};
|
||||
|
||||
frc::PIDController drivePIDController{constants::Swerve::kDriveKP,
|
||||
constants::Swerve::kDriveKI,
|
||||
constants::Swerve::kDriveKD};
|
||||
frc::PIDController steerPIDController{constants::Swerve::kSteerKP,
|
||||
constants::Swerve::kSteerKI,
|
||||
constants::Swerve::kSteerKD};
|
||||
|
||||
frc::sim::EncoderSim driveEncoderSim;
|
||||
units::ampere_t driveCurrentSim{0};
|
||||
frc::sim::EncoderSim steerEncoderSim;
|
||||
units::ampere_t steerCurrentSim{0};
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <hal/HAL.h>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
HAL_Initialize(500, 0);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
int ret = RUN_ALL_TESTS();
|
||||
return ret;
|
||||
}
|
||||
BIN
photonlib-cpp-examples/swervedriveposeestsim/swerve_module.png
Normal file
BIN
photonlib-cpp-examples/swervedriveposeestsim/swerve_module.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
photonlib-cpp-examples/swervedriveposeestsim/tag-blue.png
Normal file
BIN
photonlib-cpp-examples/swervedriveposeestsim/tag-blue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
BIN
photonlib-cpp-examples/swervedriveposeestsim/tag-green.png
Normal file
BIN
photonlib-cpp-examples/swervedriveposeestsim/tag-green.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
@@ -88,6 +88,8 @@ model {
|
||||
}
|
||||
}
|
||||
|
||||
nativeUtils.useRequiredLibrary(it, "cscore_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "cameraserver_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "wpilib_executable_shared")
|
||||
nativeUtils.useRequiredLibrary(it, "googletest_static")
|
||||
nativeUtils.useRequiredLibrary(it, "apriltag_shared")
|
||||
|
||||
Reference in New Issue
Block a user