mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-25 01:41:40 +00:00
Compare commits
19 Commits
v2025.1.1
...
v2025.2.1-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75c289f526 | ||
|
|
e97865166d | ||
|
|
53144bfcf1 | ||
|
|
ee97a1b62e | ||
|
|
01a3d31734 | ||
|
|
a546ff0819 | ||
|
|
67bc032d26 | ||
|
|
8f816cf1af | ||
|
|
e2b028abdc | ||
|
|
00fb4bdf07 | ||
|
|
7067c75525 | ||
|
|
ef82328d74 | ||
|
|
be59e3a958 | ||
|
|
99427d888a | ||
|
|
8ec2da952f | ||
|
|
a5d007e258 | ||
|
|
78b82e3a96 | ||
|
|
1303a0eaae | ||
|
|
ab41d2d1ed |
17
.github/pull_request_template.md
vendored
Normal file
17
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
## 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`. -->
|
||||
|
||||
## 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 v2024.3.1
|
||||
- [ ] If this PR addresses a bug, a regression test for it is added
|
||||
@@ -1,6 +1,6 @@
|
||||
# PhotonVision
|
||||
|
||||
[](https://github.com/PhotonVision/photonvision/actions?query=workflow%3ACI) [](https://codecov.io/gh/PhotonVision/photonvision) [](https://discord.gg/wYxTwym)
|
||||
[](https://discord.gg/wYxTwym)
|
||||
|
||||
PhotonVision is the free, fast, and easy-to-use computer vision solution for the *FIRST* Robotics Competition. You can read an overview of our features [on our website](https://photonvision.org). You can find our comprehensive documentation [here](https://docs.photonvision.org).
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id "cpp"
|
||||
id "com.diffplug.spotless" version "6.24.0"
|
||||
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.2.1"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.3.1"
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
id 'com.google.protobuf' version '0.9.3' apply false
|
||||
id 'edu.wpi.first.GradleJni' version '1.1.0'
|
||||
@@ -33,14 +33,14 @@ ext.allOutputsFolder = file("$project.buildDir/outputs")
|
||||
apply from: "versioningHelper.gradle"
|
||||
|
||||
ext {
|
||||
wpilibVersion = "2025.2.1"
|
||||
wpilibVersion = "2025.3.1"
|
||||
wpimathVersion = wpilibVersion
|
||||
openCVYear = "2025"
|
||||
openCVversion = "4.10.0-3"
|
||||
joglVersion = "2.4.0"
|
||||
javalinVersion = "5.6.2"
|
||||
libcameraDriverVersion = "v2025.0.3"
|
||||
rknnVersion = "v2025.0.0"
|
||||
rknnVersion = "dev-v2025.0.0-1-g33b6263"
|
||||
frcYear = "2025"
|
||||
mrcalVersion = "v2025.0.0";
|
||||
|
||||
|
||||
@@ -12,5 +12,6 @@ modifiableFileExclude {
|
||||
\.ico$
|
||||
\.rknn$
|
||||
\.svg$
|
||||
\.woff2$
|
||||
gradlew
|
||||
}
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
/*!
|
||||
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
|
||||
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: FontAwesome;
|
||||
src: url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);
|
||||
src: url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"), url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"), url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"), url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"), url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");
|
||||
font-weight: 400;
|
||||
font-style:normal
|
||||
}
|
||||
|
||||
.code-block-caption>.headerlink, dl dt>.headerlink, h1>.headerlink, h2>.headerlink, h3>.headerlink, h4>.headerlink, h5>.headerlink, h6>.headerlink, p.caption>.headerlink, table>caption>.headerlink {
|
||||
font-family: FontAwesome;
|
||||
font-size: 0.75em;
|
||||
|
||||
6
docs/source/_static/css/v4-font-face.min.css
vendored
Normal file
6
docs/source/_static/css/v4-font-face.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2024 Fonticons, Inc.
|
||||
*/
|
||||
@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}
|
||||
BIN
docs/source/_static/webfonts/fa-brands-400.woff2
Normal file
BIN
docs/source/_static/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
docs/source/_static/webfonts/fa-regular-400.woff2
Normal file
BIN
docs/source/_static/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
docs/source/_static/webfonts/fa-solid-900.woff2
Normal file
BIN
docs/source/_static/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
docs/source/_static/webfonts/fa-v4compatibility.woff2
Normal file
BIN
docs/source/_static/webfonts/fa-v4compatibility.woff2
Normal file
Binary file not shown.
@@ -79,6 +79,7 @@ source_suffix = [".rst", ".md"]
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_css_file("css/v4-font-face.min.css")
|
||||
app.add_css_file("css/pv-icons.css")
|
||||
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@ SSH into the Raspberry Pi (using Windows command line, or a tool like [Putty](ht
|
||||
:::{attention}
|
||||
The version of WPILibPi for the Romi is 2023.2.1, which is not compatible with the current version of PhotonVision. **If you are using WPILibPi 2023.2.1 on your Romi, you must install PhotonVision v2023.4.2 or earlier!**
|
||||
|
||||
To install a compatible version of PhotonVision, enter these commands in the SSH terminal connected to the Raspberry Pi. This will download and run the install script, which will intall PhotonVision on your Raspberry Pi and configure it to run at startup.
|
||||
To install a compatible version of PhotonVision, enter these commands in the SSH terminal connected to the Raspberry Pi. This will download and run the install script, which will install PhotonVision on your Raspberry Pi and configure it to run at startup.
|
||||
|
||||
```bash
|
||||
$ wget https://git.io/JJrEP -O install.sh
|
||||
$ sudo chmod +x install.sh
|
||||
$ sudo ./install.sh -v 2023.4.2
|
||||
$ sudo ./install.sh -v v2023.4.2
|
||||
```
|
||||
The install script requires an internet connection, so connecting the Raspberry Pi to an internet-connected router via an Ethernet cable will be the easiest solution. The pi must remain writable while you are following these steps!
|
||||
:::
|
||||
|
||||
@@ -12,7 +12,7 @@ Bonjour provides more stable networking when using Windows PCs. Install [Bonjour
|
||||
|
||||
## Installing Java
|
||||
|
||||
PhotonVision requires a JDK installed and on the system path. **JDK 17 is needed. Windows Users must use the JDK that ships with WPILib.** [Download and install it from here.](https://github.com/wpilibsuite/allwpilib/releases/tag/v2025.2.1) Either ensure the only Java on your PATH is the WPILIB Java or specify it to gradle with `-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk`:
|
||||
PhotonVision requires a JDK installed and on the system path. **JDK 17 is needed. Windows Users must use the JDK that ships with WPILib.** [Download and install it from here.](https://github.com/wpilibsuite/allwpilib/releases/tag/v2025.3.1) Either ensure the only Java on your PATH is the WPILIB Java or specify it to gradle with `-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk`:
|
||||
|
||||
```
|
||||
> ./gradlew run "-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk"
|
||||
|
||||
@@ -50,7 +50,7 @@ When a new camera (ie, one we can't match by-path to a deserialized CameraConfig
|
||||
|
||||
## Startup:
|
||||
|
||||
- GIVEN An emtpy set of deserialized Camera Configurations
|
||||
- GIVEN An empty set of deserialized Camera Configurations
|
||||
<br>WHEN PhotonVision starts
|
||||
<br>THEN no VisionModules will be started
|
||||
|
||||
@@ -72,12 +72,12 @@ When a new camera (ie, one we can't match by-path to a deserialized CameraConfig
|
||||
|
||||
## Camera (re)enumeration:
|
||||
|
||||
- GIVEN a NEW USB CAMERA is avaliable for enumeration
|
||||
- GIVEN a NEW USB CAMERA is available for enumeration
|
||||
<br>WHEN a USB camera is discovered by VisionSourceManager
|
||||
<br>AND the USB camera's VIDEO DEVICE PATH is not in the set of DESERIALIZED CAMERA CONFIGURATIONS
|
||||
<br>THEN a UNIQUE NAME will be assigned to the camera info
|
||||
|
||||
- GIVEN a NEW USB CAMERA is avaliable for enumeration
|
||||
- GIVEN a NEW USB CAMERA is available for enumeration
|
||||
<br>WHEN a USB camera is discovered by VisionSourceManager
|
||||
<br>AND the USB camera's VIDEO DEVICE PATH is in the set of DESERIALIZED CAMERA CONFIGURATIONS
|
||||
<br>THEN a UNIQUE NAME equal to the matching DESERIALIZED CAMERA CONFIGURATION will be assigned to the camera info
|
||||
@@ -86,13 +86,13 @@ When a new camera (ie, one we can't match by-path to a deserialized CameraConfig
|
||||
## Creating from a new camera
|
||||
|
||||
- Given: A UNIQUE NAME from a NEW USB CAMERA
|
||||
<br>WHEN I request a new VisionModule is created for this NEW USB CAMREA
|
||||
<br>WHEN I request a new VisionModule is created for this NEW USB CAMERA
|
||||
<br>AND the camera has a VALID USB PATH
|
||||
<br>AND the camera's VALID USB PATH is not in use by any CURRENTLY ACTIVE CAMERAS
|
||||
<br>THEN a NEW VisionModule will be started for the NEW USB CAMERA using the VALID USB PATH
|
||||
|
||||
- Given: A UNIQUE NAME from a NEW USB CAMERA
|
||||
<br>WHEN I request a new VisionModule is created for this NEW USB CAMREA
|
||||
<br>WHEN I request a new VisionModule is created for this NEW USB CAMERA
|
||||
<br>AND the camera does not have a VALID USB PATH
|
||||
<br>AND the camera's VIDEO DEVICE PATH is not in use by any CURRENTLY ACTIVE CAMERAS
|
||||
<br>THEN a NEW VisionModule will be started for the NEW USB CAMERA using the VIDEO DEVICE PATH
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
## A primer on time
|
||||
|
||||
Expecially starting around 2022 with AprilTags making localization easier, providing a way to know when a camera image was captured at became more important for localization.
|
||||
Especially starting around 2022 with AprilTags making localization easier, providing a way to know when a camera image was captured at became more important for localization.
|
||||
Since the [creation of USBFrameProvider](https://github.com/PhotonVision/photonvision/commit/f92bf670ded52b59a00352a4a49c277f01bae305), we used the time [provided by CSCore](https://github.wpilib.org/allwpilib/docs/release/java/edu/wpi/first/cscore/CvSink.html#grabFrame(org.opencv.core.Mat)) to tell when a camera image was captured at, but just keeping track of "CSCore told us frame N was captured 104.21s after the Raspberry Pi turned on" isn't very helpful. We can decompose this into asking:
|
||||
|
||||
- At what time was a particular image captured at, in the coprocessor's timebase?
|
||||
@@ -29,13 +29,13 @@ WPILib's CSCore is a platform-agnostic wrapper around Windows, Linux, and MacOS
|
||||
|
||||
Prior to https://github.com/wpilibsuite/allwpilib/pull/7609, CSCore used the [time it dequeued the buffer at](https://github.com/wpilibsuite/allwpilib/blob/17a03514bad6de195639634b3d57d5ac411d601e/cscore/src/main/native/linux/UsbCameraImpl.cpp#L559) as the image capture time. But this doesn't account for exposure time or latency introduced by the camera + USB stack + Linux itself.
|
||||
|
||||
V4L does expose (with some [very heavy caviets](https://github.com/torvalds/linux/blob/fc033cf25e612e840e545f8d5ad2edd6ba613ed5/drivers/media/usb/uvc/uvc_video.c#L600) for some troublesome cameras) its best guess at the time an image was captured at via [buffer flags](https://www.kernel.org/doc/html/v4.9/media/uapi/v4l/buffer.html#buffer-flags). In my testing, all my cameras were able to provide timestamps with both these flags set:
|
||||
V4L does expose (with some [very heavy caveats](https://github.com/torvalds/linux/blob/fc033cf25e612e840e545f8d5ad2edd6ba613ed5/drivers/media/usb/uvc/uvc_video.c#L600) for some troublesome cameras) its best guess at the time an image was captured at via [buffer flags](https://www.kernel.org/doc/html/v4.9/media/uapi/v4l/buffer.html#buffer-flags). In my testing, all my cameras were able to provide timestamps with both these flags set:
|
||||
- `V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC`: The buffer timestamp has been taken from the CLOCK_MONOTONIC clock [...] accessible via `clock_gettime()`.
|
||||
- `V4L2_BUF_FLAG_TSTAMP_SRC_SOE`: Start Of Exposure. The buffer timestamp has been taken when the exposure of the frame has begun.
|
||||
|
||||
I'm sure that we'll find a camera that doesn't play nice, because we can't have nice things :). But until then, using this timestamp gets us a free accuracy bump.
|
||||
|
||||
Other things to note: This gets us an estimate at when the camera *started* collecting photons. The camera's sensor will remain collecitng light for up to the total integration time, plus readout time for rolling shutter cameras.
|
||||
Other things to note: This gets us an estimate at when the camera *started* collecting photons. The camera's sensor will remain collecting light for up to the total integration time, plus readout time for rolling shutter cameras.
|
||||
|
||||
## Latency Testing
|
||||
|
||||
@@ -105,7 +105,7 @@ public class Robot extends TimedRobot {
|
||||
```
|
||||
</details>
|
||||
|
||||
I've decreased camera exposure as much as possible (so we know with reasonable confidence that the image was collected right at the start of the exposure time reported by V4L), but we only get back new images at 60fps. So we don't know when between frame N and N+1 the LED turned on - just that somtime between now and 1/60th of a second a go, the LED turned on.
|
||||
I've decreased camera exposure as much as possible (so we know with reasonable confidence that the image was collected right at the start of the exposure time reported by V4L), but we only get back new images at 60fps. So we don't know when between frame N and N+1 the LED turned on - just that sometime between now and 1/60th of a second a go, the LED turned on.
|
||||
|
||||
The test coprocessor was an Orange Pi 5 running a PhotonVision 2025 (Ubuntu 24.04 based) image, with an ArduCam OV9782 at 1280x800, 60fps, MJPG running a reflective pipeline.
|
||||
|
||||
@@ -133,4 +133,4 @@ With the camera capturing at 60fps, the time between successive frames is only ~
|
||||
|
||||
### Future Work
|
||||
|
||||
This test also makes no effort to isolate error from time syncronization from error introduced by frame time measurement - we're just interested in overall error. Future work could investigate the latency contribution
|
||||
This test also makes no effort to isolate error from time synchronization from error introduced by frame time measurement - we're just interested in overall error. Future work could investigate the latency contribution
|
||||
|
||||
@@ -76,7 +76,7 @@ Communication between server and clients shall occur over the User Datagram Prot
|
||||
|
||||
## Message Format
|
||||
|
||||
The message format forgoes CRCs (as these are provided by the Ethernet physical layer) or packet delimination (as our packetsa are assumed be under the network MTU). **TSP Ping** and **TSP Pong** messages shall be encoded in a manor compatible with a WPILib packed struct with respect to byte alignment and endienness.
|
||||
The message format forgoes CRCs (as these are provided by the Ethernet physical layer) or packet delineation (as our packets are assumed be under the network MTU). **TSP Ping** and **TSP Pong** messages shall be encoded in a manor compatible with a WPILib packed struct with respect to byte alignment and endianness.
|
||||
|
||||
### TSP Ping
|
||||
|
||||
@@ -98,7 +98,7 @@ The message format forgoes CRCs (as these are provided by the Ethernet physical
|
||||
|
||||
## Optional Protocol Extensions
|
||||
|
||||
Clients may publish statistics to NetworkTables. If they do, they shall publish to a key that is globally unique per participant in the Time Synronization network. If a client implements this, it shall provide the following publishers:
|
||||
Clients may publish statistics to NetworkTables. If they do, they shall publish to a key that is globally unique per participant in the Time Synchronization network. If a client implements this, it shall provide the following publishers:
|
||||
|
||||
| Key | Type | Notes |
|
||||
| ------ | ------ | ---- |
|
||||
|
||||
@@ -27,7 +27,7 @@ Using PhotonVision allows the user to calibrate for their specific camera, which
|
||||
|
||||
### Low Latency, High FPS Processing
|
||||
|
||||
PhotonVision exposes specalized hardware on select coprocessors to maximize processing speed. This allows for lower-latency detection of targets to ensure you aren't losing out on any performance.
|
||||
PhotonVision exposes specialized hardware on select coprocessors to maximize processing speed. This allows for lower-latency detection of targets to ensure you aren't losing out on any performance.
|
||||
|
||||
### Fully Open Source and Active Developer Community
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ The following example is from the PhotonLib example repository ([Java](https://g
|
||||
## Knowledge and Equipment Needed
|
||||
|
||||
- A Robot
|
||||
- A camera mounted rigidly to the robot's frame, cenetered and pointed forward.
|
||||
- A coprocessor running PhotonVision with an AprilTag or Aurco 2D Pipeline.
|
||||
- A camera mounted rigidly to the robot's frame, centered and pointed forward.
|
||||
- A coprocessor running PhotonVision with an AprilTag or Aruco 2D Pipeline.
|
||||
- [A printout of AprilTag 7](https://firstfrc.blob.core.windows.net/frc2025/FieldAssets/Apriltag_Images_and_User_Guide.pdf), mounted on a rigid and flat surface.
|
||||
|
||||
## Code
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
## How does it work?
|
||||
|
||||
PhotonVision supports object detection using neural network accelerator hardware built into Orange Pi 5/5+ coprocessors. The Neural Processing Unit, or NPU, is [used by PhotonVision](https://github.com/PhotonVision/rknn_jni/tree/main) to massively accelerate certain math operations like those needed for running ML-based object detection.
|
||||
PhotonVision supports object detection using neural network accelerator hardware built into Orange Pi 5/5+ coprocessors. Please note that the Orange Pi 5/5+ are the only coprocessors that are currently supported. The Neural Processing Unit, or NPU, is [used by PhotonVision](https://github.com/PhotonVision/rknn_jni/tree/main) to massively accelerate certain math operations like those needed for running ML-based object detection.
|
||||
|
||||
For the 2025 season, PhotonVision does not currently ship with a pre-trained detector. If teams are interested in using object detection, they can follow the custom process outlined {ref}`below <docs/objectDetection/about-object-detection:Uploading Custom Models>`.
|
||||
For the 2025 season, PhotonVision ships with a pretrained ALGAE model. A model to detect coral is not currently stable, and interested teams should ask in the Photonvision discord.
|
||||
|
||||
## Tracking Objects
|
||||
|
||||
Before you get started with object detection, ensure that you have followed the previous sections on installation, wiring, and networking. Next, open the Web UI, go to the top right card, and switch to the “Object Detection” type. You should see a screen similar to the image above.
|
||||
|
||||
PhotonVision does not currently ship with a pretrained model. Models are trained to detect one or more object "classes" (such as cars, stoplights) in an input image. For each detected object, the model outputs a bounding box around where in the image the object is located, what class the object belongs to, and a unitless confidence between 0 and 1.
|
||||
Models are trained to detect one or more object "classes" (such as cars, stoplights) in an input image. For each detected object, the model outputs a bounding box around where in the image the object is located, what class the object belongs to, and a unitless confidence between 0 and 1.
|
||||
|
||||
:::{note}
|
||||
This model output means that while its fairly easy to say that "this rectangle probably contains an object", we don't have any information about the object's orientation or location. Further math in user code would be required to make estimates about where an object is physically located relative to the camera.
|
||||
@@ -35,16 +35,21 @@ Photonvision will letterbox your camera frame to 640x640. This means that if you
|
||||
|
||||
## Training Custom Models
|
||||
|
||||
Coming soon!
|
||||
:::{warning}
|
||||
Power users only. This requires some setup, such as obtaining your own dataset and installing various tools. It's additionally advised to have a general knowledge of ML before attempting to train your own model. Additionally, this is not officially supported by Photonvision, and any problems that may arise are not attributable to Photonvision.
|
||||
:::
|
||||
|
||||
Before beginning, it is necessary to install the [rknn-toolkit2](https://github.com/airockchip/rknn-toolkit2). Then, install the relevant [Ultralytics repository](https://github.com/airockchip?tab=repositories&q=yolo&type=&language=&sort=) from this list. After training your model, export it to `rknn`. This will give you an `onnx` file, formatted for conversion. Copy this file to the relevant folder in [rknn_model_zoo](https://github.com/airockchip/rknn_model_zoo), and use the conversion script located there to convert it. If necessary, modify the script to provide the path to your training database for quantization.
|
||||
|
||||
## Uploading Custom Models
|
||||
|
||||
:::{warning}
|
||||
PhotonVision currently ONLY supports 640x640 YOLOv5 & YOLOv8 models trained and converted to `.rknn` format for RK3588 CPUs! Other models require different post-processing code and will NOT work. The model conversion process is also highly particular. Proceed with care.
|
||||
PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv5, YOLOv8, and YOLO11 models trained and converted to `.rknn` format for RK3588 CPUs! Other models require different post-processing code and will NOT work. The model conversion process is also highly particular. Proceed with care.
|
||||
:::
|
||||
|
||||
In the settings, under `Device Control`, there's an option to upload a new object detection model. Naming convention
|
||||
should be `name-verticalResolution-horizontalResolution-modelType`. Additionally, the labels
|
||||
file ought to have the same name as the RKNN file, with `-labels` appended to the end. For example, if the
|
||||
RKNN file is named `note-640-640-yolov5s.rknn`, the labels file should be named
|
||||
`note-640-640-yolov5s-labels.txt`.
|
||||
should be `name-verticalResolution-horizontalResolution-modelType`. The
|
||||
`name` should only include alphanumeric characters, periods, and underscores. Additionally, the labels
|
||||
file ought to have the same name as the RKNN file, with `-labels` appended to the end. For
|
||||
example, if the RKNN file is named `Algae_1.03.2025-640-640-yolov5s.rknn`, the labels file should be
|
||||
named `Algae_1.03.2025-640-640-yolov5s-labels.txt`.
|
||||
|
||||
@@ -106,7 +106,7 @@ You can get a [translation](https://docs.wpilib.org/en/latest/docs/software/adva
|
||||
.. code-block:: C++
|
||||
|
||||
// Calculate a translation from the camera to the target.
|
||||
frc::Translation2d translation = photonlib::PhotonUtils::EstimateCameraToTargetTranslationn(
|
||||
frc::Translation2d translation = photonlib::PhotonUtils::EstimateCameraToTargetTranslation(
|
||||
distance, frc::Rotation2d(units::degree_t(-target.GetYaw())));
|
||||
|
||||
.. code-block:: Python
|
||||
|
||||
@@ -17,6 +17,6 @@ Arducam cameras are supported for setups with multiple devices. This is possible
|
||||
3. **Save Settings**: Ensure that you save the settings after selecting the appropriate camera model for each device.
|
||||
|
||||
```{image} images/setArducamModel.png
|
||||
:alt: The camera model can be selected from the Arudcam model selector in the cameras tab
|
||||
:alt: The camera model can be selected from the Arducam model selector in the cameras tab
|
||||
:align: center
|
||||
```
|
||||
|
||||
@@ -26,7 +26,8 @@ const interactiveCols = computed(() =>
|
||||
{ name: '2020 Power Cell (7in)', value: TargetModel.CircularPowerCell7in },
|
||||
{ name: '2022 Cargo Ball (9.5in)', value: TargetModel.RapidReactCircularCargoBall },
|
||||
{ name: '2023 AprilTag 6in (16h5)', value: TargetModel.AprilTag6in_16h5 },
|
||||
{ name: '2024 AprilTag 6.5in (36h11)', value: TargetModel.AprilTag6p5in_36h11 }
|
||||
{ name: '2024 AprilTag 6.5in (36h11)', value: TargetModel.AprilTag6p5in_36h11 },
|
||||
{ name: '2025 Algae (16.25in)', value: TargetModel.ReefscapeAlgae }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ targetModel: value }, false)"
|
||||
|
||||
@@ -88,11 +88,12 @@ const supportedModels = computed(() => {
|
||||
<v-card-title>Import New Object Detection Model</v-card-title>
|
||||
<v-card-text>
|
||||
Upload a new object detection model to this device that can be used in a pipeline. Naming convention
|
||||
should be <code>name-verticalResolution-horizontalResolution-modelType</code>. Additionally, the labels
|
||||
file ought to have the same name as the RKNN file, with <code>-labels</code> appended to the end. For
|
||||
example, if the RKNN file is named <code>note-640-640-yolov5s.rknn</code>, the labels file should be
|
||||
named <code>note-640-640-yolov5s-labels.txt</code>. Note that ONLY 640x640 YOLOv5 & YOLOv8 models
|
||||
trained and converted to `.rknn` format for RK3588 CPUs are currently supported!
|
||||
should be <code>name-verticalResolution-horizontalResolution-modelType</code>. The
|
||||
<code>name</code> should only include alphanumeric characters, periods, and underscores. Additionally,
|
||||
the labels file ought to have the same name as the RKNN file, with <code>-labels</code> appended to the
|
||||
end. For example, if the RKNN file is named <code>note-640-640-yolov5s.rknn</code>, the labels file
|
||||
should be named <code>note-640-640-yolov5s-labels.txt</code>. Note that ONLY 640x640 YOLOv5 & YOLOv8
|
||||
models trained and converted to `.rknn` format for RK3588 CPUs are currently supported!
|
||||
<v-row class="mt-6 ml-4 mr-8">
|
||||
<v-file-input v-model="importRKNNFile" label="RKNN File" accept=".rknn" />
|
||||
</v-row>
|
||||
|
||||
@@ -27,7 +27,8 @@ export enum TargetModel {
|
||||
CircularPowerCell7in = 3,
|
||||
RapidReactCircularCargoBall = 4,
|
||||
AprilTag6in_16h5 = 5,
|
||||
AprilTag6p5in_36h11 = 6
|
||||
AprilTag6p5in_36h11 = 6,
|
||||
ReefscapeAlgae = 7
|
||||
}
|
||||
|
||||
export interface PipelineSettings {
|
||||
|
||||
@@ -19,7 +19,7 @@ package org.photonvision.common.configuration;
|
||||
|
||||
/**
|
||||
* Add migrations by adding the SQL commands for each migration sequentially to this array. DO NOT
|
||||
* edit or delete existing SQL commands. That will lead to producing an icompatible database.
|
||||
* edit or delete existing SQL commands. That will lead to producing an incompatible database.
|
||||
*
|
||||
* <p>You can use multiple SQL statements in one migration step as long as you separate them with a
|
||||
* semicolon (;).
|
||||
|
||||
@@ -33,6 +33,8 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
@@ -301,4 +303,66 @@ public class NeuralNetworkModelManager {
|
||||
logger.error("Error extracting models", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Pattern modelPattern =
|
||||
Pattern.compile("^([a-zA-Z0-9._]+)-(\\d+)-(\\d+)-(yolov(?:5|8|11)[nsmlx]*)\\.rknn$");
|
||||
|
||||
private static Pattern labelsPattern =
|
||||
Pattern.compile("^([a-zA-Z0-9._]+)-(\\d+)-(\\d+)-(yolov(?:5|8|11)[nsmlx]*)-labels\\.txt$");
|
||||
|
||||
/**
|
||||
* Check naming conventions for models and labels.
|
||||
*
|
||||
* <p>This is static as it is not dependent on the state of the class.
|
||||
*
|
||||
* @param modelName the name of the model
|
||||
* @param labelsName the name of the labels file
|
||||
* @throws IllegalArgumentException if the names are invalid
|
||||
*/
|
||||
public static void verifyRKNNNames(String modelName, String labelsName) {
|
||||
// check null
|
||||
if (modelName == null || labelsName == null) {
|
||||
throw new IllegalArgumentException("Model name and labels name cannot be null");
|
||||
}
|
||||
|
||||
// These patterns check that the naming convention of
|
||||
// name-widthResolution-heightResolution-modelType is followed
|
||||
|
||||
Matcher modelMatcher = modelPattern.matcher(modelName);
|
||||
Matcher labelsMatcher = labelsPattern.matcher(labelsName);
|
||||
|
||||
if (!modelMatcher.matches() || !labelsMatcher.matches()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Model name and labels name must follow the naming convention of name-widthResolution-heightResolution-modelType.rknn and name-widthResolution-heightResolution-modelType-labels.txt");
|
||||
}
|
||||
|
||||
if (!modelMatcher.group(1).equals(labelsMatcher.group(1))
|
||||
|| !modelMatcher.group(2).equals(labelsMatcher.group(2))
|
||||
|| !modelMatcher.group(3).equals(labelsMatcher.group(3))
|
||||
|| !modelMatcher.group(4).equals(labelsMatcher.group(4))) {
|
||||
throw new IllegalArgumentException("Model name and labels name must be matching.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse RKNN name and return the name, width, height, and model type.
|
||||
*
|
||||
* <p>This is static as it is not dependent on the state of the class.
|
||||
*
|
||||
* @param modelName the name of the model
|
||||
* @throws IllegalArgumentException if the model name does not follow the naming convention
|
||||
* @return an array containing the name, width, height, and model type
|
||||
*/
|
||||
public static String[] parseRKNNName(String modelName) {
|
||||
Matcher modelMatcher = modelPattern.matcher(modelName);
|
||||
|
||||
if (!modelMatcher.matches()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Model name must follow the naming convention of name-widthResolution-heightResolution-modelType.rknn");
|
||||
}
|
||||
|
||||
return new String[] {
|
||||
modelMatcher.group(1), modelMatcher.group(2), modelMatcher.group(3), modelMatcher.group(4)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.photonvision.common.logging.Logger;
|
||||
* Our blessed images inject the current version via this build workflow:
|
||||
* https://github.com/PhotonVision/photon-image-modifier/blob/2e5ddb6b599df0be921c12c8dbe7b939ecd7f615/.github/workflows/main.yml#L67
|
||||
*
|
||||
* <p>This class provides a convienent abstraction around this
|
||||
* <p>This class provides a convenient abstraction around this
|
||||
*/
|
||||
public class OsImageVersion {
|
||||
private static final Logger logger = new Logger(OsImageVersion.class, LogGroup.General);
|
||||
|
||||
@@ -145,8 +145,9 @@ public class ShellExec {
|
||||
exitCode = 0;
|
||||
if (wait) {
|
||||
try {
|
||||
process.waitFor();
|
||||
exitCode = process.exitValue();
|
||||
exitCode = process.waitFor();
|
||||
errorGobbler.join();
|
||||
outputGobbler.join();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public enum CameraQuirk {
|
||||
/** Separate red/blue gain controls available */
|
||||
@JsonAlias("AWBGain") // remove after https://github.com/PhotonVision/photonvision/issues/1488
|
||||
AwbRedBlueGain,
|
||||
/** Will not work with photonvision - Logitec C270 at least */
|
||||
/** Will not work with photonvision - Logitech C270 at least */
|
||||
CompletelyBroken,
|
||||
/** Has adjustable focus and autofocus switch */
|
||||
AdjustableFocus,
|
||||
|
||||
@@ -31,9 +31,9 @@ public class QuirkyCamera {
|
||||
// SeeCam, which has an odd exposure range
|
||||
new QuirkyCamera(
|
||||
0x2560, 0xc128, "See3Cam_24CUG", CameraQuirk.Gain, CameraQuirk.See3Cam_24CUG),
|
||||
// Chris's older generic "Logitec HD Webcam"
|
||||
// Chris's older generic "Logitech HD Webcam"
|
||||
new QuirkyCamera(0x9331, 0x5A3, CameraQuirk.CompletelyBroken),
|
||||
// Logitec C270
|
||||
// Logitech C270
|
||||
new QuirkyCamera(0x825, 0x46D, CameraQuirk.CompletelyBroken),
|
||||
// A laptop internal camera someone found broken
|
||||
new QuirkyCamera(0x0bda, 0x5510, CameraQuirk.CompletelyBroken),
|
||||
|
||||
@@ -180,7 +180,7 @@ public class USBCameraSource extends VisionSource {
|
||||
// And update the settables' FrameStaticProps
|
||||
settables.setVideoMode(oldVideoMode);
|
||||
|
||||
// Propogate our updated settables over to the frame provider
|
||||
// Propagate our updated settables over to the frame provider
|
||||
((USBFrameProvider) this.usbFrameProvider).updateSettables(this.settables);
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ public class FileSaveFrameConsumer implements Consumer<CVMat> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the match Data collected from the NT. eg : Q58 for qualfication match 58. If not in
|
||||
* Returns the match Data collected from the NT. eg : Q58 for qualification match 58. If not in
|
||||
* event, returns N/A-0-EVENTNAME
|
||||
*/
|
||||
private String getMatchData() {
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import org.opencv.core.Size;
|
||||
import org.photonvision.common.configuration.NeuralNetworkModelManager;
|
||||
import org.photonvision.jni.RknnObjectDetector;
|
||||
import org.photonvision.rknn.RknnJNI;
|
||||
|
||||
@@ -39,6 +40,8 @@ public class RknnModel implements Model {
|
||||
*
|
||||
* <p>"yolov8" -> "YOLO_V8"
|
||||
*
|
||||
* <p>"yolov11" -> "YOLO_V11"
|
||||
*
|
||||
* @param modelName The model's filename
|
||||
* @return The model version
|
||||
*/
|
||||
@@ -48,6 +51,8 @@ public class RknnModel implements Model {
|
||||
return RknnJNI.ModelVersion.YOLO_V5;
|
||||
} else if (modelName.contains("yolov8")) {
|
||||
return RknnJNI.ModelVersion.YOLO_V8;
|
||||
} else if (modelName.contains("yolov11")) {
|
||||
return RknnJNI.ModelVersion.YOLO_V11;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown model version for model " + modelName);
|
||||
}
|
||||
@@ -63,10 +68,8 @@ public class RknnModel implements Model {
|
||||
public RknnModel(File modelFile, String labels) throws IllegalArgumentException, IOException {
|
||||
this.modelFile = modelFile;
|
||||
|
||||
String[] parts = modelFile.getName().split("-");
|
||||
if (parts.length != 4) {
|
||||
throw new IllegalArgumentException("Invalid model file name: " + modelFile);
|
||||
}
|
||||
// parseRKNNName throws an IllegalArgumentException if the model name is invalid
|
||||
String[] parts = NeuralNetworkModelManager.parseRKNNName(modelFile.getName());
|
||||
|
||||
this.version = getModelVersion(parts[3]);
|
||||
|
||||
|
||||
@@ -444,7 +444,7 @@ public class VisionModule {
|
||||
return false;
|
||||
}
|
||||
|
||||
visionRunner.runSyncronously(
|
||||
visionRunner.runSynchronously(
|
||||
() -> {
|
||||
settables.setVideoModeInternal(pipelineSettings.cameraVideoModeIndex);
|
||||
settables.setBrightness(pipelineSettings.cameraBrightness);
|
||||
|
||||
@@ -206,7 +206,7 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
|
||||
parentModule.startCalibration(deserialized);
|
||||
parentModule.saveAndBroadcastAll();
|
||||
} catch (Exception e) {
|
||||
logger.error("Error deserailizing start-calibration request", e);
|
||||
logger.error("Error deserializing start-calibration request", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ public class VisionRunner {
|
||||
}
|
||||
}
|
||||
|
||||
public Future<Void> runSyncronously(Runnable runnable) {
|
||||
public Future<Void> runSynchronously(Runnable runnable) {
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
|
||||
synchronized (runnableList) {
|
||||
@@ -109,7 +109,7 @@ public class VisionRunner {
|
||||
return future;
|
||||
}
|
||||
|
||||
public <T> Future<T> runSyncronously(Callable<T> callable) {
|
||||
public <T> Future<T> runSynchronously(Callable<T> callable) {
|
||||
CompletableFuture<T> future = new CompletableFuture<>();
|
||||
|
||||
synchronized (runnableList) {
|
||||
|
||||
@@ -366,7 +366,7 @@ public class VisionSourceManager {
|
||||
* CameraConfiguration}'s matchedCameraInfo. We depend on the underlying {@link VisionSource} to
|
||||
* be robust to disconnected sources at boot
|
||||
*
|
||||
* <p>Verify that nickname is unique within the set of desesrialized camera configurations, adding
|
||||
* <p>Verify that nickname is unique within the set of deserialized camera configurations, adding
|
||||
* random characters if this isn't the case
|
||||
*/
|
||||
protected VisionSource loadVisionSourceFromCamConfig(CameraConfiguration configuration) {
|
||||
|
||||
@@ -70,7 +70,7 @@ public abstract class VisionSourceSettables {
|
||||
public abstract void setGain(int gain);
|
||||
|
||||
// Pretty uncommon so instead of abstract this is just a no-op by default
|
||||
// Overriddenn by cameras with AWB gain support
|
||||
// Overridden by cameras with AWB gain support
|
||||
public void setRedGain(int red) {}
|
||||
|
||||
public void setBlueGain(int blue) {}
|
||||
|
||||
@@ -108,6 +108,25 @@ public enum TargetModel implements Releasable {
|
||||
-Units.inchesToMeters(9.5) / 2,
|
||||
-Units.inchesToMeters(9.5) / 2)),
|
||||
0),
|
||||
k2025Algae(
|
||||
List.of(
|
||||
new Point3(
|
||||
-Units.inchesToMeters(16.25) / 2,
|
||||
-Units.inchesToMeters(16.25) / 2,
|
||||
-Units.inchesToMeters(16.25) / 2),
|
||||
new Point3(
|
||||
-Units.inchesToMeters(16.25) / 2,
|
||||
Units.inchesToMeters(16.25) / 2,
|
||||
-Units.inchesToMeters(16.25) / 2),
|
||||
new Point3(
|
||||
Units.inchesToMeters(16.25) / 2,
|
||||
Units.inchesToMeters(16.25) / 2,
|
||||
-Units.inchesToMeters(16.25) / 2),
|
||||
new Point3(
|
||||
Units.inchesToMeters(16.25) / 2,
|
||||
-Units.inchesToMeters(16.25) / 2,
|
||||
-Units.inchesToMeters(16.25) / 2)),
|
||||
0),
|
||||
// 2023 AprilTag, with 6 inch marker width (inner black square).
|
||||
@JsonAlias({"k6in_16h5"})
|
||||
kAprilTag6in_16h5(
|
||||
|
||||
@@ -287,7 +287,7 @@ public class Calibrate3dPipeTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a given camera coefficientss matrix set to "undistort" every image file found in a given
|
||||
* Uses a given camera coefficients matrix set to "undistort" every image file found in a given
|
||||
* directory and display them. Provides an easy way to visually debug the results of the
|
||||
* calibration routine. Seems to play havoc with CI and takes a chunk of time, so shouldn't
|
||||
* usually be left active in tests.
|
||||
|
||||
@@ -139,7 +139,7 @@ public class CalibrationRotationPipeTest {
|
||||
|
||||
Point[] originalPoints = {new Point(100, 100), new Point(200, 200), new Point(300, 100)};
|
||||
|
||||
// Distort the origonal points
|
||||
// Distort the original points
|
||||
var distortedOriginalPoints =
|
||||
OpenCVHelp.distortPoints(
|
||||
List.of(originalPoints),
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.pipeline;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.photonvision.common.configuration.NeuralNetworkModelManager;
|
||||
|
||||
public class ObjectDetectionTest {
|
||||
private static LinkedList<String[]> passNames =
|
||||
new LinkedList<String[]>(
|
||||
java.util.Arrays.asList(
|
||||
new String[] {"note-640-640-yolov5s.rknn", "note-640-640-yolov5s-labels.txt"},
|
||||
new String[] {"object-640-640-yolov8n.rknn", "object-640-640-yolov8n-labels.txt"},
|
||||
new String[] {
|
||||
"example_1.2-640-640-yolov5l.rknn", "example_1.2-640-640-yolov5l-labels.txt"
|
||||
},
|
||||
new String[] {"demo_3.5-640-640-yolov8m.rknn", "demo_3.5-640-640-yolov8m-labels.txt"},
|
||||
new String[] {"sample-640-640-yolov5x.rknn", "sample-640-640-yolov5x-labels.txt"},
|
||||
new String[] {
|
||||
"test_case-640-640-yolov8s.rknn", "test_case-640-640-yolov8s-labels.txt"
|
||||
},
|
||||
new String[] {
|
||||
"model_ABC-640-640-yolov5n.rknn", "model_ABC-640-640-yolov5n-labels.txt"
|
||||
},
|
||||
new String[] {"my_model-640-640-yolov8x.rknn", "my_model-640-640-yolov8x-labels.txt"},
|
||||
new String[] {"name_1.0-640-640-yolov5n.rknn", "name_1.0-640-640-yolov5n-labels.txt"},
|
||||
new String[] {
|
||||
"valid_name-640-640-yolov8s.rknn", "valid_name-640-640-yolov8s-labels.txt"
|
||||
},
|
||||
new String[] {
|
||||
"test.model-640-640-yolov5l.rknn", "test.model-640-640-yolov5l-labels.txt"
|
||||
},
|
||||
new String[] {
|
||||
"case1_test-640-640-yolov8m.rknn", "case1_test-640-640-yolov8m-labels.txt"
|
||||
},
|
||||
new String[] {"A123-640-640-yolov5x.rknn", "A123-640-640-yolov5x-labels.txt"},
|
||||
new String[] {
|
||||
"z_y_test.model-640-640-yolov8n.rknn", "z_y_test.model-640-640-yolov8n-labels.txt"
|
||||
}));
|
||||
private static LinkedList<String[]> parsedPassNames =
|
||||
new LinkedList<String[]>(
|
||||
java.util.Arrays.asList(
|
||||
new String[] {"note", "640", "640", "yolov5s"},
|
||||
new String[] {"object", "640", "640", "yolov8n"},
|
||||
new String[] {"example_1.2", "640", "640", "yolov5l"},
|
||||
new String[] {"demo_3.5", "640", "640", "yolov8m"},
|
||||
new String[] {"sample", "640", "640", "yolov5x"},
|
||||
new String[] {"test_case", "640", "640", "yolov8s"},
|
||||
new String[] {"model_ABC", "640", "640", "yolov5n"},
|
||||
new String[] {"my_model", "640", "640", "yolov8x"},
|
||||
new String[] {"name_1.0", "640", "640", "yolov5n"},
|
||||
new String[] {"valid_name", "640", "640", "yolov8s"},
|
||||
new String[] {"test.model", "640", "640", "yolov5l"},
|
||||
new String[] {"case1_test", "640", "640", "yolov8m"},
|
||||
new String[] {"A123", "640", "640", "yolov5x"},
|
||||
new String[] {"z_y_test.model", "640", "640", "yolov8n"}));
|
||||
private static LinkedList<String[]> failNames =
|
||||
new LinkedList<String[]>(
|
||||
java.util.Arrays.asList(
|
||||
new String[] {"note-yolov5s.rknn", "note-640-640-yolov5s-labels.txt"},
|
||||
new String[] {"640-640-yolov8n.rknn", "object-640-640-yolov8n-labels.txt"},
|
||||
new String[] {"example_1.2.rknn", "example_1.2-640-640-yolov5l-labels.txt"},
|
||||
new String[] {"demo_3.5-640-yolov8m.rknn", "demo_3.5-640-640-yolov8m-labels.txt"},
|
||||
new String[] {"sample-640.rknn", "sample-640-640-yolov5x-labels.txt"},
|
||||
new String[] {"test_case.txt", "test_case-640-640-yolov8s-labels.txt"},
|
||||
new String[] {"model_ABC.onnx", "model_ABC-640-640-yolov5n-labels.txt"},
|
||||
new String[] {"my_model", "my_model-640-640-yolov8x-labels.txt"},
|
||||
new String[] {"name_1.0-yolov5n.rknn", "wrong-labels.txt"},
|
||||
new String[] {"", "valid_name-640-640-yolov8s-labels.txt"},
|
||||
new String[] {null, "test.model-640-640-yolov5l-labels.txt"},
|
||||
new String[] {"case1_test-640-640-yolov8m.rknn", null},
|
||||
new String[] {"A123-640-640.rknn", "different-labels.txt"},
|
||||
new String[] {"z_y_test.model", ""}));
|
||||
|
||||
// Test the model name validation for names that ought to pass
|
||||
@ParameterizedTest
|
||||
@MethodSource("verifyPassNameProvider")
|
||||
public void testRKNNVerificationPass(String[] names) {
|
||||
NeuralNetworkModelManager.verifyRKNNNames(names[0], names[1]);
|
||||
}
|
||||
|
||||
// // Test the model name validation for names that ought to fail
|
||||
@ParameterizedTest
|
||||
@MethodSource("verifyFailNameProvider")
|
||||
public void testRNNVerificationFail(String[] names) {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> NeuralNetworkModelManager.verifyRKNNNames(names[0], names[1]));
|
||||
}
|
||||
|
||||
// Test the model name parsing
|
||||
@ParameterizedTest
|
||||
@MethodSource("parseNameProvider")
|
||||
public void testRKNNNameParsing(String[] expected, String name) {
|
||||
String[] parsed = NeuralNetworkModelManager.parseRKNNName(name);
|
||||
assertArrayEquals(expected, parsed);
|
||||
}
|
||||
|
||||
static Stream<Arguments> verifyPassNameProvider() {
|
||||
return passNames.stream().map(array -> Arguments.of((Object) array));
|
||||
}
|
||||
|
||||
static Stream<Arguments> verifyFailNameProvider() {
|
||||
return failNames.stream().map(array -> Arguments.of((Object) array));
|
||||
}
|
||||
|
||||
static Stream<Arguments> parseNameProvider() {
|
||||
// return a stream of parsed pass names, and the first element of each pass name
|
||||
return passNames.stream()
|
||||
.map(name -> Arguments.of(parsedPassNames.get(passNames.indexOf(name)), name[0]));
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,15 @@
|
||||
# SOFTWARE.
|
||||
#
|
||||
|
||||
from .estimatedRobotPose import EstimatedRobotPose # noqa
|
||||
from .packet import Packet # noqa
|
||||
from .photonCamera import PhotonCamera # noqa
|
||||
from .photonPoseEstimator import PhotonPoseEstimator, PoseStrategy # noqa
|
||||
from .estimatedRobotPose import EstimatedRobotPose
|
||||
from .packet import Packet
|
||||
from .photonCamera import PhotonCamera
|
||||
from .photonPoseEstimator import PhotonPoseEstimator, PoseStrategy
|
||||
|
||||
__all__ = (
|
||||
"EstimatedRobotPose",
|
||||
"Packet",
|
||||
"PhotonCamera",
|
||||
"PhotonPoseEstimator",
|
||||
"PoseStrategy",
|
||||
)
|
||||
|
||||
@@ -3,3 +3,11 @@ from .simCameraProperties import SimCameraProperties
|
||||
from .videoSimUtil import VideoSimUtil
|
||||
from .visionSystemSim import VisionSystemSim
|
||||
from .visionTargetSim import VisionTargetSim
|
||||
|
||||
__all__ = (
|
||||
"PhotonCameraSim",
|
||||
"SimCameraProperties",
|
||||
"VideoSimUtil",
|
||||
"VisionSystemSim",
|
||||
"VisionTargetSim",
|
||||
)
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
# no one but us chickens
|
||||
from .multiTargetPNPResult import MultiTargetPNPResult, PnpResult
|
||||
from .photonPipelineResult import PhotonPipelineMetadata, PhotonPipelineResult
|
||||
from .photonTrackedTarget import PhotonTrackedTarget
|
||||
from .TargetCorner import TargetCorner
|
||||
|
||||
from .multiTargetPNPResult import MultiTargetPNPResult, PnpResult # noqa
|
||||
from .photonPipelineResult import PhotonPipelineMetadata, PhotonPipelineResult # noqa
|
||||
from .photonTrackedTarget import PhotonTrackedTarget # noqa
|
||||
from .TargetCorner import TargetCorner # noqa
|
||||
__all__ = (
|
||||
"MultiTargetPNPResult",
|
||||
"PnpResult",
|
||||
"PhotonPipelineMetadata",
|
||||
"PhotonPipelineResult",
|
||||
"PhotonTrackedTarget",
|
||||
"TargetCorner",
|
||||
)
|
||||
|
||||
@@ -59,11 +59,11 @@ setup(
|
||||
version=versionString,
|
||||
install_requires=[
|
||||
"numpy~=2.1",
|
||||
"wpilib<2026,>=2025.2.1",
|
||||
"robotpy-wpimath<2026,>=2025.2.1",
|
||||
"robotpy-apriltag<2026,>=2025.2.1",
|
||||
"robotpy-cscore<2026,>=2025.2.1",
|
||||
"pyntcore<2026,>=2025.2.1",
|
||||
"wpilib<2026,>=2025.3.1",
|
||||
"robotpy-wpimath<2026,>=2025.3.1",
|
||||
"robotpy-apriltag<2026,>=2025.3.1",
|
||||
"robotpy-cscore<2026,>=2025.3.1",
|
||||
"pyntcore<2026,>=2025.3.1",
|
||||
"opencv-python;platform_machine!='roborio'",
|
||||
],
|
||||
description=descriptionStr,
|
||||
|
||||
@@ -43,11 +43,13 @@ import edu.wpi.first.networktables.PubSubOption;
|
||||
import edu.wpi.first.networktables.StringSubscriber;
|
||||
import edu.wpi.first.wpilibj.DriverStation;
|
||||
import edu.wpi.first.wpilibj.Timer;
|
||||
import edu.wpi.first.wpilibj.util.WPILibVersion;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import org.opencv.core.Core;
|
||||
import org.photonvision.common.hardware.VisionLEDMode;
|
||||
import org.photonvision.common.networktables.PacketSubscriber;
|
||||
import org.photonvision.targeting.PhotonPipelineResult;
|
||||
@@ -157,6 +159,76 @@ public class PhotonCamera implements AutoCloseable {
|
||||
|
||||
// HACK - start a TimeSyncServer, if we haven't yet.
|
||||
TimeSyncSingleton.load();
|
||||
|
||||
// HACK - check if things are compatible
|
||||
verifyDependencies();
|
||||
}
|
||||
|
||||
public static void verifyDependencies() {
|
||||
if (!WPILibVersion.Version.equals(PhotonVersion.wpilibTargetVersion)) {
|
||||
String bfw =
|
||||
"\n\n\n\n\n"
|
||||
+ ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"
|
||||
+ ">>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||
+ ">>> \n"
|
||||
+ ">>> You are running an incompatible version \n"
|
||||
+ ">>> of PhotonVision ! \n"
|
||||
+ ">>> \n"
|
||||
+ ">>> PhotonLib "
|
||||
+ PhotonVersion.versionString
|
||||
+ " is built for WPILib "
|
||||
+ PhotonVersion.wpilibTargetVersion
|
||||
+ "\n"
|
||||
+ ">>> but you are using WPILib "
|
||||
+ WPILibVersion.Version
|
||||
+ ">>> \n"
|
||||
+ ">>> This is neither tested nor supported. \n"
|
||||
+ ">>> You MUST update PhotonVision, \n"
|
||||
+ ">>> PhotonLib, or both. \n"
|
||||
+ ">>> Verify the output of `./gradlew dependencies` \n"
|
||||
+ ">>> \n"
|
||||
+ ">>> Your code will now crash. \n"
|
||||
+ ">>> We hope your day gets better. \n"
|
||||
+ ">>> \n"
|
||||
+ ">>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||
+ ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n";
|
||||
|
||||
DriverStation.reportWarning(bfw, false);
|
||||
DriverStation.reportError(bfw, false);
|
||||
throw new UnsupportedOperationException(bfw);
|
||||
}
|
||||
if (!Core.VERSION.equals(PhotonVersion.opencvTargetVersion)) {
|
||||
String bfw =
|
||||
"\n\n\n\n\n"
|
||||
+ ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"
|
||||
+ ">>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||
+ ">>> \n"
|
||||
+ ">>> You are running an incompatible version \n"
|
||||
+ ">>> of PhotonVision ! \n"
|
||||
+ ">>> \n"
|
||||
+ ">>> PhotonLib "
|
||||
+ PhotonVersion.versionString
|
||||
+ " is built for OpenCV "
|
||||
+ PhotonVersion.opencvTargetVersion
|
||||
+ "\n"
|
||||
+ ">>> but you are using OpenCV "
|
||||
+ Core.VERSION
|
||||
+ ">>> \n"
|
||||
+ ">>> This is neither tested nor supported. \n"
|
||||
+ ">>> You MUST update PhotonVision, \n"
|
||||
+ ">>> PhotonLib, or both. \n"
|
||||
+ ">>> Verify the output of `./gradlew dependencies` \n"
|
||||
+ ">>> \n"
|
||||
+ ">>> Your code will now crash. \n"
|
||||
+ ">>> We hope your day gets better. \n"
|
||||
+ ">>> \n"
|
||||
+ ">>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||
+ ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n";
|
||||
|
||||
DriverStation.reportWarning(bfw, false);
|
||||
DriverStation.reportError(bfw, false);
|
||||
throw new UnsupportedOperationException(bfw);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,9 +31,12 @@ import edu.wpi.first.math.Matrix;
|
||||
import edu.wpi.first.math.Pair;
|
||||
import edu.wpi.first.math.geometry.Pose2d;
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Rotation2d;
|
||||
import edu.wpi.first.math.geometry.Rotation3d;
|
||||
import edu.wpi.first.math.geometry.Transform3d;
|
||||
import edu.wpi.first.math.geometry.Translation2d;
|
||||
import edu.wpi.first.math.geometry.Translation3d;
|
||||
import edu.wpi.first.math.interpolation.TimeInterpolatableBuffer;
|
||||
import edu.wpi.first.math.numbers.N1;
|
||||
import edu.wpi.first.math.numbers.N3;
|
||||
import edu.wpi.first.math.numbers.N8;
|
||||
@@ -83,7 +86,18 @@ public class PhotonPoseEstimator {
|
||||
* Use all visible tags to compute a single pose estimate. This runs on the RoboRIO, and can
|
||||
* take a lot of time.
|
||||
*/
|
||||
MULTI_TAG_PNP_ON_RIO
|
||||
MULTI_TAG_PNP_ON_RIO,
|
||||
|
||||
/**
|
||||
* Use distance data from best visible tag to compute a Pose. This runs on the RoboRIO in order
|
||||
* to access the robot's yaw heading, and MUST have addHeadingData called every frame so heading
|
||||
* data is up-to-date.
|
||||
*
|
||||
* <p>Yields a Pose2d in estimatedRobotPose (0 for z, roll, pitch)
|
||||
*
|
||||
* <p>https://www.chiefdelphi.com/t/frc-6328-mechanical-advantage-2025-build-thread/477314/98
|
||||
*/
|
||||
PNP_DISTANCE_TRIG_SOLVE
|
||||
}
|
||||
|
||||
private AprilTagFieldLayout fieldTags;
|
||||
@@ -97,6 +111,9 @@ public class PhotonPoseEstimator {
|
||||
protected double poseCacheTimestampSeconds = -1;
|
||||
private final Set<Integer> reportedErrors = new HashSet<>();
|
||||
|
||||
private final TimeInterpolatableBuffer<Rotation2d> headingBuffer =
|
||||
TimeInterpolatableBuffer.createBuffer(1.0);
|
||||
|
||||
/**
|
||||
* Create a new PhotonPoseEstimator.
|
||||
*
|
||||
@@ -259,6 +276,30 @@ public class PhotonPoseEstimator {
|
||||
setLastPose(new Pose3d(lastPose));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add robot heading data to buffer. Must be called periodically for the
|
||||
* <b>PNP_DISTANCE_TRIG_SOLVE</b> strategy.
|
||||
*
|
||||
* @param timestampSeconds timestamp of the robot heading data.
|
||||
* @param heading Field-relative robot heading at given timestamp. Standard WPILIB field
|
||||
* coordinates.
|
||||
*/
|
||||
public void addHeadingData(double timestampSeconds, Rotation3d heading) {
|
||||
addHeadingData(timestampSeconds, heading.toRotation2d());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add robot heading data to buffer. Must be called periodically for the
|
||||
* <b>PNP_DISTANCE_TRIG_SOLVE</b> strategy.
|
||||
*
|
||||
* @param timestampSeconds timestamp of the robot heading data.
|
||||
* @param heading Field-relative robot heading at given timestamp. Standard WPILIB field
|
||||
* coordinates.
|
||||
*/
|
||||
public void addHeadingData(double timestampSeconds, Rotation2d heading) {
|
||||
headingBuffer.addSample(timestampSeconds, heading);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current transform from the center of the robot to the camera mount position
|
||||
*/
|
||||
@@ -286,8 +327,8 @@ public class PhotonPoseEstimator {
|
||||
* <li>No targets were found in the pipeline results.
|
||||
* </ul>
|
||||
*
|
||||
* Will report a warning if strategy is multi-tag-on-rio, but camera calibration data is not
|
||||
* provided
|
||||
* Will report a warning if strategy is multi-tag-on-rio because camera calibration data is not
|
||||
* provided in this overload.
|
||||
*
|
||||
* @param cameraResult The latest pipeline result from the camera
|
||||
* @return an {@link EstimatedRobotPose} with an estimated pose, timestamp, and targets used to
|
||||
@@ -341,6 +382,20 @@ public class PhotonPoseEstimator {
|
||||
return update(cameraResult, cameraMatrix, distCoeffs, this.primaryStrategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal convenience method for using a fallback strategy for update(). This should only be
|
||||
* called after timestamp checks have been done by another update() overload.
|
||||
*
|
||||
* @param cameraResult The latest pipeline result from the camera
|
||||
* @param strategy The pose strategy to use
|
||||
* @return an {@link EstimatedRobotPose} with an estimated pose, timestamp, and targets used to
|
||||
* create the estimate.
|
||||
*/
|
||||
private Optional<EstimatedRobotPose> update(
|
||||
PhotonPipelineResult cameraResult, PoseStrategy strategy) {
|
||||
return update(cameraResult, Optional.empty(), Optional.empty(), strategy);
|
||||
}
|
||||
|
||||
private Optional<EstimatedRobotPose> update(
|
||||
PhotonPipelineResult cameraResult,
|
||||
Optional<Matrix<N3, N3>> cameraMatrix,
|
||||
@@ -357,22 +412,10 @@ public class PhotonPoseEstimator {
|
||||
yield closestToReferencePoseStrategy(cameraResult, referencePose);
|
||||
}
|
||||
case AVERAGE_BEST_TARGETS -> averageBestTargetsStrategy(cameraResult);
|
||||
case MULTI_TAG_PNP_ON_RIO -> {
|
||||
if (cameraMatrix.isEmpty()) {
|
||||
DriverStation.reportWarning(
|
||||
"Camera matrix is empty for multi-tag-on-rio",
|
||||
Thread.currentThread().getStackTrace());
|
||||
yield Optional.empty();
|
||||
} else if (distCoeffs.isEmpty()) {
|
||||
DriverStation.reportWarning(
|
||||
"Camera matrix is empty for multi-tag-on-rio",
|
||||
Thread.currentThread().getStackTrace());
|
||||
yield Optional.empty();
|
||||
} else {
|
||||
yield multiTagOnRioStrategy(cameraResult, cameraMatrix, distCoeffs);
|
||||
}
|
||||
}
|
||||
case MULTI_TAG_PNP_ON_RIO ->
|
||||
multiTagOnRioStrategy(cameraResult, cameraMatrix, distCoeffs);
|
||||
case MULTI_TAG_PNP_ON_COPROCESSOR -> multiTagOnCoprocStrategy(cameraResult);
|
||||
case PNP_DISTANCE_TRIG_SOLVE -> pnpDistanceTrigSolveStrategy(cameraResult);
|
||||
};
|
||||
|
||||
if (estimatedPose.isPresent()) {
|
||||
@@ -382,42 +425,90 @@ public class PhotonPoseEstimator {
|
||||
return estimatedPose;
|
||||
}
|
||||
|
||||
private Optional<EstimatedRobotPose> multiTagOnCoprocStrategy(PhotonPipelineResult result) {
|
||||
if (result.getMultiTagResult().isPresent()) {
|
||||
var best_tf = result.getMultiTagResult().get().estimatedPose.best;
|
||||
var best =
|
||||
new Pose3d()
|
||||
.plus(best_tf) // field-to-camera
|
||||
.relativeTo(fieldTags.getOrigin())
|
||||
.plus(robotToCamera.inverse()); // field-to-robot
|
||||
return Optional.of(
|
||||
new EstimatedRobotPose(
|
||||
best,
|
||||
result.getTimestampSeconds(),
|
||||
result.getTargets(),
|
||||
PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR));
|
||||
} else {
|
||||
// We can never fall back on another multitag strategy
|
||||
return update(result, Optional.empty(), Optional.empty(), this.multiTagFallbackStrategy);
|
||||
private Optional<EstimatedRobotPose> pnpDistanceTrigSolveStrategy(PhotonPipelineResult result) {
|
||||
PhotonTrackedTarget bestTarget = result.getBestTarget();
|
||||
|
||||
if (bestTarget == null) return Optional.empty();
|
||||
|
||||
var headingSampleOpt = headingBuffer.getSample(result.getTimestampSeconds());
|
||||
if (headingSampleOpt.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Rotation2d headingSample = headingSampleOpt.get();
|
||||
|
||||
Translation2d camToTagTranslation =
|
||||
new Translation3d(
|
||||
bestTarget.getBestCameraToTarget().getTranslation().getNorm(),
|
||||
new Rotation3d(
|
||||
0,
|
||||
-Math.toRadians(bestTarget.getPitch()),
|
||||
-Math.toRadians(bestTarget.getYaw())))
|
||||
.rotateBy(robotToCamera.getRotation())
|
||||
.toTranslation2d()
|
||||
.rotateBy(headingSample);
|
||||
|
||||
var tagPoseOpt = fieldTags.getTagPose(bestTarget.getFiducialId());
|
||||
if (tagPoseOpt.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
var tagPose2d = tagPoseOpt.get().toPose2d();
|
||||
|
||||
Translation2d fieldToCameraTranslation =
|
||||
tagPose2d.getTranslation().plus(camToTagTranslation.unaryMinus());
|
||||
|
||||
Translation2d camToRobotTranslation =
|
||||
robotToCamera.getTranslation().toTranslation2d().unaryMinus().rotateBy(headingSample);
|
||||
|
||||
Pose2d robotPose =
|
||||
new Pose2d(fieldToCameraTranslation.plus(camToRobotTranslation), headingSample);
|
||||
|
||||
return Optional.of(
|
||||
new EstimatedRobotPose(
|
||||
new Pose3d(robotPose),
|
||||
result.getTimestampSeconds(),
|
||||
result.getTargets(),
|
||||
PoseStrategy.PNP_DISTANCE_TRIG_SOLVE));
|
||||
}
|
||||
|
||||
private Optional<EstimatedRobotPose> multiTagOnCoprocStrategy(PhotonPipelineResult result) {
|
||||
if (result.getMultiTagResult().isEmpty()) {
|
||||
return update(result, this.multiTagFallbackStrategy);
|
||||
}
|
||||
|
||||
var best_tf = result.getMultiTagResult().get().estimatedPose.best;
|
||||
var best =
|
||||
new Pose3d()
|
||||
.plus(best_tf) // field-to-camera
|
||||
.relativeTo(fieldTags.getOrigin())
|
||||
.plus(robotToCamera.inverse()); // field-to-robot
|
||||
return Optional.of(
|
||||
new EstimatedRobotPose(
|
||||
best,
|
||||
result.getTimestampSeconds(),
|
||||
result.getTargets(),
|
||||
PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR));
|
||||
}
|
||||
|
||||
private Optional<EstimatedRobotPose> multiTagOnRioStrategy(
|
||||
PhotonPipelineResult result,
|
||||
Optional<Matrix<N3, N3>> cameraMatrixOpt,
|
||||
Optional<Matrix<N8, N1>> distCoeffsOpt) {
|
||||
boolean hasCalibData = cameraMatrixOpt.isPresent() && distCoeffsOpt.isPresent();
|
||||
// cannot run multitagPNP, use fallback strategy
|
||||
if (!hasCalibData || result.getTargets().size() < 2) {
|
||||
return update(result, cameraMatrixOpt, distCoeffsOpt, this.multiTagFallbackStrategy);
|
||||
if (cameraMatrixOpt.isEmpty() || distCoeffsOpt.isEmpty()) {
|
||||
DriverStation.reportWarning(
|
||||
"No camera calibration data provided for multi-tag-on-rio",
|
||||
Thread.currentThread().getStackTrace());
|
||||
return update(result, this.multiTagFallbackStrategy);
|
||||
}
|
||||
|
||||
if (result.getTargets().size() < 2) {
|
||||
return update(result, this.multiTagFallbackStrategy);
|
||||
}
|
||||
|
||||
var pnpResult =
|
||||
VisionEstimation.estimateCamPosePNP(
|
||||
cameraMatrixOpt.get(), distCoeffsOpt.get(), result.getTargets(), fieldTags, tagModel);
|
||||
// try fallback strategy if solvePNP fails for some reason
|
||||
if (!pnpResult.isPresent())
|
||||
return update(result, cameraMatrixOpt, distCoeffsOpt, this.multiTagFallbackStrategy);
|
||||
if (!pnpResult.isPresent()) return update(result, this.multiTagFallbackStrategy);
|
||||
var best =
|
||||
new Pose3d()
|
||||
.plus(pnpResult.get().best) // field-to-camera
|
||||
|
||||
@@ -172,9 +172,9 @@ public final class PhotonUtils {
|
||||
* Estimates the pose of the robot in the field coordinate system, given the pose of the fiducial
|
||||
* tag, the robot relative to the camera, and the target relative to the camera.
|
||||
*
|
||||
* @param fieldRelativeTagPose Pose3D the field relative pose of the target
|
||||
* @param cameraToRobot Transform3D of the robot relative to the camera. Origin of the robot is
|
||||
* defined as the center.
|
||||
* @param fieldRelativeTagPose Pose3D the field relative pose of the target
|
||||
* @param cameraToTarget Transform3D of the target relative to the camera, returned by
|
||||
* PhotonVision
|
||||
* @return Transform3d Robot position relative to the field
|
||||
|
||||
@@ -160,11 +160,12 @@ public class SimCameraProperties {
|
||||
if (!success) throw new IOException("Requested resolution not found in calibration");
|
||||
}
|
||||
|
||||
public void setRandomSeed(long seed) {
|
||||
public SimCameraProperties setRandomSeed(long seed) {
|
||||
rand.setSeed(seed);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setCalibration(int resWidth, int resHeight, Rotation2d fovDiag) {
|
||||
public SimCameraProperties setCalibration(int resWidth, int resHeight, Rotation2d fovDiag) {
|
||||
if (fovDiag.getDegrees() < 1 || fovDiag.getDegrees() > 179) {
|
||||
fovDiag = Rotation2d.fromDegrees(MathUtil.clamp(fovDiag.getDegrees(), 1, 179));
|
||||
DriverStation.reportError(
|
||||
@@ -189,9 +190,11 @@ public class SimCameraProperties {
|
||||
// create camera intrinsics matrix
|
||||
var camIntrinsics = MatBuilder.fill(Nat.N3(), Nat.N3(), fx, 0, cx, 0, fy, cy, 0, 0, 1);
|
||||
setCalibration(resWidth, resHeight, camIntrinsics, distCoeff);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setCalibration(
|
||||
public SimCameraProperties setCalibration(
|
||||
int resWidth, int resHeight, Matrix<N3, N3> camIntrinsics, Matrix<N8, N1> distCoeffs) {
|
||||
this.resWidth = resWidth;
|
||||
this.resHeight = resHeight;
|
||||
@@ -222,43 +225,54 @@ public class SimCameraProperties {
|
||||
viewplanes.add(
|
||||
new DMatrix3(translation3d.getX(), translation3d.getY(), translation3d.getZ()));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setCalibError(double avgErrorPx, double errorStdDevPx) {
|
||||
public SimCameraProperties setCalibError(double avgErrorPx, double errorStdDevPx) {
|
||||
this.avgErrorPx = avgErrorPx;
|
||||
this.errorStdDevPx = errorStdDevPx;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fps The average frames per second the camera should process at. <b>Exposure time limits
|
||||
* FPS if set!</b>
|
||||
*/
|
||||
public void setFPS(double fps) {
|
||||
public SimCameraProperties setFPS(double fps) {
|
||||
frameSpeedMs = Math.max(1000.0 / fps, exposureTimeMs);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param exposureTimeMs The amount of time the "shutter" is open for one frame. Affects motion
|
||||
* blur. <b>Frame speed(from FPS) is limited to this!</b>
|
||||
*/
|
||||
public void setExposureTimeMs(double exposureTimeMs) {
|
||||
public SimCameraProperties setExposureTimeMs(double exposureTimeMs) {
|
||||
this.exposureTimeMs = exposureTimeMs;
|
||||
frameSpeedMs = Math.max(frameSpeedMs, exposureTimeMs);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param avgLatencyMs The average latency (from image capture to data published) in milliseconds
|
||||
* a frame should have
|
||||
*/
|
||||
public void setAvgLatencyMs(double avgLatencyMs) {
|
||||
public SimCameraProperties setAvgLatencyMs(double avgLatencyMs) {
|
||||
this.avgLatencyMs = avgLatencyMs;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param latencyStdDevMs The standard deviation in milliseconds of the latency
|
||||
*/
|
||||
public void setLatencyStdDevMs(double latencyStdDevMs) {
|
||||
public SimCameraProperties setLatencyStdDevMs(double latencyStdDevMs) {
|
||||
this.latencyStdDevMs = latencyStdDevMs;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getResWidth() {
|
||||
|
||||
@@ -27,10 +27,12 @@
|
||||
#include <hal/FRCUsageReporting.h>
|
||||
#include <net/TimeSyncServer.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <WPILibVersion.h>
|
||||
#include <frc/Errors.h>
|
||||
#include <frc/RobotController.h>
|
||||
#include <frc/Timer.h>
|
||||
@@ -39,26 +41,81 @@
|
||||
#include <wpi/json.h>
|
||||
|
||||
#include "PhotonVersion.h"
|
||||
#include "opencv2/core/utility.hpp"
|
||||
#include "photon/dataflow/structures/Packet.h"
|
||||
|
||||
inline constexpr std::string_view bfw =
|
||||
"\n\n\n\n"
|
||||
">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"
|
||||
">>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||
">>> \n"
|
||||
">>> You are running an incompatible version \n"
|
||||
">>> of PhotonVision on your coprocessor! \n"
|
||||
">>> \n"
|
||||
">>> This is neither tested nor supported. \n"
|
||||
">>> You MUST update PhotonVision, \n"
|
||||
">>> PhotonLib, or both. \n"
|
||||
">>> \n"
|
||||
">>> Your code will now crash. \n"
|
||||
">>> We hope your day gets better. \n"
|
||||
">>> \n"
|
||||
">>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||
">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"
|
||||
"\n\n";
|
||||
inline void verifyDependencies() {
|
||||
if (!(std::string_view{GetWPILibVersion()} ==
|
||||
std::string_view{photon::PhotonVersion::wpilibTargetVersion})) {
|
||||
std::string bfw =
|
||||
"\n\n\n\n\n"
|
||||
">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"
|
||||
">>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||
">>> \n"
|
||||
">>> You are running an incompatible version \n"
|
||||
">>> of PhotonVision ! \n"
|
||||
">>> \n"
|
||||
">>> PhotonLib ";
|
||||
bfw += photon::PhotonVersion::versionString;
|
||||
bfw += " is built for WPILib ";
|
||||
bfw += photon::PhotonVersion::wpilibTargetVersion;
|
||||
bfw +=
|
||||
"\n"
|
||||
">>> but you are using WPILib ";
|
||||
bfw += GetWPILibVersion();
|
||||
bfw +=
|
||||
"\n>>> \n"
|
||||
">>> This is neither tested nor supported. \n"
|
||||
">>> You MUST update PhotonVision, \n"
|
||||
">>> PhotonLib, or both. \n"
|
||||
">>> Verify the output of `./gradlew dependencies` \n"
|
||||
">>> \n"
|
||||
">>> Your code will now crash. \n"
|
||||
">>> We hope your day gets better. \n"
|
||||
">>> \n"
|
||||
">>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||
">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n";
|
||||
|
||||
FRC_ReportWarning(bfw);
|
||||
FRC_ReportError(frc::err::Error, bfw);
|
||||
throw new std::runtime_error(std::string{bfw});
|
||||
}
|
||||
if (!(std::string_view{cv::getVersionString()} ==
|
||||
std::string_view{photon::PhotonVersion::opencvTargetVersion})) {
|
||||
std::string bfw =
|
||||
"\n\n\n\n\n"
|
||||
">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"
|
||||
">>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||
">>> \n"
|
||||
">>> You are running an incompatible version \n"
|
||||
">>> of PhotonVision ! \n"
|
||||
">>> \n"
|
||||
">>> PhotonLib ";
|
||||
bfw += photon::PhotonVersion::versionString;
|
||||
bfw += " is built for OpenCV ";
|
||||
bfw += photon::PhotonVersion::opencvTargetVersion;
|
||||
bfw +=
|
||||
"\n"
|
||||
">>> but you are using OpenCV ";
|
||||
bfw += cv::getVersionString();
|
||||
bfw +=
|
||||
"\n>>> \n"
|
||||
">>> This is neither tested nor supported. \n"
|
||||
">>> You MUST update PhotonVision, \n"
|
||||
">>> PhotonLib, or both. \n"
|
||||
">>> Verify the output of `./gradlew dependencies` \n"
|
||||
">>> \n"
|
||||
">>> Your code will now crash. \n"
|
||||
">>> We hope your day gets better. \n"
|
||||
">>> \n"
|
||||
">>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||
">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n";
|
||||
|
||||
FRC_ReportWarning(bfw);
|
||||
FRC_ReportError(frc::err::Error, bfw);
|
||||
throw new std::runtime_error(std::string{bfw});
|
||||
}
|
||||
}
|
||||
|
||||
// bit of a hack -- start a TimeSync server on port 5810 (hard-coded). We want
|
||||
// to avoid calling this from static initialization
|
||||
@@ -125,6 +182,7 @@ PhotonCamera::PhotonCamera(nt::NetworkTableInstance instance,
|
||||
topicNameSubscriber(instance, PHOTON_PREFIX, {.topicsOnly = true}),
|
||||
path(rootTable->GetPath()),
|
||||
cameraName(cameraName) {
|
||||
verifyDependencies();
|
||||
HAL_Report(HALUsageReporting::kResourceType_PhotonCamera, InstanceCount);
|
||||
InstanceCount++;
|
||||
|
||||
@@ -310,7 +368,25 @@ void PhotonCamera::VerifyVersion() {
|
||||
std::string remote_uuid{remote_uuid_json};
|
||||
|
||||
if (local_uuid != remote_uuid) {
|
||||
FRC_ReportError(frc::warn::Warning, bfw);
|
||||
constexpr std::string_view bfw =
|
||||
"\n\n\n\n"
|
||||
">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"
|
||||
">>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||
">>> \n"
|
||||
">>> You are running an incompatible version \n"
|
||||
">>> of PhotonVision on your coprocessor! \n"
|
||||
">>> \n"
|
||||
">>> This is neither tested nor supported. \n"
|
||||
">>> You MUST update PhotonVision, \n"
|
||||
">>> PhotonLib, or both. \n"
|
||||
">>> \n"
|
||||
">>> Your code will now crash. \n"
|
||||
">>> We hope your day gets better. \n"
|
||||
">>> \n"
|
||||
">>> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||
">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"
|
||||
"\n\n";
|
||||
FRC_ReportWarning(bfw);
|
||||
std::string error_str = fmt::format(
|
||||
"Photonlib version {} (message definition version {}) does not match "
|
||||
"coprocessor version {} (message definition version {})!",
|
||||
|
||||
@@ -350,18 +350,17 @@ frc::Pose3d detail::ToPose3d(const cv::Mat& tvec, const cv::Mat& rvec) {
|
||||
|
||||
std::optional<EstimatedRobotPose> PhotonPoseEstimator::MultiTagOnCoprocStrategy(
|
||||
PhotonPipelineResult result) {
|
||||
if (result.MultiTagResult()) {
|
||||
const auto field2camera = result.MultiTagResult()->estimatedPose.best;
|
||||
|
||||
const auto fieldToRobot =
|
||||
frc::Pose3d() + field2camera + m_robotToCamera.Inverse();
|
||||
return photon::EstimatedRobotPose(fieldToRobot, result.GetTimestamp(),
|
||||
result.GetTargets(),
|
||||
MULTI_TAG_PNP_ON_COPROCESSOR);
|
||||
if (!result.MultiTagResult()) {
|
||||
return Update(result, this->multiTagFallbackStrategy);
|
||||
}
|
||||
|
||||
return Update(result, std::nullopt, std::nullopt,
|
||||
this->multiTagFallbackStrategy);
|
||||
const auto field2camera = result.MultiTagResult()->estimatedPose.best;
|
||||
|
||||
const auto fieldToRobot =
|
||||
frc::Pose3d() + field2camera + m_robotToCamera.Inverse();
|
||||
return photon::EstimatedRobotPose(fieldToRobot, result.GetTimestamp(),
|
||||
result.GetTargets(),
|
||||
MULTI_TAG_PNP_ON_COPROCESSOR);
|
||||
}
|
||||
|
||||
std::optional<EstimatedRobotPose> PhotonPoseEstimator::MultiTagOnRioStrategy(
|
||||
@@ -370,19 +369,17 @@ std::optional<EstimatedRobotPose> PhotonPoseEstimator::MultiTagOnRioStrategy(
|
||||
std::optional<PhotonCamera::DistortionMatrix> distCoeffs) {
|
||||
using namespace frc;
|
||||
|
||||
// Need at least 2 targets
|
||||
if (!result.HasTargets() || result.GetTargets().size() < 2) {
|
||||
return Update(result, std::nullopt, std::nullopt,
|
||||
this->multiTagFallbackStrategy);
|
||||
}
|
||||
|
||||
if (!camMat || !distCoeffs) {
|
||||
FRC_ReportError(frc::warn::Warning,
|
||||
"No camera calibration data provided to "
|
||||
"PhotonPoseEstimator::MultiTagOnRioStrategy!",
|
||||
"");
|
||||
return Update(result, std::nullopt, std::nullopt,
|
||||
this->multiTagFallbackStrategy);
|
||||
return Update(result, this->multiTagFallbackStrategy);
|
||||
}
|
||||
|
||||
// Need at least 2 targets
|
||||
if (!result.HasTargets() || result.GetTargets().size() < 2) {
|
||||
return Update(result, this->multiTagFallbackStrategy);
|
||||
}
|
||||
|
||||
auto const targets = result.GetTargets();
|
||||
@@ -408,7 +405,7 @@ std::optional<EstimatedRobotPose> PhotonPoseEstimator::MultiTagOnRioStrategy(
|
||||
|
||||
// We should only do multi-tag if at least 2 tags (* 4 corners/tag)
|
||||
if (imagePoints.size() < 8) {
|
||||
return Update(result, camMat, distCoeffs, this->multiTagFallbackStrategy);
|
||||
return Update(result, this->multiTagFallbackStrategy);
|
||||
}
|
||||
|
||||
// Output mats for results
|
||||
|
||||
@@ -24,13 +24,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <regex>
|
||||
#include <string>
|
||||
|
||||
namespace photon {
|
||||
namespace PhotonVersion {
|
||||
extern const char* versionString;
|
||||
extern const char* buildDate;
|
||||
extern const bool isRelease;
|
||||
extern const char* wpilibTargetVersion;
|
||||
extern const char* opencvTargetVersion;
|
||||
} // namespace PhotonVersion
|
||||
} // namespace photon
|
||||
|
||||
@@ -204,6 +204,21 @@ class PhotonPoseEstimator {
|
||||
|
||||
inline void InvalidatePoseCache() { poseCacheTimestamp = -1_s; }
|
||||
|
||||
/**
|
||||
* Internal convenience method for using a fallback strategy for update().
|
||||
* This should only be called after timestamp checks have been done by another
|
||||
* update() overload.
|
||||
*
|
||||
* @param cameraResult The latest pipeline result from the camera
|
||||
* @param strategy The pose strategy to use
|
||||
* @return an EstimatedRobotPose with an estimated pose, timestamp, and
|
||||
* targets used to create the estimate.
|
||||
*/
|
||||
std::optional<EstimatedRobotPose> Update(const PhotonPipelineResult& result,
|
||||
PoseStrategy strategy) {
|
||||
return Update(result, std::nullopt, std::nullopt, strategy);
|
||||
}
|
||||
|
||||
std::optional<EstimatedRobotPose> Update(
|
||||
const PhotonPipelineResult& result,
|
||||
std::optional<PhotonCamera::CameraMatrix> cameraMatrixData,
|
||||
|
||||
@@ -122,7 +122,7 @@ class PhotonCameraTest {
|
||||
|
||||
private static Stream<Arguments> testNtOffsets() {
|
||||
return Stream.of(
|
||||
// various initializaiton orders
|
||||
// various initialization orders
|
||||
Arguments.of(1, 10, 30, 30),
|
||||
Arguments.of(10, 2, 30, 30),
|
||||
Arguments.of(10, 10, 30, 30),
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
package org.photonvision;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -41,6 +42,10 @@ import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.photonvision.PhotonPoseEstimator.PoseStrategy;
|
||||
import org.photonvision.estimation.TargetModel;
|
||||
import org.photonvision.simulation.PhotonCameraSim;
|
||||
import org.photonvision.simulation.SimCameraProperties;
|
||||
import org.photonvision.simulation.VisionTargetSim;
|
||||
import org.photonvision.targeting.PhotonPipelineResult;
|
||||
import org.photonvision.targeting.PhotonTrackedTarget;
|
||||
import org.photonvision.targeting.TargetCorner;
|
||||
@@ -488,6 +493,68 @@ class PhotonPoseEstimatorTest {
|
||||
assertEquals(1, pose.getZ(), .01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pnpDistanceTrigSolve() {
|
||||
PhotonCameraInjector cameraOne = new PhotonCameraInjector();
|
||||
PhotonCameraSim cameraOneSim =
|
||||
new PhotonCameraSim(cameraOne, SimCameraProperties.PERFECT_90DEG());
|
||||
|
||||
List<VisionTargetSim> simTargets =
|
||||
aprilTags.getTags().stream()
|
||||
.map((AprilTag x) -> new VisionTargetSim(x.pose, TargetModel.kAprilTag36h11, x.ID))
|
||||
.toList();
|
||||
|
||||
/* Compound Rolled + Pitched + Yaw */
|
||||
|
||||
Transform3d compoundTestTransform =
|
||||
new Transform3d(
|
||||
-Units.inchesToMeters(12),
|
||||
-Units.inchesToMeters(11),
|
||||
3,
|
||||
new Rotation3d(
|
||||
Units.degreesToRadians(37), Units.degreesToRadians(6), Units.degreesToRadians(60)));
|
||||
|
||||
var estimator =
|
||||
new PhotonPoseEstimator(
|
||||
aprilTags, PoseStrategy.PNP_DISTANCE_TRIG_SOLVE, compoundTestTransform);
|
||||
|
||||
/* this is the real pose of the robot base we test against */
|
||||
var realPose = new Pose3d(7.3, 4.42, 0, new Rotation3d(0, 0, 2.197));
|
||||
PhotonPipelineResult result =
|
||||
cameraOneSim.process(
|
||||
1, realPose.transformBy(estimator.getRobotToCameraTransform()), simTargets);
|
||||
|
||||
estimator.addHeadingData(result.getTimestampSeconds(), realPose.getRotation().toRotation2d());
|
||||
|
||||
var estimatedPose = estimator.update(result);
|
||||
var pose = estimatedPose.get().estimatedPose;
|
||||
|
||||
assertEquals(realPose.getX(), pose.getX(), .01);
|
||||
assertEquals(realPose.getY(), pose.getY(), .01);
|
||||
assertEquals(0.0, pose.getZ(), .01);
|
||||
|
||||
/* Straight on */
|
||||
|
||||
Transform3d straightOnTestTransform = new Transform3d(0, 0, 3, new Rotation3d(0, 0, 0));
|
||||
|
||||
estimator.setRobotToCameraTransform(straightOnTestTransform);
|
||||
|
||||
/* Pose to compare with */
|
||||
realPose = new Pose3d(4.81, 2.38, 0, new Rotation3d(0, 0, 2.818));
|
||||
result =
|
||||
cameraOneSim.process(
|
||||
1, realPose.transformBy(estimator.getRobotToCameraTransform()), simTargets);
|
||||
|
||||
estimator.addHeadingData(result.getTimestampSeconds(), realPose.getRotation().toRotation2d());
|
||||
|
||||
estimatedPose = estimator.update(result);
|
||||
pose = estimatedPose.get().estimatedPose;
|
||||
|
||||
assertEquals(realPose.getX(), pose.getX(), .01);
|
||||
assertEquals(realPose.getY(), pose.getY(), .01);
|
||||
assertEquals(0.0, pose.getZ(), .01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cacheIsInvalidated() {
|
||||
PhotonCameraInjector cameraOne = new PhotonCameraInjector();
|
||||
@@ -645,6 +712,72 @@ class PhotonPoseEstimatorTest {
|
||||
assertEquals(2.15, pose.getZ(), .01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultiTagOnRioFallback() {
|
||||
PhotonCameraInjector camera = new PhotonCameraInjector();
|
||||
camera.result =
|
||||
new PhotonPipelineResult(
|
||||
0,
|
||||
11 * 1_000_000,
|
||||
1_100_000,
|
||||
1024,
|
||||
List.of(
|
||||
new PhotonTrackedTarget(
|
||||
3.0,
|
||||
-4.0,
|
||||
9.0,
|
||||
4.0,
|
||||
0,
|
||||
-1,
|
||||
-1,
|
||||
new Transform3d(new Translation3d(1, 2, 3), new Rotation3d(1, 2, 3)),
|
||||
new Transform3d(new Translation3d(1, 2, 3), new Rotation3d(1, 2, 3)),
|
||||
0.7,
|
||||
List.of(
|
||||
new TargetCorner(1, 2),
|
||||
new TargetCorner(3, 4),
|
||||
new TargetCorner(5, 6),
|
||||
new TargetCorner(7, 8)),
|
||||
List.of(
|
||||
new TargetCorner(1, 2),
|
||||
new TargetCorner(3, 4),
|
||||
new TargetCorner(5, 6),
|
||||
new TargetCorner(7, 8))),
|
||||
new PhotonTrackedTarget(
|
||||
3.0,
|
||||
-4.0,
|
||||
9.1,
|
||||
6.7,
|
||||
1,
|
||||
-1,
|
||||
-1,
|
||||
new Transform3d(new Translation3d(4, 2, 3), new Rotation3d(0, 0, 0)),
|
||||
new Transform3d(new Translation3d(4, 2, 3), new Rotation3d(1, 5, 3)),
|
||||
0.3,
|
||||
List.of(
|
||||
new TargetCorner(1, 2),
|
||||
new TargetCorner(3, 4),
|
||||
new TargetCorner(5, 6),
|
||||
new TargetCorner(7, 8)),
|
||||
List.of(
|
||||
new TargetCorner(1, 2),
|
||||
new TargetCorner(3, 4),
|
||||
new TargetCorner(5, 6),
|
||||
new TargetCorner(7, 8)))));
|
||||
PhotonPoseEstimator estimator =
|
||||
new PhotonPoseEstimator(aprilTags, PoseStrategy.MULTI_TAG_PNP_ON_RIO, Transform3d.kZero);
|
||||
estimator.setMultiTagFallbackStrategy(PoseStrategy.LOWEST_AMBIGUITY);
|
||||
|
||||
Optional<EstimatedRobotPose> estimatedPose = estimator.update(camera.result);
|
||||
Pose3d pose = estimatedPose.get().estimatedPose;
|
||||
// Make sure values match what we'd expect for the LOWEST_AMBIGUITY strategy
|
||||
assertAll(
|
||||
() -> assertEquals(11, estimatedPose.get().timestampSeconds),
|
||||
() -> assertEquals(1, pose.getX(), 1e-9),
|
||||
() -> assertEquals(3, pose.getY(), 1e-9),
|
||||
() -> assertEquals(2, pose.getZ(), 1e-9));
|
||||
}
|
||||
|
||||
private static class PhotonCameraInjector extends PhotonCamera {
|
||||
public PhotonCameraInjector() {
|
||||
super("Test");
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
package org.photonvision;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
@@ -56,4 +58,9 @@ public class PhotonVersionTest {
|
||||
Assertions.assertFalse(versionMatches("", "v2021.1.6"));
|
||||
Assertions.assertFalse(versionMatches("v2021.1.6", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNominalDeps() {
|
||||
assertDoesNotThrow(PhotonCamera::verifyDependencies);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,6 +419,49 @@ TEST(PhotonPoseEstimatorTest, PoseCache) {
|
||||
|
||||
EXPECT_FALSE(estimatedPose);
|
||||
}
|
||||
|
||||
TEST(PhotonPoseEstimatorTest, MultiTagOnRioFallback) {
|
||||
photon::PhotonCamera cameraOne = photon::PhotonCamera("test");
|
||||
|
||||
std::vector<photon::PhotonTrackedTarget> targets{
|
||||
photon::PhotonTrackedTarget{
|
||||
3.0, -4.0, 9.0, 4.0, 0, -1, -1.f,
|
||||
frc::Transform3d(frc::Translation3d(1_m, 2_m, 3_m),
|
||||
frc::Rotation3d(1_rad, 2_rad, 3_rad)),
|
||||
frc::Transform3d(frc::Translation3d(1_m, 2_m, 3_m),
|
||||
frc::Rotation3d(1_rad, 2_rad, 3_rad)),
|
||||
0.7, corners, detectedCorners},
|
||||
photon::PhotonTrackedTarget{
|
||||
3.0, -4.0, 9.1, 6.7, 1, -1, -1.f,
|
||||
frc::Transform3d(frc::Translation3d(4_m, 2_m, 3_m),
|
||||
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
|
||||
frc::Transform3d(frc::Translation3d(4_m, 2_m, 3_m),
|
||||
frc::Rotation3d(0_rad, 0_rad, 0_rad)),
|
||||
0.3, corners, detectedCorners}};
|
||||
|
||||
cameraOne.test = true;
|
||||
cameraOne.testResult = {photon::PhotonPipelineResult{
|
||||
photon::PhotonPipelineMetadata{0, 0, 2000, 1000}, targets, std::nullopt}};
|
||||
cameraOne.testResult[0].SetReceiveTimestamp(units::second_t(11));
|
||||
|
||||
photon::PhotonPoseEstimator estimator(aprilTags, photon::LOWEST_AMBIGUITY,
|
||||
frc::Transform3d{});
|
||||
|
||||
std::optional<photon::EstimatedRobotPose> estimatedPose;
|
||||
for (const auto& result : cameraOne.GetAllUnreadResults()) {
|
||||
estimatedPose = estimator.Update(result);
|
||||
}
|
||||
ASSERT_TRUE(estimatedPose);
|
||||
frc::Pose3d pose = estimatedPose.value().estimatedPose;
|
||||
|
||||
// Make sure values match what we'd expect for the LOWEST_AMBIGUITY strategy
|
||||
EXPECT_NEAR(11, units::unit_cast<double>(estimatedPose.value().timestamp),
|
||||
.02);
|
||||
EXPECT_NEAR(1, units::unit_cast<double>(pose.X()), .01);
|
||||
EXPECT_NEAR(3, units::unit_cast<double>(pose.Y()), .01);
|
||||
EXPECT_NEAR(2, units::unit_cast<double>(pose.Z()), .01);
|
||||
}
|
||||
|
||||
TEST(PhotonPoseEstimatorTest, CopyResult) {
|
||||
std::vector<photon::PhotonTrackedTarget> targets{};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Like Rosmsg. But worse.
|
||||
|
||||
The code for a single type is split across 3 files. Let's look at PnpResult:
|
||||
- [The struct definition](src/struct/pnpresult_struct.h): This is the data the object holds. Auto-generated. The data this object holds can be primitives or other, fully-deserialized types (like Vec2)
|
||||
- [The user class](src/targeting/pnpresult_struct.h): This is the fully-deserialized PnpResult type. This contains extra functions users might need to expose like `Amgiguity`, or other computed helper things.
|
||||
- [The user class](src/targeting/pnpresult_struct.h): This is the fully-deserialized PnpResult type. This contains extra functions users might need to expose like `Ambiguity`, or other computed helper things.
|
||||
- [The serde interface](src/serde/pnpresult_struct.h): This is a template specialization for converting the user class to/from bytes
|
||||
|
||||
## Prior art
|
||||
|
||||
@@ -29,7 +29,6 @@ import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.imageio.ImageIO;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.opencv.core.Mat;
|
||||
@@ -432,7 +431,7 @@ public class RequestHandler {
|
||||
var tempPath2 = Files.createTempFile("photonvision-kernelogs", ".txt");
|
||||
// In the command below:
|
||||
// dmesg = output all kernel logs since current boot
|
||||
// cat /var/log/kern.log = output all kernal logs since first boot
|
||||
// cat /var/log/kern.log = output all kernel logs since first boot
|
||||
shell.executeBashCommand(
|
||||
"journalctl -u photonvision.service > "
|
||||
+ tempPath.toAbsolutePath()
|
||||
@@ -467,8 +466,8 @@ public class RequestHandler {
|
||||
}
|
||||
} catch (IOException e) {
|
||||
ctx.status(500);
|
||||
ctx.result("There was an error while exporting journactl logs");
|
||||
logger.error("There was an error while exporting journactl logs", e);
|
||||
ctx.result("There was an error while exporting journalctl logs");
|
||||
logger.error("There was an error while exporting journalctl logs", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,24 +572,9 @@ public class RequestHandler {
|
||||
}
|
||||
|
||||
// verify naming convention
|
||||
// this check will need to be modified if different model types are added
|
||||
|
||||
Pattern modelPattern = Pattern.compile("^[a-zA-Z0-9]+-\\d+-\\d+-yolov[58][a-z]*\\.rknn$");
|
||||
|
||||
Pattern labelsPattern =
|
||||
Pattern.compile("^[a-zA-Z0-9]+-\\d+-\\d+-yolov[58][a-z]*-labels\\.txt$");
|
||||
|
||||
if (!modelPattern.matcher(modelFile.filename()).matches()
|
||||
|| !labelsPattern.matcher(labelsFile.filename()).matches()
|
||||
|| !(modelFile
|
||||
.filename()
|
||||
.substring(0, modelFile.filename().indexOf("-"))
|
||||
.equals(labelsFile.filename().substring(0, labelsFile.filename().indexOf("-"))))) {
|
||||
ctx.status(400);
|
||||
ctx.result("The uploaded files were not named correctly.");
|
||||
logger.error("The uploaded object detection model files were not named correctly.");
|
||||
return;
|
||||
}
|
||||
// throws IllegalArgumentException if the model name is invalid
|
||||
NeuralNetworkModelManager.verifyRKNNNames(modelFile.filename(), labelsFile.filename());
|
||||
|
||||
// TODO move into neural network manager
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
algae
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
note
|
||||
Binary file not shown.
@@ -164,7 +164,7 @@ public class PhotonPipelineResult
|
||||
|
||||
/**
|
||||
* Returns the estimated time the frame was taken, in the Time Sync Server's time base (nt::Now).
|
||||
* This is calculated using the estiamted offset between Time Sync Server time and local time. The
|
||||
* This is calculated using the estimated offset between Time Sync Server time and local time. The
|
||||
* robot shall run a server, so the offset shall be 0.
|
||||
*
|
||||
* @return The timestamp in seconds
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
plugins {
|
||||
id "cpp"
|
||||
id "google-test-test-suite"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.2.1"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.3.1"
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -11,8 +11,8 @@ repositories {
|
||||
|
||||
wpi.maven.useLocal = false
|
||||
wpi.maven.useDevelopment = false
|
||||
wpi.versions.wpilibVersion = "2025.2.1"
|
||||
wpi.versions.wpimathVersion = "2025.2.1"
|
||||
wpi.versions.wpilibVersion = "2025.3.1"
|
||||
wpi.versions.wpimathVersion = "2025.3.1"
|
||||
|
||||
// Define my targets (RoboRIO) and artifacts (deployable files)
|
||||
// This is added by GradleRIO's backing project DeployUtils.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
plugins {
|
||||
id "cpp"
|
||||
id "google-test-test-suite"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.2.1"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.3.1"
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -11,8 +11,8 @@ repositories {
|
||||
|
||||
wpi.maven.useLocal = false
|
||||
wpi.maven.useDevelopment = false
|
||||
wpi.versions.wpilibVersion = "2025.2.1"
|
||||
wpi.versions.wpimathVersion = "2025.2.1"
|
||||
wpi.versions.wpilibVersion = "2025.3.1"
|
||||
wpi.versions.wpimathVersion = "2025.3.1"
|
||||
|
||||
// Define my targets (RoboRIO) and artifacts (deployable files)
|
||||
// This is added by GradleRIO's backing project DeployUtils.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
plugins {
|
||||
id "cpp"
|
||||
id "google-test-test-suite"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.2.1"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.3.1"
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -11,8 +11,8 @@ repositories {
|
||||
|
||||
wpi.maven.useLocal = false
|
||||
wpi.maven.useDevelopment = false
|
||||
wpi.versions.wpilibVersion = "2025.2.1"
|
||||
wpi.versions.wpimathVersion = "2025.2.1"
|
||||
wpi.versions.wpilibVersion = "2025.3.1"
|
||||
wpi.versions.wpimathVersion = "2025.3.1"
|
||||
|
||||
// Define my targets (RoboRIO) and artifacts (deployable files)
|
||||
// This is added by GradleRIO's backing project DeployUtils.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id "java"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.2.1"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.3.1"
|
||||
}
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
@@ -13,8 +13,8 @@ repositories {
|
||||
}
|
||||
|
||||
wpi.maven.useDevelopment = true
|
||||
wpi.versions.wpilibVersion = "2025.2.1"
|
||||
wpi.versions.wpimathVersion = "2025.2.1"
|
||||
wpi.versions.wpilibVersion = "2025.3.1"
|
||||
wpi.versions.wpimathVersion = "2025.3.1"
|
||||
|
||||
|
||||
// Define my targets (RoboRIO) and artifacts (deployable files)
|
||||
|
||||
@@ -143,8 +143,8 @@ public class SwerveDrive {
|
||||
}
|
||||
|
||||
/**
|
||||
* Command the swerve modules to the desired states. Velocites exceeding the maximum speed will be
|
||||
* desaturated (while preserving the ratios between modules).
|
||||
* Command the swerve modules to the desired states. Velocities exceeding the maximum speed will
|
||||
* be desaturated (while preserving the ratios between modules).
|
||||
*
|
||||
* @param openLoop If swerve modules should use feedforward only and ignore velocity feedback
|
||||
* control.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id "java"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.2.1"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.3.1"
|
||||
}
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
@@ -9,8 +9,8 @@ targetCompatibility = JavaVersion.VERSION_17
|
||||
def ROBOT_MAIN_CLASS = "frc.robot.Main"
|
||||
|
||||
wpi.maven.useDevelopment = true
|
||||
wpi.versions.wpilibVersion = "2025.2.1"
|
||||
wpi.versions.wpimathVersion = "2025.2.1"
|
||||
wpi.versions.wpilibVersion = "2025.3.1"
|
||||
wpi.versions.wpimathVersion = "2025.3.1"
|
||||
|
||||
|
||||
// Define my targets (RoboRIO) and artifacts (deployable files)
|
||||
|
||||
@@ -143,8 +143,8 @@ public class SwerveDrive {
|
||||
}
|
||||
|
||||
/**
|
||||
* Command the swerve modules to the desired states. Velocites exceeding the maximum speed will be
|
||||
* desaturated (while preserving the ratios between modules).
|
||||
* Command the swerve modules to the desired states. Velocities exceeding the maximum speed will
|
||||
* be desaturated (while preserving the ratios between modules).
|
||||
*
|
||||
* @param openLoop If swerve modules should use feedforward only and ignore velocity feedback
|
||||
* control.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id "java"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.2.1"
|
||||
id "edu.wpi.first.GradleRIO" version "2025.3.1"
|
||||
}
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
@@ -9,8 +9,8 @@ targetCompatibility = JavaVersion.VERSION_17
|
||||
def ROBOT_MAIN_CLASS = "frc.robot.Main"
|
||||
|
||||
wpi.maven.useDevelopment = true
|
||||
wpi.versions.wpilibVersion = "2025.2.1"
|
||||
wpi.versions.wpimathVersion = "2025.2.1"
|
||||
wpi.versions.wpilibVersion = "2025.3.1"
|
||||
wpi.versions.wpimathVersion = "2025.3.1"
|
||||
|
||||
|
||||
// Define my targets (RoboRIO) and artifacts (deployable files)
|
||||
|
||||
@@ -143,8 +143,8 @@ public class SwerveDrive {
|
||||
}
|
||||
|
||||
/**
|
||||
* Command the swerve modules to the desired states. Velocites exceeding the maximum speed will be
|
||||
* desaturated (while preserving the ratios between modules).
|
||||
* Command the swerve modules to the desired states. Velocities exceeding the maximum speed will
|
||||
* be desaturated (while preserving the ratios between modules).
|
||||
*
|
||||
* @param openLoop If swerve modules should use feedforward only and ignore velocity feedback
|
||||
* control.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[tool.robotpy]
|
||||
|
||||
# Version of robotpy this project depends on
|
||||
robotpy_version = "2025.2.1"
|
||||
robotpy_version = "2025.3.1"
|
||||
|
||||
# Which extra RobotPy components should be installed
|
||||
# -> equivalent to `pip install robotpy[extra1, ...]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[tool.robotpy]
|
||||
|
||||
# Version of robotpy this project depends on
|
||||
robotpy_version = "2025.2.1"
|
||||
robotpy_version = "2025.3.1"
|
||||
|
||||
# Which extra RobotPy components should be installed
|
||||
# -> equivalent to `pip install robotpy[extra1, ...]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[tool.robotpy]
|
||||
|
||||
# Version of robotpy this project depends on
|
||||
robotpy_version = "2025.2.1"
|
||||
robotpy_version = "2025.3.1"
|
||||
|
||||
# Which extra RobotPy components should be installed
|
||||
# -> equivalent to `pip install robotpy[extra1, ...]
|
||||
|
||||
@@ -30,5 +30,9 @@ namespace photon {
|
||||
const char* versionString = "${version}";
|
||||
const char* buildDate = "${date}";
|
||||
const bool isRelease = strncmp(dev_, versionString, strlen(dev_)) != 0;
|
||||
|
||||
// Versions of dependant libraries
|
||||
const char* wpilibTargetVersion = "${wpilibVersion}";
|
||||
const char* opencvTargetVersion = "${opencvVersion}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ public final class PhotonVersion {
|
||||
public static final String buildDate = "${date}";
|
||||
public static final boolean isRelease = !versionString.startsWith("dev");
|
||||
|
||||
// Versions of dependant libraries
|
||||
public static final String wpilibTargetVersion = "${wpilibVersion}";
|
||||
public static final String opencvTargetVersion = "${opencvVersion}";
|
||||
|
||||
public static final boolean versionMatches(String other) {
|
||||
String c = versionString;
|
||||
Pattern p = Pattern.compile("v[0-9]+.[0-9]+.[0-9]+");
|
||||
|
||||
@@ -34,7 +34,11 @@ gradle.allprojects {
|
||||
String date = DateTimeFormatter.ofPattern("yyyy-M-d hh:mm:ss").format(LocalDateTime.now())
|
||||
File versionFileOut = new File(path.toAbsolutePath().toString())
|
||||
versionFileOut.delete()
|
||||
def read = versionFileIn.text.replace('${version}', version).replace('${date}', date)
|
||||
def read = versionFileIn.text.replace('${version}', version)
|
||||
.replace('${date}', date)
|
||||
.replace('${wpilibVersion}', wpilibVersion)
|
||||
// Note that OpenCV is usually {VERSION}-{some suffix}, we just want the first bit
|
||||
.replace('${opencvVersion}', openCVversion.split("-").first())
|
||||
if (!versionFileOut.parentFile.exists()) versionFileOut.parentFile.mkdirs()
|
||||
if (!versionFileOut.exists()) versionFileOut.createNewFile()
|
||||
versionFileOut.write(read)
|
||||
|
||||
Reference in New Issue
Block a user