Compare commits

...

244 Commits

Author SHA1 Message Date
Tyler Veness
2f310a748c [wpimath] Fix DCMotor.getSpeed() (#5061)
This bug didn't occur in C++ because the units system caught it at
compile time.
2023-02-05 13:21:16 -08:00
Nick Hadley
b43ec87f57 [wpilib] ElevatorSim: Fix WouldHitLimit methods (#5057) 2023-02-05 11:58:53 -08:00
Peter Johnson
19267bef0c [ntcore] Output warning on property set on unpublished topic (#5059)
Previously this was a debug-level message. This can primarily impact
users who call SetPersistent() on an entry before calling SetDefault().
2023-02-05 11:57:29 -08:00
Peter Johnson
84cbd48d84 [ntcore] Handle excludeSelf on SetDefault (#5058) 2023-02-05 11:57:09 -08:00
Peter Johnson
1f35750865 [cameraserver] Add GetInstance() to all functions (#5054)
GetInstance() is required to start the event listener that creates the
network table entries.

This is a C++ only change; Java uses static's and thus doesn't need this.

The right fix is to implement cscore's AddListener() immediate notification,
but that's much too invasive of a change to do this year.

This fixes the common use cases, but doesn't fix all cases, as e.g. creating
a UsbCamera manually before calling any CameraServer functions will still
have the issue, but there's an easy workaround--call
CameraServer::SetSize() prior to creating any cameras.
2023-02-05 11:28:53 -08:00
Peter Johnson
8230fc631d [wpilib] Revert throw on nonexistent SimDevice name in SimDeviceSim (#5053)
This breaks current vendor use of SimDeviceSim.

This reverts commit d991f6e435 (#5041).
2023-02-05 11:27:55 -08:00
Peter Johnson
b879a6f8c6 [wpinet] WebSocket: When Close() is called, call closed immediately (#5047)
This provides the closed callback with the real reason for the
connection being closed.  Keep closed from being called twice by adding
a check in SetClosed().
2023-02-03 22:59:19 -08:00
Peter Johnson
49459d3e45 [ntcore] Change wire timeout to fixed 1 second (#5048)
Previously the timeout was 10 times the update rate, so with low update
rates it could be as small as 50 ms, causing spurious disconnects when
large or many topics were published.
2023-02-03 22:05:41 -08:00
Jordan McMichael
4079eabe9b [wpimath] Discard stale pose estimates (#5045)
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2023-02-03 22:04:30 -08:00
Peter Johnson
fe5d226a19 [glass] Fix option for debug-level NT logging (#5049) 2023-02-03 22:03:45 -08:00
Peter Johnson
b7535252c2 [ntcore] Don't leak buffers in rare WS shutdown case (#5046)
If the request called the callback after the WebSocket had been
destroyed, the buffers were leaked.
2023-02-03 21:56:35 -08:00
Peter Johnson
b61ac6db33 [ntcore] Add client disconnect function (#5022)
As setServer doesn't disconnect, it's useful to have a function that
disconnects without needing to completely stop the client.
2023-02-03 15:28:00 -08:00
Ryan Blue
7b828ce84f [wpimath] Add nearest to Pose2d and Translation2d (#4882)
Co-authored-by: David Vo <auscompgeek@users.noreply.github.com>
2023-02-03 15:27:16 -08:00
Michael Leong
08a536291b [examples] Improvements to Elevator Simulation Example (#4937)
Co-authored-by: Abhay Shukla <105139789+aboombadev@users.noreply.github.com>
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
Co-authored-by: Ryan Blue <ryanzblue@gmail.com>
2023-02-03 15:23:06 -08:00
Peter Johnson
193a10d020 [wpigui] Limit frame rate to 120 fps by default (#5030)
Limiting with vsync is apparently unreliable on a number of systems;
this resulted in high CPU/GPU usage.

Also add current actual frame rate to about dialog of GUI tools.
2023-02-03 15:21:52 -08:00
sciencewhiz
7867bbde0e [wpilib] Clarify DS functions provided by FMS (NFC) (#5043) 2023-02-03 15:21:12 -08:00
Peter Johnson
fa7c01b598 [glass] Add option for debug-level NT logging (#5007) 2023-02-03 15:20:03 -08:00
truher
2b81610248 [wpiutil] Add msgpack to datalog Python example (#5032) 2023-02-03 15:19:44 -08:00
Tyler Veness
a4a369b8da CONTRIBUTING.md: Add unicodeit CLI to math docs guidelines (#5031) 2023-02-03 15:19:01 -08:00
Ryan Blue
d991f6e435 [wpilib] Throw on nonexistent SimDevice name in SimDeviceSim constructor (#5041)
Previously this would just create a object that was otherwise non-functional.
2023-02-03 15:18:31 -08:00
Peter Johnson
a27a047ae8 [hal] Check for null in getSimDeviceName JNI (#5038) 2023-02-01 23:25:55 -08:00
Starlight220
2f96cae31a [examples] Hatchbots: Add telemetry (#5011) 2023-01-31 23:44:18 -08:00
Peter Johnson
83ef8f9658 [simulation] GUI: Fix buffer overflow in joystick axes copy (#5036)
This was using an incorrect sizeof which would copy excessive data and
overwrite the button data.
2023-01-31 23:40:22 -08:00
Starlight220
4054893669 [commands] Fix C++ Select() factory (#5024)
Update example to use it.
2023-01-29 07:23:12 -08:00
Sriman Achanta
f75acd11ce [commands] Use Timer.restart() (#5023) 2023-01-29 07:21:07 -08:00
Tyler Veness
8bf67b1b33 [wpimath] PIDController::Calculate(double, double): update setpoint flag (#5021)
Fixes #5020.
2023-01-29 07:18:48 -08:00
Peter Johnson
49bb1358d8 [wpiutil] MemoryBuffer: Fix GetMemoryBufferForStream (#5017)
This would previously just write past the end of the buffer, smashing
the stack.  It's only called in the case when a non-file or block device
is used as the file.
2023-01-28 22:38:34 -08:00
Peter Johnson
9c4c07c0f9 [wpiutil] Remove NDEBUG check for debug-level logging (#5018)
This adds minimal overhead but is useful when debugging release
binaries.
2023-01-28 14:13:58 -08:00
Peter Johnson
1a47cc2e86 [ntcore] Use full handle when subscribing (#5013)
Just using the index is insufficient because of Subscriber overlap with
MultiSubscriber.
2023-01-27 09:14:38 -08:00
Ryan Blue
7cd30cffbc Ignore networktables.json (#5006) 2023-01-26 09:21:58 -08:00
DeltaDizzy
92aecab2ef [commands] Command controllers are not subclasses (NFC) (#5000) 2023-01-25 15:20:29 -08:00
Peter Johnson
8785bba080 [ntcore] Special-case default timestamps (#5003)
Previously, a setDefault() on the server could override a client doing a
real set() if the time offset between client and server was negative,
resulting in a negative timestamp from the client.  This is a not
uncommon situation with robot code, as the robot code always starts at
time 0, so any clients that set values earlier (in real time) would have
negative timestamps.

Also improve special casing of 0 in the transmit side to make sure a
normal timestamp will never get sent as 0.
2023-01-25 11:36:13 -08:00
Peter Johnson
9e5b7b8040 [ntcore] Handle topicsonly followed by value subscribe (#4991)
Previously this wouldn't send the last value on the value subscribe if a
topics only subscription already existed.

Also start adding server implementation unit tests.
2023-01-24 21:50:38 -08:00
Sriman Achanta
917906530a [wpilib] Add Timer::Restart() (#4963) 2023-01-23 14:50:46 -08:00
Tyler Veness
00aa66e4fd [wpimath] Remove extraneous assignments from DiscretizeAB() (#4967) 2023-01-23 09:46:12 -08:00
Starlight220
893320544a [examples] C++ RamseteCommand: Fix units (#4954)
Also fix the memory leak in the command-based auto.
2023-01-22 11:20:35 -08:00
Evan
b95d0e060d [wpilib] XboxController: Fix docs discrepancy (NFC) (#4993)
Comments for leftTrigger said they attach to the right trigger of the controller.
2023-01-21 22:09:55 -08:00
Peter Johnson
008232b43c [ntcore] Write empty persistent file if none found (#4996)
This avoids the warning appearing on every startup when persistent
values aren't used.

Also add note to message saying it can be ignored if persistent values
aren't expected.
2023-01-21 22:09:24 -08:00
Starlight220
522be348f4 [examples] Rewrite tags (NFC) (#4961) 2023-01-21 15:24:10 -08:00
Tyler Veness
d48a83dee2 [wpimath] Update Wikipedia links for quaternion to Euler angle conversion (NFC) (#4995) 2023-01-21 15:16:35 -08:00
Peter Johnson
504fa22143 [wpimath] Workaround intellisense Eigen issue (#4992)
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2023-01-21 15:16:12 -08:00
Brady Schindler
b2b25bf09f [commands] Fix docs inconsistency for toggleOnFalse(Command) (NFC) (#4978) 2023-01-20 23:47:37 -08:00
Thad House
ce3dc4eb3b [hal] Properly use control word that is in sync with DS data (#4989) 2023-01-20 23:46:56 -08:00
Thad House
1ea48caa7d [wpilib] Fix C++ ADXRS450 and Java SPI gyro defs (#4988) 2023-01-20 13:25:13 -08:00
Thad House
fb101925a7 [build] Include wpimathjni in commands binaries (#4981) 2023-01-19 22:05:32 -08:00
Thad House
657951f6dd [starter] Add a process starter for use by the installer for launching tools (#4931) 2023-01-19 20:09:35 -08:00
Tyler Veness
a60ca9d71c [examples] Update AprilTag field load API usage (#4975) 2023-01-19 17:01:17 -08:00
Tyler Veness
f8a45f1558 [wpimath] Remove print statements from tests (#4977) 2023-01-19 17:00:35 -08:00
sciencewhiz
ecba8b99a8 [examples] Fix swapped arguments in MecanumControllerCommand example (#4976) 2023-01-18 21:25:49 -08:00
Berke Sinan Yetkin
e95e88fdf9 [examples] Add comment to drivedistanceoffboard example (#4877)
Co-authored-by: Ryan Blue <ryanzblue@gmail.com>
2023-01-18 20:46:41 -08:00
CarloWoolsey
371d15dec3 [examples] Add Computer Vision Pose Estimation and Latency Compensation Example (#4901)
This PR updates the existing differentialdriveposeestimator example to include computer vision pose estimation and latency compensation.

The example generates a simulated cameraToTarget transformation, which is then fed into ComputerVisionUtil.objectToRobotPose() to compute the robot's field-relative position exclusively from vision measurements. The vision measurements are applied through DifferentialDrivePoseEstimator.addVisionMeasurement().

The updated example constructs an AprilTagFieldLayout from JSON. This requires a deploy directory, something which isn't currently supported in wpilibjExamples and wpilibcExamples.
2023-01-18 20:46:05 -08:00
Peter Johnson
cb9b8938af [sim] Enable docking in the GUI (#4960) 2023-01-18 20:42:58 -08:00
Brennen Puth
3b084ecbe0 [apriltag] AprilTagFieldLayout: Improve API shape for loading builtin JSONs (#4949) 2023-01-18 20:42:39 -08:00
Matt
27ba096ea1 [wpilib] Fix MOI calculation error in SingleJointedArmSim (#4968)
Previous calculation derivation mixed up length and distance to CG.
2023-01-18 20:40:39 -08:00
Jordan McMichael
42c997a3c4 [wpimath] Fix Pose3d exponential and clean up Pose3d logarithm (#4970)
Implementation based on this paper: https://ethaneade.org/lie.pdf
2023-01-18 20:38:03 -08:00
Tyler Veness
5f1a025f27 [wpilibj] Fix typo in MecanumDrive docs (NFC) (#4969) 2023-01-18 13:47:27 -08:00
Tyler Veness
0ebf79b54c [wpimath] Fix typo in Pose3d::Exp() docs (NFC) (#4966) 2023-01-18 13:46:45 -08:00
Oliver W
a8c465f3fb [wpimath] HolonomicDriveController: Add getters for the controllers (#4948) 2023-01-16 08:33:15 -08:00
Starlight220
a7b1ab683d [wpilibc] Add unit test for fast deconstruction of GenericHID (#4953) 2023-01-16 08:28:06 -08:00
Starlight220
bd6479dc29 [build] Add Spotless for JSON (#4956) 2023-01-16 08:26:46 -08:00
Thad House
5cb0340a8c [hal, wpilib] Load joystick values upon code initialization (#4950)
During HAL_Initialize, wait up to 100ms for a DS packet to be received. Then in RobotBase, right after calling HAL_Initialize, call each language's RefreshData function to force a high level DS update. If the DS is connected, will get joystick data. If there is no data, nothing different will happen, but in that case there's no joysticks anyway.
2023-01-15 16:36:44 -08:00
sciencewhiz
ab0e8c37a7 [readme] Update build requirements (NFC) (#4947)
Change to adoptium, and add Xcode min version
2023-01-15 15:19:24 -08:00
Tyler Veness
b74ac1c645 [build] Add apriltag to C++ cmake example builds (#4944)
This fixes compilation of the apriltag vision example on my machine.
2023-01-13 23:24:14 -08:00
Doug Wegscheid
cf1a411acf [examples] Add example programs for AprilTags detection (#4932)
Co-authored-by: Peter Johnson <johnson.peter@gmail.com>
2023-01-13 23:08:45 -08:00
sciencewhiz
1e05b21ab5 [wpimath] Fix PID atSetpoint to not return true prematurely (#4906)
Wait until setpoint and measurement have been set.
2023-01-13 22:26:30 -08:00
ohowe
e5a6197633 [wpimath] Fix SwerveDriveKinematics not initializing a new array each time (#4942)
This is problematic if you call it twice before utilizing the result.
2023-01-13 20:16:50 -08:00
Peter Johnson
039edcc23f [ntcore] Queue current value on subscriber creation (#4938)
This fixes a potential race condition in code that only uses readQueue.
2023-01-13 20:07:24 -08:00
Matt
f7f19207e0 [wpimath] Allow multiple vision measurements from same timestamp (#4917)
Co-authored-by: Jordan McMichael <jlmcmchl@gmail.com>
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2023-01-11 23:04:30 -08:00
Starlight220
befd12911c [commands] Delete UB-causing rvalue variants of CommandPtr methods (#4923)
Co-authored-by: Ryan Blue <ryanzblue@gmail.com>
2023-01-11 22:53:04 -08:00
superpenguin612
34519de60a [commands] Fix spacing in command composition exception (#4924) 2023-01-11 11:46:33 -08:00
Ryan Blue
dc4355c031 [hal] Add handle constructor and name getters for sim devices (#4925) 2023-01-11 11:45:15 -08:00
Ryan Blue
53d8d33bca [hal, wpilibj] Add missing distance per pulse functions to EncoderSim (#4928)
Also fix C++ and Java EncoderSim.setDistancePerPulse() not propagating value to SimEncoderData.
2023-01-11 11:43:56 -08:00
bovlb
530ae40614 [apriltag] Explain what April tag poses represent (NFC) (#4930) 2023-01-11 11:42:30 -08:00
Starlight220
79f565191e [examples] DigitalCommunication, I2CCommunication: Add tests (#4865) 2023-01-08 16:33:53 -08:00
Starlight220
2cd9be413f [wpilib, examples] Cleanup PotentiometerPID, Ultrasonic, UltrasonicPID examples (#4893)
Fix C++ Ultrasonic to return correct units.
2023-01-08 16:33:07 -08:00
Matt
babb0c1fcf [apriltag] Add 2023 field layout JSON (#4912) 2023-01-08 16:30:45 -08:00
ohowe
330ba45f9c [wpimath] Fix swerve kinematics util classes equals function (#4907)
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2023-01-08 16:29:35 -08:00
sciencewhiz
51272ef6b3 [fieldImages] Add 2023 field (#4915) 2023-01-08 16:28:25 -08:00
Starlight220
0d105ab771 [commands] Deduplicate command test utils (#4897) 2023-01-08 07:44:53 -08:00
Tyler Veness
cf4235ea36 [wpiutil] Guard MSVC pragma in SymbolExports.h (#4911)
MinGW gives an unknown pragma warning on Windows.
2023-01-07 16:41:40 -08:00
Ryan Blue
2d4b7b9147 [build] Update opencv version in opencv.gradle (#4909) 2023-01-06 18:09:58 -08:00
Peter Johnson
aec6f3d506 [ntcore] Fix client flush behavior (#4903)
We need to ignore per-publisher send periods when flushing.

Also fix NT4 client to use flush async's (same as NT3 client).
2023-01-04 23:36:26 -08:00
Peter Johnson
bfe346c76a [build] Fix cmake java resources (#4898)
These need to be relative paths, but GLOB generates absolute paths by
default.
2023-01-04 08:16:47 -08:00
Ryan Blue
83f1860047 [wpilib] Add/update documentation to PneumaticBase and subclasses (NFC) (#4881)
Co-authored-by: Starlight220 <53231611+Starlight220@users.noreply.github.com>
2023-01-02 10:23:59 -08:00
Dustin Spicuzza
9872e676d8 [commands] Make Subsystem destructor virtual (#4892)
C++ best practice if there are other functions that are virtual.
2023-01-02 07:59:12 -08:00
Dustin Spicuzza
25db20e49d [hal] Fix segfault in various HAL functions (#4891) 2023-01-01 23:19:04 -08:00
ohowe
b0c6724eed [glass] Add hamburger menu icon to titlebars (#4874)
This does the same thing as right clicking, but provides a visual indicator.
The icon disappears if the window is too small or docked (right click keeps working).
2023-01-01 20:05:09 -08:00
Tyler Veness
f0fa8205ac Add missing compiler flags and fix warnings (#4889)
This makes the build fail on warnings. It caught two out-of-bounds reads
and a deprecation warning.
2023-01-01 08:14:19 -08:00
Dustin Spicuzza
42fc4cb6bc [wpiutil] SafeThread: Provide start/stop hooks (#4880)
Co-authored-by: David Vo <auscompgeek@users.noreply.github.com>
2022-12-31 15:40:45 -08:00
Sriman Achanta
cc166c98d2 [templates] Add Command-based skeleton template (#4861)
Co-authored-by: Starlight220 <53231611+Starlight220@users.noreply.github.com>
2022-12-31 13:18:43 -08:00
Thad House
3f51f10ad3 [build] Update to 2023v3 image (#4886) 2022-12-31 13:16:41 -08:00
Peter Johnson
1562eae74a [ntcore] Refactor meta-topic decoding from glass (#4809) 2022-12-31 12:01:51 -08:00
Thad House
b632b288a3 Fix usages of std::min and std::max to be windows safe (#4887) 2022-12-31 12:00:45 -08:00
Dustin Spicuzza
c11bd2720f [wpilibc] Add internal function to reset Shuffleboard instance (#4884)
Needed for RobotPy test framework.
2022-12-31 10:43:48 -08:00
Peter Johnson
f1151d375f [ntcore] Add method to get server time offset (#4847)
Also exposes this as an event signaled when the offset is updated due to
a ping response from the server.
2022-12-30 20:15:57 -08:00
Ryan Blue
fe1b62647f [hal,wpilib] Update documentation for getComments (NFC) (#4879) 2022-12-30 04:15:37 -08:00
Thad House
c49a45abbd [build] Fix examples linking in incorrect jni library (#4873) 2022-12-29 08:07:59 -06:00
Griffin Della Grotte
bc3d01a721 [build] Add platform check to doxygen plugin (#4862)
This allows the build to work on aarch64 and other platforms without downloadable doxygen binaries.
2022-12-27 22:43:21 -06:00
Ryan Blue
bc473240ae Add Jetbrains Fleet folder to .gitignore (#4872) 2022-12-27 22:41:13 -06:00
Tyler Veness
2121bd5fb8 [wpimath] Remove RKF45 (#4870)
RKDP is strictly better in terms of accuracy per unit of work. We used
RKF45 for sim physics in the 2021 season, but we transitioned to RKDP
before the 2022 season.
2022-12-27 19:29:59 -06:00
Griffin Della Grotte
835f8470d6 [build] Fix roborio cross-compiler on arm hosts (#4864) 2022-12-27 10:26:53 -08:00
Peter Johnson
6cfe5de00d [ntcore] Don't deadlock server on early destroy (#4863)
It was possible to deadlock on instance destroy if the server had started
but had not yet fully initialized its handles.
2022-12-27 10:25:48 -08:00
Ryan Blue
2ac41f3edc [hal, wpilib] Add RobotController.getComments() (#4463) 2022-12-26 11:39:51 -08:00
Sriman Achanta
26bdbf3d41 Java optimization and formatting fixes (#4857) 2022-12-26 11:37:53 -08:00
Sriman Achanta
92149efa11 Spelling and grammar cleanups (#4849) 2022-12-26 11:32:13 -08:00
Ryan Blue
176fddeb4c [commands] Add functions to HID classes to allow use of axes as BooleanEvents/Triggers (#4762) 2022-12-26 11:29:14 -08:00
Starlight220
87a34af367 [templates] Add bindings to command-based template (#4838) 2022-12-26 11:28:06 -08:00
Starlight220
4534e75787 [examples] Remove redundant MotorControl example (#4837)
The MotorControlEncoder had the exact same content, with the addition of an encoder. No point in having both examples.
2022-12-26 11:27:20 -08:00
Griffin Della Grotte
1cbebaa2f7 [commands] Remove final semicolon from test macro definition (#4859) 2022-12-26 00:14:59 -06:00
Ryan Blue
6efb9ee405 [commands] Add constructor for SwerveControllerCommand that takes a HolonomicDriveController (#4785)
Also adds copy and move constructors to HolonomicDriveController.
2022-12-25 18:48:27 -08:00
Thad House
1e7fcd5637 [cscore] Change run loop functions to not be mac specific (#4854) 2022-12-25 13:37:59 -06:00
Peter Johnson
1f940e2b60 [apriltag] Add C++ wrappers, rewrite Java/JNI to match (#4842)
This provides a consistent class-based interface to the underlying C
library from both C++ and Java.

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2022-12-25 08:15:43 -08:00
Starlight220
a6d127aedf [build] Add missing task dependency in wpilibjExamples (#4852) 2022-12-25 07:37:10 -08:00
Thad House
b893b3d6d3 [cscore] Add support for USB cameras on macOS (#4846)
The main restriction is there must be an event loop running on the main thread.
No special action is required for GUI applications, but for non-GUI applications, a
RunOsxRunLoop() function is provided that needs to be called from the main thread.
2022-12-25 07:36:00 -08:00
Peter Johnson
1696a490fa [glass] Add support for alternate NT ports (#4848) 2022-12-24 20:18:07 -06:00
Peter Johnson
40a22d69bc [glass] Add support for alternate NT ports (#4848) 2022-12-24 20:17:45 -06:00
Sriman Achanta
e84dbfede0 [wpilib] GenericHID: Add rumble both option (#4843) 2022-12-24 11:28:52 -08:00
CarloWoolsey
8aa9dbfa90 [examples] Link apriltag package in examples build.gradle (#4845) 2022-12-23 20:04:59 -06:00
Ryan Blue
eda2fa8a17 [build] Update Spotless (#4840)
Removes JVM args workaround needed for old version of spotless to run on JDK 17.

2 files had formatting updates.
2022-12-21 08:54:41 -06:00
sciencewhiz
d20594db0d Fix typos (#4839) 2022-12-21 08:53:00 -06:00
Ryan Blue
dd8ecfdd54 [commands] Fix typo in waitUntil docs (NFC) (#4841) 2022-12-21 08:24:25 -06:00
Thad House
17ceebfff4 [apriltag] Clean up apriltag JNI (#4823) 2022-12-19 21:06:36 -08:00
Ryan Blue
8b74ab389d [examples] RapidReactCommandBot: Fix array indices (#4833) 2022-12-18 16:54:37 -06:00
David Vo
1aad3489c2 [sim] Implement PD total current and power (#4830) 2022-12-18 08:00:31 -06:00
Ashray._.g
2744991771 [wpimath] Fix docs in SwerveModulePosition (#4825)
Changed m/s and speeds to m and distance.
2022-12-16 05:48:20 -08:00
Peter Johnson
ffbf6a1fa2 [commands] Disable regularly failing unit test (#4824) 2022-12-15 21:53:08 -08:00
Starlight220
fbabd0ef15 [commands] Enhance Command Sendable implementations (#4822) 2022-12-15 18:28:52 -08:00
Thad House
7713f68772 [hal] Use atomic rather then mutex for DS Data updates (#4787)
Using an atomic here means we are never going against a lock that is touchable from user code. That should make reading the DS data from the DS callback even safer.
2022-12-15 18:27:52 -08:00
Starlight220
701995d6cc [examples] Update Command-based starter project (#4778) 2022-12-15 09:40:14 -08:00
Thad House
bf7068ac27 [wpilibc] Add missing PPS implementation for C++ (#4821) 2022-12-15 09:28:42 -08:00
Peter Johnson
aae0f52ca6 [ntcore] NetworkTable: fix visibility of get/set value (#4820) 2022-12-14 19:43:53 -08:00
Thad House
ee02fb7ba7 [hal] Add support for Pulse-Per-Second signal (#4819) 2022-12-14 18:15:34 -08:00
Thad House
518916ba02 [wpilib] Fix DS mode thread event being manual reset accidentally (#4818)
These need to be automatic reset.
2022-12-14 10:17:13 -08:00
Thad House
3997c6635b [hal] Update to new image, use new TCP notify callback and new duty cycle API (#4774) 2022-12-13 22:37:42 -08:00
Michael Leong
cc8675a4e5 [examples] Add comment on how to view elevator sim (NFC) (#4482) 2022-12-13 22:34:00 -08:00
Peter Johnson
fb2c170b6e [ntcore] Simplify local startup (#4803)
The current approach was slightly more efficient but didn't correctly
handle meta-topic publishing for topics published before the server was
started.
2022-12-13 22:31:44 -08:00
shueja-personal
7ba8a9ee1f [wpimath] ProfiledPIDController: Add to SendableRegistry (#4656)
Co-authored-by: Ryan Blue <ryanzblue@gmail.com>
2022-12-13 22:31:06 -08:00
Sriman Achanta
c569d8e523 [wpilib] Joystick.getMagnitude(): use hypot() function (#4816) 2022-12-13 22:29:09 -08:00
bovlb
2a5e89fa97 [apriltag] Improve description of pose coordinates (NFC) (#4810)
The "bottom-left" language appears to be based on [a 2-D diagram  on another page](https://docs.wpilib.org/en/stable/docs/software/advanced-controls/geometry/coordinate-systems.html) and doesn't seem very helpful here.
2022-12-13 22:27:43 -08:00
Drew Williams
cc003c6c38 [apriltag] Fix AprilTagFieldLayout JSON name (#4814) 2022-12-13 22:27:08 -08:00
Sriman Achanta
5522916123 [commands] CommandXBoxController bumper documentation fix (NFC) (#4815) 2022-12-13 22:26:35 -08:00
Peter Johnson
967b30de3a [glass] Fix NT view UpdateClients() bug (#4808)
If UpdateClients() was called in the same update batch as an entry
removal, it could crash in GetEntry() due to a null entry caused by
deletion before a removal erase pass was made.
2022-12-12 23:38:36 -08:00
Tyler Veness
3270d4fc86 [wpimath] Rewrite pose estimator docs (#4807) 2022-12-12 20:30:52 -08:00
Thad House
be39678447 [apriltag] Add test to ensure apriltagjni loads (#4805) 2022-12-12 20:30:25 -08:00
Starlight220
61c75deb2a [commands] Test no-op behavior of scheduling a scheduled command (#4806) 2022-12-12 20:29:47 -08:00
Peter Johnson
a865f48e96 [ntcore] Pass pub/sub options as a unified PubSubOptions struct (#4794)
In Java, PubSubOption is still used for passing options, but this
simplifies C++ use substantially, as it allows aggregate construction.
2022-12-12 19:28:15 -08:00
Ryan Blue
f66a667321 [commands] Fix incorrect Trigger docs (NFC) (#4792) 2022-12-12 15:38:29 -08:00
Peter Johnson
f8d4e9866e [ntcore] Clean up ntcore_test.h (#4804)
The RPC functions are no longer implemented.

Also make header C-safe.
2022-12-12 15:36:54 -08:00
Tyler Veness
7e84ea891f [wpimath] Fix ComputerVisionUtil transform example in parameter docs (NFC) (#4800) 2022-12-11 22:44:08 -08:00
Charlie
da3ec1be10 [wpimath] Change terminology for ArmFeedforward gravity gain (NFC) (#4791) 2022-12-11 22:43:44 -08:00
Ryan Blue
944dd7265d [wpilibc] Add C++ Notifier error handling, update java notifier error message (#4795) 2022-12-11 22:42:22 -08:00
Ryan Blue
6948cea67a [wpiutil] Fix MemoryBuffer initialization (#4797)
Dereferencing an end iterator is undefined behavior and causes a failed assertion when running in Windows debug.
2022-12-11 07:39:04 -08:00
Ryan Blue
a31459bce6 [wpiutil] Fix UnescapeCString overflow when inputSize < 2 (#4796)
Add tests for empty, small, and string without escapes.
2022-12-11 07:36:38 -08:00
ohowe
4a0ad6b48c [wpimath] Rotation2d: Add reference to angleModulus in docs (NFC) (#4786) 2022-12-09 23:35:15 -08:00
Peter Johnson
e6552d272e [ntcore] Remove table multi-subscriber (#4789)
This wasn't well thought out, and leaks horribly in Java.

Reverts part of #4640.
2022-12-09 21:25:29 -08:00
Ryan Blue
bde383f763 [hal] Replace const char* with std::string_view in Driver Station sim functions (#4532) 2022-12-09 13:10:23 -08:00
Ryan Blue
5a52b51443 [hal] Add RobotController.getSerialNumber() (#4783) 2022-12-08 21:58:55 -08:00
Thad House
69a66ec5ec [wpilib] Fix multiple motor safety issues (#4784)
Java was missing the motor safety thread entirely
C++ accidentally used a manual reset event, causing the motor safety thread to spin.
C++ PWMMotorController would not feed the watch kitty.
Both languages would call feed() from the StopMotor call, causing some ping ponging.
2022-12-08 19:47:47 -08:00
sciencewhiz
989c9fb29a [wpimath] Revert Rotation2D change that limits angles (#4781)
Reverts "[wpimath] Constrain Rotation2d range to -pi to pi (#4611)"
This reverts commit d1d458db2b.

This broke multiple teams code in beta. It is also easier to limit the angle externally, then reconstruct a larger angle that got limited. This additionally adds comments to clarify the behavior and retains tests that were added in the reverted commit, and fixes a javadoc comment angle reference.
2022-12-08 18:10:44 -08:00
Peter Johnson
0f5b08ec69 [wpigui] Update imgui to 1.89.1+ (#4780) 2022-12-08 00:24:27 -08:00
Starlight220
fba191099c [examples] AddressableLED: Add unit test (#4779) 2022-12-07 21:47:47 -08:00
Peter Johnson
b390cad095 [wpilibj] Consistently use ErrorMessages.requireNonNullParam (#4776)
Also remove wpilibj version of ErrorMessages and consistently use static import.
2022-12-07 21:46:26 -08:00
Peter Johnson
b9772214d9 [wpilib] Sendable: Don't call setter for getter changes 2022-12-07 21:29:51 -08:00
Peter Johnson
342c375a71 [ntcore] Add subscriber option to exclude single publisher
This enables limited self-exclusion for entries seeing their own changes
or similarly with a pub/sub pair.
2022-12-07 21:29:51 -08:00
Peter Johnson
b0e4053087 [ntcore] Use int for options instead of double
Periodic time is stored in milliseconds.
2022-12-07 21:29:51 -08:00
Peter Johnson
f3e666b7bb [cscore] Convert YUYV and UYVY directly to grayscale (#4777)
This is significantly more efficient than converting by way of BGR.
2022-12-07 10:36:40 -08:00
William Toth
b300518bd1 [hal] Add CAN Stream API to Java through JNI bindings (#4193)
The CAN Stream API allows defining an buffer to receive an
arbitrary set of CAN messages, based on an ID and a mask. Messages
are added to this queue separate of other CAN APIs. This means the
messages can be receive without impacting other APIs such as
vendor APIs.

This enables things like detection of what devices are on the
bus, or custom decoding, without using vendor APIs.

Co-authored-by: Thad House <thadhouse1@gmail.com>
2022-12-06 21:58:09 -08:00
Peter Johnson
be27171236 [wpilibj] Shuffleboard: Check for null sendable (#4772)
Adding a null sendable to a container could result in a delayed NullPointerException.
2022-12-06 21:14:09 -08:00
Starlight220
4bbdbdfb48 [commands] Move GroupedCommands to CommandScheduler (#4728)
Move the command group checking functionality from CommandGroupBase into CommandScheduler.
Update references to grouping as composition for clarity (because explicitly grouping isn't the only way to do it).
Deprecate the static factory methods parallel, race, and deadline in CommandGroupBase in favor of the identical ones in Commands.
2022-12-06 21:13:31 -08:00
Tyler Veness
f18fd41ac3 [wpimath] Remove broken and obsoleted ComputerVisionUtil functions (#4775)
The ComputerVisionUtil class was added before AprilTag support was
announced. Now that it has, the functions for estimating a pose from
range and yaw are no longer needed; it's just better to get the pose
directly from the AprilTag.

The coordinate system on some function arguments was confusing or didn't
match the NWU convention the rest of the library uses. It's easier to
remove the functions now and add them back after they're fixed since the
fixes aren't trivial.

The range function was removed because it uses pitch and yaw in the
camera's spherical coordinate system, which is obsoleted by AprilTags.
AprilTags give you a 6DOF pose directly, so range can be obtained via
Pose2d.getTranslation().getDistance().

Fixes #4757.
2022-12-06 21:11:27 -08:00
Peter Johnson
2d0faecf4f [glass] DataSource: Add spinlock to protect value (#4771)
A lock is needed here due to HAL async callbacks.
2022-12-06 21:09:14 -08:00
Peter Johnson
348bd107fc [hal] Add CANManufacturer for The Thrifty Bot (#4773) 2022-12-06 21:08:45 -08:00
Starlight220
3149dc64b8 [examples] HatchbotInlined: Use Subsystem factories (#4765) 2022-12-06 15:10:39 -08:00
Jordan McMichael
8618dd4160 [glass, wpilib] Replace remaining references to Speed Controller with Motor Controller (#4769) 2022-12-05 20:06:43 -08:00
Peter Johnson
72e21a1ed1 [apriltag] Use wpilibsuite fork of apriltag (#4764) 2022-12-05 19:59:24 -08:00
Ryan Blue
eab0d929e6 [commands] CommandGenericHID POV methods: Fix docs (NFC) (#4760) 2022-12-05 13:46:46 -08:00
Ryan Blue
6789869663 [wpilib] Call set(0) rather than disable for stopMotor (#4763)
For PWM motor controllers, this is going to be the fastest way to stop the motor,
as disabling the PWM output will have a small delay due to the motor controller
needing to detect the missing signal.

Note Set(0) is not a safe approach for CAN motor controllers, which may have closed
loop operation, non-% output set() calls, etc.
2022-12-05 13:33:54 -08:00
Dustin Spicuzza
c9dea2968d [cscore] Emit warning that USB Camera isn't supported on OSX (#4766)
Reduces user confusion, as right now there's really no indication that something is wrong other than there's no camera,
2022-12-05 13:30:13 -08:00
sciencewhiz
8f402645f5 [commands] Fix PIDSubsystem setSetpoint behavior (#4759)
No longer stores a temporary setpoint in PIDSubsystem, instead
immediately sending to PIDController. This fixes an issue where the
setpoint didn't take effect until the Subsystem Periodic method ran, and
could cause commands to finish early if they were scheduled after the
subsystem periodic method ran because it used the old setpoint.
2022-12-03 11:32:46 -08:00
Tyler Veness
f24ad1d715 [build] Upgrade to googletest 1.12.1 (#4752)
This fixes GCC 12 warnings for googletest internals.
2022-12-03 11:32:08 -08:00
Jaci Brunning
ff88756864 [wpimath] Add new DCMotor functions for alternative calculations and reduction calculation (#4749) 2022-12-03 09:47:16 -08:00
superpenguin612
f58873db8e [wpimath] Remove extra terms in matrix for pose estimator docs (#4756) 2022-12-03 09:46:34 -08:00
superpenguin612
37e969b41a [wpimath] Add constructors to pose estimators with default standard deviations (#4754) 2022-12-03 09:46:10 -08:00
Tyler Veness
13cdc29382 [ci] Rename comment command from "/wpiformat" to "/format" (#4755)
It now runs more than just wpiformat.
2022-12-02 18:24:06 -08:00
Tyler Veness
6e23985ae6 [examples] Add main include directory to test builds (#4751)
This fixes the following compilation errors:
```
/home/tav/frc/wpilib/allwpilib/wpilibcExamples/src/main/cpp/examples/UnitTest/cpp/subsystems/Intake.cpp:5:10: fatal error: subsystems/Intake.h: No such file or directory
    5 | #include "subsystems/Intake.h"
      |          ^~~~~~~~~~~~~~~~~~~~~

/home/tav/frc/wpilib/allwpilib/wpilibcExamples/src/test/cpp/examples/UnitTest/cpp/subsystems/IntakeTest.cpp:11:10: fatal error: Constants.h: No such file or directory
   11 | #include "Constants.h"
      |          ^~~~~~~~~~~~~
```
2022-12-02 10:37:49 -08:00
Starlight220
66bb0ffb2c [examples] Add unit testing infrastructure (#4646) 2022-12-02 08:40:14 -08:00
Tyler Veness
74cc86c4c5 [wpimath] Make transform tests use pose/transform equality operators (#4675)
Also add more nonzeros to Transform3d tests to make them more
comprehensive.
2022-12-02 08:36:57 -08:00
Jordan McMichael
e22d8cc343 [wpimath] Use Odometry for internal state in Pose Estimation (#4668)
This effectively replaces the Unscented Kalman Filter used for Pose Estimation with the Odometry model, and uses a recalculable Kalman gain matrix to update pose estimations whenever a vision measurement is added.

Notably, this change removes the need for the confusing generics used in Java, and the C++ implementation got quite a bit less complex as well.

Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2022-12-02 08:36:10 -08:00
Ryan Blue
68dba92630 [ci] Update mac and windows builds to Java 17 (#4750) 2022-12-02 08:31:38 -08:00
Tyler Veness
23bfc2d9ab [sim] Remove unmaintained Gazebo support (#4736) 2022-12-01 20:46:47 -08:00
Starlight220
1f1461e254 [wpilib] Add method to enable/disable LiveWindow in test mode (#4678) 2022-12-01 13:28:06 -08:00
Tyler Veness
eae68fc165 [wpimath] Add tolerance for Rotation3d rotation matrix special orthogonality (#4744) 2022-11-30 19:51:31 -08:00
Tyler Veness
4c4545fb4b [apriltag] Suppress warning (#4743) 2022-11-30 19:51:18 -08:00
Tyler Veness
16ffaa754d [docs] Generate docs for apriltag subproject (#4745) 2022-11-30 19:50:52 -08:00
Thad House
5e74ff26d8 [apriltag, build] Update native utils, add apriltag impl and JNI (#4733)
Co-authored-by: Peter Johnson <johnson.peter@gmail.com>
2022-11-30 00:16:29 -08:00
Dustin Spicuzza
53875419a1 [hal] Allow overriding stderr printing by HAL_SendError (#4742) 2022-11-29 23:19:15 -08:00
Peter Johnson
aa6499e920 [ntcore] Fix special topic multi-subscriber handling (#4740)
It now matches server behavior, where "" doesn't match special topics.

Also fixes duplicate notification that could occur in some cases.
2022-11-29 21:58:57 -08:00
Peter Johnson
df70351107 [build] Fix cmake install of thirdparty includes (#4741) 2022-11-29 21:58:38 -08:00
Peter Johnson
e9bd50ff9b [glass] NT view: clear meta-topic info on disconnect (#4732) 2022-11-29 21:57:48 -08:00
Peter Johnson
9b319fd56b [ntcore] Add sub option for local vs remote changes (#4731)
This is the subscriber readQueue version of the local value listener flag.
2022-11-29 21:57:20 -08:00
Peter Johnson
18d28ec5e3 [ntcore] Remove duplicate value checking from ClientImpl
This is no longer required with the duplicate value checking in
LocalStorage.
2022-11-29 09:49:47 -08:00
Peter Johnson
bdfb625211 [ntcore] Send duplicate values to network if necessary
Essentially this separates duplicate value detection between network and
local.
2022-11-29 09:49:47 -08:00
Ryan Blue
21003e34eb [commands] Update Subsystem factories and example to return CommandBase (#4729)
Also update rapidreactcommandbot example factories to fit this convention (as in #4655).

C++ does not need an update as CommandPtr already uses CommandBase (#4677).
2022-11-28 19:47:18 -08:00
Starlight220
70080457d5 [commands] Refactor ProxyScheduleCommand, SelectCommand into ProxyCommand (#4534) 2022-11-28 14:43:10 -08:00
Colin Wong
e82cd5147b [wpilib] Tweak Color HSV formula and use in AddressableLED (#4724)
The Color algorithm was tweaked to:
a) not produce incorrect values if the user happens to input a hue outside the [0, 180) range, and
b) more accurately convert the hue remainder from range 0-30 to 0-255. The current conversion vastly overshoots the multiplier (converts 0-30 to 0-270) and relies on clamping the value when constructing the Color object to produce a slightly incorrect result.
2022-11-28 14:42:22 -08:00
Colin Wong
ec124bb662 [commands] Allow unsetting a subsystem's default command (#4621) 2022-11-28 14:03:14 -08:00
Ryan Blue
2b2aa8eef7 [examples] Update all examples to use NWU coordinate conventions (#4725) 2022-11-28 13:49:49 -08:00
Starlight220
cb38bacfe8 [commands] Revert to original Trigger implementation (#4673)
Trigger was refactored to use BooleanEvent when it was introduced in #4104.
This reverts to the original implementation until edge-based BooleanEvents can be fixed.
2022-11-28 13:48:48 -08:00
Starlight220
15561338d5 [commands] Remove one more default command isFinished check (#4727) 2022-11-28 13:44:37 -08:00
Ryan Blue
ca35a2e097 Add simgui files to .gitignore (#4726) 2022-11-28 08:57:57 -08:00
Starlight220
20dbae0cee [examples] Renovate command-based examples (#4409)
Refactor some examples to use newer features, such as HID factories, library-provided command factories, CommandPtr (C++), as well as new idioms such as static/instance command factories.
2022-11-28 08:55:13 -08:00
Eli Barnett
1a59737f40 [commands] Add convenience factories (#4460)
Co-authored-by: Starlight220 <53231611+Starlight220@users.noreply.github.com>
2022-11-28 07:41:25 -08:00
Tyler Veness
42b6d4e3f7 Use defaulted comparison operators in C++ (#4723)
Comparison operators which compared against every class member variable
now use C++20's default comparison operators.

Also remove operator!= that in C++20 is now auto-generated from operator==.
2022-11-27 21:01:01 -08:00
Peter Johnson
135c13958f [wpigui] Add FontAwesome (#4713) 2022-11-27 20:00:17 -08:00
Peter Johnson
ffbfc61532 [ntcore] Add NetworkTable table-specific listeners (#4640)
These are similar, but not quite identical to, the NT3 NetworkTable
table listeners.

Also add table topic-only multi-subscriber to ensure functions like
getKeys() work properly regardless of other subscriptions.
2022-11-27 19:46:34 -08:00
Starlight220
8958b2a4da [commands] Add property tests for command compositions (#4715) 2022-11-27 16:23:56 -08:00
Starlight220
e4ac09077c [wpilib] Add link to MotorSafety article (#4720) 2022-11-27 16:23:06 -08:00
Starlight220
f40de0c120 [commands] Add C++ factory templates (#4686) 2022-11-27 11:27:44 -08:00
Peter Johnson
51fa3e851f [build] cmake: Use FetchContent instead of ExternalProject (#4714)
Also switch to using thirdparty-fonts instead of generating them.
2022-11-26 23:05:41 -08:00
Peter Johnson
1da84b2255 [wpigui] Reload fonts to scale rather than preloading (#4712) 2022-11-26 22:30:38 -08:00
Peter Johnson
e43e2fbc84 [wpiutil] StringExtras: Add UnescapeCString (#4707)
Based on implementation in glass but enhanced for generic use.
2022-11-26 18:21:45 -08:00
Peter Johnson
5804d8fa84 [ntcore] Server: Properly handle multiple subscribers (#4717)
Previously, only the first subscriber was actually matched to a topic
when a topic was created; this was a problem when later publishing
values as a client could have both a topic-only subscriber and a normal
subscriber, and only the first one would end up being subscribed to the
topic.
2022-11-26 17:02:22 -08:00
Peter Johnson
169ef5fabf [glass] Update NT view for topicsOnly and sendAll changes (#4718) 2022-11-26 17:01:40 -08:00
Starlight220
148759ef54 [examples] CANPDP: Expand properties shown (#4687) 2022-11-25 23:51:15 -08:00
Starlight220
58ed112b51 [commands] RepeatCommand: restart on following iteration (#4706)
This fixes InstantCommand.repeatedly().
2022-11-25 23:50:42 -08:00
Ryan Blue
dd1da77d20 [readme] Fix broken CI badge (#4710) 2022-11-25 23:49:47 -08:00
Ryan Blue
7cda85df20 [build] Check Gradle plugin repo last to fix CI (#4711) 2022-11-25 23:48:18 -08:00
Thad House
7ed9b13277 [build] Bump version plugin to fix null tag (#4705) 2022-11-24 22:10:59 -08:00
Tyler Veness
6b4f26225d [apriltag] Fix pluralization of apriltag artifacts (#4671) 2022-11-24 09:06:38 -08:00
Peter Johnson
b2d2924b72 [cscore] Add Y16 image support (#4702) 2022-11-24 09:06:06 -08:00
Peter Johnson
34ec89c041 [wpilibc] Shuffleboard SimpleWidget: Return pointer instead of reference (#4703)
Based on beta test feedback, returning a pointer is more intuitive, as
typically the return value is late bound to an instance variable.
2022-11-24 09:05:37 -08:00
Peter Johnson
e15200068d [ci] Disable HW testbench runs (#4704)
These are currently broken with no timetable to fix.
2022-11-24 09:04:57 -08:00
Starlight220
d5200db6cd [wpimath] Rename HolonomicDriveController.calculate params (#4683) 2022-11-23 23:13:50 -08:00
Tyler Veness
2ee3d86de4 [wpimath] Clarify Rotation3d roll-pitch-yaw direction (#4699) 2022-11-23 23:12:59 -08:00
Peter Johnson
9f0a8b930f [cscore] Use MFVideoFormat_L8 for Gray on Windows (#4701) 2022-11-23 22:15:56 -08:00
Peter Johnson
2bca43779e [cscore] Add UYVY image support (#4700) 2022-11-23 22:00:31 -08:00
ohowe
4307d0ee8b [glass] Plot: allow for more than 11 plots (#4685)
Since m_windows is sorted using the ascii, when "Plot <10>" is reached it will be before "Plot <2>" in `m_windows` which makes it so it will not add a new plot after the id 10 is reached. This also fixes a potential issue of someone manually changing an id in the file, which would break adding a new plot in some circumstances.
2022-11-23 13:57:29 -08:00
Starlight220
3fe8d355a1 [examples] StateSpaceDifferentialDriveSimulation: Use encoder reversed constants (#4682)
This matches the Java example.
2022-11-22 10:29:40 -08:00
Peter Johnson
b44034dadc [ntcore] Allow duplicate client IDs on server (#4676)
Currently, the server rejects duplicate client IDs. As we want to make
the client implementation as simple as possible, instead deduplicate the
name on the server side by appending "@" and a count.

NT4 spec has been updated for this change.
2022-11-22 10:27:49 -08:00
Starlight220
52d2c53888 [commands] Rename Java factory wait() to waitSeconds() (#4684)
This is needed to avoid a conflict with Object.wait() when using static imports.
C++ doesn't have this issue, and has units, so Wait() still makes sense there.
2022-11-22 10:16:27 -08:00
Thad House
76e918f71e [build] Fix JNI artifacts linking to incorrect libraries (#4680) 2022-11-21 20:33:46 -08:00
Starlight220
0bee875aff [commands] Change C++ CommandPtr to use CommandBase (#4677) 2022-11-21 09:45:50 -08:00
Peter Johnson
98e922313b [glass] Don't check IsConnected for NT widgets (#4674)
Checking this isn't required, and prevented these widgets from working
in the simulation GUI without another NT client connected.
2022-11-20 17:27:28 -08:00
amquake
9a36373b8f [apriltag] Switch 2022 apriltag layout length and width values (#4670) 2022-11-19 09:11:08 -08:00
1046 changed files with 27323 additions and 15275 deletions

View File

@@ -1,5 +1,4 @@
---
Language: Cpp
BasedOnStyle: Google
AccessModifierOffset: -1
AlignAfterOpenBracket: Align

View File

@@ -4,8 +4,8 @@ on:
types: [ created ]
jobs:
wpiformat:
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/wpiformat')
format:
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/format')
runs-on: ubuntu-22.04
steps:
- name: React Rocket
@@ -60,5 +60,5 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Commit
git commit -am "wpiformat"
git commit -am "Formatting fixes"
git push

View File

@@ -1,19 +0,0 @@
name: Gazebo
on: [pull_request, push]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
build:
name: "Build"
runs-on: ubuntu-22.04
container: wpilib/gazebo-ubuntu:22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Build with Gradle
run: ./gradlew simulation:frc_gazebo_plugins:build simulation:halsim_gazebo:build -PbuildServer -PforceGazebo

View File

@@ -46,7 +46,7 @@ jobs:
build-host:
env:
MACOSX_DEPLOYMENT_TARGET: 10.14
MACOSX_DEPLOYMENT_TARGET: 10.15
strategy:
fail-fast: false
matrix:
@@ -82,7 +82,7 @@ jobs:
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 11
java-version: 17
architecture: ${{ matrix.architecture }}
- name: Import Developer ID Certificate
uses: wpilibsuite/import-signing-certificate@v1

9
.gitignore vendored
View File

@@ -5,6 +5,12 @@ doxygen.log
build*/
!buildSrc/
simgui-ds.json
simgui-window.json
simgui.json
networktables.json
# Created by the jenkins test script
test-reports
@@ -15,6 +21,9 @@ test-reports
.idea/
out/
# Fleet
.fleet
# Created by http://www.gitignore.io
### Linux ###

View File

@@ -18,6 +18,7 @@ generatedFileExclude {
FRCNetComm\.java$
simulation/gz_msgs/src/include/simulation/gz_msgs/msgs\.h$
fieldImages/src/main/native/resources/
apriltag/src/test/resources/
}
repoRootNameOverride {

View File

@@ -40,8 +40,39 @@ So you want to contribute your changes back to WPILib. Great! We have a few cont
WPILib uses modified Google style guides for both C++ and Java, which can be found in the [styleguide repository](https://github.com/wpilibsuite/styleguide). Autoformatters are available for many popular editors at https://github.com/google/styleguide. Running wpiformat is required for all contributions and is enforced by our continuous integration system.
While the library should be fully formatted according to the styles, additional elements of the style guide were not followed when the library was initially created. All new code should follow the guidelines. If you are looking for some easy ramp-up tasks, finding areas that don't follow the style guide and fixing them is very welcome.
### Math documentation
When writing math expressions in documentation, use https://www.unicodeit.net/ to convert LaTeX to a Unicode equivalent that's easier to read. Not all expressions will translate (e.g., superscripts of superscripts) so focus on making it readable by someone who isn't familiar with LaTeX. If content on multiple lines needs to be aligned in Doxygen/Javadoc comments (e.g., integration/summation limits, matrices packed with square brackets and superscripts for them), put them in @verbatim/@endverbatim blocks in Doxygen or `<pre>` tags in Javadoc so they render with monospace font.
The LaTeX to Unicode conversions can also be done locally via the unicodeit Python package. To install it, execute:
```bash
pip install --user unicodeit
```
Here's example usage:
```bash
$ python -m unicodeit.cli 'x_{k+1} = Ax_k + Bu_k'
xₖ₊₁ = Axₖ + Buₖ
```
On Linux, this process can be streamlined further by adding the following Bash function to your .bashrc (requires `wl-clipboard` on Wayland or `xclip` on X11):
```bash
# Converts LaTeX to Unicode, prints the result, and copies it to the clipboard
uc() {
if [ $WAYLAND_DISPLAY ]; then
python -m unicodeit.cli $@ | tee >(wl-copy -n)
else
python -m unicodeit.cli $@ | tee >(xclip -sel)
fi
}
```
Here's example usage:
```bash
$ uc 'x_{k+1} = Ax_k + Bu_k'
xₖ₊₁ = Axₖ + Buₖ
```
## Submitting Changes
### Pull Request Format

View File

@@ -1,6 +1,6 @@
# WPILib Project
![CI](https://github.com/wpilibsuite/allwpilib/workflows/CI/badge.svg)
[![Gradle](https://github.com/wpilibsuite/allwpilib/actions/workflows/gradle.yml/badge.svg?branch=main)](https://github.com/wpilibsuite/allwpilib/actions/workflows/gradle.yml)
[![C++ Documentation](https://img.shields.io/badge/documentation-c%2B%2B-blue)](https://github.wpilib.org/allwpilib/docs/development/cpp/)
[![Java Documentation](https://img.shields.io/badge/documentation-java-orange)](https://github.wpilib.org/allwpilib/docs/development/java/)
@@ -15,7 +15,6 @@ Welcome to the WPILib project. This repository contains the HAL, WPILibJ, and WP
- [Faster builds](#faster-builds)
- [Using Development Builds](#using-development-builds)
- [Custom toolchain location](#custom-toolchain-location)
- [Gazebo simulation](#gazebo-simulation)
- [Formatting/Linting](#formattinglinting)
- [CMake](#cmake)
- [Publishing](#publishing)
@@ -41,7 +40,7 @@ Using Gradle makes building WPILib very straightforward. It only has a few depen
## Requirements
- [JDK 11](https://adoptopenjdk.net/)
- [JDK 11](https://adoptium.net/temurin/releases/?version=11)
- Note that the JRE is insufficient; the full JDK is required
- On Ubuntu, run `sudo apt install openjdk-11-jdk`
- On Windows, install the JDK 11 .msi from the link above
@@ -49,7 +48,7 @@ Using Gradle makes building WPILib very straightforward. It only has a few depen
- C++ compiler
- On Linux, install GCC 11 or greater
- On Windows, install [Visual Studio Community 2022](https://visualstudio.microsoft.com/vs/community/) and select the C++ programming language during installation (Gradle can't use the build tools for Visual Studio)
- On macOS, install the Xcode command-line build tools via `xcode-select --install`
- On macOS, install the Xcode command-line build tools via `xcode-select --install`. Xcode 13 or later is required.
- ARM compiler toolchain
- Run `./gradlew installRoboRioToolchain` after cloning this repository
- If the WPILib installer was used, this toolchain is already installed
@@ -114,33 +113,14 @@ If you have installed the FRC Toolchain to a directory other than the default, o
./gradlew build -PtoolChainPath=some/path/to/frc/toolchain/bin
```
### Gazebo simulation
If you also want to force building Gazebo simulation support, add -PforceGazebo. This requires gazebo_transport. We have tested on 14.04 and 15.05, but any correct install of Gazebo should work, even on Windows if you build Gazebo from source. Correct means CMake needs to be able to find gazebo-config.cmake. See [The Gazebo website](https://gazebosim.org/) for installation instructions.
```bash
./gradlew build -PforceGazebo
```
If you prefer to use CMake directly, the you can still do so.
The common CMake tasks are wpilibcSim, frc_gazebo_plugins, and gz_msgs
```bash
mkdir build #run this in the root of allwpilib
cd build
cmake ..
make
```
### Formatting/linting
Once a PR has been submitted, formatting can be run in CI by commenting `/format` on the PR. A new commit will be pushed with the formatting changes.
#### wpiformat
wpiformat can be executed anywhere in the repository via `py -3 -m wpiformat -clang 14` on Windows or `python3 -m wpiformat -clang 14` on other platforms.
Once a PR has been submitted, formatting can be run in CI by commenting `/wpiformat` on the PR. A new commit will be pushed with the formatting changes.
#### Java Code Quality Tools
The Java code quality tools Checkstyle, PMD, and Spotless can be run via `./gradlew javaFormat`. SpotBugs can be run via the `spotbugsMain`, `spotbugsTest`, and `spotbugsDev` tasks. These tools will all be run automatically by the `build` task. To disable this behavior, pass the `-PskipJavaFormat` flag.
@@ -164,9 +144,7 @@ The maven artifacts are described in [MavenArtifacts.md](MavenArtifacts.md)
## Structure and Organization
The main WPILib code you're probably looking for is in WPILibJ and WPILibC. Those directories are split into shared, sim, and athena. Athena contains the WPILib code meant to run on your roboRIO. Sim is WPILib code meant to run on your computer with Gazebo, and shared is code shared between the two. Shared code must be platform-independent, since it will be compiled with both the ARM cross-compiler and whatever desktop compiler you are using (g++, msvc, etc...).
The Simulation directory contains extra simulation tools and libraries, such as gz_msgs and JavaGazebo. See sub-directories for more information.
The main WPILib code you're probably looking for is in WPILibJ and WPILibC. Those directories are split into shared, sim, and athena. Athena contains the WPILib code meant to run on your roboRIO. Sim is WPILib code meant to run on your computer, and shared is code shared between the two. Shared code must be platform-independent, since it will be compiled with both the ARM cross-compiler and whatever desktop compiler you are using (g++, msvc, etc...).
The integration test directories for C++ and Java contain test code that runs on our test-system. When you submit code for review, it is tested by those programs. If you add new functionality you should make sure to write tests for it so we don't break it in the future.

View File

@@ -1,41 +1,118 @@
project(fieldImages)
project(apriltag)
include(CompileWarnings)
include(GenResources)
include(FetchContent)
FetchContent_Declare(
apriltaglib
GIT_REPOSITORY https://github.com/wpilibsuite/apriltag.git
GIT_TAG ad31e33d20f9782b7239cb15cde57c56c91383ad
)
# Don't use apriltag's CMakeLists.txt due to conflicting naming and JNI
FetchContent_GetProperties(apriltaglib)
if(NOT apriltaglib_POPULATED)
FetchContent_Populate(apriltaglib)
endif()
aux_source_directory(${apriltaglib_SOURCE_DIR}/common APRILTAGLIB_COMMON_SRC)
file(GLOB TAG_FILES ${apriltaglib_SOURCE_DIR}/tag*.c)
set(APRILTAGLIB_SRCS ${apriltaglib_SOURCE_DIR}/apriltag.c ${apriltaglib_SOURCE_DIR}/apriltag_pose.c ${apriltaglib_SOURCE_DIR}/apriltag_quad_thresh.c)
file(GLOB apriltag_jni_src src/main/native/cpp/jni/AprilTagJNI.cpp)
if (WITH_JAVA)
find_package(Java REQUIRED)
include(UseJava)
find_package(Java REQUIRED)
find_package(JNI REQUIRED)
include(UseJava)
set(CMAKE_JAVA_COMPILE_FLAGS "-encoding" "UTF8" "-Xlint:unchecked")
file(GLOB JACKSON_JARS "${WPILIB_BINARY_DIR}/wpiutil/thirdparty/jackson/*.jar")
set(CMAKE_JNI_TARGET true)
set(CMAKE_JAVA_INCLUDE_PATH apriltags.jar ${JACKSON_JARS})
file(GLOB EJML_JARS "${WPILIB_BINARY_DIR}/wpimath/thirdparty/ejml/*.jar")
file(GLOB JACKSON_JARS "${WPILIB_BINARY_DIR}/wpiutil/thirdparty/jackson/*.jar")
find_file(OPENCV_JAR_FILE
NAMES opencv-${OpenCV_VERSION_MAJOR}${OpenCV_VERSION_MINOR}${OpenCV_VERSION_PATCH}.jar
PATHS ${OPENCV_JAVA_INSTALL_DIR} ${OpenCV_INSTALL_PATH}/bin ${OpenCV_INSTALL_PATH}/share/java
NO_DEFAULT_PATH)
set(CMAKE_JAVA_INCLUDE_PATH apriltag.jar ${EJML_JARS} ${JACKSON_JARS})
file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java)
file(GLOB_RECURSE JAVA_RESOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} src/main/native/resources/*.json)
add_jar(apriltag_jar
SOURCES ${JAVA_SOURCES}
RESOURCES NAMESPACE "edu/wpi/first/apriltag" ${JAVA_RESOURCES}
INCLUDE_JARS wpimath_jar ${EJML_JARS} wpiutil_jar ${OPENCV_JAR_FILE}
OUTPUT_NAME apriltag
GENERATE_NATIVE_HEADERS apriltag_jni_headers)
get_property(APRILTAG_JAR_FILE TARGET apriltag_jar PROPERTY JAR_FILE)
install(FILES ${APRILTAG_JAR_FILE} DESTINATION "${java_lib_dest}")
set_property(TARGET apriltag_jar PROPERTY FOLDER "java")
add_library(apriltagjni ${apriltag_jni_src})
wpilib_target_warnings(apriltagjni)
target_link_libraries(apriltagjni PUBLIC apriltag)
set_property(TARGET apriltagjni PROPERTY FOLDER "libraries")
target_link_libraries(apriltagjni PRIVATE apriltag_jni_headers)
add_dependencies(apriltagjni apriltag_jar)
if (MSVC)
install(TARGETS apriltagjni RUNTIME DESTINATION "${jni_lib_dest}" COMPONENT Runtime)
endif()
install(TARGETS apriltagjni EXPORT apriltagjni DESTINATION "${main_lib_dest}")
file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java)
file(GLOB_RECURSE JAVA_RESOURCES src/main/native/resources/*.json)
add_jar(apriltags_jar SOURCES ${JAVA_SOURCES} RESOURCES NAMESPACE "edu/wpi/first/apriltag" ${JAVA_RESOURCES} INCLUDE_JARS wpimath_jar OUTPUT_NAME apriltag)
endif()
GENERATE_RESOURCES(src/main/native/resources/edu/wpi/first/apriltag generated/main/cpp APRILTAG frc apriltag_resources_src)
GENERATE_RESOURCES(src/main/native/resources/edu/wpi/first/apriltag generated/main/cpp APRILTAG frc apriltags_resources_src)
file(GLOB apriltag_native_src src/main/native/cpp/*.cpp)
file(GLOB_RECURSE apriltags_native_src src/main/native/cpp/*.cpp)
add_library(apriltag ${apriltag_native_src} ${apriltag_resources_src} ${APRILTAGLIB_SRCS} ${APRILTAGLIB_COMMON_SRC} ${TAG_FILES})
set_target_properties(apriltag PROPERTIES DEBUG_POSTFIX "d")
add_library(apriltags ${apriltags_native_src} ${apriltags_resources_src})
set_target_properties(apriltags PROPERTIES DEBUG_POSTFIX "d")
set_property(TARGET apriltag PROPERTY FOLDER "libraries")
target_compile_features(apriltag PUBLIC cxx_std_20)
wpilib_target_warnings(apriltag)
# disable warnings that apriltaglib can't handle
if (MSVC)
target_compile_options(apriltag PRIVATE /wd4018)
else()
target_compile_options(apriltag PRIVATE -Wno-sign-compare -Wno-gnu-zero-variadic-macro-arguments)
endif()
set_property(TARGET apriltags PROPERTY FOLDER "libraries")
target_compile_features(apriltags PUBLIC cxx_std_20)
wpilib_target_warnings(apriltags)
target_link_libraries(apriltags wpimath)
target_link_libraries(apriltag wpimath)
target_include_directories(apriltags PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/include>
$<INSTALL_INTERFACE:${include_dest}/apriltags>)
target_include_directories(apriltag PUBLIC
$<BUILD_INTERFACE:${apriltaglib_SOURCE_DIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/include>
$<INSTALL_INTERFACE:${include_dest}/apriltag>)
install(TARGETS apriltag EXPORT apriltag DESTINATION "${main_lib_dest}")
install(DIRECTORY src/main/native/include/ DESTINATION "${include_dest}/apriltag")
if (WITH_JAVA AND MSVC)
install(TARGETS apriltag RUNTIME DESTINATION "${jni_lib_dest}" COMPONENT Runtime)
endif()
if (WITH_FLAT_INSTALL)
set (apriltag_config_dir ${wpilib_dest})
else()
set (apriltag_config_dir share/apriltag)
endif()
configure_file(apriltag-config.cmake.in ${WPILIB_BINARY_DIR}/apriltag-config.cmake )
install(FILES ${WPILIB_BINARY_DIR}/apriltag-config.cmake DESTINATION ${apriltag_config_dir})
install(EXPORT apriltag DESTINATION ${apriltag_config_dir})
if (WITH_TESTS)
wpilib_add_test(apriltags src/test/native/cpp)
target_include_directories(apriltags_test PRIVATE src/test/native/include)
target_link_libraries(apriltags_test apriltags gmock_main)
wpilib_add_test(apriltag src/test/native/cpp)
target_include_directories(apriltag_test PRIVATE src/test/native/include)
target_link_libraries(apriltag_test apriltag gmock_main)
endif()

View File

@@ -0,0 +1,7 @@
include(CMakeFindDependencyMacro)
@FILENAME_DEP_REPLACE@
@WPIMATH_DEP_REPLACE@
@WPIUTIL_DEP_REPLACE@
@FILENAME_DEP_REPLACE@
include(${SELF_DIR}/apriltag.cmake)

View File

@@ -1,15 +1,21 @@
apply from: "${rootDir}/shared/resources.gradle"
ext {
nativeName = 'apriltags'
nativeName = 'apriltag'
devMain = 'edu.wpi.first.apriltag.DevMain'
useJava = true
useCpp = true
sharedCvConfigs = [
apriltagDev : [],
apriltagTest: []]
staticCvConfigs = []
def generateTask = createGenerateResourcesTask('main', 'APRILTAGS', 'frc', project)
def generateTask = createGenerateResourcesTask('main', 'APRILTAG', 'frc', project)
tasks.withType(CppCompile) {
dependsOn generateTask
}
extraSetup = {
splitSetup = {
it.sources {
resourcesCpp(CppSourceSet) {
source {
@@ -23,10 +29,13 @@ ext {
evaluationDependsOn(':wpimath')
apply from: "${rootDir}/shared/javacpp/setupBuild.gradle"
apply from: "${rootDir}/shared/jni/setupBuild.gradle"
apply from: "${rootDir}/shared/apriltaglib.gradle"
apply from: "${rootDir}/shared/opencv.gradle"
dependencies {
implementation project(':wpimath')
devImplementation project(':wpimath')
}
sourceSets {
@@ -37,7 +46,6 @@ sourceSets {
}
}
model {
components {}
binaries {
@@ -47,12 +55,15 @@ model {
}
it.cppCompiler.define 'WPILIB_EXPORTS'
lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
lib project: ':wpimath', library: 'wpimath', linkage: 'shared'
if ((it instanceof NativeExecutableBinarySpec || it instanceof GoogleTestTestSuiteBinarySpec) && it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
nativeUtils.useRequiredLibrary(it, 'ni_link_libraries', 'ni_runtime_libraries')
if (it.component.name == "${nativeName}JNI") {
lib project: ':wpimath', library: 'wpimath', linkage: 'static'
lib project: ':wpiutil', library: 'wpiutil', linkage: 'static'
} else {
lib project: ':wpimath', library: 'wpimath', linkage: 'shared'
lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
}
nativeUtils.useRequiredLibrary(it, 'apriltaglib')
}
}
tasks {

View File

@@ -8,6 +8,12 @@ public final class DevMain {
/** Main entry point. */
public static void main(String[] args) {
System.out.println("Hello World!");
AprilTagDetector detector = new AprilTagDetector();
detector.addFamily("tag16h5");
AprilTagDetector.Config config = new AprilTagDetector.Config();
config.refineEdges = false;
detector.setConfig(config);
detector.close();
}
private DevMain() {}

View File

@@ -2,4 +2,10 @@
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
int main() {}
#include "frc/apriltag/AprilTagDetector.h"
int main() {
frc::AprilTagDetector detector;
detector.AddFamily("tag16h5");
detector.SetConfig({.refineEdges = false});
}

View File

@@ -0,0 +1,190 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.apriltag;
import edu.wpi.first.math.MatBuilder;
import edu.wpi.first.math.Matrix;
import edu.wpi.first.math.Nat;
import edu.wpi.first.math.numbers.N3;
import java.util.Arrays;
/** A detection of an AprilTag tag. */
public class AprilTagDetection {
/**
* Gets the decoded tag's family name.
*
* @return Decoded family name
*/
public String getFamily() {
return m_family;
}
/**
* Gets the decoded ID of the tag.
*
* @return Decoded ID
*/
public int getId() {
return m_id;
}
/**
* Gets how many error bits were corrected. Note: accepting large numbers of corrected errors
* leads to greatly increased false positive rates. NOTE: As of this implementation, the detector
* cannot detect tags with a hamming distance greater than 2.
*
* @return Hamming distance (number of corrected error bits)
*/
public int getHamming() {
return m_hamming;
}
/**
* Gets a measure of the quality of the binary decoding process: the average difference between
* the intensity of a data bit versus the decision threshold. Higher numbers roughly indicate
* better decodes. This is a reasonable measure of detection accuracy only for very small tags--
* not effective for larger tags (where we could have sampled anywhere within a bit cell and still
* gotten a good detection.)
*
* @return Decision margin
*/
public float getDecisionMargin() {
return m_decisionMargin;
}
/**
* Gets the 3x3 homography matrix describing the projection from an "ideal" tag (with corners at
* (-1,1), (1,1), (1,-1), and (-1, -1)) to pixels in the image.
*
* @return Homography matrix data
*/
@SuppressWarnings("PMD.MethodReturnsInternalArray")
public double[] getHomography() {
return m_homography;
}
/**
* Gets the 3x3 homography matrix describing the projection from an "ideal" tag (with corners at
* (-1,1), (1,1), (1,-1), and (-1, -1)) to pixels in the image.
*
* @return Homography matrix
*/
public Matrix<N3, N3> getHomographyMatrix() {
return new MatBuilder<>(Nat.N3(), Nat.N3()).fill(m_homography);
}
/**
* Gets the center of the detection in image pixel coordinates.
*
* @return Center point X coordinate
*/
public double getCenterX() {
return m_centerX;
}
/**
* Gets the center of the detection in image pixel coordinates.
*
* @return Center point Y coordinate
*/
public double getCenterY() {
return m_centerY;
}
/**
* Gets a corner of the tag in image pixel coordinates. These always wrap counter-clock wise
* around the tag.
*
* @param ndx Corner index (range is 0-3, inclusive)
* @return Corner point X coordinate
*/
public double getCornerX(int ndx) {
return m_corners[ndx * 2];
}
/**
* Gets a corner of the tag in image pixel coordinates. These always wrap counter-clock wise
* around the tag.
*
* @param ndx Corner index (range is 0-3, inclusive)
* @return Corner point Y coordinate
*/
public double getCornerY(int ndx) {
return m_corners[ndx * 2 + 1];
}
/**
* Gets the corners of the tag in image pixel coordinates. These always wrap counter-clock wise
* around the tag.
*
* @return Corner point array (X and Y for each corner in order)
*/
@SuppressWarnings("PMD.MethodReturnsInternalArray")
public double[] getCorners() {
return m_corners;
}
private final String m_family;
private final int m_id;
private final int m_hamming;
private final float m_decisionMargin;
private final double[] m_homography;
private final double m_centerX;
private final double m_centerY;
private final double[] m_corners;
/**
* Constructs a new detection result. Used from JNI.
*
* @param family family
* @param id id
* @param hamming hamming
* @param decisionMargin dm
* @param homography homography
* @param centerX centerX
* @param centerY centerY
* @param corners corners
*/
@SuppressWarnings("PMD.ArrayIsStoredDirectly")
public AprilTagDetection(
String family,
int id,
int hamming,
float decisionMargin,
double[] homography,
double centerX,
double centerY,
double[] corners) {
m_family = family;
m_id = id;
m_hamming = hamming;
m_decisionMargin = decisionMargin;
m_homography = homography;
m_centerX = centerX;
m_centerY = centerY;
m_corners = corners;
}
@Override
public String toString() {
return "DetectionResult [centerX="
+ m_centerX
+ ", centerY="
+ m_centerY
+ ", corners="
+ Arrays.toString(m_corners)
+ ", decisionMargin="
+ m_decisionMargin
+ ", hamming="
+ m_hamming
+ ", homography="
+ Arrays.toString(m_homography)
+ ", family="
+ m_family
+ ", id="
+ m_id
+ "]";
}
}

View File

@@ -0,0 +1,281 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.apriltag;
import edu.wpi.first.apriltag.jni.AprilTagJNI;
import org.opencv.core.Mat;
/**
* An AprilTag detector engine. This is expensive to set up and tear down, so most use cases should
* only create one of these, add a family to it, set up any other configuration, and repeatedly call
* Detect().
*/
public class AprilTagDetector implements AutoCloseable {
/** Detector configuration. */
@SuppressWarnings("MemberName")
public static class Config {
/**
* How many threads should be used for computation. Default is single-threaded operation (1
* thread).
*/
public int numThreads = 1;
/**
* Quad decimation. Detection of quads can be done on a lower-resolution image, improving speed
* at a cost of pose accuracy and a slight decrease in detection rate. Decoding the binary
* payload is still done at full resolution. Default is 2.0.
*/
public float quadDecimate = 2.0f;
/**
* What Gaussian blur should be applied to the segmented image (used for quad detection). Very
* noisy images benefit from non-zero values (e.g. 0.8). Default is 0.0.
*/
public float quadSigma;
/**
* When true, the edges of the each quad are adjusted to "snap to" strong gradients nearby. This
* is useful when decimation is employed, as it can increase the quality of the initial quad
* estimate substantially. Generally recommended to be on (true). Default is true.
*
* <p>Very computationally inexpensive. Option is ignored if quad_decimate = 1.
*/
public boolean refineEdges = true;
/**
* How much sharpening should be done to decoded images. This can help decode small tags but may
* or may not help in odd lighting conditions or low light conditions. Default is 0.25.
*/
public double decodeSharpening = 0.25;
/**
* Debug mode. When true, the decoder writes a variety of debugging images to the current
* working directory at various stages through the detection process. This is slow and should
* *not* be used on space-limited systems such as the RoboRIO. Default is disabled (false).
*/
public boolean debug;
public Config() {}
Config(
int numThreads,
float quadDecimate,
float quadSigma,
boolean refineEdges,
double decodeSharpening,
boolean debug) {
this.numThreads = numThreads;
this.quadDecimate = quadDecimate;
this.quadSigma = quadSigma;
this.refineEdges = refineEdges;
this.decodeSharpening = decodeSharpening;
this.debug = debug;
}
@Override
public int hashCode() {
return numThreads
+ Float.hashCode(quadDecimate)
+ Float.hashCode(quadSigma)
+ Boolean.hashCode(refineEdges)
+ Double.hashCode(decodeSharpening)
+ Boolean.hashCode(debug);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Config)) {
return false;
}
Config other = (Config) obj;
return numThreads == other.numThreads
&& quadDecimate == other.quadDecimate
&& quadSigma == other.quadSigma
&& refineEdges == other.refineEdges
&& decodeSharpening == other.decodeSharpening
&& debug == other.debug;
}
}
/** Quad threshold parameters. */
@SuppressWarnings("MemberName")
public static class QuadThresholdParameters {
/** Threshold used to reject quads containing too few pixels. Default is 5 pixels. */
public int minClusterPixels = 5;
/**
* How many corner candidates to consider when segmenting a group of pixels into a quad. Default
* is 10.
*/
public int maxNumMaxima = 10;
/**
* Critical angle, in radians. The detector will reject quads where pairs of edges have angles
* that are close to straight or close to 180 degrees. Zero means that no quads are rejected.
* Default is 10 degrees.
*/
public double criticalAngle = 10 * Math.PI / 180.0;
/**
* When fitting lines to the contours, the maximum mean squared error allowed. This is useful in
* rejecting contours that are far from being quad shaped; rejecting these quads "early" saves
* expensive decoding processing. Default is 10.0.
*/
public float maxLineFitMSE = 10.0f;
/**
* Minimum brightness offset. When we build our model of black &amp; white pixels, we add an
* extra check that the white model must be (overall) brighter than the black model. How much
* brighter? (in pixel values, [0,255]). Default is 5.
*/
public int minWhiteBlackDiff = 5;
/**
* Whether the thresholded image be should be deglitched. Only useful for very noisy images.
* Default is disabled (false).
*/
public boolean deglitch;
public QuadThresholdParameters() {}
QuadThresholdParameters(
int minClusterPixels,
int maxNumMaxima,
double criticalAngle,
float maxLineFitMSE,
int minWhiteBlackDiff,
boolean deglitch) {
this.minClusterPixels = minClusterPixels;
this.maxNumMaxima = maxNumMaxima;
this.criticalAngle = criticalAngle;
this.maxLineFitMSE = maxLineFitMSE;
this.minWhiteBlackDiff = minWhiteBlackDiff;
this.deglitch = deglitch;
}
@Override
public int hashCode() {
return minClusterPixels
+ maxNumMaxima
+ Double.hashCode(criticalAngle)
+ Float.hashCode(maxLineFitMSE)
+ minWhiteBlackDiff
+ Boolean.hashCode(deglitch);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof QuadThresholdParameters)) {
return false;
}
QuadThresholdParameters other = (QuadThresholdParameters) obj;
return minClusterPixels == other.minClusterPixels
&& maxNumMaxima == other.maxNumMaxima
&& criticalAngle == other.criticalAngle
&& maxLineFitMSE == other.maxLineFitMSE
&& minWhiteBlackDiff == other.minWhiteBlackDiff
&& deglitch == other.deglitch;
}
}
public AprilTagDetector() {
m_native = AprilTagJNI.createDetector();
}
@Override
public void close() {
if (m_native != 0) {
AprilTagJNI.destroyDetector(m_native);
}
m_native = 0;
}
/**
* Sets detector configuration.
*
* @param config Configuration
*/
public void setConfig(Config config) {
AprilTagJNI.setDetectorConfig(m_native, config);
}
/**
* Gets detector configuration.
*
* @return Configuration
*/
public Config getConfig() {
return AprilTagJNI.getDetectorConfig(m_native);
}
/**
* Sets quad threshold parameters.
*
* @param params Parameters
*/
public void setQuadThresholdParameters(QuadThresholdParameters params) {
AprilTagJNI.setDetectorQTP(m_native, params);
}
/**
* Gets quad threshold parameters.
*
* @return Parameters
*/
public QuadThresholdParameters getQuadThresholdParameters() {
return AprilTagJNI.getDetectorQTP(m_native);
}
/**
* Adds a family of tags to be detected.
*
* @param fam Family name, e.g. "tag16h5"
* @throws IllegalArgumentException if family name not recognized
*/
public void addFamily(String fam) {
addFamily(fam, 2);
}
/**
* Adds a family of tags to be detected.
*
* @param fam Family name, e.g. "tag16h5"
* @param bitsCorrected maximum number of bits to correct
* @throws IllegalArgumentException if family name not recognized
*/
public void addFamily(String fam, int bitsCorrected) {
if (!AprilTagJNI.addFamily(m_native, fam, bitsCorrected)) {
throw new IllegalArgumentException("unknown family name '" + fam + "'");
}
}
/**
* Removes a family of tags from the detector.
*
* @param fam Family name, e.g. "tag16h5"
*/
public void removeFamily(String fam) {
AprilTagJNI.removeFamily(m_native, fam);
}
/** Unregister all families. */
public void clearFamilies() {
AprilTagJNI.clearFamilies(m_native);
}
/**
* Detect tags from an 8-bit image.
*
* @param img 8-bit OpenCV Mat image
* @return Results (array of AprilTagDetection)
*/
public AprilTagDetection[] detect(Mat img) {
return AprilTagJNI.detect(m_native, img.cols(), img.rows(), img.cols(), img.dataAddr());
}
private long m_native;
}

View File

@@ -33,11 +33,13 @@ import java.util.Optional;
* meters with "width" and "length" values. This is to account for arbitrary field sizes when
* transforming the poses.
*
* <p>Pose3ds are assumed to be measured from the bottom-left corner of the field, when the blue
* alliance is at the left. By default, Pose3ds will be returned as declared when calling {@link
* AprilTagFieldLayout#getTagPose(int)}. {@link #setOrigin(OriginPosition)} can be used to transform
* the poses returned from {@link AprilTagFieldLayout#getTagPose(int)} to be correct relative to a
* different coordinate frame.
* <p>Pose3ds in the JSON are measured using the normal FRC coordinate system, NWU with the origin
* at the bottom-right corner of the blue alliance wall. {@link #setOrigin(OriginPosition)} can be
* used to change the poses returned from {@link AprilTagFieldLayout#getTagPose(int)} to be from the
* perspective of a specific alliance.
*
* <p>Tag poses represent the center of the tag, with a zero rotation representing a tag that is
* upright and facing away from the (blue) alliance wall (that is, towards the opposing alliance).
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
@@ -115,8 +117,8 @@ public class AprilTagFieldLayout {
* Sets the origin based on a predefined enumeration of coordinate frame origins. The origins are
* calculated from the field dimensions.
*
* <p>This transforms the Pose3ds returned by {@link #getTagPose(int)} to return the correct pose
* relative to a predefined coordinate frame.
* <p>This transforms the Pose3d objects returned by {@link #getTagPose(int)} to return the
* correct pose relative to a predefined coordinate frame.
*
* @param origin The predefined origin
*/
@@ -140,8 +142,8 @@ public class AprilTagFieldLayout {
/**
* Sets the origin for tag pose transformation.
*
* <p>This transforms the Pose3ds returned by {@link #getTagPose(int)} to return the correct pose
* relative to the provided origin.
* <p>This transforms the Pose3d objects returned by {@link #getTagPose(int)} to return the
* correct pose relative to the provided origin.
*
* @param origin The new origin for tag transformations
*/
@@ -187,7 +189,10 @@ public class AprilTagFieldLayout {
}
/**
* Deserializes a field layout from a resource within a jar file.
* Deserializes a field layout from a resource within a internal jar file.
*
* <p>Users should use {@link AprilTagFields#loadAprilTagLayoutField()} to load official layouts
* and {@link #AprilTagFieldLayout(String)} for custom layouts.
*
* @param resourcePath The absolute path of the resource
* @return The deserialized layout

View File

@@ -4,17 +4,30 @@
package edu.wpi.first.apriltag;
import java.io.IOException;
public enum AprilTagFields {
k2022RapidReact("2022-rapidreact.json");
k2022RapidReact("2022-rapidreact.json"),
k2023ChargedUp("2023-chargedup.json");
public static final String kBaseResourceDir = "/edu/wpi/first/apriltag/";
/** Alias to the current game. */
public static final AprilTagFields kDefaultField = k2022RapidReact;
public static final AprilTagFields kDefaultField = k2023ChargedUp;
public final String m_resourceFile;
AprilTagFields(String resourceFile) {
m_resourceFile = kBaseResourceDir + resourceFile;
}
/**
* Get a {@link AprilTagFieldLayout} from the resource JSON.
*
* @return AprilTagFieldLayout of the field
* @throws IOException If the layout does not exist
*/
public AprilTagFieldLayout loadAprilTagLayoutField() throws IOException {
return AprilTagFieldLayout.loadFromResource(m_resourceFile);
}
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.apriltag;
import edu.wpi.first.math.geometry.Transform3d;
/** A pair of AprilTag pose estimates. */
@SuppressWarnings("MemberName")
public class AprilTagPoseEstimate {
/**
* Constructs a pose estimate.
*
* @param pose1 first pose
* @param pose2 second pose
* @param error1 error of first pose
* @param error2 error of second pose
*/
public AprilTagPoseEstimate(Transform3d pose1, Transform3d pose2, double error1, double error2) {
this.pose1 = pose1;
this.pose2 = pose2;
this.error1 = error1;
this.error2 = error2;
}
/**
* Get the ratio of pose reprojection errors, called ambiguity. Numbers above 0.2 are likely to be
* ambiguous.
*
* @return The ratio of pose reprojection errors.
*/
public double getAmbiguity() {
double min = Math.min(error1, error2);
double max = Math.max(error1, error2);
if (max > 0) {
return min / max;
} else {
return -1;
}
}
/** Pose 1. */
public final Transform3d pose1;
/** Pose 2. */
public final Transform3d pose2;
/** Object-space error of pose 1. */
public final double error1;
/** Object-space error of pose 2. */
public final double error2;
}

View File

@@ -0,0 +1,190 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.apriltag;
import edu.wpi.first.apriltag.jni.AprilTagJNI;
import edu.wpi.first.math.geometry.Transform3d;
/** Pose estimators for AprilTag tags. */
public class AprilTagPoseEstimator {
/** Configuration for the pose estimator. */
@SuppressWarnings("MemberName")
public static class Config {
/**
* Creates a pose estimator configuration.
*
* @param tagSize tag size, in meters
* @param fx camera horizontal focal length, in pixels
* @param fy camera vertical focal length, in pixels
* @param cx camera horizontal focal center, in pixels
* @param cy camera vertical focal center, in pixels
*/
public Config(double tagSize, double fx, double fy, double cx, double cy) {
this.tagSize = tagSize;
this.fx = fx;
this.fy = fy;
this.cx = cx;
this.cy = cy;
}
public double tagSize;
public double fx;
public double fy;
public double cx;
public double cy;
@Override
public int hashCode() {
return Double.hashCode(tagSize)
+ Double.hashCode(fx)
+ Double.hashCode(fy)
+ Double.hashCode(cx)
+ Double.hashCode(cy);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Config)) {
return false;
}
Config other = (Config) obj;
return tagSize == other.tagSize
&& fx == other.fx
&& fy == other.fy
&& cx == other.cx
&& cy == other.cy;
}
}
/**
* Creates estimator.
*
* @param config Configuration
*/
public AprilTagPoseEstimator(Config config) {
m_config = new Config(config.tagSize, config.fx, config.fy, config.cx, config.cy);
}
/**
* Sets estimator configuration.
*
* @param config Configuration
*/
public void setConfig(Config config) {
m_config.tagSize = config.tagSize;
m_config.fx = config.fx;
m_config.fy = config.fy;
m_config.cx = config.cx;
m_config.cy = config.cy;
}
/**
* Gets estimator configuration.
*
* @return Configuration
*/
public Config getConfig() {
return new Config(m_config.tagSize, m_config.fx, m_config.fy, m_config.cx, m_config.cy);
}
/**
* Estimates the pose of the tag using the homography method described in [1].
*
* @param detection Tag detection
* @return Pose estimate
*/
public Transform3d estimateHomography(AprilTagDetection detection) {
return estimateHomography(detection.getHomography());
}
/**
* Estimates the pose of the tag using the homography method described in [1].
*
* @param homography Homography 3x3 matrix data
* @return Pose estimate
*/
public Transform3d estimateHomography(double[] homography) {
return AprilTagJNI.estimatePoseHomography(
homography, m_config.tagSize, m_config.fx, m_config.fy, m_config.cx, m_config.cy);
}
/**
* Estimates the pose of the tag. This returns one or two possible poses for the tag, along with
* the object-space error of each.
*
* <p>This uses the homography method described in [1] for the initial estimate. Then Orthogonal
* Iteration [2] is used to refine this estimate. Then [3] is used to find a potential second
* local minima and Orthogonal Iteration is used to refine this second estimate.
*
* <p>[1]: E. Olson, “Apriltag: A robust and flexible visual fiducial system,” in 2011 IEEE
* International Conference on Robotics and Automation, May 2011, pp. 34003407.
*
* <p>[2]: Lu, G. D. Hager and E. Mjolsness, "Fast and globally convergent pose estimation from
* video images," in IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 22, no.
* 6, pp. 610-622, June 2000. doi: 10.1109/34.862199
*
* <p>[3]: Schweighofer and A. Pinz, "Robust Pose Estimation from a Planar Target," in IEEE
* Transactions on Pattern Analysis and Machine Intelligence, vol. 28, no. 12, pp. 2024-2030, Dec.
* 2006. doi: 10.1109/TPAMI.2006.252
*
* @param detection Tag detection
* @param nIters Number of iterations
* @return Initial and (possibly) second pose estimates
*/
public AprilTagPoseEstimate estimateOrthogonalIteration(AprilTagDetection detection, int nIters) {
return estimateOrthogonalIteration(detection.getHomography(), detection.getCorners(), nIters);
}
/**
* Estimates the pose of the tag. This returns one or two possible poses for the tag, along with
* the object-space error of each.
*
* @param homography Homography 3x3 matrix data
* @param corners Corner point array (X and Y for each corner in order)
* @param nIters Number of iterations
* @return Initial and (possibly) second pose estimates
*/
public AprilTagPoseEstimate estimateOrthogonalIteration(
double[] homography, double[] corners, int nIters) {
return AprilTagJNI.estimatePoseOrthogonalIteration(
homography,
corners,
m_config.tagSize,
m_config.fx,
m_config.fy,
m_config.cx,
m_config.cy,
nIters);
}
/**
* Estimates tag pose. This method is an easier to use interface to
* EstimatePoseOrthogonalIteration(), running 50 iterations and returning the pose with the lower
* object-space error.
*
* @param detection Tag detection
* @return Pose estimate
*/
public Transform3d estimate(AprilTagDetection detection) {
return estimate(detection.getHomography(), detection.getCorners());
}
/**
* Estimates tag pose. This method is an easier to use interface to
* EstimatePoseOrthogonalIteration(), running 50 iterations and returning the pose with the lower
* object-space error.
*
* @param homography Homography 3x3 matrix data
* @param corners Corner point array (X and Y for each corner in order)
* @return Pose estimate
*/
public Transform3d estimate(double[] homography, double[] corners) {
return AprilTagJNI.estimatePose(
homography, corners, m_config.tagSize, m_config.fx, m_config.fy, m_config.cx, m_config.cy);
}
private final Config m_config;
}

View File

@@ -0,0 +1,90 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.apriltag.jni;
import edu.wpi.first.apriltag.AprilTagDetection;
import edu.wpi.first.apriltag.AprilTagDetector;
import edu.wpi.first.apriltag.AprilTagPoseEstimate;
import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.util.RuntimeLoader;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
public class AprilTagJNI {
static boolean libraryLoaded = false;
static RuntimeLoader<AprilTagJNI> loader = null;
public static class Helper {
private static AtomicBoolean extractOnStaticLoad = new AtomicBoolean(true);
public static boolean getExtractOnStaticLoad() {
return extractOnStaticLoad.get();
}
public static void setExtractOnStaticLoad(boolean load) {
extractOnStaticLoad.set(load);
}
}
static {
if (Helper.getExtractOnStaticLoad()) {
try {
loader =
new RuntimeLoader<>(
"apriltagjni", RuntimeLoader.getDefaultExtractionRoot(), AprilTagJNI.class);
loader.loadLibrary();
} catch (IOException ex) {
ex.printStackTrace();
System.exit(1);
}
libraryLoaded = true;
}
}
public static native long createDetector();
public static native void destroyDetector(long det);
public static native void setDetectorConfig(long det, AprilTagDetector.Config config);
public static native AprilTagDetector.Config getDetectorConfig(long det);
public static native void setDetectorQTP(
long det, AprilTagDetector.QuadThresholdParameters params);
public static native AprilTagDetector.QuadThresholdParameters getDetectorQTP(long det);
public static native boolean addFamily(long det, String fam, int bitsCorrected);
public static native void removeFamily(long det, String fam);
public static native void clearFamilies(long det);
public static native AprilTagDetection[] detect(
long det, int width, int height, int stride, long bufAddr);
public static native Transform3d estimatePoseHomography(
double[] homography, double tagSize, double fx, double fy, double cx, double cy);
public static native AprilTagPoseEstimate estimatePoseOrthogonalIteration(
double[] homography,
double[] corners,
double tagSize,
double fx,
double fy,
double cx,
double cy,
int nIters);
public static native Transform3d estimatePose(
double[] homography,
double[] corners,
double tagSize,
double fx,
double fy,
double cx,
double cy);
}

View File

@@ -8,14 +8,6 @@
using namespace frc;
bool AprilTag::operator==(const AprilTag& other) const {
return ID == other.ID && pose == other.pose;
}
bool AprilTag::operator!=(const AprilTag& other) const {
return !operator==(other);
}
void frc::to_json(wpi::json& json, const AprilTag& apriltag) {
json = wpi::json{{"ID", apriltag.ID}, {"pose", apriltag.pose}};
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "frc/apriltag/AprilTagDetection.h"
#include <type_traits>
#ifdef _WIN32
#pragma warning(disable : 4200)
#elif defined(__clang__)
#pragma clang diagnostic ignored "-Wc99-extensions"
#elif defined(__GNUC__)
#pragma GCC diagnostic ignored "-Wpedantic"
#endif
#include "apriltag.h"
using namespace frc;
static_assert(sizeof(AprilTagDetection) == sizeof(apriltag_detection_t),
"structure sizes don't match");
static_assert(std::is_standard_layout_v<AprilTagDetection>,
"AprilTagDetection is not standard layout?");
std::string_view AprilTagDetection::GetFamily() const {
return static_cast<const apriltag_family_t*>(family)->name;
}
std::span<const double, 9> AprilTagDetection::GetHomography() const {
return std::span<const double, 9>{static_cast<matd_t*>(H)->data, 9};
}
Eigen::Matrix3d AprilTagDetection::GetHomographyMatrix() const {
return Eigen::Map<Eigen::Matrix<double, 3, 3, Eigen::RowMajor>>{
static_cast<matd_t*>(H)->data};
}

View File

@@ -0,0 +1,200 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "frc/apriltag/AprilTagDetector.h"
#include <cmath>
#include <numbers>
#ifdef _WIN32
#pragma warning(disable : 4200)
#elif defined(__clang__)
#pragma clang diagnostic ignored "-Wc99-extensions"
#elif defined(__GNUC__)
#pragma GCC diagnostic ignored "-Wpedantic"
#endif
#include "apriltag.h"
#include "tag16h5.h"
#include "tag25h9.h"
#include "tag36h11.h"
#include "tagCircle21h7.h"
#include "tagCircle49h12.h"
#include "tagCustom48h12.h"
#include "tagStandard41h12.h"
#include "tagStandard52h13.h"
using namespace frc;
AprilTagDetector::Results::Results(void* impl, const private_init&)
: span{reinterpret_cast<AprilTagDetection**>(
static_cast<zarray_t*>(impl)->data),
static_cast<size_t>(static_cast<zarray_t*>(impl)->size)},
m_impl{impl} {}
AprilTagDetector::Results& AprilTagDetector::Results::operator=(Results&& rhs) {
Destroy();
m_impl = rhs.m_impl;
rhs.m_impl = nullptr;
return *this;
}
void AprilTagDetector::Results::Destroy() {
if (m_impl) {
apriltag_detections_destroy(static_cast<zarray_t*>(m_impl));
}
}
AprilTagDetector::AprilTagDetector() : m_impl{apriltag_detector_create()} {}
AprilTagDetector& AprilTagDetector::operator=(AprilTagDetector&& rhs) {
Destroy();
m_impl = rhs.m_impl;
rhs.m_impl = nullptr;
m_families = std::move(rhs.m_families);
rhs.m_families.clear();
m_qtpCriticalAngle = rhs.m_qtpCriticalAngle;
return *this;
}
void AprilTagDetector::SetConfig(const Config& config) {
auto& impl = *static_cast<apriltag_detector_t*>(m_impl);
impl.nthreads = config.numThreads;
impl.quad_decimate = config.quadDecimate;
impl.quad_sigma = config.quadSigma;
impl.refine_edges = config.refineEdges;
impl.decode_sharpening = config.decodeSharpening;
impl.debug = config.debug;
}
AprilTagDetector::Config AprilTagDetector::GetConfig() const {
auto& impl = *static_cast<apriltag_detector_t*>(m_impl);
return {
.numThreads = impl.nthreads,
.quadDecimate = impl.quad_decimate,
.quadSigma = impl.quad_sigma,
.refineEdges = impl.refine_edges,
.decodeSharpening = impl.decode_sharpening,
.debug = impl.debug,
};
}
void AprilTagDetector::SetQuadThresholdParameters(
const QuadThresholdParameters& params) {
auto& qtp = static_cast<apriltag_detector_t*>(m_impl)->qtp;
qtp.min_cluster_pixels = params.minClusterPixels;
qtp.max_nmaxima = params.maxNumMaxima;
qtp.critical_rad = params.criticalAngle.value();
qtp.cos_critical_rad = std::cos(params.criticalAngle.value());
qtp.max_line_fit_mse = params.maxLineFitMSE;
qtp.min_white_black_diff = params.minWhiteBlackDiff;
qtp.deglitch = params.deglitch;
m_qtpCriticalAngle = params.criticalAngle;
}
AprilTagDetector::QuadThresholdParameters
AprilTagDetector::GetQuadThresholdParameters() const {
auto& qtp = static_cast<apriltag_detector_t*>(m_impl)->qtp;
return {
.minClusterPixels = qtp.min_cluster_pixels,
.maxNumMaxima = qtp.max_nmaxima,
.criticalAngle = m_qtpCriticalAngle,
.maxLineFitMSE = qtp.max_line_fit_mse,
.minWhiteBlackDiff = qtp.min_white_black_diff,
.deglitch = qtp.deglitch != 0,
};
}
bool AprilTagDetector::AddFamily(std::string_view fam, int bitsCorrected) {
auto& data = m_families[fam];
if (data) {
return true; // already detecting
}
// create the family
if (fam == "tag16h5") {
data = tag16h5_create();
} else if (fam == "tag25h9") {
data = tag25h9_create();
} else if (fam == "tag36h11") {
data = tag36h11_create();
} else if (fam == "tagCircle21h7") {
data = tagCircle21h7_create();
} else if (fam == "tagCircle49h12") {
data = tagCircle49h12_create();
} else if (fam == "tagStandard41h12") {
data = tagStandard41h12_create();
} else if (fam == "tagStandard52h13") {
data = tagStandard52h13_create();
} else if (fam == "tagCustom48h12") {
data = tagCustom48h12_create();
}
if (!data) {
m_families.erase(fam); // don't keep null value
return false; // can't add
}
apriltag_detector_add_family_bits(static_cast<apriltag_detector_t*>(m_impl),
static_cast<apriltag_family_t*>(data),
bitsCorrected);
return true;
}
void AprilTagDetector::RemoveFamily(std::string_view fam) {
auto it = m_families.find(fam);
if (it != m_families.end()) {
apriltag_detector_remove_family(
static_cast<apriltag_detector_t*>(m_impl),
static_cast<apriltag_family_t*>(it->second));
DestroyFamily(it->getKey(), it->second);
m_families.erase(it);
}
}
void AprilTagDetector::ClearFamilies() {
apriltag_detector_clear_families(static_cast<apriltag_detector_t*>(m_impl));
DestroyFamilies();
m_families.clear();
}
AprilTagDetector::Results AprilTagDetector::Detect(int width, int height,
int stride, uint8_t* buf) {
image_u8_t img{width, height, stride, buf};
return {
apriltag_detector_detect(static_cast<apriltag_detector_t*>(m_impl), &img),
Results::private_init{}};
}
void AprilTagDetector::Destroy() {
if (m_impl) {
apriltag_detector_destroy(static_cast<apriltag_detector_t*>(m_impl));
}
DestroyFamilies();
}
void AprilTagDetector::DestroyFamilies() {
for (auto&& entry : m_families) {
DestroyFamily(entry.getKey(), entry.second);
}
}
void AprilTagDetector::DestroyFamily(std::string_view name, void* data) {
auto fam = static_cast<apriltag_family_t*>(data);
if (name == "tag16h5") {
tag16h5_destroy(fam);
} else if (name == "tag25h9") {
tag25h9_destroy(fam);
} else if (name == "tag36h11") {
tag36h11_destroy(fam);
} else if (name == "tagCircle21h7") {
tagCircle21h7_destroy(fam);
} else if (name == "tagCircle49h12") {
tagCircle49h12_destroy(fam);
} else if (name == "tagStandard41h12") {
tagStandard41h12_destroy(fam);
} else if (name == "tagStandard52h13") {
tagStandard52h13_destroy(fam);
} else if (name == "tagCustom48h12") {
tagCustom48h12_destroy(fam);
}
}

View File

@@ -29,7 +29,7 @@ AprilTagFieldLayout::AprilTagFieldLayout(std::string_view path) {
m_apriltags[tag.ID] = tag;
}
m_fieldWidth = units::meter_t{json.at("field").at("width").get<double>()};
m_fieldLength = units::meter_t{json.at("field").at("height").get<double>()};
m_fieldLength = units::meter_t{json.at("field").at("length").get<double>()};
}
AprilTagFieldLayout::AprilTagFieldLayout(std::vector<AprilTag> apriltags,
@@ -81,16 +81,6 @@ void AprilTagFieldLayout::Serialize(std::string_view path) {
output.flush();
}
bool AprilTagFieldLayout::operator==(const AprilTagFieldLayout& other) const {
return m_apriltags == other.m_apriltags && m_origin == other.m_origin &&
m_fieldLength == other.m_fieldLength &&
m_fieldWidth == other.m_fieldWidth;
}
bool AprilTagFieldLayout::operator!=(const AprilTagFieldLayout& other) const {
return !operator==(other);
}
void frc::to_json(wpi::json& json, const AprilTagFieldLayout& layout) {
std::vector<AprilTag> tagVector;
tagVector.reserve(layout.m_apriltags.size());

View File

@@ -10,6 +10,7 @@ namespace frc {
// C++ generated from resource files
std::string_view GetResource_2022_rapidreact_json();
std::string_view GetResource_2023_chargedup_json();
AprilTagFieldLayout LoadAprilTagLayoutField(AprilTagField field) {
std::string_view fieldString;
@@ -17,6 +18,9 @@ AprilTagFieldLayout LoadAprilTagLayoutField(AprilTagField field) {
case AprilTagField::k2022RapidReact:
fieldString = GetResource_2022_rapidreact_json();
break;
case AprilTagField::k2023ChargedUp:
fieldString = GetResource_2023_chargedup_json();
break;
case AprilTagField::kNumFields:
throw std::invalid_argument("Invalid Field");
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "frc/apriltag/AprilTagPoseEstimate.h"
#include <algorithm>
using namespace frc;
double AprilTagPoseEstimate::GetAmbiguity() const {
auto min = (std::min)(error1, error2);
auto max = (std::max)(error1, error2);
if (max > 0) {
return min / max;
} else {
return -1;
}
}

View File

@@ -0,0 +1,154 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "frc/apriltag/AprilTagPoseEstimator.h"
#include <Eigen/QR>
#include "frc/apriltag/AprilTagDetection.h"
#ifdef _WIN32
#pragma warning(disable : 4200)
#elif defined(__clang__)
#pragma clang diagnostic ignored "-Wc99-extensions"
#elif defined(__GNUC__)
#pragma GCC diagnostic ignored "-Wpedantic"
#endif
#include "apriltag.h"
#include "apriltag_pose.h"
using namespace frc;
static Eigen::Matrix3d OrthogonalizeRotationMatrix(
const Eigen::Matrix3d& input) {
Eigen::HouseholderQR<Eigen::Matrix3d> qr{input};
Eigen::Matrix3d Q = qr.householderQ();
Eigen::Matrix3d R = qr.matrixQR().triangularView<Eigen::Upper>();
// Fix signs in R if they're < 0 so it's close to an identity matrix
// (our QR decomposition implementation sometimes flips the signs of
// columns)
for (int colR = 0; colR < 3; ++colR) {
if (R(colR, colR) < 0) {
for (int rowQ = 0; rowQ < 3; ++rowQ) {
Q(rowQ, colR) = -Q(rowQ, colR);
}
}
}
return Q;
}
static Transform3d MakePose(const apriltag_pose_t& pose) {
if (!pose.R || !pose.t) {
return {};
}
return {Translation3d{units::meter_t{pose.t->data[0]},
units::meter_t{pose.t->data[1]},
units::meter_t{pose.t->data[2]}},
Rotation3d{OrthogonalizeRotationMatrix(
Eigen::Map<Eigen::Matrix<double, 3, 3, Eigen::RowMajor>>{
pose.R->data})}};
}
static apriltag_detection_info_t MakeDetectionInfo(
const apriltag_detection_t* det,
const AprilTagPoseEstimator::Config& config) {
return {const_cast<apriltag_detection_t*>(det),
config.tagSize.value(),
config.fx,
config.fy,
config.cx,
config.cy};
}
static apriltag_detection_t MakeBasicDet(
std::span<const double, 9> homography,
const std::span<const double, 8>* corners) {
apriltag_detection_t detection;
detection.H = matd_create(3, 3);
std::memcpy(detection.H->data, homography.data(), 9 * sizeof(double));
if (corners) {
for (int i = 0; i < 4; i++) {
detection.p[i][0] = (*corners)[i * 2];
detection.p[i][1] = (*corners)[i * 2 + 1];
}
}
return detection;
}
static Transform3d DoEstimateHomography(
const apriltag_detection_t* detection,
const AprilTagPoseEstimator::Config& config) {
auto info = MakeDetectionInfo(detection, config);
apriltag_pose_t pose;
estimate_pose_for_tag_homography(&info, &pose);
return MakePose(pose);
}
Transform3d AprilTagPoseEstimator::EstimateHomography(
const AprilTagDetection& detection) const {
return DoEstimateHomography(
reinterpret_cast<const apriltag_detection_t*>(&detection), m_config);
}
Transform3d AprilTagPoseEstimator::EstimateHomography(
std::span<const double, 9> homography) const {
auto detection = MakeBasicDet(homography, nullptr);
auto rv = DoEstimateHomography(&detection, m_config);
matd_destroy(detection.H);
return rv;
}
static AprilTagPoseEstimate DoEstimateOrthogonalIteration(
const apriltag_detection_t* detection,
const AprilTagPoseEstimator::Config& config, int nIters) {
auto info = MakeDetectionInfo(detection, config);
apriltag_pose_t pose1, pose2;
double err1, err2;
estimate_tag_pose_orthogonal_iteration(&info, &err1, &pose1, &err2, &pose2,
nIters);
return {MakePose(pose1), MakePose(pose2), err1, err2};
}
AprilTagPoseEstimate AprilTagPoseEstimator::EstimateOrthogonalIteration(
const AprilTagDetection& detection, int nIters) const {
return DoEstimateOrthogonalIteration(
reinterpret_cast<const apriltag_detection_t*>(&detection), m_config,
nIters);
}
AprilTagPoseEstimate AprilTagPoseEstimator::EstimateOrthogonalIteration(
std::span<const double, 9> homography, std::span<const double, 8> corners,
int nIters) const {
auto detection = MakeBasicDet(homography, &corners);
auto rv = DoEstimateOrthogonalIteration(&detection, m_config, nIters);
matd_destroy(detection.H);
return rv;
}
static Transform3d DoEstimate(const apriltag_detection_t* detection,
const AprilTagPoseEstimator::Config& config) {
auto info = MakeDetectionInfo(detection, config);
apriltag_pose_t pose;
estimate_tag_pose(&info, &pose);
return MakePose(pose);
}
Transform3d AprilTagPoseEstimator::Estimate(
const AprilTagDetection& detection) const {
return DoEstimate(reinterpret_cast<const apriltag_detection_t*>(&detection),
m_config);
}
Transform3d AprilTagPoseEstimator::Estimate(
std::span<const double, 9> homography,
std::span<const double, 8> corners) const {
auto detection = MakeBasicDet(homography, &corners);
auto rv = DoEstimate(&detection, m_config);
matd_destroy(detection.H);
return rv;
}

View File

@@ -0,0 +1,595 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include <cstdio>
#include <cstring>
#include <wpi/jni_util.h>
#include "edu_wpi_first_apriltag_jni_AprilTagJNI.h"
#include "frc/apriltag/AprilTagDetector.h"
#include "frc/apriltag/AprilTagPoseEstimator.h"
using namespace frc;
using namespace wpi::java;
static JavaVM* jvm = nullptr;
static JClass detectionCls;
static JClass detectorConfigCls;
static JClass detectorQTPCls;
static JClass poseEstimateCls;
static JClass quaternionCls;
static JClass rotation3dCls;
static JClass transform3dCls;
static JClass translation3dCls;
static JException illegalArgEx;
static JException nullPointerEx;
static const JClassInit classes[] = {
{"edu/wpi/first/apriltag/AprilTagDetection", &detectionCls},
{"edu/wpi/first/apriltag/AprilTagDetector$Config", &detectorConfigCls},
{"edu/wpi/first/apriltag/AprilTagDetector$QuadThresholdParameters",
&detectorQTPCls},
{"edu/wpi/first/apriltag/AprilTagPoseEstimate", &poseEstimateCls},
{"edu/wpi/first/math/geometry/Quaternion", &quaternionCls},
{"edu/wpi/first/math/geometry/Rotation3d", &rotation3dCls},
{"edu/wpi/first/math/geometry/Transform3d", &transform3dCls},
{"edu/wpi/first/math/geometry/Translation3d", &translation3dCls}};
static const JExceptionInit exceptions[] = {
{"java/lang/IllegalArgumentException", &illegalArgEx},
{"java/lang/NullPointerException", &nullPointerEx}};
extern "C" {
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
jvm = vm;
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
// Cache references to classes
for (auto& c : classes) {
*c.cls = JClass(env, c.name);
if (!*c.cls) {
std::fprintf(stderr, "could not load class %s\n", c.name);
return JNI_ERR;
}
}
for (auto& c : exceptions) {
*c.cls = JException(env, c.name);
if (!*c.cls) {
std::fprintf(stderr, "could not load exception %s\n", c.name);
return JNI_ERR;
}
}
return JNI_VERSION_1_6;
}
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return;
}
// Delete global references
for (auto& c : classes) {
c.cls->free(env);
}
for (auto& c : exceptions) {
c.cls->free(env);
}
jvm = nullptr;
}
} // extern "C"
//
// Conversions from Java to C++ objects
//
static AprilTagDetector::Config FromJavaDetectorConfig(JNIEnv* env,
jobject jconfig) {
if (!jconfig) {
return {};
}
#define FIELD(name, sig) \
static jfieldID name##Field = nullptr; \
if (!name##Field) { \
name##Field = env->GetFieldID(detectorConfigCls, #name, sig); \
}
FIELD(numThreads, "I");
FIELD(quadDecimate, "F");
FIELD(quadSigma, "F");
FIELD(refineEdges, "Z");
FIELD(decodeSharpening, "D");
FIELD(debug, "Z");
#undef FIELD
#define FIELD(ctype, jtype, name) \
.name = static_cast<ctype>(env->Get##jtype##Field(jconfig, name##Field))
return {
FIELD(int, Int, numThreads),
FIELD(float, Float, quadDecimate),
FIELD(float, Float, quadSigma),
FIELD(bool, Boolean, refineEdges),
FIELD(double, Double, decodeSharpening),
FIELD(bool, Boolean, debug),
};
#undef GET
#undef FIELD
}
static AprilTagDetector::QuadThresholdParameters FromJavaDetectorQTP(
JNIEnv* env, jobject jparams) {
if (!jparams) {
return {};
}
#define FIELD(name, sig) \
static jfieldID name##Field = nullptr; \
if (!name##Field) { \
name##Field = env->GetFieldID(detectorQTPCls, #name, sig); \
}
FIELD(minClusterPixels, "I");
FIELD(maxNumMaxima, "I");
FIELD(criticalAngle, "D");
FIELD(maxLineFitMSE, "F");
FIELD(minWhiteBlackDiff, "I");
FIELD(deglitch, "Z");
#undef FIELD
#define FIELD(ctype, jtype, name) \
.name = static_cast<ctype>(env->Get##jtype##Field(jparams, name##Field))
return {
FIELD(int, Int, minClusterPixels),
FIELD(int, Int, maxNumMaxima),
.criticalAngle = units::radian_t{static_cast<double>(
env->GetDoubleField(jparams, criticalAngleField))},
FIELD(float, Float, maxLineFitMSE),
FIELD(int, Int, minWhiteBlackDiff),
FIELD(bool, Boolean, deglitch),
};
#undef GET
#undef FIELD
}
//
// Conversions from C++ to Java objects
//
static jobject MakeJObject(JNIEnv* env, const AprilTagDetection& detect) {
static jmethodID constructor = env->GetMethodID(
detectionCls, "<init>", "(Ljava/lang/String;IIF[DDD[D)V");
if (!constructor) {
return nullptr;
}
JLocal<jstring> fam{env, MakeJString(env, detect.GetFamily())};
auto homography = detect.GetHomography();
JLocal<jdoubleArray> harr{
env, MakeJDoubleArray(
env, {reinterpret_cast<const jdouble*>(homography.data()),
homography.size()})};
double cornersBuf[8];
auto corners = detect.GetCorners(cornersBuf);
JLocal<jdoubleArray> carr{
env,
MakeJDoubleArray(env, {reinterpret_cast<const jdouble*>(corners.data()),
corners.size()})};
auto center = detect.GetCenter();
return env->NewObject(detectionCls, constructor, fam.obj(),
static_cast<jint>(detect.GetId()),
static_cast<jint>(detect.GetHamming()),
static_cast<jfloat>(detect.GetDecisionMargin()),
harr.obj(), static_cast<jdouble>(center.x),
static_cast<jdouble>(center.y), carr.obj());
}
static jobjectArray MakeJObject(JNIEnv* env,
std::span<const AprilTagDetection* const> arr) {
jobjectArray jarr = env->NewObjectArray(arr.size(), detectionCls, nullptr);
if (!jarr) {
return nullptr;
}
for (size_t i = 0; i < arr.size(); ++i) {
JLocal<jobject> elem{env, MakeJObject(env, *arr[i])};
env->SetObjectArrayElement(jarr, i, elem.obj());
}
return jarr;
}
static jobject MakeJObject(JNIEnv* env,
const AprilTagDetector::Config& config) {
static jmethodID constructor =
env->GetMethodID(detectorConfigCls, "<init>", "(IFFZDZ)V");
if (!constructor) {
return nullptr;
}
return env->NewObject(detectorConfigCls, constructor,
static_cast<jint>(config.numThreads),
static_cast<jfloat>(config.quadDecimate),
static_cast<jfloat>(config.quadSigma),
static_cast<jboolean>(config.refineEdges),
static_cast<jdouble>(config.decodeSharpening),
static_cast<jboolean>(config.debug));
}
static jobject MakeJObject(
JNIEnv* env, const AprilTagDetector::QuadThresholdParameters& params) {
static jmethodID constructor =
env->GetMethodID(detectorQTPCls, "<init>", "(IIDFIZ)V");
if (!constructor) {
return nullptr;
}
return env->NewObject(detectorQTPCls, constructor,
static_cast<jint>(params.minClusterPixels),
static_cast<jint>(params.maxNumMaxima),
static_cast<jdouble>(params.criticalAngle),
static_cast<jfloat>(params.maxLineFitMSE),
static_cast<jint>(params.minWhiteBlackDiff),
static_cast<jboolean>(params.deglitch));
}
static jobject MakeJObject(JNIEnv* env, const Translation3d& xlate) {
static jmethodID constructor =
env->GetMethodID(translation3dCls, "<init>", "(DDD)V");
if (!constructor) {
return nullptr;
}
return env->NewObject(
translation3dCls, constructor, static_cast<jdouble>(xlate.X()),
static_cast<jdouble>(xlate.Y()), static_cast<jdouble>(xlate.Z()));
}
static jobject MakeJObject(JNIEnv* env, const Quaternion& q) {
static jmethodID constructor =
env->GetMethodID(quaternionCls, "<init>", "(DDDD)V");
if (!constructor) {
return nullptr;
}
return env->NewObject(quaternionCls, constructor, static_cast<jdouble>(q.W()),
static_cast<jdouble>(q.X()),
static_cast<jdouble>(q.Y()),
static_cast<jdouble>(q.Z()));
}
static jobject MakeJObject(JNIEnv* env, const Rotation3d& rot) {
static jmethodID constructor = env->GetMethodID(
rotation3dCls, "<init>", "(Ledu/wpi/first/math/geometry/Quaternion;)V");
if (!constructor) {
return nullptr;
}
JLocal<jobject> q{env, MakeJObject(env, rot.GetQuaternion())};
return env->NewObject(rotation3dCls, constructor, q.obj());
}
static jobject MakeJObject(JNIEnv* env, const Transform3d& xform) {
static jmethodID constructor =
env->GetMethodID(transform3dCls, "<init>",
"(Ledu/wpi/first/math/geometry/Translation3d;"
"Ledu/wpi/first/math/geometry/Rotation3d;)V");
if (!constructor) {
return nullptr;
}
JLocal<jobject> xlate{env, MakeJObject(env, xform.Translation())};
JLocal<jobject> rot{env, MakeJObject(env, xform.Rotation())};
return env->NewObject(transform3dCls, constructor, xlate.obj(), rot.obj());
}
static jobject MakeJObject(JNIEnv* env, const AprilTagPoseEstimate& est) {
static jmethodID constructor =
env->GetMethodID(poseEstimateCls, "<init>",
"(Ledu/wpi/first/math/geometry/Transform3d;"
"Ledu/wpi/first/math/geometry/Transform3d;DD)V");
if (!constructor) {
return nullptr;
}
JLocal<jobject> pose1{env, MakeJObject(env, est.pose1)};
JLocal<jobject> pose2{env, MakeJObject(env, est.pose2)};
return env->NewObject(poseEstimateCls, constructor, pose1.obj(), pose2.obj(),
static_cast<jdouble>(est.error1),
static_cast<jdouble>(est.error2));
}
extern "C" {
/*
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
* Method: createDetector
* Signature: ()J
*/
JNIEXPORT jlong JNICALL
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_createDetector
(JNIEnv* env, jclass)
{
return reinterpret_cast<jlong>(new AprilTagDetector);
}
/*
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
* Method: destroyDetector
* Signature: (J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_destroyDetector
(JNIEnv* env, jclass, jlong det)
{
delete reinterpret_cast<AprilTagDetector*>(det);
}
/*
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
* Method: setDetectorConfig
* Signature: (JLjava/lang/Object;)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_setDetectorConfig
(JNIEnv* env, jclass, jlong det, jobject config)
{
if (det == 0) {
nullPointerEx.Throw(env, "det cannot be null");
return;
}
reinterpret_cast<AprilTagDetector*>(det)->SetConfig(
FromJavaDetectorConfig(env, config));
}
/*
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
* Method: getDetectorConfig
* Signature: (J)Ljava/lang/Object;
*/
JNIEXPORT jobject JNICALL
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_getDetectorConfig
(JNIEnv* env, jclass, jlong det)
{
if (det == 0) {
nullPointerEx.Throw(env, "det cannot be null");
return nullptr;
}
return MakeJObject(env,
reinterpret_cast<AprilTagDetector*>(det)->GetConfig());
}
/*
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
* Method: setDetectorQTP
* Signature: (JLjava/lang/Object;)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_setDetectorQTP
(JNIEnv* env, jclass, jlong det, jobject params)
{
if (det == 0) {
nullPointerEx.Throw(env, "det cannot be null");
return;
}
reinterpret_cast<AprilTagDetector*>(det)->SetQuadThresholdParameters(
FromJavaDetectorQTP(env, params));
}
/*
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
* Method: getDetectorQTP
* Signature: (J)Ljava/lang/Object;
*/
JNIEXPORT jobject JNICALL
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_getDetectorQTP
(JNIEnv* env, jclass, jlong det)
{
if (det == 0) {
nullPointerEx.Throw(env, "det cannot be null");
return nullptr;
}
return MakeJObject(
env,
reinterpret_cast<AprilTagDetector*>(det)->GetQuadThresholdParameters());
}
/*
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
* Method: addFamily
* Signature: (JLjava/lang/String;I)Z
*/
JNIEXPORT jboolean JNICALL
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_addFamily
(JNIEnv* env, jclass, jlong det, jstring fam, jint bitsCorrected)
{
if (det == 0) {
nullPointerEx.Throw(env, "det cannot be null");
return false;
}
if (!fam) {
nullPointerEx.Throw(env, "fam cannot be null");
return false;
}
return reinterpret_cast<AprilTagDetector*>(det)->AddFamily(
JStringRef{env, fam}, bitsCorrected);
}
/*
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
* Method: removeFamily
* Signature: (JLjava/lang/String;)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_removeFamily
(JNIEnv* env, jclass, jlong det, jstring fam)
{
if (det == 0) {
nullPointerEx.Throw(env, "det cannot be null");
return;
}
if (!fam) {
nullPointerEx.Throw(env, "fam cannot be null");
return;
}
reinterpret_cast<AprilTagDetector*>(det)->RemoveFamily(JStringRef{env, fam});
}
/*
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
* Method: clearFamilies
* Signature: (J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_clearFamilies
(JNIEnv* env, jclass, jlong det)
{
if (det == 0) {
nullPointerEx.Throw(env, "det cannot be null");
return;
}
reinterpret_cast<AprilTagDetector*>(det)->ClearFamilies();
}
/*
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
* Method: detect
* Signature: (JIIIJ)[Ljava/lang/Object;
*/
JNIEXPORT jobjectArray JNICALL
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_detect
(JNIEnv* env, jclass, jlong det, jint width, jint height, jint stride,
jlong bufAddr)
{
if (det == 0) {
nullPointerEx.Throw(env, "det cannot be null");
return nullptr;
}
if (bufAddr == 0) {
nullPointerEx.Throw(env, "bufAddr cannot be null");
return nullptr;
}
return MakeJObject(
env, reinterpret_cast<AprilTagDetector*>(det)->Detect(
width, height, stride, reinterpret_cast<uint8_t*>(bufAddr)));
}
/*
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
* Method: estimatePoseHomography
* Signature: ([DDDDDD)Ljava/lang/Object;
*/
JNIEXPORT jobject JNICALL
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_estimatePoseHomography
(JNIEnv* env, jclass, jdoubleArray homography, jdouble tagSize, jdouble fx,
jdouble fy, jdouble cx, jdouble cy)
{
if (!homography) {
nullPointerEx.Throw(env, "homography cannot be null");
return nullptr;
}
JDoubleArrayRef harr{env, homography};
if (harr.size() != 9) {
illegalArgEx.Throw(env, "homography array must be size 9");
return nullptr;
}
AprilTagPoseEstimator estimator({units::meter_t{tagSize}, fx, fy, cx, cy});
return MakeJObject(env, estimator.EstimateHomography(
std::span<const double, 9>{harr.array()}));
}
/*
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
* Method: estimatePoseOrthogonalIteration
* Signature: ([D[DDDDDDI)Ljava/lang/Object;
*/
JNIEXPORT jobject JNICALL
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_estimatePoseOrthogonalIteration
(JNIEnv* env, jclass, jdoubleArray homography, jdoubleArray corners,
jdouble tagSize, jdouble fx, jdouble fy, jdouble cx, jdouble cy, jint nIters)
{
// homography
if (!homography) {
nullPointerEx.Throw(env, "homography cannot be null");
return nullptr;
}
JDoubleArrayRef harr{env, homography};
if (harr.size() != 9) {
illegalArgEx.Throw(env, "homography array must be size 9");
return nullptr;
}
// corners
if (!corners) {
nullPointerEx.Throw(env, "corners cannot be null");
return nullptr;
}
JDoubleArrayRef carr{env, corners};
if (carr.size() != 8) {
illegalArgEx.Throw(env, "corners array must be size 8");
return nullptr;
}
AprilTagPoseEstimator estimator({units::meter_t{tagSize}, fx, fy, cx, cy});
return MakeJObject(env,
estimator.EstimateOrthogonalIteration(
std::span<const double, 9>{harr.array()},
std::span<const double, 8>{carr.array()}, nIters));
}
/*
* Class: edu_wpi_first_apriltag_jni_AprilTagJNI
* Method: estimatePose
* Signature: ([D[DDDDDD)Ljava/lang/Object;
*/
JNIEXPORT jobject JNICALL
Java_edu_wpi_first_apriltag_jni_AprilTagJNI_estimatePose
(JNIEnv* env, jclass, jdoubleArray homography, jdoubleArray corners,
jdouble tagSize, jdouble fx, jdouble fy, jdouble cx, jdouble cy)
{
// homography
if (!homography) {
nullPointerEx.Throw(env, "homography cannot be null");
return nullptr;
}
JDoubleArrayRef harr{env, homography};
if (harr.size() != 9) {
illegalArgEx.Throw(env, "homography array must be size 9");
return nullptr;
}
// corners
if (!corners) {
nullPointerEx.Throw(env, "corners cannot be null");
return nullptr;
}
JDoubleArrayRef carr{env, corners};
if (carr.size() != 8) {
illegalArgEx.Throw(env, "corners array must be size 8");
return nullptr;
}
AprilTagPoseEstimator estimator({units::meter_t{tagSize}, fx, fy, cx, cy});
return MakeJObject(
env, estimator.Estimate(std::span<const double, 9>{harr.array()},
std::span<const double, 8>{carr.array()}));
}
} // extern "C"

View File

@@ -21,19 +21,8 @@ struct WPILIB_DLLEXPORT AprilTag {
/**
* Checks equality between this AprilTag and another object.
*
* @param other The other object.
* @return Whether the two objects are equal.
*/
bool operator==(const AprilTag& other) const;
/**
* Checks inequality between this AprilTag and another object.
*
* @param other The other object.
* @return Whether the two objects are not equal.
*/
bool operator!=(const AprilTag& other) const;
bool operator==(const AprilTag&) const = default;
};
WPILIB_DLLEXPORT

View File

@@ -0,0 +1,160 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#include <stdint.h>
#include <span>
#include <string_view>
#include <wpi/SymbolExports.h>
#include "frc/EigenCore.h"
namespace frc {
/**
* A detection of an AprilTag tag.
*/
class WPILIB_DLLEXPORT AprilTagDetection final {
public:
AprilTagDetection() = delete;
AprilTagDetection(const AprilTagDetection&) = delete;
AprilTagDetection& operator=(const AprilTagDetection&) = delete;
/** A point. Used for center and corner points. */
struct Point {
double x;
double y;
};
/**
* Gets the decoded tag's family name.
*
* @return Decoded family name
*/
std::string_view GetFamily() const;
/**
* Gets the decoded ID of the tag.
*
* @return Decoded ID
*/
int GetId() const { return id; }
/**
* Gets how many error bits were corrected. Note: accepting large numbers of
* corrected errors leads to greatly increased false positive rates.
* NOTE: As of this implementation, the detector cannot detect tags with
* a hamming distance greater than 2.
*
* @return Hamming distance (number of corrected error bits)
*/
int GetHamming() const { return hamming; }
/**
* Gets a measure of the quality of the binary decoding process: the
* average difference between the intensity of a data bit versus
* the decision threshold. Higher numbers roughly indicate better
* decodes. This is a reasonable measure of detection accuracy
* only for very small tags-- not effective for larger tags (where
* we could have sampled anywhere within a bit cell and still
* gotten a good detection.)
*
* @return Decision margin
*/
float GetDecisionMargin() const { return decision_margin; }
/**
* Gets the 3x3 homography matrix describing the projection from an
* "ideal" tag (with corners at (-1,1), (1,1), (1,-1), and (-1,
* -1)) to pixels in the image.
*
* @return Homography matrix data
*/
std::span<const double, 9> GetHomography() const;
/**
* Gets the 3x3 homography matrix describing the projection from an
* "ideal" tag (with corners at (-1,1), (1,1), (1,-1), and (-1,
* -1)) to pixels in the image.
*
* @return Homography matrix
*/
Eigen::Matrix3d GetHomographyMatrix() const;
/**
* Gets the center of the detection in image pixel coordinates.
*
* @return Center point
*/
const Point& GetCenter() const { return *reinterpret_cast<const Point*>(c); }
/**
* Gets a corner of the tag in image pixel coordinates. These always
* wrap counter-clock wise around the tag.
*
* @param ndx Corner index (range is 0-3, inclusive)
* @return Corner point
*/
const Point& GetCorner(int ndx) const {
return *reinterpret_cast<const Point*>(p[ndx]);
}
/**
* Gets the corners of the tag in image pixel coordinates. These always
* wrap counter-clock wise around the tag.
*
* @param cornersBuf Corner point array (X and Y for each corner in order)
* @return Corner point array (copy of cornersBuf span)
*/
std::span<double, 8> GetCorners(std::span<double, 8> cornersBuf) const {
for (int i = 0; i < 4; i++) {
cornersBuf[i * 2] = p[i][0];
cornersBuf[i * 2 + 1] = p[i][1];
}
return cornersBuf;
}
private:
// This class *must* be standard-layout-compatible with apriltag_detection
// as we use reinterpret_cast from that structure. This means the below
// members must exactly match the contents of the apriltag_detection struct.
// The tag family.
void* family;
// The decoded ID of the tag.
int id;
// How many error bits were corrected? Note: accepting large numbers of
// corrected errors leads to greatly increased false positive rates.
// NOTE: As of this implementation, the detector cannot detect tags with
// a hamming distance greater than 2.
int hamming;
// A measure of the quality of the binary decoding process: the
// average difference between the intensity of a data bit versus
// the decision threshold. Higher numbers roughly indicate better
// decodes. This is a reasonable measure of detection accuracy
// only for very small tags-- not effective for larger tags (where
// we could have sampled anywhere within a bit cell and still
// gotten a good detection.)
float decision_margin;
// The 3x3 homography matrix describing the projection from an
// "ideal" tag (with corners at (-1,1), (1,1), (1,-1), and (-1,
// -1)) to pixels in the image.
void* H;
// The center of the detection in image pixel coordinates.
double c[2];
// The corners of the tag in image pixel coordinates. These always
// wrap counter-clock wise around the tag.
double p[4][2];
};
} // namespace frc

View File

@@ -0,0 +1,260 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#include <stdint.h>
#include <memory>
#include <span>
#include <string_view>
#include <utility>
#include <units/angle.h>
#include <wpi/StringMap.h>
#include <wpi/SymbolExports.h>
#include "frc/apriltag/AprilTagDetection.h"
namespace frc {
/**
* An AprilTag detector engine. This is expensive to set up and tear down, so
* most use cases should only create one of these, add a family to it, set up
* any other configuration, and repeatedly call Detect().
*/
class WPILIB_DLLEXPORT AprilTagDetector {
public:
/** Detector configuration. */
struct Config {
bool operator==(const Config&) const = default;
/**
* How many threads should be used for computation. Default is
* single-threaded operation (1 thread).
*/
int numThreads = 1;
/**
* Quad decimation. Detection of quads can be done on a lower-resolution
* image, improving speed at a cost of pose accuracy and a slight decrease
* in detection rate. Decoding the binary payload is still done at full
* resolution. Default is 2.0.
*/
float quadDecimate = 2.0f;
/**
* What Gaussian blur should be applied to the segmented image (used for
* quad detection). Very noisy images benefit from non-zero values (e.g.
* 0.8). Default is 0.0.
*/
float quadSigma = 0.0f;
/**
* When true, the edges of the each quad are adjusted to "snap to" strong
* gradients nearby. This is useful when decimation is employed, as it can
* increase the quality of the initial quad estimate substantially.
* Generally recommended to be on (true). Default is true.
*
* Very computationally inexpensive. Option is ignored if
* quad_decimate = 1.
*/
bool refineEdges = true;
/**
* How much sharpening should be done to decoded images. This can help
* decode small tags but may or may not help in odd lighting conditions or
* low light conditions. Default is 0.25.
*/
double decodeSharpening = 0.25;
/**
* Debug mode. When true, the decoder writes a variety of debugging images
* to the current working directory at various stages through the detection
* process. This is slow and should *not* be used on space-limited systems
* such as the RoboRIO. Default is disabled (false).
*/
bool debug = false;
};
/** Quad threshold parameters. */
struct QuadThresholdParameters {
bool operator==(const QuadThresholdParameters&) const = default;
/**
* Threshold used to reject quads containing too few pixels. Default is 5
* pixels.
*/
int minClusterPixels = 5;
/**
* How many corner candidates to consider when segmenting a group of pixels
* into a quad. Default is 10.
*/
int maxNumMaxima = 10;
/**
* Critical angle. The detector will reject quads where pairs of edges have
* angles that are close to straight or close to 180 degrees. Zero means
* that no quads are rejected. Default is 10 degrees.
*/
units::radian_t criticalAngle = 10_deg;
/**
* When fitting lines to the contours, the maximum mean squared error
* allowed. This is useful in rejecting contours that are far from being
* quad shaped; rejecting these quads "early" saves expensive decoding
* processing. Default is 10.0.
*/
float maxLineFitMSE = 10.0f;
/**
* Minimum brightness offset. When we build our model of black & white
* pixels, we add an extra check that the white model must be (overall)
* brighter than the black model. How much brighter? (in pixel values,
* [0,255]). Default is 5.
*/
int minWhiteBlackDiff = 5;
/**
* Whether the thresholded image be should be deglitched. Only useful for
* very noisy images. Default is disabled (false).
*/
bool deglitch = false;
};
/**
* Array of detection results. Each array element is a pointer to an
* AprilTagDetection.
*/
class WPILIB_DLLEXPORT Results
: public std::span<AprilTagDetection const* const> {
struct private_init {};
friend class AprilTagDetector;
public:
Results() = default;
Results(void* impl, const private_init&);
~Results() { Destroy(); }
Results(const Results&) = delete;
Results& operator=(const Results&) = delete;
Results(Results&& rhs) : span{std::move(rhs)}, m_impl{rhs.m_impl} {
rhs.m_impl = nullptr;
}
Results& operator=(Results&& rhs);
private:
void Destroy();
void* m_impl = nullptr;
};
AprilTagDetector();
~AprilTagDetector() { Destroy(); }
AprilTagDetector(const AprilTagDetector&) = delete;
AprilTagDetector& operator=(const AprilTagDetector&) = delete;
AprilTagDetector(AprilTagDetector&& rhs)
: m_impl{rhs.m_impl},
m_families{std::move(rhs.m_families)},
m_qtpCriticalAngle{rhs.m_qtpCriticalAngle} {
rhs.m_impl = nullptr;
}
AprilTagDetector& operator=(AprilTagDetector&& rhs);
/**
* @{
* @name Configuration functions
*/
/**
* Sets detector configuration.
*
* @param config Configuration
*/
void SetConfig(const Config& config);
/**
* Gets detector configuration.
*
* @return Configuration
*/
Config GetConfig() const;
/**
* Sets quad threshold parameters.
*
* @param params Parameters
*/
void SetQuadThresholdParameters(const QuadThresholdParameters& params);
/**
* Gets quad threshold parameters.
*
* @return Parameters
*/
QuadThresholdParameters GetQuadThresholdParameters() const;
/** @} */
/**
* @{
* @name Tag family functions
*/
/**
* Adds a family of tags to be detected.
*
* @param fam Family name, e.g. "tag16h5"
* @param bitsCorrected
* @return False if family can't be found
*/
bool AddFamily(std::string_view fam, int bitsCorrected = 2);
/**
* Removes a family of tags from the detector.
*
* @param fam Family name, e.g. "tag16h5"
*/
void RemoveFamily(std::string_view fam);
/**
* Unregister all families.
*/
void ClearFamilies();
/** @} */
/**
* Detect tags from an 8-bit image.
*
* @param width width of the image
* @param height height of the image
* @param stride number of bytes between image rows (often the same as width)
* @param buf image buffer
* @return Results (array of AprilTagDetection pointers)
*/
Results Detect(int width, int height, int stride, uint8_t* buf);
/**
* Detect tags from an 8-bit image.
*
* @param width width of the image
* @param height height of the image
* @param buf image buffer
* @return Results (array of AprilTagDetection pointers)
*/
Results Detect(int width, int height, uint8_t* buf) {
return Detect(width, height, width, buf);
}
private:
void Destroy();
void DestroyFamilies();
void DestroyFamily(std::string_view name, void* data);
void* m_impl;
wpi::StringMap<void*> m_families;
units::radian_t m_qtpCriticalAngle = 10_deg;
};
} // namespace frc

View File

@@ -0,0 +1,18 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#include <opencv2/core/mat.hpp>
#include "frc/apriltag/AprilTagDetector.h"
namespace frc {
inline AprilTagDetector::Results AprilTagDetect(AprilTagDetector& detector,
cv::Mat& image) {
return detector.Detect(image.cols, image.rows, image.data);
}
} // namespace frc

View File

@@ -31,13 +31,14 @@ namespace frc {
* "width" and "length" values. This is to account for arbitrary field sizes
* when transforming the poses.
*
* Pose3ds are assumed to be measured from the bottom-left corner of the field,
* when the blue alliance is at the left. By default, Pose3ds will be returned
* as declared when calling GetTagPose(int).
* SetOrigin(AprilTagFieldLayout::OriginPosition) can be used to transform the
* poses returned by GetTagPose(int) to be correct relative to a different
* coordinate frame.
*/
* Pose3ds in the JSON are measured using the normal FRC coordinate system, NWU
* with the origin at the bottom-right corner of the blue alliance wall.
* SetOrigin(OriginPosition) can be used to change the poses returned from
* GetTagPose(int) to be from the perspective of a specific alliance.
*
* Tag poses represent the center of the tag, with a zero rotation representing
* a tag that is upright and facing away from the (blue) alliance wall (that is,
* towards the opposing alliance). */
class WPILIB_DLLEXPORT AprilTagFieldLayout {
public:
enum class OriginPosition {
@@ -103,19 +104,8 @@ class WPILIB_DLLEXPORT AprilTagFieldLayout {
/*
* Checks equality between this AprilTagFieldLayout and another object.
*
* @param other The other object.
* @return Whether the two objects are equal.
*/
bool operator==(const AprilTagFieldLayout& other) const;
/**
* Checks inequality between this AprilTagFieldLayout and another object.
*
* @param other The other object.
* @return Whether the two objects are not equal.
*/
bool operator!=(const AprilTagFieldLayout& other) const;
bool operator==(const AprilTagFieldLayout&) const = default;
private:
std::unordered_map<int, AprilTag> m_apriltags;

View File

@@ -14,6 +14,7 @@ namespace frc {
enum class AprilTagField {
k2022RapidReact,
k2023ChargedUp,
// This is a placeholder for denoting the last supported field. This should
// always be the last entry in the enum and should not be used by users

View File

@@ -0,0 +1,36 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#include <wpi/SymbolExports.h>
#include "frc/geometry/Transform3d.h"
namespace frc {
/** A pair of AprilTag pose estimates. */
struct WPILIB_DLLEXPORT AprilTagPoseEstimate {
/** Pose 1. */
Transform3d pose1;
/** Pose 2. */
Transform3d pose2;
/** Object-space error of pose 1. */
double error1;
/** Object-space error of pose 2. */
double error2;
/**
* Gets the ratio of pose reprojection errors, called ambiguity. Numbers
* above 0.2 are likely to be ambiguous.
*
* @return The ratio of pose reprojection errors.
*/
double GetAmbiguity() const;
};
} // namespace frc

View File

@@ -0,0 +1,145 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#include <span>
#include <units/length.h>
#include <wpi/SymbolExports.h>
#include "frc/apriltag/AprilTagPoseEstimate.h"
#include "frc/geometry/Transform3d.h"
namespace frc {
class AprilTagDetection;
/** Pose estimators for AprilTag tags. */
class WPILIB_DLLEXPORT AprilTagPoseEstimator {
public:
/** Configuration for the pose estimator. */
struct Config {
bool operator==(const Config&) const = default;
/** The tag size. */
units::meter_t tagSize;
/** Camera horizontal focal length, in pixels. */
double fx;
/** Camera vertical focal length, in pixels. */
double fy;
/** Camera horizontal focal center, in pixels. */
double cx;
/** Camera vertical focal center, in pixels. */
double cy;
};
/**
* Creates estimator.
*
* @param config Configuration
*/
explicit AprilTagPoseEstimator(const Config& config) : m_config{config} {}
/**
* Sets estimator configuration.
*
* @param config Configuration
*/
void SetConfig(const Config& config) { m_config = config; }
/**
* Gets estimator configuration.
*
* @return Configuration
*/
const Config& GetConfig() const { return m_config; }
/**
* Estimates the pose of the tag using the homography method described in [1].
*
* @param detection Tag detection
* @return Pose estimate
*/
Transform3d EstimateHomography(const AprilTagDetection& detection) const;
/**
* Estimates the pose of the tag using the homography method described in [1].
*
* @param homography Homography 3x3 matrix data
* @return Pose estimate
*/
Transform3d EstimateHomography(std::span<const double, 9> homography) const;
/**
* Estimates the pose of the tag. This returns one or two possible poses for
* the tag, along with the object-space error of each.
*
* This uses the homography method described in [1] for the initial estimate.
* Then Orthogonal Iteration [2] is used to refine this estimate. Then [3] is
* used to find a potential second local minima and Orthogonal Iteration is
* used to refine this second estimate.
*
* [1]: E. Olson, “Apriltag: A robust and flexible visual fiducial system,” in
* 2011 IEEE International Conference on Robotics and Automation,
* May 2011, pp. 34003407.
* [2]: Lu, G. D. Hager and E. Mjolsness, "Fast and globally convergent pose
* estimation from video images," in IEEE Transactions on Pattern
* Analysis and Machine Intelligence, vol. 22, no. 6, pp. 610-622, June 2000.
* doi: 10.1109/34.862199
* [3]: Schweighofer and A. Pinz, "Robust Pose Estimation from a Planar
* Target," in IEEE Transactions on Pattern Analysis and Machine Intelligence,
* vol. 28, no. 12, pp. 2024-2030, Dec. 2006. doi: 10.1109/TPAMI.2006.252
*
* @param detection Tag detection
* @param nIters Number of iterations
* @return Initial and (possibly) second pose estimates
*/
AprilTagPoseEstimate EstimateOrthogonalIteration(
const AprilTagDetection& detection, int nIters) const;
/**
* Estimates the pose of the tag. This returns one or two possible poses for
* the tag, along with the object-space error of each.
*
* @param homography Homography 3x3 matrix data
* @param corners Corner point array (X and Y for each corner in order)
* @param nIters Number of iterations
* @return Initial and (possibly) second pose estimates
*/
AprilTagPoseEstimate EstimateOrthogonalIteration(
std::span<const double, 9> homography, std::span<const double, 8> corners,
int nIters) const;
/**
* Estimates tag pose. This method is an easier to use interface to
* EstimatePoseOrthogonalIteration(), running 50 iterations and returning the
* pose with the lower object-space error.
*
* @param detection Tag detection
* @return Pose estimate
*/
Transform3d Estimate(const AprilTagDetection& detection) const;
/**
* Estimates tag pose. This method is an easier to use interface to
* EstimatePoseOrthogonalIteration(), running 50 iterations and returning the
* pose with the lower object-space error.
*
* @param homography Homography 3x3 matrix data
* @param corners Corner point array (X and Y for each corner in order)
* @return Pose estimate
*/
Transform3d Estimate(std::span<const double, 9> homography,
std::span<const double, 8> corners) const;
private:
Config m_config;
};
} // namespace frc

View File

@@ -1,415 +1,440 @@
{
"tags" : [ {
"ID" : 0,
"pose" : {
"translation" : {
"x" : -0.0035306,
"y" : 7.578928199999999,
"z" : 0.8858503999999999
},
"rotation" : {
"quaternion" : {
"W" : 1.0,
"X" : 0.0,
"Y" : 0.0,
"Z" : 0.0
"tags": [
{
"ID": 0,
"pose": {
"translation": {
"x": -0.0035306,
"y": 7.578928199999999,
"z": 0.8858503999999999
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 1,
"pose": {
"translation": {
"x": 3.2327088,
"y": 5.486654,
"z": 1.7254728
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 2,
"pose": {
"translation": {
"x": 3.067812,
"y": 5.3305202,
"z": 1.3762228
},
"rotation": {
"quaternion": {
"W": 0.7071067811865476,
"X": 0.0,
"Y": 0.0,
"Z": -0.7071067811865475
}
}
}
},
{
"ID": 3,
"pose": {
"translation": {
"x": 0.0039878,
"y": 5.058536999999999,
"z": 0.80645
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 4,
"pose": {
"translation": {
"x": 0.0039878,
"y": 3.5124898,
"z": 0.80645
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 5,
"pose": {
"translation": {
"x": 0.12110719999999998,
"y": 1.7178274,
"z": 0.8906002000000001
},
"rotation": {
"quaternion": {
"W": 0.9196502204050923,
"X": 0.0,
"Y": 0.0,
"Z": 0.39273842708457407
}
}
}
},
{
"ID": 6,
"pose": {
"translation": {
"x": 0.8733027999999999,
"y": 0.9412985999999999,
"z": 0.8906002000000001
},
"rotation": {
"quaternion": {
"W": 0.9196502204050923,
"X": 0.0,
"Y": 0.0,
"Z": 0.39273842708457407
}
}
}
},
{
"ID": 7,
"pose": {
"translation": {
"x": 1.6150844,
"y": 0.15725139999999999,
"z": 0.8906002000000001
},
"rotation": {
"quaternion": {
"W": 0.9196502204050923,
"X": 0.0,
"Y": 0.0,
"Z": 0.39273842708457407
}
}
}
},
{
"ID": 10,
"pose": {
"translation": {
"x": 16.4627306,
"y": 0.6506718,
"z": 0.8858503999999999
},
"rotation": {
"quaternion": {
"W": 6.123233995736766E-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 11,
"pose": {
"translation": {
"x": 13.2350002,
"y": 2.743454,
"z": 1.7254728
},
"rotation": {
"quaternion": {
"W": 6.123233995736766E-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 12,
"pose": {
"translation": {
"x": 13.391388000000001,
"y": 2.8998418,
"z": 1.3762228
},
"rotation": {
"quaternion": {
"W": 0.7071067811865476,
"X": 0.0,
"Y": 0.0,
"Z": 0.7071067811865475
}
}
}
},
{
"ID": 13,
"pose": {
"translation": {
"x": 16.4552122,
"y": 3.1755079999999998,
"z": 0.80645
},
"rotation": {
"quaternion": {
"W": 6.123233995736766E-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 14,
"pose": {
"translation": {
"x": 16.4552122,
"y": 4.7171356,
"z": 0.80645
},
"rotation": {
"quaternion": {
"W": 6.123233995736766E-17,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 15,
"pose": {
"translation": {
"x": 16.3350194,
"y": 6.5149729999999995,
"z": 0.8937752
},
"rotation": {
"quaternion": {
"W": -0.37298778257580906,
"X": -0.0,
"Y": 0.0,
"Z": 0.9278362538989199
}
}
}
},
{
"ID": 16,
"pose": {
"translation": {
"x": 15.5904946,
"y": 7.292695599999999,
"z": 0.8906002000000001
},
"rotation": {
"quaternion": {
"W": -0.37298778257580906,
"X": -0.0,
"Y": 0.0,
"Z": 0.9278362538989199
}
}
}
},
{
"ID": 17,
"pose": {
"translation": {
"x": 14.847188999999998,
"y": 8.0691228,
"z": 0.8906002000000001
},
"rotation": {
"quaternion": {
"W": -0.37298778257580906,
"X": -0.0,
"Y": 0.0,
"Z": 0.9278362538989199
}
}
}
},
{
"ID": 40,
"pose": {
"translation": {
"x": 7.874127,
"y": 4.9131728,
"z": 0.7032752
},
"rotation": {
"quaternion": {
"W": 0.5446390350150271,
"X": 0.0,
"Y": 0.0,
"Z": 0.838670567945424
}
}
}
},
{
"ID": 41,
"pose": {
"translation": {
"x": 7.4312271999999995,
"y": 3.759327,
"z": 0.7032752
},
"rotation": {
"quaternion": {
"W": -0.20791169081775934,
"X": -0.0,
"Y": 0.0,
"Z": 0.9781476007338057
}
}
}
},
{
"ID": 42,
"pose": {
"translation": {
"x": 8.585073,
"y": 3.3164272,
"z": 0.7032752
},
"rotation": {
"quaternion": {
"W": 0.838670567945424,
"X": 0.0,
"Y": 0.0,
"Z": -0.5446390350150271
}
}
}
},
{
"ID": 43,
"pose": {
"translation": {
"x": 9.0279728,
"y": 4.470273,
"z": 0.7032752
},
"rotation": {
"quaternion": {
"W": 0.9781476007338057,
"X": 0.0,
"Y": 0.0,
"Z": 0.20791169081775934
}
}
}
},
{
"ID": 50,
"pose": {
"translation": {
"x": 7.6790296,
"y": 4.3261534,
"z": 2.4177244
},
"rotation": {
"quaternion": {
"W": 0.17729273396782605,
"X": -0.22744989571511945,
"Y": 0.04215534644161733,
"Z": 0.9565859910053995
}
}
}
},
{
"ID": 51,
"pose": {
"translation": {
"x": 8.0182466,
"y": 3.5642296,
"z": 2.4177244
},
"rotation": {
"quaternion": {
"W": -0.5510435465842192,
"X": -0.19063969497246985,
"Y": -0.13102303230819815,
"Z": 0.8017733354717242
}
}
}
},
{
"ID": 52,
"pose": {
"translation": {
"x": 8.7801704,
"y": 3.9034466,
"z": 2.4177244
},
"rotation": {
"quaternion": {
"W": -0.9565859910053994,
"X": -0.04215534644161739,
"Y": -0.22744989571511942,
"Z": 0.17729273396782633
}
}
}
},
{
"ID": 53,
"pose": {
"translation": {
"x": 8.4409534,
"y": 4.6653704,
"z": 2.4177244
},
"rotation": {
"quaternion": {
"W": 0.8017733354717241,
"X": -0.1310230323081982,
"Y": 0.19063969497246983,
"Z": 0.5510435465842194
}
}
}
}
}, {
"ID" : 1,
"pose" : {
"translation" : {
"x" : 3.2327088,
"y" : 5.486654,
"z" : 1.7254728
},
"rotation" : {
"quaternion" : {
"W" : 1.0,
"X" : 0.0,
"Y" : 0.0,
"Z" : 0.0
}
}
}
}, {
"ID" : 2,
"pose" : {
"translation" : {
"x" : 3.067812,
"y" : 5.3305202,
"z" : 1.3762228
},
"rotation" : {
"quaternion" : {
"W" : 0.7071067811865476,
"X" : 0.0,
"Y" : 0.0,
"Z" : -0.7071067811865475
}
}
}
}, {
"ID" : 3,
"pose" : {
"translation" : {
"x" : 0.0039878,
"y" : 5.058536999999999,
"z" : 0.80645
},
"rotation" : {
"quaternion" : {
"W" : 1.0,
"X" : 0.0,
"Y" : 0.0,
"Z" : 0.0
}
}
}
}, {
"ID" : 4,
"pose" : {
"translation" : {
"x" : 0.0039878,
"y" : 3.5124898,
"z" : 0.80645
},
"rotation" : {
"quaternion" : {
"W" : 1.0,
"X" : 0.0,
"Y" : 0.0,
"Z" : 0.0
}
}
}
}, {
"ID" : 5,
"pose" : {
"translation" : {
"x" : 0.12110719999999998,
"y" : 1.7178274,
"z" : 0.8906002000000001
},
"rotation" : {
"quaternion" : {
"W" : 0.9196502204050923,
"X" : 0.0,
"Y" : 0.0,
"Z" : 0.39273842708457407
}
}
}
}, {
"ID" : 6,
"pose" : {
"translation" : {
"x" : 0.8733027999999999,
"y" : 0.9412985999999999,
"z" : 0.8906002000000001
},
"rotation" : {
"quaternion" : {
"W" : 0.9196502204050923,
"X" : 0.0,
"Y" : 0.0,
"Z" : 0.39273842708457407
}
}
}
}, {
"ID" : 7,
"pose" : {
"translation" : {
"x" : 1.6150844,
"y" : 0.15725139999999999,
"z" : 0.8906002000000001
},
"rotation" : {
"quaternion" : {
"W" : 0.9196502204050923,
"X" : 0.0,
"Y" : 0.0,
"Z" : 0.39273842708457407
}
}
}
}, {
"ID" : 10,
"pose" : {
"translation" : {
"x" : 16.4627306,
"y" : 0.6506718,
"z" : 0.8858503999999999
},
"rotation" : {
"quaternion" : {
"W" : 6.123233995736766E-17,
"X" : 0.0,
"Y" : 0.0,
"Z" : 1.0
}
}
}
}, {
"ID" : 11,
"pose" : {
"translation" : {
"x" : 13.2350002,
"y" : 2.743454,
"z" : 1.7254728
},
"rotation" : {
"quaternion" : {
"W" : 6.123233995736766E-17,
"X" : 0.0,
"Y" : 0.0,
"Z" : 1.0
}
}
}
}, {
"ID" : 12,
"pose" : {
"translation" : {
"x" : 13.391388000000001,
"y" : 2.8998418,
"z" : 1.3762228
},
"rotation" : {
"quaternion" : {
"W" : 0.7071067811865476,
"X" : 0.0,
"Y" : 0.0,
"Z" : 0.7071067811865475
}
}
}
}, {
"ID" : 13,
"pose" : {
"translation" : {
"x" : 16.4552122,
"y" : 3.1755079999999998,
"z" : 0.80645
},
"rotation" : {
"quaternion" : {
"W" : 6.123233995736766E-17,
"X" : 0.0,
"Y" : 0.0,
"Z" : 1.0
}
}
}
}, {
"ID" : 14,
"pose" : {
"translation" : {
"x" : 16.4552122,
"y" : 4.7171356,
"z" : 0.80645
},
"rotation" : {
"quaternion" : {
"W" : 6.123233995736766E-17,
"X" : 0.0,
"Y" : 0.0,
"Z" : 1.0
}
}
}
}, {
"ID" : 15,
"pose" : {
"translation" : {
"x" : 16.3350194,
"y" : 6.5149729999999995,
"z" : 0.8937752
},
"rotation" : {
"quaternion" : {
"W" : -0.37298778257580906,
"X" : -0.0,
"Y" : 0.0,
"Z" : 0.9278362538989199
}
}
}
}, {
"ID" : 16,
"pose" : {
"translation" : {
"x" : 15.5904946,
"y" : 7.292695599999999,
"z" : 0.8906002000000001
},
"rotation" : {
"quaternion" : {
"W" : -0.37298778257580906,
"X" : -0.0,
"Y" : 0.0,
"Z" : 0.9278362538989199
}
}
}
}, {
"ID" : 17,
"pose" : {
"translation" : {
"x" : 14.847188999999998,
"y" : 8.0691228,
"z" : 0.8906002000000001
},
"rotation" : {
"quaternion" : {
"W" : -0.37298778257580906,
"X" : -0.0,
"Y" : 0.0,
"Z" : 0.9278362538989199
}
}
}
}, {
"ID" : 40,
"pose" : {
"translation" : {
"x" : 7.874127,
"y" : 4.9131728,
"z" : 0.7032752
},
"rotation" : {
"quaternion" : {
"W" : 0.5446390350150271,
"X" : 0.0,
"Y" : 0.0,
"Z" : 0.838670567945424
}
}
}
}, {
"ID" : 41,
"pose" : {
"translation" : {
"x" : 7.4312271999999995,
"y" : 3.759327,
"z" : 0.7032752
},
"rotation" : {
"quaternion" : {
"W" : -0.20791169081775934,
"X" : -0.0,
"Y" : 0.0,
"Z" : 0.9781476007338057
}
}
}
}, {
"ID" : 42,
"pose" : {
"translation" : {
"x" : 8.585073,
"y" : 3.3164272,
"z" : 0.7032752
},
"rotation" : {
"quaternion" : {
"W" : 0.838670567945424,
"X" : 0.0,
"Y" : 0.0,
"Z" : -0.5446390350150271
}
}
}
}, {
"ID" : 43,
"pose" : {
"translation" : {
"x" : 9.0279728,
"y" : 4.470273,
"z" : 0.7032752
},
"rotation" : {
"quaternion" : {
"W" : 0.9781476007338057,
"X" : 0.0,
"Y" : 0.0,
"Z" : 0.20791169081775934
}
}
}
}, {
"ID" : 50,
"pose" : {
"translation" : {
"x" : 7.6790296,
"y" : 4.3261534,
"z" : 2.4177244
},
"rotation" : {
"quaternion" : {
"W" : 0.17729273396782605,
"X" : -0.22744989571511945,
"Y" : 0.04215534644161733,
"Z" : 0.9565859910053995
}
}
}
}, {
"ID" : 51,
"pose" : {
"translation" : {
"x" : 8.0182466,
"y" : 3.5642296,
"z" : 2.4177244
},
"rotation" : {
"quaternion" : {
"W" : -0.5510435465842192,
"X" : -0.19063969497246985,
"Y" : -0.13102303230819815,
"Z" : 0.8017733354717242
}
}
}
}, {
"ID" : 52,
"pose" : {
"translation" : {
"x" : 8.7801704,
"y" : 3.9034466,
"z" : 2.4177244
},
"rotation" : {
"quaternion" : {
"W" : -0.9565859910053994,
"X" : -0.04215534644161739,
"Y" : -0.22744989571511942,
"Z" : 0.17729273396782633
}
}
}
}, {
"ID" : 53,
"pose" : {
"translation" : {
"x" : 8.4409534,
"y" : 4.6653704,
"z" : 2.4177244
},
"rotation" : {
"quaternion" : {
"W" : 0.8017733354717241,
"X" : -0.1310230323081982,
"Y" : 0.19063969497246983,
"Z" : 0.5510435465842194
}
}
}
} ],
"field" : {
"length" : 8.2296,
"width" : 16.4592
],
"field": {
"length": 16.4592,
"width": 8.2296
}
}

View File

@@ -0,0 +1,152 @@
{
"tags": [
{
"ID": 1,
"pose": {
"translation": {
"x": 15.513558,
"y": 1.071626,
"z": 0.462788
},
"rotation": {
"quaternion": {
"W": 0.0,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 2,
"pose": {
"translation": {
"x": 15.513558,
"y": 2.748026,
"z": 0.462788
},
"rotation": {
"quaternion": {
"W": 0.0,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 3,
"pose": {
"translation": {
"x": 15.513558,
"y": 4.424426,
"z": 0.462788
},
"rotation": {
"quaternion": {
"W": 0.0,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 4,
"pose": {
"translation": {
"x": 16.178784,
"y": 6.749796,
"z": 0.695452
},
"rotation": {
"quaternion": {
"W": 0.0,
"X": 0.0,
"Y": 0.0,
"Z": 1.0
}
}
}
},
{
"ID": 5,
"pose": {
"translation": {
"x": 0.36195,
"y": 6.749796,
"z": 0.695452
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 6,
"pose": {
"translation": {
"x": 1.02743,
"y": 4.424426,
"z": 0.462788
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 7,
"pose": {
"translation": {
"x": 1.02743,
"y": 2.748026,
"z": 0.462788
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
},
{
"ID": 8,
"pose": {
"translation": {
"x": 1.02743,
"y": 1.071626,
"z": 0.462788
},
"rotation": {
"quaternion": {
"W": 1.0,
"X": 0.0,
"Y": 0.0,
"Z": 0.0
}
}
}
}
],
"field": {
"length": 16.54175,
"width": 8.0137
}
}

View File

@@ -0,0 +1,264 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.apriltag;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.util.Units;
import edu.wpi.first.util.RuntimeLoader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
@SuppressWarnings("PMD.MutableStaticState")
class AprilTagDetectorTest {
@SuppressWarnings("MemberName")
AprilTagDetector detector;
static RuntimeLoader<Core> loader;
@BeforeAll
static void beforeAll() {
try {
loader =
new RuntimeLoader<>(
Core.NATIVE_LIBRARY_NAME, RuntimeLoader.getDefaultExtractionRoot(), Core.class);
loader.loadLibrary();
} catch (IOException ex) {
fail(ex);
}
}
@BeforeEach
void beforeEach() {
detector = new AprilTagDetector();
}
@AfterEach
void afterEach() {
detector.close();
}
@Test
void testConfigDefaults() {
var config = detector.getConfig();
assertEquals(new AprilTagDetector.Config(), config);
}
@Test
void testQtpDefaults() {
var params = detector.getQuadThresholdParameters();
assertEquals(new AprilTagDetector.QuadThresholdParameters(), params);
}
@Test
void testSetConfigNumThreads() {
var newConfig = new AprilTagDetector.Config();
newConfig.numThreads = 2;
detector.setConfig(newConfig);
var config = detector.getConfig();
assertEquals(2, config.numThreads);
}
@Test
void testQtpMinClusterPixels() {
var newParams = new AprilTagDetector.QuadThresholdParameters();
newParams.minClusterPixels = 8;
detector.setQuadThresholdParameters(newParams);
var params = detector.getQuadThresholdParameters();
assertEquals(8, params.minClusterPixels);
}
@Test
void testAdd16h5() {
assertDoesNotThrow(() -> detector.addFamily("tag16h5"));
// duplicate addition is also okay
assertDoesNotThrow(() -> detector.addFamily("tag16h5"));
}
@Test
void testAdd25h9() {
assertDoesNotThrow(() -> detector.addFamily("tag25h9"));
}
@Test
void testAdd36h11() {
assertDoesNotThrow(() -> detector.addFamily("tag36h11"));
}
@Test
void testAddMultiple() {
assertDoesNotThrow(() -> detector.addFamily("tag16h5"));
assertDoesNotThrow(() -> detector.addFamily("tag36h11"));
}
@Test
void testRemoveFamily() {
// okay to remove non-existent family
detector.removeFamily("tag16h5");
// add and remove
detector.addFamily("tag16h5");
detector.removeFamily("tag16h5");
}
@SuppressWarnings("PMD.AssignmentInOperand")
public Mat loadImage(String resource) throws IOException {
Mat encoded;
try (InputStream is = getClass().getResource(resource).openStream()) {
try (ByteArrayOutputStream os = new ByteArrayOutputStream(is.available())) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
encoded = new Mat(1, os.size(), CvType.CV_8U);
encoded.put(0, 0, os.toByteArray());
}
}
Mat image = Imgcodecs.imdecode(encoded, Imgcodecs.IMREAD_COLOR);
encoded.release();
Imgproc.cvtColor(image, image, Imgproc.COLOR_BGR2GRAY);
return image;
}
@Test
void testDecodeAndPose() {
detector.addFamily("tag16h5");
detector.addFamily("tag36h11");
Mat image;
try {
image = loadImage("tag1_640_480.jpg");
} catch (IOException ex) {
fail(ex);
return;
}
try {
AprilTagDetection[] results = detector.detect(image);
assertEquals(1, results.length);
assertEquals("tag36h11", results[0].getFamily());
assertEquals(1, results[0].getId());
assertEquals(0, results[0].getHamming());
var estimator =
new AprilTagPoseEstimator(new AprilTagPoseEstimator.Config(0.2, 500, 500, 320, 240));
AprilTagPoseEstimate est = estimator.estimateOrthogonalIteration(results[0], 50);
assertEquals(new Transform3d(), est.pose2);
Transform3d pose = estimator.estimate(results[0]);
assertEquals(est.pose1, pose);
} finally {
image.release();
}
}
/**
* This tag is rotated such that the top is closer to the camera than the bottom. In the camera
* frame, with +x to the right, this is a rotation about +X by 45 degrees.
*/
@Test
void testPoseRotatedX() {
detector.addFamily("tag16h5");
Mat image;
try {
image = loadImage("tag2_45deg_X.png");
} catch (IOException ex) {
fail(ex);
return;
}
try {
AprilTagDetection[] results = detector.detect(image);
assertEquals(1, results.length);
var estimator =
new AprilTagPoseEstimator(
new AprilTagPoseEstimator.Config(
0.2, 500, 500, image.cols() / 2.0, image.rows() / 2.0));
AprilTagPoseEstimate est = estimator.estimateOrthogonalIteration(results[0], 50);
assertEquals(Units.degreesToRadians(45), est.pose1.getRotation().getX(), 0.1);
assertEquals(Units.degreesToRadians(0), est.pose1.getRotation().getY(), 0.1);
assertEquals(Units.degreesToRadians(0), est.pose1.getRotation().getZ(), 0.1);
} finally {
image.release();
}
}
/**
* This tag is rotated such that the right is closer to the camera than the left. In the camera
* frame, with +y down, this is a rotation of 45 degrees about +y.
*/
@Test
void testPoseRotatedY() {
detector.addFamily("tag16h5");
Mat image;
try {
image = loadImage("tag2_45deg_y.png");
} catch (IOException ex) {
fail(ex);
return;
}
try {
AprilTagDetection[] results = detector.detect(image);
assertEquals(1, results.length);
var estimator =
new AprilTagPoseEstimator(
new AprilTagPoseEstimator.Config(
0.2, 500, 500, image.cols() / 2.0, image.rows() / 2.0));
AprilTagPoseEstimate est = estimator.estimateOrthogonalIteration(results[0], 50);
assertEquals(Units.degreesToRadians(0), est.pose1.getRotation().getX(), 0.1);
assertEquals(Units.degreesToRadians(45), est.pose1.getRotation().getY(), 0.1);
assertEquals(Units.degreesToRadians(0), est.pose1.getRotation().getZ(), 0.1);
} finally {
image.release();
}
}
/** This tag is facing right at the camera -- no rotation should be observed. */
@Test
void testPoseStraightOn() {
detector.addFamily("tag16h5");
Mat image;
try {
image = loadImage("tag2_16h5_straight.png");
} catch (IOException ex) {
fail(ex);
return;
}
try {
AprilTagDetection[] results = detector.detect(image);
assertEquals(1, results.length);
var estimator =
new AprilTagPoseEstimator(
new AprilTagPoseEstimator.Config(
0.2, 500, 500, image.cols() / 2.0, image.rows() / 2.0));
AprilTagPoseEstimate est = estimator.estimateOrthogonalIteration(results[0], 50);
assertEquals(Units.degreesToRadians(0), est.pose1.getRotation().getX(), 0.1);
assertEquals(Units.degreesToRadians(0), est.pose1.getRotation().getY(), 0.1);
assertEquals(Units.degreesToRadians(0), est.pose1.getRotation().getZ(), 0.1);
} finally {
image.release();
}
}
}

View File

@@ -23,16 +23,13 @@ class LoadConfigTest {
@ParameterizedTest
@EnumSource(AprilTagFields.class)
void testLoad(AprilTagFields field) {
AprilTagFieldLayout layout =
Assertions.assertDoesNotThrow(
() -> AprilTagFieldLayout.loadFromResource(field.m_resourceFile));
AprilTagFieldLayout layout = Assertions.assertDoesNotThrow(field::loadAprilTagLayoutField);
assertNotNull(layout);
}
@Test
void test2022RapidReact() throws IOException {
AprilTagFieldLayout layout =
AprilTagFieldLayout.loadFromResource(AprilTagFields.k2022RapidReact.m_resourceFile);
AprilTagFieldLayout layout = AprilTagFields.k2022RapidReact.loadAprilTagLayoutField();
// Blue Hangar Truss - Hub
Pose3d expectedPose =

View File

@@ -0,0 +1,67 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "frc/apriltag/AprilTagDetector.h"
#include "gtest/gtest.h"
using namespace frc;
TEST(AprilTagDetectorTest, ConfigDefaults) {
AprilTagDetector detector;
auto config = detector.GetConfig();
ASSERT_EQ(config, AprilTagDetector::Config{});
}
TEST(AprilTagDetectorTest, QtpDefaults) {
AprilTagDetector detector;
auto params = detector.GetQuadThresholdParameters();
ASSERT_EQ(params, AprilTagDetector::QuadThresholdParameters{});
}
TEST(AprilTagDetectorTest, SetConfigNumThreads) {
AprilTagDetector detector;
detector.SetConfig({.numThreads = 2});
auto config = detector.GetConfig();
ASSERT_EQ(config.numThreads, 2);
}
TEST(AprilTagDetectorTest, QtpMinClusterPixels) {
AprilTagDetector detector;
detector.SetQuadThresholdParameters({.minClusterPixels = 8});
auto params = detector.GetQuadThresholdParameters();
ASSERT_EQ(params.minClusterPixels, 8);
}
TEST(AprilTagDetectorTest, Add16h5) {
AprilTagDetector detector;
ASSERT_TRUE(detector.AddFamily("tag16h5"));
// duplicate addition is also okay
ASSERT_TRUE(detector.AddFamily("tag16h5"));
}
TEST(AprilTagDetectorTest, Add25h9) {
AprilTagDetector detector;
ASSERT_TRUE(detector.AddFamily("tag25h9"));
}
TEST(AprilTagDetectorTest, Add36h11) {
AprilTagDetector detector;
ASSERT_TRUE(detector.AddFamily("tag36h11"));
}
TEST(AprilTagDetectorTest, AddMultiple) {
AprilTagDetector detector;
ASSERT_TRUE(detector.AddFamily("tag16h5"));
ASSERT_TRUE(detector.AddFamily("tag36h11"));
}
TEST(AprilTagDetectorTest, RemoveFamily) {
AprilTagDetector detector;
// okay to remove non-existent family
detector.RemoveFamily("tag16h5");
// add and remove
detector.AddFamily("tag16h5");
detector.RemoveFamily("tag16h5");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -38,6 +38,7 @@ stages:
- stage: TestBench
displayName: Test Bench
condition: false
jobs:
- job: Cpp
displayName: C++

View File

@@ -13,7 +13,7 @@ buildscript {
plugins {
id 'base'
id 'edu.wpi.first.wpilib.versioning.WPILibVersioningPlugin' version '2023.0.0'
id 'edu.wpi.first.wpilib.versioning.WPILibVersioningPlugin' version '2023.0.1'
id 'edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin' version '2020.2'
id 'edu.wpi.first.NativeUtils' apply false
id 'edu.wpi.first.GradleJni' version '1.1.0'
@@ -22,7 +22,7 @@ plugins {
id 'visual-studio'
id 'net.ltgt.errorprone' version '2.0.2' apply false
id 'com.github.johnrengelman.shadow' version '7.1.2' apply false
id 'com.diffplug.spotless' version '6.4.2' apply false
id 'com.diffplug.spotless' version '6.12.0' apply false
id 'com.github.spotbugs' version '5.0.8' apply false
}

View File

@@ -1,9 +1,13 @@
repositories {
maven {
url "https://plugins.gradle.org/m2/"
mavenLocal()
maven {
url = 'https://frcmaven.wpi.edu/artifactory/ex-gradle'
}
mavenCentral()
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
implementation "edu.wpi.first:native-utils:2023.8.0"
implementation "edu.wpi.first:native-utils:2023.11.1"
}

View File

@@ -474,6 +474,7 @@ cs::UsbCamera CameraServer::StartAutomaticCapture() {
}
cs::UsbCamera CameraServer::StartAutomaticCapture(int dev) {
::GetInstance();
cs::UsbCamera camera{fmt::format("USB Camera {}", dev), dev};
StartAutomaticCapture(camera);
auto csShared = GetCameraServerShared();
@@ -483,6 +484,7 @@ cs::UsbCamera CameraServer::StartAutomaticCapture(int dev) {
cs::UsbCamera CameraServer::StartAutomaticCapture(std::string_view name,
int dev) {
::GetInstance();
cs::UsbCamera camera{name, dev};
StartAutomaticCapture(camera);
auto csShared = GetCameraServerShared();
@@ -492,6 +494,7 @@ cs::UsbCamera CameraServer::StartAutomaticCapture(std::string_view name,
cs::UsbCamera CameraServer::StartAutomaticCapture(std::string_view name,
std::string_view path) {
::GetInstance();
cs::UsbCamera camera{name, path};
StartAutomaticCapture(camera);
auto csShared = GetCameraServerShared();
@@ -517,6 +520,7 @@ cs::AxisCamera CameraServer::AddAxisCamera(std::span<const std::string> hosts) {
cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
std::string_view host) {
::GetInstance();
cs::AxisCamera camera{name, host};
StartAutomaticCapture(camera);
auto csShared = GetCameraServerShared();
@@ -526,6 +530,7 @@ cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
const char* host) {
::GetInstance();
cs::AxisCamera camera{name, host};
StartAutomaticCapture(camera);
auto csShared = GetCameraServerShared();
@@ -535,6 +540,7 @@ cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
const std::string& host) {
::GetInstance();
cs::AxisCamera camera{name, host};
StartAutomaticCapture(camera);
auto csShared = GetCameraServerShared();
@@ -544,6 +550,7 @@ cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
std::span<const std::string> hosts) {
::GetInstance();
cs::AxisCamera camera{name, hosts};
StartAutomaticCapture(camera);
auto csShared = GetCameraServerShared();
@@ -552,10 +559,11 @@ cs::AxisCamera CameraServer::AddAxisCamera(std::string_view name,
}
cs::MjpegServer CameraServer::AddSwitchedCamera(std::string_view name) {
auto& inst = ::GetInstance();
// create a dummy CvSource
cs::CvSource source{name, cs::VideoMode::PixelFormat::kMJPEG, 160, 120, 30};
cs::MjpegServer server = StartAutomaticCapture(source);
::GetInstance().m_fixedSources[server.GetHandle()] = source.GetHandle();
inst.m_fixedSources[server.GetHandle()] = source.GetHandle();
return server;
}
@@ -632,6 +640,7 @@ cs::CvSink CameraServer::GetVideo(std::string_view name) {
cs::CvSource CameraServer::PutVideo(std::string_view name, int width,
int height) {
::GetInstance();
cs::CvSource source{name, cs::VideoMode::kMJPEG, width, height, 30};
StartAutomaticCapture(source);
return source;
@@ -648,6 +657,7 @@ cs::MjpegServer CameraServer::AddServer(std::string_view name) {
}
cs::MjpegServer CameraServer::AddServer(std::string_view name, int port) {
::GetInstance();
cs::MjpegServer server{name, port};
AddServer(server);
return server;

View File

@@ -13,6 +13,10 @@ cppSrcFileInclude {
\.cpp$
}
modifiableFileExclude {
objcpp
}
licenseUpdateExclude {
src/main/native/cpp/default_init_allocator\.h$
}

View File

@@ -11,6 +11,7 @@ file(GLOB
cscore_native_src src/main/native/cpp/*.cpp)
file(GLOB cscore_linux_src src/main/native/linux/*.cpp)
file(GLOB cscore_osx_src src/main/native/osx/*.cpp)
file(GLOB cscore_osx_objc_src src/main/native/objcpp/*.mm)
file(GLOB cscore_windows_src src/main/native/windows/*.cpp)
add_library(cscore ${cscore_native_src})
@@ -18,7 +19,9 @@ set_target_properties(cscore PROPERTIES DEBUG_POSTFIX "d")
if(NOT MSVC)
if (APPLE)
target_sources(cscore PRIVATE ${cscore_osx_src})
target_sources(cscore PRIVATE ${cscore_osx_src} ${cscore_osx_objc_src})
target_compile_options(cscore PRIVATE "-fobjc-arc")
set_target_properties(cscore PROPERTIES LINK_FLAGS "-framework CoreFoundation -framework AVFoundation -framework Foundation -framework CoreMedia -framework CoreVideo")
else()
target_sources(cscore PRIVATE ${cscore_linux_src})
endif()

View File

@@ -2,16 +2,16 @@ import org.gradle.internal.os.OperatingSystem
ext {
nativeName = 'cscore'
devMain = 'edu.wpi.cscore.DevMain'
devMain = 'edu.wpi.first.cscore.DevMain'
}
// Removed because having the objective-cpp plugin added breaks
// embedded tools and its toolchain check. It causes an obj-cpp
// source set to be added to all binaries, even cross binaries
// with no support.
// if (OperatingSystem.current().isMacOsX()) {
// apply plugin: 'objective-cpp'
// }
if (OperatingSystem.current().isMacOsX()) {
apply plugin: 'objective-cpp'
}
apply from: "${rootDir}/shared/jni/setupBuild.gradle"
@@ -61,8 +61,13 @@ model {
if (!it.buildable || !(it instanceof NativeBinarySpec)) {
return
}
lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
if (it.component.name == "${nativeName}JNI") {
lib project: ':wpinet', library: 'wpinet', linkage: 'static'
lib project: ':wpiutil', library: 'wpiutil', linkage: 'static'
} else {
lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
}
}
}
}
@@ -82,16 +87,16 @@ ext {
splitSetup = {
if (it.targetPlatform.operatingSystem.isMacOsX()) {
it.sources {
// macObjCpp(ObjectiveCppSourceSet) {
// source {
// srcDirs = ['src/main/native/objcpp']
// include '**/*.mm'
// }
// exportedHeaders {
// srcDirs 'src/main/native/include'
// include '**/*.h'
// }
// }
macObjCpp(ObjectiveCppSourceSet) {
source {
srcDirs = ['src/main/native/objcpp']
include '**/*.mm'
}
exportedHeaders {
srcDirs 'src/main/native/include', 'src/main/native/cpp'
include '**/*.h'
}
}
cscoreMacCpp(CppSourceSet) {
source {
srcDirs 'src/main/native/osx'
@@ -152,6 +157,12 @@ Action<List<String>> symbolFilter = { symbols ->
symbols.removeIf({ !it.startsWith('CS_') })
} as Action<List<String>>;
run {
if (OperatingSystem.current().isMacOsX()) {
jvmArgs("-XstartOnFirstThread");
}
}
nativeUtils.exportsConfigs {
cscore {
x64ExcludeSymbols = [
@@ -176,6 +187,8 @@ nativeUtils.exportsConfigs {
}
}
apply from: "${rootDir}/shared/imgui.gradle"
model {
components {
examplesMap.each { key, value ->
@@ -188,7 +201,7 @@ model {
lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
lib project: ':wpigui', library: 'wpigui', linkage: 'static'
lib library: 'cscore', linkage: 'shared'
nativeUtils.useRequiredLibrary(it, 'imgui_static')
nativeUtils.useRequiredLibrary(it, 'imgui')
if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) {
it.buildable = false
return

View File

@@ -390,4 +390,10 @@ public class CameraServerJNI {
public static native long allocateRawFrame();
public static native void freeRawFrame(long frame);
public static native void runMainRunLoop();
public static native int runMainRunLoopTimeout(double timeoutSeconds);
public static native void stopMainRunLoop();
}

View File

@@ -13,7 +13,9 @@ public class VideoMode {
kYUYV(2),
kRGB565(3),
kBGR(4),
kGray(5);
kGray(5),
kY16(6),
kUYVY(7);
private final int value;

View File

@@ -217,7 +217,7 @@ Image* Frame::ConvertImpl(Image* image, VideoMode::PixelFormat pixelFormat,
// Color convert
switch (pixelFormat) {
case VideoMode::kRGB565:
// If source is YUYV or Gray, need to convert to BGR first
// If source is YUYV, UYVY, Gray, or Y16, need to convert to BGR first
if (cur->pixelFormat == VideoMode::kYUYV) {
// Check to see if BGR version already exists...
if (Image* newImage =
@@ -226,6 +226,14 @@ Image* Frame::ConvertImpl(Image* image, VideoMode::PixelFormat pixelFormat,
} else {
cur = ConvertYUYVToBGR(cur);
}
} else if (cur->pixelFormat == VideoMode::kUYVY) {
// Check to see if BGR version already exists...
if (Image* newImage =
GetExistingImage(cur->width, cur->height, VideoMode::kBGR)) {
cur = newImage;
} else {
cur = ConvertUYVYToBGR(cur);
}
} else if (cur->pixelFormat == VideoMode::kGray) {
// Check to see if BGR version already exists...
if (Image* newImage =
@@ -234,18 +242,35 @@ Image* Frame::ConvertImpl(Image* image, VideoMode::PixelFormat pixelFormat,
} else {
cur = ConvertGrayToBGR(cur);
}
}
return ConvertBGRToRGB565(cur);
case VideoMode::kGray:
// If source is YUYV or RGB565, need to convert to BGR first
if (cur->pixelFormat == VideoMode::kYUYV) {
} else if (cur->pixelFormat == VideoMode::kY16) {
// Check to see if BGR version already exists...
if (Image* newImage =
GetExistingImage(cur->width, cur->height, VideoMode::kBGR)) {
cur = newImage;
} else if (Image* newImage = GetExistingImage(cur->width, cur->height,
VideoMode::kGray)) {
cur = ConvertGrayToBGR(newImage);
} else {
cur = ConvertYUYVToBGR(cur);
cur = ConvertGrayToBGR(ConvertY16ToGray(cur));
}
}
return ConvertBGRToRGB565(cur);
case VideoMode::kGray:
case VideoMode::kY16:
// If source is also grayscale, convert directly
if (pixelFormat == VideoMode::kGray &&
cur->pixelFormat == VideoMode::kY16) {
return ConvertY16ToGray(cur);
} else if (pixelFormat == VideoMode::kY16 &&
cur->pixelFormat == VideoMode::kGray) {
return ConvertGrayToY16(cur);
}
// If source is YUYV, UYVY, convert directly to Gray
// If RGB565, need to convert to BGR first
if (cur->pixelFormat == VideoMode::kYUYV) {
cur = ConvertYUYVToGray(cur);
} else if (cur->pixelFormat == VideoMode::kUYVY) {
cur = ConvertUYVYToGray(cur);
} else if (cur->pixelFormat == VideoMode::kRGB565) {
// Check to see if BGR version already exists...
if (Image* newImage =
@@ -254,12 +279,18 @@ Image* Frame::ConvertImpl(Image* image, VideoMode::PixelFormat pixelFormat,
} else {
cur = ConvertRGB565ToBGR(cur);
}
cur = ConvertBGRToGray(cur);
}
return ConvertBGRToGray(cur);
if (pixelFormat == VideoMode::kY16) {
cur = ConvertGrayToY16(cur);
}
return cur;
case VideoMode::kBGR:
case VideoMode::kMJPEG:
if (cur->pixelFormat == VideoMode::kYUYV) {
cur = ConvertYUYVToBGR(cur);
} else if (cur->pixelFormat == VideoMode::kUYVY) {
cur = ConvertUYVYToBGR(cur);
} else if (cur->pixelFormat == VideoMode::kRGB565) {
cur = ConvertRGB565ToBGR(cur);
} else if (cur->pixelFormat == VideoMode::kGray) {
@@ -268,9 +299,23 @@ Image* Frame::ConvertImpl(Image* image, VideoMode::PixelFormat pixelFormat,
} else {
return ConvertGrayToMJPEG(cur, defaultJpegQuality);
}
} else if (cur->pixelFormat == VideoMode::kY16) {
// Check to see if Gray version already exists...
if (Image* newImage =
GetExistingImage(cur->width, cur->height, VideoMode::kGray)) {
cur = newImage;
} else {
cur = ConvertY16ToGray(cur);
}
if (pixelFormat == VideoMode::kBGR) {
return ConvertGrayToBGR(cur);
} else {
return ConvertGrayToMJPEG(cur, defaultJpegQuality);
}
}
break;
case VideoMode::kYUYV:
case VideoMode::kUYVY:
default:
return nullptr; // Unsupported
}
@@ -351,6 +396,72 @@ Image* Frame::ConvertYUYVToBGR(Image* image) {
return rv;
}
Image* Frame::ConvertYUYVToGray(Image* image) {
if (!image || image->pixelFormat != VideoMode::kYUYV) {
return nullptr;
}
// Allocate a grayscale image
auto newImage =
m_impl->source.AllocImage(VideoMode::kGray, image->width, image->height,
image->width * image->height);
// Convert
cv::cvtColor(image->AsMat(), newImage->AsMat(), cv::COLOR_YUV2GRAY_YUYV);
// Save the result
Image* rv = newImage.release();
if (m_impl) {
std::scoped_lock lock(m_impl->mutex);
m_impl->images.push_back(rv);
}
return rv;
}
Image* Frame::ConvertUYVYToBGR(Image* image) {
if (!image || image->pixelFormat != VideoMode::kUYVY) {
return nullptr;
}
// Allocate a BGR image
auto newImage =
m_impl->source.AllocImage(VideoMode::kBGR, image->width, image->height,
image->width * image->height * 3);
// Convert
cv::cvtColor(image->AsMat(), newImage->AsMat(), cv::COLOR_YUV2BGR_UYVY);
// Save the result
Image* rv = newImage.release();
if (m_impl) {
std::scoped_lock lock(m_impl->mutex);
m_impl->images.push_back(rv);
}
return rv;
}
Image* Frame::ConvertUYVYToGray(Image* image) {
if (!image || image->pixelFormat != VideoMode::kUYVY) {
return nullptr;
}
// Allocate a grayscale image
auto newImage =
m_impl->source.AllocImage(VideoMode::kGray, image->width, image->height,
image->width * image->height);
// Convert
cv::cvtColor(image->AsMat(), newImage->AsMat(), cv::COLOR_YUV2GRAY_UYVY);
// Save the result
Image* rv = newImage.release();
if (m_impl) {
std::scoped_lock lock(m_impl->mutex);
m_impl->images.push_back(rv);
}
return rv;
}
Image* Frame::ConvertBGRToRGB565(Image* image) {
if (!image || image->pixelFormat != VideoMode::kBGR) {
return nullptr;
@@ -509,6 +620,50 @@ Image* Frame::ConvertGrayToMJPEG(Image* image, int quality) {
return rv;
}
Image* Frame::ConvertGrayToY16(Image* image) {
if (!image || image->pixelFormat != VideoMode::kGray) {
return nullptr;
}
// Allocate a Y16 image
auto newImage =
m_impl->source.AllocImage(VideoMode::kY16, image->width, image->height,
image->width * image->height * 2);
// Convert with linear scaling
image->AsMat().convertTo(newImage->AsMat(), CV_16U, 256);
// Save the result
Image* rv = newImage.release();
if (m_impl) {
std::scoped_lock lock(m_impl->mutex);
m_impl->images.push_back(rv);
}
return rv;
}
Image* Frame::ConvertY16ToGray(Image* image) {
if (!image || image->pixelFormat != VideoMode::kY16) {
return nullptr;
}
// Allocate a Grayscale image
auto newImage =
m_impl->source.AllocImage(VideoMode::kGray, image->width, image->height,
image->width * image->height);
// Scale min to 0 and max to 255
cv::normalize(image->AsMat(), newImage->AsMat(), 255, 0, cv::NORM_MINMAX);
// Save the result
Image* rv = newImage.release();
if (m_impl) {
std::scoped_lock lock(m_impl->mutex);
m_impl->images.push_back(rv);
}
return rv;
}
Image* Frame::GetImageImpl(int width, int height,
VideoMode::PixelFormat pixelFormat,
int requiredJpegQuality, int defaultJpegQuality) {

View File

@@ -195,12 +195,17 @@ class Frame {
Image* ConvertMJPEGToBGR(Image* image);
Image* ConvertMJPEGToGray(Image* image);
Image* ConvertYUYVToBGR(Image* image);
Image* ConvertYUYVToGray(Image* image);
Image* ConvertUYVYToBGR(Image* image);
Image* ConvertUYVYToGray(Image* image);
Image* ConvertBGRToRGB565(Image* image);
Image* ConvertRGB565ToBGR(Image* image);
Image* ConvertBGRToGray(Image* image);
Image* ConvertGrayToBGR(Image* image);
Image* ConvertBGRToMJPEG(Image* image, int quality);
Image* ConvertGrayToMJPEG(Image* image, int quality);
Image* ConvertGrayToY16(Image* image);
Image* ConvertY16ToGray(Image* image);
Image* GetImage(int width, int height, VideoMode::PixelFormat pixelFormat) {
if (pixelFormat == VideoMode::kMJPEG) {

View File

@@ -74,6 +74,8 @@ class Image {
switch (pixelFormat) {
case VideoMode::kYUYV:
case VideoMode::kRGB565:
case VideoMode::kY16:
case VideoMode::kUYVY:
type = CV_8UC2;
break;
case VideoMode::kBGR:

View File

@@ -460,6 +460,12 @@ void MjpegServerImpl::ConnThread::SendHTML(wpi::raw_ostream& os,
case VideoMode::kGray:
os << "gray";
break;
case VideoMode::kY16:
os << "Y16";
break;
case VideoMode::kUYVY:
os << "UYVY";
break;
default:
os << "unknown";
break;
@@ -569,6 +575,12 @@ void MjpegServerImpl::ConnThread::SendJSON(wpi::raw_ostream& os,
case VideoMode::kGray:
os << "gray";
break;
case VideoMode::kY16:
os << "Y16";
break;
case VideoMode::kUYVY:
os << "UYVY";
break;
default:
os << "unknown";
break;
@@ -740,8 +752,10 @@ void MjpegServerImpl::ConnThread::SendStream(wpi::raw_socket_ostream& os) {
// for adding it if required.
addDHT = JpegNeedsDHT(data, &size, &locSOF);
break;
case VideoMode::kYUYV:
case VideoMode::kUYVY:
case VideoMode::kRGB565:
case VideoMode::kYUYV:
case VideoMode::kY16:
default:
// Bad frame; sleep for 10 ms so we don't consume all processor time.
std::this_thread::sleep_for(std::chrono::milliseconds(10));

View File

@@ -26,6 +26,8 @@ void RawSourceImpl::PutFrame(const CS_RawFrame& image) {
switch (image.pixelFormat) {
case VideoMode::kYUYV:
case VideoMode::kRGB565:
case VideoMode::kY16:
case VideoMode::kUYVY:
type = CV_8UC2;
break;
case VideoMode::kBGR:

View File

@@ -198,6 +198,10 @@ bool SourceImpl::SetConfigJson(const wpi::json& config, CS_Status* status) {
mode.pixelFormat = cs::VideoMode::kBGR;
} else if (wpi::equals_lower(str, "gray")) {
mode.pixelFormat = cs::VideoMode::kGray;
} else if (wpi::equals_lower(str, "y16")) {
mode.pixelFormat = cs::VideoMode::kY16;
} else if (wpi::equals_lower(str, "uyvy")) {
mode.pixelFormat = cs::VideoMode::kUYVY;
} else {
SWARNING("SetConfigJson: could not understand pixel format value '{}'",
str);
@@ -360,6 +364,12 @@ wpi::json SourceImpl::GetConfigJsonObject(CS_Status* status) {
case VideoMode::kGray:
pixelFormat = "gray";
break;
case VideoMode::kY16:
pixelFormat = "y16";
break;
case VideoMode::kUYVY:
pixelFormat = "uyvy";
break;
default:
break;
}

View File

@@ -50,6 +50,7 @@ class SourceImpl : public PropertyContainer {
void SetConnectionStrategy(CS_ConnectionStrategy strategy) {
m_strategy = static_cast<int>(strategy);
NumSinksChanged();
}
bool IsEnabled() const {
return m_strategy == CS_CONNECTION_KEEP_OPEN ||

View File

@@ -154,7 +154,7 @@ template <typename T>
inline std::span<T>
UnlimitedHandleResource<THandle, TStruct, typeValue, TMutex>::GetAll(
wpi::SmallVectorImpl<T>& vec) {
ForEach([&](THandle handle, const TStruct& data) { vec.push_back(handle); });
ForEach([&](THandle handle, const TStruct&) { vec.push_back(handle); });
return vec;
}

View File

@@ -13,6 +13,7 @@
#include "cscore_cpp.h"
#include "cscore_cv.h"
#include "cscore_raw.h"
#include "cscore_runloop.h"
#include "edu_wpi_first_cscore_CameraServerJNI.h"
namespace cv {
@@ -2226,4 +2227,40 @@ Java_edu_wpi_first_cscore_CameraServerJNI_freeRawFrame
delete ptr;
}
/*
* Class: edu_wpi_first_cscore_CameraServerJNI
* Method: runMainRunLoop
* Signature: ()V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_cscore_CameraServerJNI_runMainRunLoop
(JNIEnv*, jclass)
{
cs::RunMainRunLoop();
}
/*
* Class: edu_wpi_first_cscore_CameraServerJNI
* Method: runMainRunLoopTimeout
* Signature: (D)I
*/
JNIEXPORT jint JNICALL
Java_edu_wpi_first_cscore_CameraServerJNI_runMainRunLoopTimeout
(JNIEnv*, jclass, jdouble timeoutSeconds)
{
return cs::RunMainRunLoopTimeout(timeoutSeconds);
}
/*
* Class: edu_wpi_first_cscore_CameraServerJNI
* Method: stopMainRunLoop
* Signature: ()V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_cscore_CameraServerJNI_stopMainRunLoop
(JNIEnv*, jclass)
{
return cs::StopMainRunLoop();
}
} // extern "C"

View File

@@ -93,7 +93,9 @@ enum CS_PixelFormat {
CS_PIXFMT_YUYV,
CS_PIXFMT_RGB565,
CS_PIXFMT_BGR,
CS_PIXFMT_GRAY
CS_PIXFMT_GRAY,
CS_PIXFMT_Y16,
CS_PIXFMT_UYVY
};
/**

View File

@@ -68,7 +68,9 @@ struct VideoMode : public CS_VideoMode {
kYUYV = CS_PIXFMT_YUYV,
kRGB565 = CS_PIXFMT_RGB565,
kBGR = CS_PIXFMT_BGR,
kGray = CS_PIXFMT_GRAY
kGray = CS_PIXFMT_GRAY,
kY16 = CS_PIXFMT_Y16,
kUYVY = CS_PIXFMT_UYVY
};
VideoMode() {
pixelFormat = 0;
@@ -89,7 +91,10 @@ struct VideoMode : public CS_VideoMode {
height == other.height && fps == other.fps;
}
bool operator!=(const VideoMode& other) const { return !(*this == other); }
bool CompareWithoutFps(const VideoMode& other) const {
return pixelFormat == other.pixelFormat && width == other.width &&
height == other.height;
}
};
/**

View File

@@ -140,8 +140,6 @@ class VideoSource {
return m_handle == other.m_handle;
}
bool operator!=(const VideoSource& other) const { return !(*this == other); }
/**
* Get the kind of the source.
*/
@@ -736,8 +734,6 @@ class VideoSink {
return m_handle == other.m_handle;
}
bool operator!=(const VideoSink& other) const { return !(*this == other); }
/**
* Get the kind of the sink.
*/

View File

@@ -4,10 +4,8 @@
#pragma once
class Switch {
public:
virtual ~Switch() = default;
/// \brief Returns true when the switch is triggered.
virtual bool Get() = 0;
};
namespace cs {
void RunMainRunLoop();
int RunMainRunLoopTimeout(double timeoutSeconds);
void StopMainRunLoop();
} // namespace cs

View File

@@ -0,0 +1,38 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include <wpi/Synchronization.h>
#include "cscore_runloop.h"
static wpi::Event& GetInstance() {
static wpi::Event event;
return event;
}
namespace cs {
void RunMainRunLoop() {
wpi::Event& event = GetInstance();
wpi::WaitForObject(event.GetHandle());
}
int RunMainRunLoopTimeout(double timeoutSeconds) {
wpi::Event& event = GetInstance();
bool timedOut = false;
bool signaled =
wpi::WaitForObject(event.GetHandle(), timeoutSeconds, &timedOut);
if (timedOut) {
return 3;
}
if (signaled) {
return 2;
}
return 1;
}
void StopMainRunLoop() {
wpi::Event& event = GetInstance();
event.Set();
}
} // namespace cs

View File

@@ -82,6 +82,10 @@ static VideoMode::PixelFormat ToPixelFormat(__u32 pixelFormat) {
return VideoMode::kBGR;
case V4L2_PIX_FMT_GREY:
return VideoMode::kGray;
case V4L2_PIX_FMT_Y16:
return VideoMode::kY16;
case V4L2_PIX_FMT_UYVY:
return VideoMode::kUYVY;
default:
return VideoMode::kUnknown;
}
@@ -100,6 +104,10 @@ static __u32 FromPixelFormat(VideoMode::PixelFormat pixelFormat) {
return V4L2_PIX_FMT_BGR24;
case VideoMode::kGray:
return V4L2_PIX_FMT_GREY;
case VideoMode::kY16:
return V4L2_PIX_FMT_Y16;
case VideoMode::kUYVY:
return V4L2_PIX_FMT_UYVY;
default:
return 0;
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "cscore_runloop.h"
#include <CoreFoundation/CFRunLoop.h>
#import <Foundation/Foundation.h>
namespace cs {
void RunMainRunLoop() {
if (CFRunLoopGetMain() != CFRunLoopGetCurrent()) {
NSLog(@"This method can only be called from the main thread");
return;
}
CFRunLoopRun();
}
int RunMainRunLoopTimeout(double timeoutSeconds) {
if (CFRunLoopGetMain() != CFRunLoopGetCurrent()) {
NSLog(@"This method can only be called from the main thread");
return -1;
}
return CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeoutSeconds, false);
}
void StopMainRunLoop() {
CFRunLoopStop(CFRunLoopGetMain());
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#import <AVFoundation/AVFoundation.h>
#include <memory>
namespace cs {
class UsbCameraImpl;
}
@interface UsbCameraDelegate
: NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
@property(nonatomic) std::weak_ptr<cs::UsbCameraImpl> cppImpl;
- (void)captureOutput:(AVCaptureOutput*)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection*)connection;
@end

View File

@@ -0,0 +1,67 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#import "UsbCameraDelegate.h"
#include "UsbCameraImpl.h"
#include <wpi/timestamp.h>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
@implementation UsbCameraDelegate
- (id)init {
self = [super init];
return self;
}
- (void)captureOutput:(AVCaptureOutput*)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection*)connection {
(void)captureOutput;
(void)sampleBuffer;
(void)connection;
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
return;
}
// Buffer always comes in a 32BGRA
auto imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CVPixelBufferLockBaseAddress(imageBuffer, 0);
void* baseaddress = CVPixelBufferGetBaseAddress(imageBuffer);
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
size_t rowBytes = CVPixelBufferGetBytesPerRow(imageBuffer);
OSType pixelFormat = CVPixelBufferGetPixelFormatType(imageBuffer);
if (rowBytes == 0) {
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
return;
}
if (pixelFormat != kCVPixelFormatType_32BGRA) {
NSLog(@"Unknown Pixel Format %u", pixelFormat);
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
return;
}
size_t currSize = width * 3 * height;
auto tmpMat = cv::Mat(height, width, CV_8UC4, baseaddress, rowBytes);
auto image = sharedThis->AllocImage(cs::VideoMode::PixelFormat::kBGR, width,
height, currSize);
cv::cvtColor(tmpMat, image->AsMat(), cv::COLOR_BGRA2BGR);
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
sharedThis->objcPutFrame(std::move(image), wpi::Now());
}
@end

View File

@@ -0,0 +1,96 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#import <AVFoundation/AVFoundation.h>
#import "UsbCameraDelegate.h"
#import "UsbCameraImplObjc.h"
#include <memory>
#include <string>
#include <optional>
#include "SourceImpl.h"
namespace cs {
struct CameraFPSRange {
int min;
int max;
bool IsWithinRange(int fps) { return fps >= min && fps <= max; }
};
struct CameraModeStore {
VideoMode mode;
AVCaptureDeviceFormat* format;
std::vector<CameraFPSRange> fpsRanges;
};
class UsbCameraImpl : public SourceImpl {
public:
UsbCameraImpl(std::string_view name, wpi::Logger& logger, Notifier& notifier,
Telemetry& telemetry, std::string_view path);
UsbCameraImpl(std::string_view name, wpi::Logger& logger, Notifier& notifier,
Telemetry& telemetry, int deviceId);
~UsbCameraImpl() override;
void Start() override;
// Property functions
void SetProperty(int property, int value, CS_Status* status) override;
void SetStringProperty(int property, std::string_view value,
CS_Status* status) override;
// Standard common camera properties
void SetBrightness(int brightness, CS_Status* status) override;
int GetBrightness(CS_Status* status) const override;
void SetWhiteBalanceAuto(CS_Status* status) override;
void SetWhiteBalanceHoldCurrent(CS_Status* status) override;
void SetWhiteBalanceManual(int value, CS_Status* status) override;
void SetExposureAuto(CS_Status* status) override;
void SetExposureHoldCurrent(CS_Status* status) override;
void SetExposureManual(int value, CS_Status* status) override;
bool SetVideoMode(const VideoMode& mode, CS_Status* status) override;
bool SetPixelFormat(VideoMode::PixelFormat pixelFormat,
CS_Status* status) override;
bool SetResolution(int width, int height, CS_Status* status) override;
bool SetFPS(int fps, CS_Status* status) override;
void NumSinksChanged() override;
void NumSinksEnabledChanged() override;
cs::Notifier& objcGetNotifier() { return m_notifier; }
void objcSwapVideoModes(std::vector<VideoMode>& modes) {
std::scoped_lock lock(m_mutex);
m_videoModes.swap(modes);
}
void objcSetVideoMode(const VideoMode& mode) {
std::scoped_lock lock(m_mutex);
m_mode = mode;
}
void objcPutFrame(std::unique_ptr<Image> image, Frame::Time time) {
PutFrame(std::move(image), time);
}
const VideoMode& objcGetVideoMode() const { return m_mode; }
std::vector<CameraModeStore>& objcGetPlatformVideoModes() {
return m_platformModes;
}
wpi::Logger& objcGetLogger() { return m_logger; }
UsbCameraImplObjc* cppGetObjc() { return m_objc; }
private:
UsbCameraImplObjc* m_objc;
std::vector<CameraModeStore> m_platformModes;
VideoMode m_mode;
};
} // namespace cs

View File

@@ -0,0 +1,203 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#include <iostream>
#include <vector>
#include <string>
#include <wpi/timestamp.h>
#pragma GCC diagnostic ignored "-Wunused-parameter"
#include "Handle.h"
#include "Log.h"
#include "Notifier.h"
#include "Instance.h"
#include "c_util.h"
#include "cscore_cpp.h"
#include "opencv2/imgproc.hpp"
#include "UsbCameraImpl.h"
namespace cs {
UsbCameraImpl::UsbCameraImpl(std::string_view name, wpi::Logger& logger,
Notifier& notifier, Telemetry& telemetry,
std::string_view path)
: SourceImpl{name, logger, notifier, telemetry} {
UsbCameraImplObjc* objc = [[UsbCameraImplObjc alloc] init];
objc.path = [[NSString alloc] initWithBytes:path.data()
length:path.size()
encoding:NSUTF8StringEncoding];
m_objc = objc;
}
UsbCameraImpl::UsbCameraImpl(std::string_view name, wpi::Logger& logger,
Notifier& notifier, Telemetry& telemetry,
int deviceId)
: SourceImpl{name, logger, notifier, telemetry} {
UsbCameraImplObjc* objc = [[UsbCameraImplObjc alloc] init];
objc.path = nil;
objc.deviceId = deviceId;
m_objc = objc;
}
UsbCameraImpl::~UsbCameraImpl() {
m_objc = nil;
}
void UsbCameraImpl::Start() {
[m_objc start];
}
// Property functions
void UsbCameraImpl::SetProperty(int property, int value, CS_Status* status) {
[m_objc setProperty:property withValue:value status:status];
}
void UsbCameraImpl::SetStringProperty(int property, std::string_view value,
CS_Status* status) {
[m_objc setStringProperty:property withValue:&value status:status];
}
// Standard common camera properties
void UsbCameraImpl::SetBrightness(int brightness, CS_Status* status) {
[m_objc setBrightness:brightness status:status];
}
int UsbCameraImpl::GetBrightness(CS_Status* status) const {
return [m_objc getBrightness:status];
}
void UsbCameraImpl::SetWhiteBalanceAuto(CS_Status* status) {
[m_objc setWhiteBalanceAuto:status];
}
void UsbCameraImpl::SetWhiteBalanceHoldCurrent(CS_Status* status) {
[m_objc setWhiteBalanceHoldCurrent:status];
}
void UsbCameraImpl::SetWhiteBalanceManual(int value, CS_Status* status) {
[m_objc setWhiteBalanceManual:value status:status];
}
void UsbCameraImpl::SetExposureAuto(CS_Status* status) {
[m_objc setExposureAuto:status];
}
void UsbCameraImpl::SetExposureHoldCurrent(CS_Status* status) {
[m_objc setExposureHoldCurrent:status];
}
void UsbCameraImpl::SetExposureManual(int value, CS_Status* status) {
[m_objc setExposureManual:value status:status];
}
bool UsbCameraImpl::SetVideoMode(const VideoMode& mode, CS_Status* status) {
return [m_objc setVideoMode:mode status:status];
}
bool UsbCameraImpl::SetPixelFormat(VideoMode::PixelFormat pixelFormat,
CS_Status* status) {
return [m_objc setPixelFormat:pixelFormat status:status];
}
bool UsbCameraImpl::SetResolution(int width, int height, CS_Status* status) {
return [m_objc setResolutionWidth:width withHeight:height status:status];
}
bool UsbCameraImpl::SetFPS(int fps, CS_Status* status) {
return [m_objc setFPS:fps status:status];
}
void UsbCameraImpl::NumSinksChanged() {
[m_objc numSinksChanged];
}
void UsbCameraImpl::NumSinksEnabledChanged() {
[m_objc numSinksEnabledChanged];
}
CS_Source CreateUsbCameraDev(std::string_view name, int dev,
CS_Status* status) {
std::vector<UsbCameraInfo> devices = cs::EnumerateUsbCameras(status);
if (static_cast<int>(devices.size()) > dev) {
return CreateUsbCameraPath(name, devices[dev].path, status);
}
auto& inst = Instance::GetInstance();
return inst.CreateSource(CS_SOURCE_USB, std::make_shared<UsbCameraImpl>(
name, inst.logger, inst.notifier,
inst.telemetry, dev));
}
CS_Source CreateUsbCameraPath(std::string_view name, std::string_view path,
CS_Status* status) {
(void)status;
auto& inst = Instance::GetInstance();
auto val = std::make_shared<UsbCameraImpl>(name, inst.logger, inst.notifier,
inst.telemetry, path);
val->cppGetObjc().cppImpl = val;
return inst.CreateSource(CS_SOURCE_USB, val);
}
std::vector<UsbCameraInfo> EnumerateUsbCameras(CS_Status* status) {
@autoreleasepool {
(void)status;
std::vector<UsbCameraInfo> retval;
NSArray<AVCaptureDeviceType>* deviceTypes = @[
AVCaptureDeviceTypeBuiltInWideAngleCamera,
AVCaptureDeviceTypeExternalUnknown
];
AVCaptureDeviceDiscoverySession* session = [AVCaptureDeviceDiscoverySession
discoverySessionWithDeviceTypes:deviceTypes
mediaType:AVMediaTypeVideo
position:AVCaptureDevicePositionUnspecified];
NSArray* captureDevices = [session devices];
int count = 0;
for (id device in captureDevices) {
NSString* name = [device localizedName];
NSString* uniqueIdentifier = [(AVCaptureDevice*)device uniqueID];
retval.push_back(
{count, [uniqueIdentifier UTF8String], [name UTF8String], {}});
count++;
}
return retval;
}
}
void SetUsbCameraPath(CS_Source source, std::string_view path,
CS_Status* status) {
auto data = Instance::GetInstance().GetSource(source);
if (!data || data->kind != CS_SOURCE_USB) {
*status = CS_INVALID_HANDLE;
return;
}
[static_cast<UsbCameraImpl&>(*data->source).cppGetObjc()
setNewCameraPath:&path];
}
std::string GetUsbCameraPath(CS_Source source, CS_Status* status) {
auto data = Instance::GetInstance().GetSource(source);
if (!data || data->kind != CS_SOURCE_USB) {
*status = CS_INVALID_HANDLE;
return std::string{};
}
std::string ret;
[static_cast<UsbCameraImpl&>(*data->source).cppGetObjc()
getCurrentCameraPath:&ret];
return ret;
}
UsbCameraInfo GetUsbCameraInfo(CS_Source source, CS_Status* status) {
UsbCameraInfo info;
auto data = Instance::GetInstance().GetSource(source);
if (!data || data->kind != CS_SOURCE_USB) {
*status = CS_INVALID_HANDLE;
return info;
}
[static_cast<UsbCameraImpl&>(*data->source).cppGetObjc()
getCurrentCameraPath:&info.path];
[static_cast<UsbCameraImpl&>(*data->source).cppGetObjc()
getCameraName:&info.name];
info.productId = 0;
info.vendorId = 0;
// ParseVidAndPid(info.path, &info.productId, &info.vendorId);
info.dev = -1; // We have lost dev information by this point in time.
return info;
}
} // namespace cs

View File

@@ -0,0 +1,71 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#import <AVFoundation/AVFoundation.h>
#import "UsbCameraDelegate.h"
#include <memory>
#include <string_view>
#include "cscore_cpp.h"
namespace cs {
class UsbCameraImpl;
}
@interface UsbCameraImplObjc : NSObject
@property(nonatomic) AVCaptureDeviceFormat* currentFormat;
@property(nonatomic) int currentFPS;
@property(nonatomic) std::weak_ptr<cs::UsbCameraImpl> cppImpl;
@property(nonatomic) dispatch_queue_t sessionQueue;
@property(nonatomic) NSString* path;
@property(nonatomic) int deviceId;
@property(nonatomic) bool propertiesCached;
@property(nonatomic) bool streaming;
@property(nonatomic) bool deviceValid;
@property(nonatomic) bool isAuthorized;
@property(nonatomic) AVCaptureDevice* videoDevice;
@property(nonatomic) AVCaptureDeviceInput* videoInput;
@property(nonatomic) UsbCameraDelegate* callback;
@property(nonatomic) AVCaptureVideoDataOutput* videoOutput;
@property(nonatomic) AVCaptureSession* session;
- (void)start;
// Property functions
- (void)setProperty:(int)property
withValue:(int)value
status:(CS_Status*)status;
- (void)setStringProperty:(int)property
withValue:(std::string_view*)value
status:(CS_Status*)status;
// Standard common camera properties
- (void)setBrightness:(int)brightness status:(CS_Status*)status;
- (int)getBrightness:(CS_Status*)status;
- (void)setWhiteBalanceAuto:(CS_Status*)status;
- (void)setWhiteBalanceHoldCurrent:(CS_Status*)status;
- (void)setWhiteBalanceManual:(int)value status:(CS_Status*)status;
- (void)setExposureAuto:(CS_Status*)status;
- (void)setExposureHoldCurrent:(CS_Status*)status;
- (void)setExposureManual:(int)value status:(CS_Status*)status;
- (bool)setVideoMode:(const cs::VideoMode&)mode status:(CS_Status*)status;
- (bool)setPixelFormat:(cs::VideoMode::PixelFormat)pixelFormat
status:(CS_Status*)status;
- (bool)setResolutionWidth:(int)width
withHeight:(int)height
status:(CS_Status*)status;
- (bool)setFPS:(int)fps status:(CS_Status*)status;
- (void)numSinksChanged;
- (void)numSinksEnabledChanged;
- (void)getCurrentCameraPath:(std::string*)path;
- (void)getCameraName:(std::string*)name;
- (void)setNewCameraPath:(std::string_view*)path;
@end

View File

@@ -0,0 +1,669 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#import "UsbCameraImplObjc.h"
#include "UsbCameraImpl.h"
#pragma GCC diagnostic ignored "-Wunused-parameter"
#include "Notifier.h"
#include "Log.h"
template <typename S, typename... Args>
inline void NamedLog(UsbCameraImplObjc* objc, unsigned int level,
const char* file, unsigned int line, const S& format,
Args&&... args) {
auto sharedThis = objc.cppImpl.lock();
if (!sharedThis) {
return;
}
wpi::Logger& logger = sharedThis->objcGetLogger();
std::string_view name = sharedThis->GetName();
if (logger.HasLogger() && level >= logger.min_level()) {
cs::NamedLogV(logger, level, file, line, name, format,
fmt::make_format_args(args...));
}
}
#define OBJCLOG(level, format, ...) \
NamedLog(self, level, __FILE__, __LINE__, \
FMT_STRING(format) __VA_OPT__(, ) __VA_ARGS__)
#define OBJCERROR(format, ...) \
OBJCLOG(::wpi::WPI_LOG_ERROR, format __VA_OPT__(, ) __VA_ARGS__)
#define OBJCWARNING(format, ...) \
OBJCLOG(::wpi::WPI_LOG_WARNING, format __VA_OPT__(, ) __VA_ARGS__)
#define OBJCINFO(format, ...) \
OBJCLOG(::wpi::WPI_LOG_INFO, format __VA_OPT__(, ) __VA_ARGS__)
#ifdef NDEBUG
#define OBJCDEBUG(format, ...) \
do { \
} while (0)
#define OBJCDEBUG1(format, ...) \
do { \
} while (0)
#define OBJCDEBUG2(format, ...) \
do { \
} while (0)
#define OBJCDEBUG3(format, ...) \
do { \
} while (0)
#define OBJCDEBUG4(format, ...) \
do { \
} while (0)
#else
#define OBJCDEBUG(format, ...) \
OBJCLOG(::wpi::WPI_LOG_DEBUG, format __VA_OPT__(, ) __VA_ARGS__)
#define OBJCDEBUG1(format, ...) \
OBJCLOG(::wpi::WPI_LOG_DEBUG1, format __VA_OPT__(, ) __VA_ARGS__)
#define OBJCDEBUG2(format, ...) \
OBJCLOG(::wpi::WPI_LOG_DEBUG2, format __VA_OPT__(, ) __VA_ARGS__)
#define OBJCDEBUG3(format, ...) \
OBJCLOG(::wpi::WPI_LOG_DEBUG3, format __VA_OPT__(, ) __VA_ARGS__)
#define OBJCDEBUG4(format, ...) \
OBJCLOG(::wpi::WPI_LOG_DEBUG4, format __VA_OPT__(, ) __VA_ARGS__)
#endif
using namespace cs;
@implementation UsbCameraImplObjc
- (void)start {
switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
case AVAuthorizationStatusAuthorized:
self.isAuthorized = true;
break;
default:
OBJCERROR(
"Camera access explicitly blocked for application. No cameras are "
"accessable");
self.isAuthorized = false;
// TODO log
break;
case AVAuthorizationStatusNotDetermined:
dispatch_suspend(self.sessionQueue);
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo
completionHandler:^(BOOL granted) {
self.isAuthorized = granted;
dispatch_resume(self.sessionQueue);
}];
break;
}
dispatch_async(self.sessionQueue, ^{
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(cameraConnected:)
name:AVCaptureDeviceWasConnectedNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(cameraDisconnected:)
name:AVCaptureDeviceWasDisconnectedNotification
object:nil];
[self deviceConnect];
});
}
// Property functions
- (void)setProperty:(int)property
withValue:(int)value
status:(CS_Status*)status {
}
- (void)setStringProperty:(int)property
withValue:(std::string_view*)value
status:(CS_Status*)status {
}
// Standard common camera properties
- (void)setBrightness:(int)brightness status:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
}
- (int)getBrightness:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
return 0;
}
- (void)setWhiteBalanceAuto:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
}
- (void)setWhiteBalanceHoldCurrent:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
}
- (void)setWhiteBalanceManual:(int)value status:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
}
- (void)setExposureAuto:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
}
- (void)setExposureHoldCurrent:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
}
- (void)setExposureManual:(int)value status:(CS_Status*)status {
*status = CS_INVALID_PROPERTY;
}
- (bool)setVideoMode:(const cs::VideoMode&)mode status:(CS_Status*)status {
dispatch_async_and_wait(self.sessionQueue, ^{
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
*status = CS_READ_FAILED;
return;
}
[self internalSetMode:mode status:status];
});
return true;
}
- (bool)setPixelFormat:(cs::VideoMode::PixelFormat)pixelFormat
status:(CS_Status*)status {
dispatch_async_and_wait(self.sessionQueue, ^{
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
*status = CS_READ_FAILED;
return;
}
VideoMode newMode;
newMode = sharedThis->objcGetVideoMode();
newMode.pixelFormat = pixelFormat;
[self internalSetMode:newMode status:status];
});
return true;
}
- (bool)setResolutionWidth:(int)width
withHeight:(int)height
status:(CS_Status*)status {
dispatch_async_and_wait(self.sessionQueue, ^{
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
*status = CS_READ_FAILED;
return;
}
VideoMode newMode;
newMode = sharedThis->objcGetVideoMode();
newMode.width = width;
newMode.height = height;
[self internalSetMode:newMode status:status];
});
return true;
}
- (void)internalSetMode:(const cs::VideoMode&)newMode
status:(CS_Status*)status {
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
*status = CS_READ_FAILED;
return;
}
// If device is not connected, just apply and leave.
if (!self.propertiesCached) {
sharedThis->objcSetVideoMode(newMode);
*status = CS_OK;
return;
}
if (newMode != sharedThis->objcGetVideoMode()) {
OBJCDEBUG3("Trying Mode {} {} {} {}", newMode.pixelFormat, newMode.width,
newMode.height, newMode.fps);
int localFPS = 0;
AVCaptureDeviceFormat* newModeType = [self deviceCheckModeValid:&newMode
withFps:&localFPS];
if (newModeType == nil) {
*status = CS_UNSUPPORTED_MODE;
return;
}
self.currentFormat = newModeType;
self.currentFPS = localFPS;
sharedThis->objcSetVideoMode(newMode);
[self deviceDisconnect];
[self deviceConnect];
sharedThis->objcGetNotifier().NotifySourceVideoMode(*sharedThis, newMode);
}
*status = CS_OK;
}
- (bool)setFPS:(int)fps status:(CS_Status*)status {
dispatch_async_and_wait(self.sessionQueue, ^{
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
*status = CS_READ_FAILED;
return;
}
VideoMode newMode;
newMode = sharedThis->objcGetVideoMode();
newMode.fps = fps;
[self internalSetMode:newMode status:status];
});
return true;
}
- (void)numSinksChanged {
dispatch_async(self.sessionQueue, ^{
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
return;
}
if (!sharedThis->IsEnabled()) {
[self deviceStreamOff];
} else if (!self.streaming && sharedThis->IsEnabled()) {
[self deviceStreamOn];
}
});
}
- (void)numSinksEnabledChanged {
[self numSinksChanged];
}
// All above is direct forwarders from C++, must always dispatch to loop
- (void)getCurrentCameraPath:(std::string*)path {
dispatch_async_and_wait(self.sessionQueue, ^{
if (self.videoDevice == nil) {
return;
}
*path = [self.videoDevice.uniqueID UTF8String];
});
}
- (void)getCameraName:(std::string*)name {
dispatch_async_and_wait(self.sessionQueue, ^{
if (self.videoDevice == nil) {
return;
}
*name = [self.videoDevice.localizedName UTF8String];
});
}
- (void)setNewCameraPath:(std::string_view*)path {
dispatch_async_and_wait(self.sessionQueue, ^{
NSString* nsPath = [[NSString alloc] initWithBytes:path->data()
length:path->size()
encoding:NSUTF8StringEncoding];
if (self.path != nil && [self.path isEqualToString:nsPath]) {
return;
}
self.path = nsPath;
[self deviceDisconnect];
[self deviceConnect];
});
}
// All above are called from C++, must always dispatch to loop
- (void)deviceCacheProperties {
if (self.session == nil) {
return;
}
}
static cs::VideoMode::PixelFormat FourCCToPixelFormat(FourCharCode fourcc) {
switch (fourcc) {
case kCVPixelFormatType_422YpCbCr8_yuvs:
case kCVPixelFormatType_422YpCbCr8FullRange:
return cs::VideoMode::PixelFormat::kYUYV;
default:
return cs::VideoMode::PixelFormat::kBGR;
}
}
- (void)deviceCacheVideoModes {
if (self.session == nil) {
return;
}
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
return;
}
std::vector<CameraModeStore>& platformModes =
sharedThis->objcGetPlatformVideoModes();
platformModes.clear();
std::vector<VideoMode> modes;
@autoreleasepool {
NSArray<AVCaptureDeviceFormat*>* formats = self.videoDevice.formats;
int count = 0;
for (AVCaptureDeviceFormat* format in formats) {
CMFormatDescriptionRef cmformat = format.formatDescription;
CMVideoDimensions s1 = CMVideoFormatDescriptionGetDimensions(cmformat);
FourCharCode fourcc = CMFormatDescriptionGetMediaSubType(cmformat);
auto videoFormat = FourCCToPixelFormat(fourcc);
NSArray<AVFrameRateRange*>* frameRates =
format.videoSupportedFrameRateRanges;
CameraModeStore store;
store.mode.pixelFormat = videoFormat;
store.mode.width = static_cast<int>(s1.width);
store.mode.height = static_cast<int>(s1.height);
store.format = format;
int maxFps = 0;
for (AVFrameRateRange* rate in frameRates) {
CMTime highest = rate.minFrameDuration;
CMTime lowest = rate.maxFrameDuration;
int highestFps = highest.timescale / static_cast<double>(highest.value);
int lowestFps = lowest.timescale / static_cast<double>(lowest.value);
store.fpsRanges.emplace_back(CameraFPSRange{lowestFps, highestFps});
if (highestFps > maxFps) {
maxFps = highestFps;
}
}
store.mode.fps = maxFps;
modes.emplace_back(store.mode);
platformModes.emplace_back(store);
count++;
}
}
sharedThis->objcSwapVideoModes(modes);
sharedThis->objcGetNotifier().NotifySource(*sharedThis,
CS_SOURCE_VIDEOMODES_UPDATED);
}
- (AVCaptureDeviceFormat*)deviceCheckModeValid:(const cs::VideoMode*)toCheck
withFps:(int*)fps {
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
return nil;
}
OBJCDEBUG3("Checking mode {} {} {} {}", toCheck->pixelFormat, toCheck->width,
toCheck->height, toCheck->fps);
std::vector<CameraModeStore>& platformModes =
sharedThis->objcGetPlatformVideoModes();
// Find the matching mode
auto match = std::find_if(platformModes.begin(), platformModes.end(),
[&](CameraModeStore& input) {
return input.mode.CompareWithoutFps(*toCheck);
});
if (match == platformModes.end()) {
return nil;
}
// Check FPS
for (CameraFPSRange& range : match->fpsRanges) {
OBJCDEBUG3("Checking Range {} {}", range.min, range.max);
if (range.IsWithinRange(toCheck->fps)) {
*fps = toCheck->fps;
return match->format;
}
}
return nil;
}
- (void)deviceCacheMode {
if (!self.session) {
return;
}
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
return;
}
std::vector<CameraModeStore>& platformModes =
sharedThis->objcGetPlatformVideoModes();
if (platformModes.size() == 0) {
return;
}
if (self.currentFormat == nil) {
int localFps = 0;
self.currentFormat =
[self deviceCheckModeValid:&sharedThis->objcGetVideoMode()
withFps:&localFps];
if (self.currentFormat == nil) {
self.currentFormat = self.videoDevice.activeFormat;
auto result = std::find_if(platformModes.begin(), platformModes.end(),
[f = self.currentFormat](CameraModeStore& i) {
return [f isEqual:i.format];
});
if (result == platformModes.end()) {
auto& firstSupported = platformModes[0];
self.currentFormat = firstSupported.format;
self.currentFPS = firstSupported.mode.fps;
sharedThis->objcSetVideoMode(firstSupported.mode);
} else {
self.currentFPS = result->mode.fps;
sharedThis->objcSetVideoMode(result->mode);
}
} else {
self.currentFPS = localFps;
}
}
[self deviceSetMode];
sharedThis->objcGetNotifier().NotifySourceVideoMode(
*sharedThis, sharedThis->objcGetVideoMode());
}
- (void)deviceSetMode {
self.deviceValid = true;
}
- (bool)deviceStreamOn {
if (self.streaming) {
return false;
}
if (!self.deviceValid) {
return false;
}
self.streaming = true;
[self.session startRunning];
if ([self.videoDevice lockForConfiguration:nil]) {
if (self.currentFormat != nil) {
self.videoDevice.activeFormat = self.currentFormat;
}
if (self.currentFPS != 0) {
self.videoDevice.activeVideoMinFrameDuration =
CMTimeMake(1, self.currentFPS);
self.videoDevice.activeVideoMaxFrameDuration =
CMTimeMake(1, self.currentFPS);
}
[self.videoDevice unlockForConfiguration];
} else {
OBJCERROR("Failed to lock for configuration");
}
return true;
}
- (bool)deviceStreamOff {
if (self.streaming) {
[self.session stopRunning];
}
self.streaming = false;
return true;
}
- (id)init {
self = [super init];
// TODO pass in name, make this queue specific
self.sessionQueue =
dispatch_queue_create("session queue", DISPATCH_QUEUE_SERIAL);
return self;
}
- (void)deviceDisconnect {
std::string pathStr = [self.path UTF8String];
OBJCINFO("Disconnected from {}", pathStr);
[self deviceStreamOff];
self.session = nil;
self.videoOutput = nil;
self.callback = nil;
self.videoInput = nil;
self.videoDevice = nil;
self.streaming = false;
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
return;
}
sharedThis->SetConnected(false);
}
- (bool)deviceConnect {
if (!self.isAuthorized) {
OBJCERROR(
"Camera access not authorized for application. No cameras are "
"accessable");
return false;
}
OSType pixelFormat = kCVPixelFormatType_32BGRA;
NSDictionary* pixelBufferOptions =
@{(id)kCVPixelBufferPixelFormatTypeKey : @(pixelFormat)};
if (self.session != nil) {
return true;
}
auto sharedThis = self.cppImpl.lock();
if (!sharedThis) {
return false;
}
if (self.path == nil) {
OBJCINFO("Starting for device id {}", self.deviceId);
// Enumerate Devices
CS_Status status = 0;
auto cameras = cs::EnumerateUsbCameras(&status);
if (static_cast<int>(cameras.size()) <= self.deviceId) {
return false;
}
std::string& path = cameras[self.deviceId].path;
self.path = [[NSString alloc] initWithBytes:path.data()
length:path.size()
encoding:NSUTF8StringEncoding];
}
std::string pathStr = [self.path UTF8String];
OBJCINFO("Connecting to USB camera on {}", pathStr);
self.videoDevice = [AVCaptureDevice deviceWithUniqueID:self.path];
if (self.videoDevice == nil) {
OBJCWARNING("Device Not found");
goto err;
}
self.videoInput = [AVCaptureDeviceInput deviceInputWithDevice:self.videoDevice
error:nil];
if (self.videoInput == nil) {
OBJCWARNING("Creating AVCaptureDeviceInput failed");
goto err;
}
self.callback = [[UsbCameraDelegate alloc] init];
if (self.callback == nil) {
OBJCWARNING("Creating Camera Callback failed");
goto err;
}
self.callback.cppImpl = self.cppImpl;
self.videoOutput = [[AVCaptureVideoDataOutput alloc] init];
if (self.videoOutput == nil) {
OBJCWARNING("Creating AVCaptureVideoDataOutput failed");
goto err;
}
[self.videoOutput setSampleBufferDelegate:self.callback
queue:self.sessionQueue];
self.videoOutput.videoSettings = pixelBufferOptions;
self.videoOutput.alwaysDiscardsLateVideoFrames = YES;
self.session = [[AVCaptureSession alloc] init];
if (self.session == nil) {
OBJCWARNING("Creating AVCaptureSession failed");
goto err;
}
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(sessionRuntimeError:)
name:AVCaptureSessionRuntimeErrorNotification
object:self.session];
[self.session addInput:self.videoInput];
[self.session addOutput:self.videoOutput];
sharedThis->SetDescription([self.videoDevice.localizedName UTF8String]);
if (!self.propertiesCached) {
OBJCDEBUG3("Caching properties");
[self deviceCacheProperties];
[self deviceCacheVideoModes];
[self deviceCacheMode];
self.propertiesCached = true;
} else {
OBJCDEBUG3("Restoring Video Mode");
[self deviceSetMode];
}
sharedThis->SetConnected(true);
if (sharedThis->IsEnabled()) {
[self deviceStreamOn];
}
return true;
err:
self.session = nil;
self.videoOutput = nil;
self.callback = nil;
self.videoInput = nil;
self.videoDevice = nil;
return false;
}
// Helpers
- (void)sessionRuntimeError:(NSNotification*)notification {
@autoreleasepool {
NSError* error = notification.userInfo[AVCaptureSessionErrorKey];
const char* str = [error.description UTF8String];
if (str) {
std::string errorStr = str;
OBJCERROR("Capture session runtime error: {}", errorStr);
}
}
}
- (void)cameraDisconnected:(NSNotification*)notification {
AVCaptureDevice* device = notification.object;
dispatch_async(self.sessionQueue, ^{
if (self.path != nil && [device.uniqueID isEqualToString:self.path]) {
[self deviceDisconnect];
}
});
}
- (void)cameraConnected:(NSNotification*)notification {
AVCaptureDevice* device = notification.object;
dispatch_async(self.sessionQueue, ^{
if (self.path == nil || [device.uniqueID isEqualToString:self.path]) {
[self deviceConnect];
}
});
}
@end

View File

@@ -0,0 +1,111 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#import <AVFoundation/AVFoundation.h>
#include "UsbCameraListener.h"
#pragma GCC diagnostic ignored "-Wunused-parameter"
#include "Notifier.h"
using namespace cs;
@interface UsbCameraListenerImpl : NSObject
@property(nonatomic) Notifier* notifier;
@property BOOL started;
@property(nonatomic) dispatch_queue_t sessionQueue;
- (void)start;
- (void)stop;
@end
@implementation UsbCameraListenerImpl
- (id)init {
self = [super init];
self.sessionQueue =
dispatch_queue_create("Camera Listener", DISPATCH_QUEUE_SERIAL);
return self;
}
- (void)start {
dispatch_async(self.sessionQueue, ^{
if (self.started) {
return;
}
self.started = YES;
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(camerasChanged:)
name:AVCaptureDeviceWasConnectedNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(camerasChanged:)
name:AVCaptureDeviceWasDisconnectedNotification
object:nil];
});
}
- (void)stop {
dispatch_async_and_wait(self.sessionQueue, ^{
if (!self.started) {
return;
}
self.started = NO;
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:AVCaptureDeviceWasConnectedNotification
object:nil];
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:AVCaptureDeviceWasDisconnectedNotification
object:nil];
});
}
- (void)camerasChanged:(NSNotification*)notification {
@autoreleasepool {
dispatch_async(self.sessionQueue, ^{
if (!self.started) {
return;
}
AVCaptureDevice* device = notification.object;
if ([device.deviceType
isEqualToString:AVCaptureDeviceTypeBuiltInWideAngleCamera] ||
[device.deviceType
isEqualToString:AVCaptureDeviceTypeExternalUnknown]) {
self.notifier->NotifyUsbCamerasChanged();
}
});
}
}
@end
class UsbCameraListener::Impl {
public:
UsbCameraListenerImpl* listener;
explicit Impl(Notifier& notifier) {
listener = [[UsbCameraListenerImpl alloc] init];
listener.notifier = &notifier;
}
};
UsbCameraListener::UsbCameraListener(wpi::Logger&, Notifier& notifier)
: m_impl{std::make_unique<Impl>(notifier)} {}
UsbCameraListener::~UsbCameraListener() {
Stop();
}
void UsbCameraListener::Start() {
[m_impl->listener start];
}
void UsbCameraListener::Stop() {
[m_impl->listener stop];
}

View File

@@ -1,12 +0,0 @@
#import <Foundation/Foundation.h>
@interface XYZPerson : NSObject
- (void)sayHello;
@end
@implementation XYZPerson
- (void)sayHello {
NSLog(@"Hello, World!");
}
@end

View File

@@ -1,41 +0,0 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "cscore_cpp.h"
namespace cs {
CS_Source CreateUsbCameraDev(std::string_view name, int dev,
CS_Status* status) {
*status = CS_INVALID_HANDLE;
return 0;
}
CS_Source CreateUsbCameraPath(std::string_view name, std::string_view path,
CS_Status* status) {
*status = CS_INVALID_HANDLE;
return 0;
}
void SetUsbCameraPath(CS_Source source, std::string_view path,
CS_Status* status) {
*status = CS_INVALID_HANDLE;
}
std::string GetUsbCameraPath(CS_Source source, CS_Status* status) {
*status = CS_INVALID_HANDLE;
return std::string{};
}
UsbCameraInfo GetUsbCameraInfo(CS_Source source, CS_Status* status) {
*status = CS_INVALID_HANDLE;
return UsbCameraInfo{};
}
std::vector<UsbCameraInfo> EnumerateUsbCameras(CS_Status* status) {
*status = CS_INVALID_HANDLE;
return std::vector<UsbCameraInfo>{};
}
} // namespace cs

View File

@@ -1,17 +0,0 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "UsbCameraListener.h"
using namespace cs;
class UsbCameraListener::Impl {};
UsbCameraListener::UsbCameraListener(wpi::Logger& logger, Notifier& notifier) {}
UsbCameraListener::~UsbCameraListener() = default;
void UsbCameraListener::Start() {}
void UsbCameraListener::Stop() {}

View File

@@ -0,0 +1,38 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include <wpi/Synchronization.h>
#include "cscore_runloop.h"
static wpi::Event& GetInstance() {
static wpi::Event event;
return event;
}
namespace cs {
void RunMainRunLoop() {
wpi::Event& event = GetInstance();
wpi::WaitForObject(event.GetHandle());
}
int RunMainRunLoopTimeout(double timeoutSeconds) {
wpi::Event& event = GetInstance();
bool timedOut = false;
bool signaled =
wpi::WaitForObject(event.GetHandle(), timeoutSeconds, &timedOut);
if (timedOut) {
return 3;
}
if (signaled) {
return 2;
}
return 1;
}
void StopMainRunLoop() {
wpi::Event& event = GetInstance();
event.Set();
}
} // namespace cs

View File

@@ -356,6 +356,12 @@ void UsbCameraImpl::ProcessFrame(IMFSample* videoSample,
tmpMat.total());
tmpMat.copyTo(dest->AsMat());
break;
case cs::VideoMode::PixelFormat::kY16:
tmpMat = cv::Mat(mode.height, mode.width, CV_8UC2, ptr, pitch);
dest =
AllocImage(VideoMode::kY16, tmpMat.cols, tmpMat.rows, tmpMat.total());
tmpMat.copyTo(dest->AsMat());
break;
case cs::VideoMode::PixelFormat::kBGR:
tmpMat = cv::Mat(mode.height, mode.width, CV_8UC3, ptr, pitch);
dest = AllocImage(VideoMode::kBGR, tmpMat.cols, tmpMat.rows,
@@ -368,6 +374,12 @@ void UsbCameraImpl::ProcessFrame(IMFSample* videoSample,
tmpMat.total() * 2);
tmpMat.copyTo(dest->AsMat());
break;
case cs::VideoMode::PixelFormat::kUYVY:
tmpMat = cv::Mat(mode.height, mode.width, CV_8UC2, ptr, pitch);
dest = AllocImage(VideoMode::kUYVY, tmpMat.cols, tmpMat.rows,
tmpMat.total() * 2);
tmpMat.copyTo(dest->AsMat());
break;
default:
doFinalSet = false;
break;
@@ -461,9 +473,10 @@ LRESULT UsbCameraImpl::PumpMain(HWND hwnd, UINT uiMsg, WPARAM wParam,
static cs::VideoMode::PixelFormat GetFromGUID(const GUID& guid) {
// Compare GUID to one of the supported ones
if (IsEqualGUID(guid, MFVideoFormat_NV12)) {
// GrayScale
if (IsEqualGUID(guid, MFVideoFormat_L8)) {
return cs::VideoMode::PixelFormat::kGray;
} else if (IsEqualGUID(guid, MFVideoFormat_L16)) {
return cs::VideoMode::PixelFormat::kY16;
} else if (IsEqualGUID(guid, MFVideoFormat_YUY2)) {
return cs::VideoMode::PixelFormat::kYUYV;
} else if (IsEqualGUID(guid, MFVideoFormat_RGB24)) {
@@ -472,6 +485,8 @@ static cs::VideoMode::PixelFormat GetFromGUID(const GUID& guid) {
return cs::VideoMode::PixelFormat::kMJPEG;
} else if (IsEqualGUID(guid, MFVideoFormat_RGB565)) {
return cs::VideoMode::PixelFormat::kRGB565;
} else if (IsEqualGUID(guid, MFVideoFormat_UYVY)) {
return cs::VideoMode::PixelFormat::kUYVY;
} else {
return cs::VideoMode::PixelFormat::kUnknown;
}

View File

@@ -25,6 +25,7 @@ if (!project.hasProperty('onlylinuxathena')) {
def wpilibVersionFileOutput = file("$buildDir/generated/main/cpp/WPILibVersion.cpp")
apply from: "${rootDir}/shared/libssh.gradle"
apply from: "${rootDir}/shared/imgui.gradle"
task generateCppVersion() {
description = 'Generates the wpilib version class'
@@ -103,7 +104,7 @@ if (!project.hasProperty('onlylinuxathena')) {
lib project: ':glass', library: 'glass', linkage: 'static'
lib project: ':wpiutil', library: 'wpiutil', linkage: 'static'
lib project: ':wpigui', library: 'wpigui', linkage: 'static'
nativeUtils.useRequiredLibrary(it, 'imgui_static', 'libssh')
nativeUtils.useRequiredLibrary(it, 'imgui', 'libssh')
if (it.targetPlatform.operatingSystem.isWindows()) {
it.linker.args << 'Gdi32.lib' << 'Shell32.lib' << 'd3d11.lib' << 'd3dcompiler.lib'
it.linker.args << 'ws2_32.lib' << 'advapi32.lib' << 'crypt32.lib' << 'user32.lib'

View File

@@ -104,6 +104,8 @@ static void DisplayMainMenu() {
ImGui::Text("v%s", GetWPILibVersion());
ImGui::Separator();
ImGui::Text("Save location: %s", glass::GetStorageDir().c_str());
ImGui::Text("%.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate,
ImGui::GetIO().Framerate);
if (ImGui::Button("Close")) {
ImGui::CloseCurrentPopup();
}

View File

@@ -3,16 +3,17 @@ plugins {
id "org.ysb33r.doxygen" version "0.7.0"
}
evaluationDependsOn(':wpiutil')
evaluationDependsOn(':wpinet')
evaluationDependsOn(':ntcore')
evaluationDependsOn(':apriltag')
evaluationDependsOn(':cameraserver')
evaluationDependsOn(':cscore')
evaluationDependsOn(':hal')
evaluationDependsOn(':cameraserver')
evaluationDependsOn(':wpimath')
evaluationDependsOn(':ntcore')
evaluationDependsOn(':wpilibNewCommands')
evaluationDependsOn(':wpilibc')
evaluationDependsOn(':wpilibj')
evaluationDependsOn(':wpilibNewCommands')
evaluationDependsOn(':wpimath')
evaluationDependsOn(':wpinet')
evaluationDependsOn(':wpiutil')
def baseArtifactIdCpp = 'documentation'
def artifactGroupIdCpp = 'edu.wpi.first.wpilibc'
@@ -27,20 +28,28 @@ def outputsFolder = file("$project.buildDir/outputs")
def cppProjectZips = []
def cppIncludeRoots = []
cppProjectZips.add(project(':hal').cppHeadersZip)
cppProjectZips.add(project(':wpiutil').cppHeadersZip)
cppProjectZips.add(project(':wpinet').cppHeadersZip)
cppProjectZips.add(project(':ntcore').cppHeadersZip)
cppProjectZips.add(project(':cscore').cppHeadersZip)
cppProjectZips.add(project(':apriltag').cppHeadersZip)
cppProjectZips.add(project(':cameraserver').cppHeadersZip)
cppProjectZips.add(project(':wpimath').cppHeadersZip)
cppProjectZips.add(project(':wpilibc').cppHeadersZip)
cppProjectZips.add(project(':cscore').cppHeadersZip)
cppProjectZips.add(project(':hal').cppHeadersZip)
cppProjectZips.add(project(':ntcore').cppHeadersZip)
cppProjectZips.add(project(':wpilibNewCommands').cppHeadersZip)
cppProjectZips.add(project(':wpilibc').cppHeadersZip)
cppProjectZips.add(project(':wpimath').cppHeadersZip)
cppProjectZips.add(project(':wpinet').cppHeadersZip)
cppProjectZips.add(project(':wpiutil').cppHeadersZip)
doxygen {
executables {
doxygen version : '1.9.4',
baseURI : 'https://frcmaven.wpi.edu/artifactory/generic-release-mirror/doxygen'
// Doxygen binaries are only provided for x86_64 platforms
// Other platforms will need to provide doxygen via their system
// See below maven and https://doxygen.nl/download.html for provided binaries
String arch = System.getProperty("os.arch");
if (arch.equals("x86_64") || arch.equals("amd64")) {
executables {
doxygen version : '1.9.4',
baseURI : 'https://frcmaven.wpi.edu/artifactory/generic-release-mirror/doxygen'
}
}
}
@@ -161,7 +170,7 @@ doxygen {
warn_if_undocumented false
warn_no_paramdoc true
//enable doxygen preprocessor expansion of WPI_DEPRECATED to fix SpeedController docs
//enable doxygen preprocessor expansion of WPI_DEPRECATED to fix MotorController docs
enable_preprocessing true
macro_expansion true
expand_only_predef true
@@ -205,19 +214,20 @@ task generateJavaDocs(type: Javadoc) {
options.addBooleanOption("Xdoclint:html,missing,reference,syntax", true)
options.addBooleanOption('html5', true)
options.linkSource(true)
dependsOn project(':wpilibj').generateJavaVersion
dependsOn project(':hal').generateUsageReporting
dependsOn project(':wpimath').generateNat
dependsOn project(':ntcore').ntcoreGenerateJavaTypes
source project(':hal').sourceSets.main.java
source project(':wpiutil').sourceSets.main.java
source project(':wpinet').sourceSets.main.java
source project(':cscore').sourceSets.main.java
source project(':ntcore').sourceSets.main.java
source project(':wpimath').sourceSets.main.java
source project(':wpilibj').sourceSets.main.java
dependsOn project(':wpilibj').generateJavaVersion
dependsOn project(':wpimath').generateNat
source project(':apriltag').sourceSets.main.java
source project(':cameraserver').sourceSets.main.java
source project(':cscore').sourceSets.main.java
source project(':hal').sourceSets.main.java
source project(':ntcore').sourceSets.main.java
source project(':wpilibNewCommands').sourceSets.main.java
source project(':wpilibj').sourceSets.main.java
source project(':wpimath').sourceSets.main.java
source project(':wpinet').sourceSets.main.java
source project(':wpiutil').sourceSets.main.java
source configurations.javaSource.collect { zipTree(it) }
include '**/*.java'
failOnError = true

View File

@@ -11,7 +11,7 @@ if (WITH_JAVA)
set(CMAKE_JAVA_INCLUDE_PATH fieldImages.jar ${JACKSON_JARS})
file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java)
file(GLOB_RECURSE JAVA_RESOURCES src/main/native/resources/*.json src/main/native/resources/*.png src/main/native/resources/*.jpg)
file(GLOB_RECURSE JAVA_RESOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} src/main/native/resources/*.json src/main/native/resources/*.png src/main/native/resources/*.jpg)
add_jar(field_images_jar SOURCES ${JAVA_SOURCES} RESOURCES NAMESPACE "edu/wpi/first/fields" ${JAVA_RESOURCES} OUTPUT_NAME fieldImages)
get_property(FIELD_IMAGES_JAR_FILE TARGET field_images_jar PROPERTY JAR_FILE)

View File

@@ -14,12 +14,13 @@ public enum Fields {
k2021GalacticSearchA("2021-galacticsearcha.json"),
k2021GalacticSearchB("2021-galacticsearchb.json"),
k2021Slalom("2021-slalompath.json"),
k2022RapidReact("2022-rapidreact.json");
k2022RapidReact("2022-rapidreact.json"),
k2023ChargedUp("2023-chargedup.json");
public static final String kBaseResourceDir = "/edu/wpi/first/fields/";
/** Alias to the current game. */
public static final Fields kDefaultField = k2022RapidReact;
public static final Fields kDefaultField = k2023ChargedUp;
public final String m_resourceFile;

View File

@@ -0,0 +1,12 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#include <string_view>
namespace fields {
std::string_view GetResource_2023_chargedup_json();
std::string_view GetResource_2023_field_png();
} // namespace fields

View File

@@ -2,9 +2,18 @@
"game": "FIRST Power Up",
"field-image": "2018-field.jpg",
"field-corners": {
"top-left": [125, 20],
"bottom-right": [827, 370]
"top-left": [
125,
20
],
"bottom-right": [
827,
370
]
},
"field-size": [54, 27],
"field-size": [
54,
27
],
"field-unit": "feet"
}

View File

@@ -1,10 +1,19 @@
{
"game" : "Destination: Deep Space",
"field-image" : "2019-field.jpg",
"field-corners": {
"top-left" : [217, 40],
"bottom-right" : [1372, 615]
},
"field-size" : [54, 27],
"field-unit" : "foot"
"game": "Destination: Deep Space",
"field-image": "2019-field.jpg",
"field-corners": {
"top-left": [
217,
40
],
"bottom-right": [
1372,
615
]
},
"field-size": [
54,
27
],
"field-unit": "foot"
}

View File

@@ -1,10 +1,19 @@
{
"game" : "Infinite Recharge",
"field-image" : "2020-field.png",
"field-corners": {
"top-left" : [96, 25],
"bottom-right" : [1040, 514]
},
"field-size" : [52.4375, 26.9375],
"field-unit" : "foot"
}
"game": "Infinite Recharge",
"field-image": "2020-field.png",
"field-corners": {
"top-left": [
96,
25
],
"bottom-right": [
1040,
514
]
},
"field-size": [
52.4375,
26.9375
],
"field-unit": "foot"
}

View File

@@ -1,10 +1,19 @@
{
"game": "Barrel Racing Path",
"field-image": "2021-barrel.png",
"field-corners": {
"top-left": [20, 20],
"bottom-right": [780, 400]
},
"field-size": [30, 15],
"field-unit": "feet"
}
"game": "Barrel Racing Path",
"field-image": "2021-barrel.png",
"field-corners": {
"top-left": [
20,
20
],
"bottom-right": [
780,
400
]
},
"field-size": [
30,
15
],
"field-unit": "feet"
}

View File

@@ -1,10 +1,19 @@
{
"game": "Bounce Path",
"field-image": "2021-bounce.png",
"field-corners": {
"top-left": [20, 20],
"bottom-right": [780, 400]
},
"field-size": [30, 15],
"field-unit": "feet"
}
"game": "Bounce Path",
"field-image": "2021-bounce.png",
"field-corners": {
"top-left": [
20,
20
],
"bottom-right": [
780,
400
]
},
"field-size": [
30,
15
],
"field-unit": "feet"
}

View File

@@ -1,10 +1,19 @@
{
"game": "Galactic Search A",
"field-image": "2021-galacticsearcha.png",
"field-corners": {
"top-left": [20, 20],
"bottom-right": [780, 400]
},
"field-size": [30, 15],
"field-unit": "feet"
}
"game": "Galactic Search A",
"field-image": "2021-galacticsearcha.png",
"field-corners": {
"top-left": [
20,
20
],
"bottom-right": [
780,
400
]
},
"field-size": [
30,
15
],
"field-unit": "feet"
}

View File

@@ -1,10 +1,19 @@
{
"game": "Galactic Search B",
"field-image": "2021-galacticsearchb.png",
"field-corners": {
"top-left": [20, 20],
"bottom-right": [780, 400]
},
"field-size": [30, 15],
"field-unit": "feet"
}
"game": "Galactic Search B",
"field-image": "2021-galacticsearchb.png",
"field-corners": {
"top-left": [
20,
20
],
"bottom-right": [
780,
400
]
},
"field-size": [
30,
15
],
"field-unit": "feet"
}

View File

@@ -1,10 +1,19 @@
{
"game": "Infinite Recharge 2021",
"field-image": "2021-field.png",
"field-corners": {
"top-left": [127, 34],
"bottom-right": [1323, 649]
},
"field-size": [52.4375, 26.9375],
"field-unit": "foot"
}
"game": "Infinite Recharge 2021",
"field-image": "2021-field.png",
"field-corners": {
"top-left": [
127,
34
],
"bottom-right": [
1323,
649
]
},
"field-size": [
52.4375,
26.9375
],
"field-unit": "foot"
}

View File

@@ -1,10 +1,19 @@
{
"game": "Slalom Path",
"field-image": "2021-slalom.png",
"field-corners": {
"top-left": [20, 20],
"bottom-right": [780, 400]
},
"field-size": [30, 15],
"field-unit": "feet"
}
"game": "Slalom Path",
"field-image": "2021-slalom.png",
"field-corners": {
"top-left": [
20,
20
],
"bottom-right": [
780,
400
]
},
"field-size": [
30,
15
],
"field-unit": "feet"
}

View File

@@ -1,10 +1,19 @@
{
"game": "Rapid React",
"field-image": "2022-field.png",
"field-corners": {
"top-left": [74, 50],
"bottom-right": [1774, 900]
},
"field-size": [54, 27],
"field-unit": "foot"
}
"game": "Rapid React",
"field-image": "2022-field.png",
"field-corners": {
"top-left": [
74,
50
],
"bottom-right": [
1774,
900
]
},
"field-size": [
54,
27
],
"field-unit": "foot"
}

View File

@@ -0,0 +1,19 @@
{
"game": "Charged Up",
"field-image": "2023-field.png",
"field-corners": {
"top-left": [
46,
36
],
"bottom-right": [
1088,
544
]
},
"field-size": [
54.27083,
26.2916
],
"field-unit": "foot"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Some files were not shown because too many files have changed in this diff Show More