Compare commits

...

3 Commits

Author SHA1 Message Date
Sam Freund
b7a0fad54c Set max target limit to 50 (#2320)
## Description

<!-- What changed? Why? (the code + comments should speak for itself on
the "how") -->

<!-- Fun screenshots or a cool video or something are super helpful as
well. If this touches platform-specific behavior, this is where test
evidence should be collected. -->

<!-- Any issues this pull request closes or pull requests this
supersedes should be linked with `Closes #issuenumber`. -->

closes #2318
closes #2319 

For the 2027 game, teams might want to detect more than 10 results.
Therefore, we're increasing the limit.

We also ran into an issue with our sim, where a user can create too many
objects and cause an overflow. We implement that same limit of 50
targets here.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2026-01-21 16:44:49 -08:00
Gold856
e73420d62a Clarify when estimate methods return empty (#2323)
## Description

Clarifies when estimate methods return an empty optional. This also
fixes some parity issues with Constrained SolvePnP not checking the
optional returned by the heading buffer.

Resolves #2322.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2026-01-21 23:13:47 +00:00
Jay Ticku
12f74423d9 Added clarifying details to wiring part of docs for wiring regulator to coprocessor (#2293)
## Description
Added specific details to the wiring section of the photonvision docs
for two wiring methods for connecting a power regulator to a
coprocessor. This aims to help prevent any possible misunderstandings of
how to wire a regulator to a coprocessor.

## Meta

Merge checklist:
- [ ] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [ ] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added

Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
2026-01-20 05:36:38 +00:00
12 changed files with 137 additions and 24 deletions

View File

@@ -108,7 +108,7 @@ When taking in a result from a `PhotonCamera`, PhotonPoseEstimator offers nine p
flat on the floor. This computation takes place on the RoboRIO, and should not take more than 2ms.
This also requires addHeadingData to be called every frame so heading data is up to date.
Calling one of the `estimate<strategy>Pose()` methods on your `PhotonPoseEstimator` will return an `Optional<EstimatedRobotPose>`, which includes a `Pose3d` of the latest estimated pose (using the selected strategy) along with a `double` of the timestamp when the robot pose was estimated. The recommended way to use the estimatePose methods is to
Calling one of the `estimate<strategy>Pose()` methods on your `PhotonPoseEstimator` will return an `Optional<EstimatedRobotPose>`, which will be empty if there are no detected tags, not enough detected tags (for multi-tag strategies), missing data (typically heading data), or if the internal solvers failed (this is a rare scenario). `EstimatedRobotPose` includes a `Pose3d` of the latest estimated pose (using the selected strategy) along with a `double` of the timestamp when the robot pose was estimated. The recommended way to use the estimatePose methods is to
1. do estimation with one of MultiTag methods, check if the result is empty, then
2. fallback to single tag estimation using a method like `estimateLowestAmbiguityPose`.

View File

@@ -2,7 +2,11 @@
## Coprocessor with regulator
1. **IT IS STRONGLY RECOMMENDED** to use one of the recommended power regulators to prevent vision from cutting out from voltage drops while operating the robot. We recommend wiring the regulator directly to the power header pins or using a locking USB C cable. In any case we recommend hot gluing the connector.
1. **IT IS STRONGLY RECOMMENDED** to use one of the recommended power regulators to prevent vision from cutting out from voltage drops while operating the robot. We recommend wiring the regulator directly to the power header pins using either of the two methods listed below or using a locking USB C cable.
* Method 1: Soldering to GPIO Header Pins
* Using 20 AWG or preferably 18 AWG wires, solder two wires from the regulator to the power header pins on the coprocessor and cover with heat-shrink tubing.
* Method 2: Using a Wire-to-Board Connector
* Using a wire-to-board connector with 20 AWG or preferably 18 AWG wires, connect two wires from the regulator to the power header pins on the coprocessor. To prevent the connector from becoming unseated, we recommend applying hot glue to the connector.
2. Run an ethernet cable from your coprocessor to your network switch / radio.

View File

@@ -26,7 +26,7 @@ import org.photonvision.vision.pipeline.result.CVPipelineResult;
public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelineSettings>
implements Releasable {
static final int MAX_MULTI_TARGET_RESULTS = 10;
static final int MAX_MULTI_TARGET_RESULTS = 50;
protected S settings;
protected FrameStaticProperties frameStaticProperties;

View File

@@ -271,6 +271,9 @@ class PhotonCameraSim:
camRt = RotTrlTransform3d.makeRelativeTo(cameraPose)
for tgt in targets:
if len(detectableTgts) >= 50:
break
# pose isn't visible, skip to next
if not self.canSeeTargetPose(cameraPose, tgt):
continue

View File

@@ -78,6 +78,35 @@ def test_VisibilityCupidShuffle() -> None:
assert camera.getLatestResult().hasTargets()
def test_bunchaTargets() -> None:
visionSysSim = VisionSystemSim("Test")
camera = PhotonCamera("camera")
cameraSim = PhotonCameraSim(camera)
visionSysSim.addCamera(cameraSim, Transform3d())
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
for i in range(100):
targetPose = Pose3d(
Translation3d(15.98 + i * 0.1, 0.0, 2.0), Rotation3d(0, 0, math.pi)
)
visionSysSim.addVisionTargets(
[
VisionTargetSim(
targetPose,
TargetModel.createPlanar(width=1.0, height=1.0),
4774 + i,
)
]
)
robotPose = Pose2d(Translation2d(2.0, 0.0), Rotation2d.fromDegrees(5.0))
visionSysSim.update(robotPose)
assert len(camera.getLatestResult().getTargets()) == 50
def test_NotVisibleVert1() -> None:
targetPose = Pose3d(Translation3d(15.98, 0.0, 2.0), Rotation3d(0, 0, math.pi))

View File

@@ -686,7 +686,7 @@ public class PhotonPoseEstimator {
*
* @param cameraResult A pipeline result from the camera.
* @return An {@link EstimatedRobotPose} with an estimated pose, timestamp, and targets used to
* create the estimate.
* create the estimate, or an empty optional if there's no targets or heading data.
*/
public Optional<EstimatedRobotPose> estimatePnpDistanceTrigSolvePose(
PhotonPipelineResult cameraResult) {
@@ -758,7 +758,8 @@ public class PhotonPoseEstimator {
* @param headingScaleFactor If headingFree is false, this weights the cost of changing our robot
* heading estimate against the tag corner reprojection error cont.
* @return An {@link EstimatedRobotPose} with an estimated pose, timestamp, and targets used to
* create the estimate.
* create the estimate, or an empty optional if there's no targets or heading data, or if the
* solver fails to solve the problem.
*/
public Optional<EstimatedRobotPose> estimateConstrainedSolvepnpPose(
PhotonPipelineResult cameraResult,
@@ -770,6 +771,18 @@ public class PhotonPoseEstimator {
if (!shouldEstimate(cameraResult)) {
return Optional.empty();
}
// Need heading if heading fixed
if (!headingFree) {
if (headingBuffer.getSample(cameraResult.getTimestampSeconds()).isEmpty()) {
return Optional.empty();
} else {
// If heading fixed, force rotation component
seedPose =
new Pose3d(
seedPose.getTranslation(),
new Rotation3d(headingBuffer.getSample(cameraResult.getTimestampSeconds()).get()));
}
}
var pnpResult =
VisionEstimation.estimateRobotPoseConstrainedSolvepnp(
cameraMatrix,
@@ -799,7 +812,8 @@ public class PhotonPoseEstimator {
*
* @param cameraResult A pipeline result from the camera.
* @return An {@link EstimatedRobotPose} with an estimated pose, timestamp, and targets used to
* create the estimate.
* create the estimate, or an empty optional if there's no targets, no multi-tag results, or
* multi-tag is disabled in the web UI.
*/
public Optional<EstimatedRobotPose> estimateCoprocMultiTagPose(
PhotonPipelineResult cameraResult) {
@@ -829,7 +843,8 @@ public class PhotonPoseEstimator {
* @param cameraMatrix Camera intrinsics from camera calibration data
* @param distCoeffs Distortion coefficients from camera calibration data.
* @return An {@link EstimatedRobotPose} with an estimated pose, timestamp, and targets used to
* create the estimate.
* create the estimate, or an empty optional if there's less than 2 targets visible or
* SolvePnP fails.
*/
public Optional<EstimatedRobotPose> estimateRioMultiTagPose(
PhotonPipelineResult cameraResult, Matrix<N3, N3> cameraMatrix, Matrix<N8, N1> distCoeffs) {
@@ -861,7 +876,7 @@ public class PhotonPoseEstimator {
*
* @param cameraResult A pipeline result from the camera.
* @return An {@link EstimatedRobotPose} with an estimated pose, timestamp, and targets used to
* create the estimate.
* create the estimate, or an empty optional if there's no targets.
*/
public Optional<EstimatedRobotPose> estimateLowestAmbiguityPose(
PhotonPipelineResult cameraResult) {
@@ -911,7 +926,7 @@ public class PhotonPoseEstimator {
*
* @param cameraResult A pipeline result from the camera.
* @return An {@link EstimatedRobotPose} with an estimated pose, timestamp, and targets used to
* create the estimate.
* create the estimate, or an empty optional if there's no targets.
*/
public Optional<EstimatedRobotPose> estimateClosestToCameraHeightPose(
PhotonPipelineResult cameraResult) {
@@ -989,7 +1004,7 @@ public class PhotonPoseEstimator {
* @param cameraResult A pipeline result from the camera.
* @param referencePose reference pose to check vector magnitude difference against.
* @return An {@link EstimatedRobotPose} with an estimated pose, timestamp, and targets used to
* create the estimate.
* create the estimate, or an empty optional if there's no targets.
*/
public Optional<EstimatedRobotPose> estimateClosestToReferencePose(
PhotonPipelineResult cameraResult, Pose3d referencePose) {
@@ -1062,7 +1077,7 @@ public class PhotonPoseEstimator {
*
* @param cameraResult A pipeline result from the camera.
* @return An {@link EstimatedRobotPose} with an estimated pose, timestamp, and targets used to
* create the estimate.
* create the estimate, or an empty optional if there's no targets.
*/
public Optional<EstimatedRobotPose> estimateAverageBestTargetsPose(
PhotonPipelineResult cameraResult) {

View File

@@ -442,6 +442,10 @@ public class PhotonCameraSim implements AutoCloseable {
Mat.zeros(videoFrameSize, CvType.CV_8UC1).assignTo(videoSimFrameRaw);
for (var tgt : targets) {
if (detectableTgts.size() >= 50) {
break;
}
// pose isn't visible, skip to next
if (!canSeeTargetPose(cameraPose, tgt)) continue;

View File

@@ -634,13 +634,18 @@ PhotonPoseEstimator::EstimateConstrainedSolvepnpPose(
if (!ShouldEstimate(cameraResult)) {
return std::nullopt;
}
// Need heading if heading fixed
if (!headingFree) {
seedPose = frc::Pose3d{
seedPose.Translation(),
frc::Rotation3d{
headingBuffer.Sample(cameraResult.GetTimestamp()).value()}};
if (!headingBuffer.Sample(cameraResult.GetTimestamp())) {
return std::nullopt;
} else {
// If heading fixed, force rotation component
seedPose = frc::Pose3d{
seedPose.Translation(),
frc::Rotation3d{
headingBuffer.Sample(cameraResult.GetTimestamp()).value()}};
}
}
std::vector<photon::PhotonTrackedTarget> targets{
cameraResult.GetTargets().begin(), cameraResult.GetTargets().end()};

View File

@@ -131,6 +131,10 @@ PhotonPipelineResult PhotonCameraSim::Process(
blankFrame.assignTo(videoSimFrameRaw);
for (const auto& tgt : targets) {
if (detectableTgts.size() >= 50) {
break;
}
if (!CanSeeTargetPose(cameraPose, tgt)) {
continue;
}

View File

@@ -294,7 +294,7 @@ class PhotonPoseEstimator {
*
* @param cameraResult A pipeline result from the camera.
* @return An EstimatedRobotPose with an estimated pose, timestamp, and
* targets used to create the estimate.
* targets used to create the estimate, or std::nullopt if there's no targets.
*/
std::optional<EstimatedRobotPose> EstimateLowestAmbiguityPose(
PhotonPipelineResult cameraResult);
@@ -306,7 +306,7 @@ class PhotonPoseEstimator {
*
* @param cameraResult A pipeline result from the camera.
* @return An EstimatedRobotPose with an estimated pose, timestamp and
* targets used to create the estimate.
* targets used to create the estimate, or std::nullopt if there's no targets.
*/
std::optional<EstimatedRobotPose> EstimateClosestToCameraHeightPose(
PhotonPipelineResult cameraResult);
@@ -319,7 +319,7 @@ class PhotonPoseEstimator {
* @param referencePose reference pose to check vector magnitude difference
* against.
* @return An EstimatedRobotPose with an estimated pose, timestamp, and
* targets used to create the estimate.
* targets used to create the estimate, or std::nullopt if there's no targets.
*/
std::optional<EstimatedRobotPose> EstimateClosestToReferencePose(
PhotonPipelineResult cameraResult, frc::Pose3d referencePose);
@@ -331,7 +331,8 @@ class PhotonPoseEstimator {
*
* @param cameraResult A pipeline result from the camera.
* @return An EstimatedRobotPose with an estimated pose, timestamp, and
* targets used to create the estimate.
* targets used to create the estimate or std::nullopt if there's no targets,
* no multi-tag results, or multi-tag is disabled in the web UI.
*/
std::optional<EstimatedRobotPose> EstimateCoprocMultiTagPose(
PhotonPipelineResult cameraResult);
@@ -345,7 +346,8 @@ class PhotonPoseEstimator {
* @param cameraMatrix Camera intrinsics from camera calibration data.
* @param distCoeffs Distortion coefficients from camera calibration data.
* @return An EstimatedRobotPose with an estimated pose, timestamp, and
* targets used to create the estimate.
* targets used to create the estimate, or std::nullopt if there's less than 2
* targets visible or SolvePnP fails.
*/
std::optional<EstimatedRobotPose> EstimateRioMultiTagPose(
PhotonPipelineResult cameraResult, PhotonCamera::CameraMatrix camMat,
@@ -363,7 +365,8 @@ class PhotonPoseEstimator {
*
* @param cameraResult A pipeline result from the camera.
* @return An EstimatedRobotPose with an estimated pose, timestamp, and
* targets used to create the estimate.
* targets used to create the estimate, or std::nullopt if there's no targets
* or heading data.
*/
std::optional<EstimatedRobotPose> EstimatePnpDistanceTrigSolvePose(
PhotonPipelineResult cameraResult);
@@ -372,7 +375,7 @@ class PhotonPoseEstimator {
* Return the average of the best target poses using ambiguity as weight.
* @param cameraResult A pipeline result from the camera.
* @return An EstimatedRobotPose with an estimated pose, timestamp, and
* targets used to create the estimate.
* targets used to create the estimate, or std::nullopt if there's no targets.
*/
std::optional<EstimatedRobotPose> EstimateAverageBestTargetsPose(
PhotonPipelineResult cameraResult);
@@ -401,7 +404,8 @@ class PhotonPoseEstimator {
* changing our robot heading estimate against the tag corner reprojection
* error cost.
* @return An EstimatedRobotPose with an estimated pose, timestamp, and
* targets used to create the estimate.
* targets used to create the estimate, or std::nullopt if there's no targets
* or heading data, or if the solver fails to solve the problem.
*/
std::optional<EstimatedRobotPose> EstimateConstrainedSolvepnpPose(
photon::PhotonPipelineResult cameraResult,

View File

@@ -179,6 +179,28 @@ class VisionSystemSimTest {
assertTrue(result.hasTargets());
}
@Test
public void testBunchaTargets() {
var visionSysSim = new VisionSystemSim("Test");
var camera = new PhotonCamera(inst, "camera");
var cameraSim = new PhotonCameraSim(camera);
visionSysSim.addCamera(cameraSim, new Transform3d());
cameraSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(80));
for (int i = 0; i < 100; i++) {
final var targetPose =
new Pose3d(new Translation3d(15.98 + i * 0.1, 0, 1), new Rotation3d(0, 0, Math.PI));
visionSysSim.addVisionTargets(new VisionTargetSim(targetPose, new TargetModel(0.5, 0.5), i));
}
var robotPose = new Pose2d(new Translation2d(5, 0), Rotation2d.fromDegrees(5));
visionSysSim.update(robotPose);
var res = waitForSequenceNumber(camera, 1);
assertEquals(50, res.getTargets().size());
}
@Test
public void testNotVisibleVert1() {
final var targetPose =

View File

@@ -117,6 +117,29 @@ TEST_F(VisionSystemSimTest, TestVisibilityCupidShuffle) {
ASSERT_TRUE(camera.GetLatestResult().HasTargets());
}
TEST_F(VisionSystemSimTest, TestBunchaTargets) {
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});
std::vector<photon::VisionTargetSim> targets;
for (int i = 0; i < 100; i++) {
targets.emplace_back(
frc::Pose3d{
frc::Translation3d{15.98_m + i * 0.1_m, 0_m, 1_m},
frc::Rotation3d{0_rad, 0_rad, units::radian_t{std::numbers::pi}}},
photon::TargetModel{0.5_m, 0.5_m}, i);
}
visionSysSim.AddVisionTargets(targets);
frc::Pose2d robotPose{frc::Translation2d{5_m, 0_m}, frc::Rotation2d{5_deg}};
visionSysSim.Update(robotPose);
ASSERT_EQ(camera.GetLatestResult().targets.size(), 50u);
}
TEST_F(VisionSystemSimTest, TestNotVisibleVert1) {
frc::Pose3d targetPose{
frc::Translation3d{15.98_m, 0_m, 1_m},