photonlibpy 2025.3.2__tar.gz → 2026.0.1a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/PKG-INFO +2 -2
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/photonCamera.py +10 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/photonPoseEstimator.py +107 -3
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/simulation/photonCameraSim.py +6 -5
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/targeting/photonPipelineResult.py +4 -7
- photonlibpy-2026.0.1a1/photonlibpy/version.py +2 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy.egg-info/PKG-INFO +2 -2
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy.egg-info/SOURCES.txt +9 -1
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy.egg-info/top_level.txt +1 -0
- photonlibpy-2026.0.1a1/test/__init__.py +0 -0
- photonlibpy-2026.0.1a1/test/openCVHelp_test.py +205 -0
- photonlibpy-2026.0.1a1/test/photonCamera_test.py +36 -0
- photonlibpy-2026.0.1a1/test/photonPoseEstimator_test.py +384 -0
- photonlibpy-2026.0.1a1/test/photonlibpy_test.py +42 -0
- photonlibpy-2026.0.1a1/test/simCameraProperties_test.py +99 -0
- photonlibpy-2026.0.1a1/test/testUtil.py +65 -0
- photonlibpy-2026.0.1a1/test/visionSystemSim_test.py +617 -0
- photonlibpy-2025.3.2/photonlibpy/version.py +0 -2
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/__init__.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/estimatedRobotPose.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/estimation/__init__.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/estimation/cameraTargetRelation.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/estimation/openCVHelp.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/estimation/rotTrlTransform3d.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/estimation/targetModel.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/estimation/visionEstimation.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/generated/MultiTargetPNPResultSerde.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/generated/PhotonPipelineMetadataSerde.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/generated/PhotonPipelineResultSerde.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/generated/PhotonTrackedTargetSerde.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/generated/PnpResultSerde.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/generated/TargetCornerSerde.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/generated/__init__.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/networktables/NTTopicSet.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/networktables/__init__.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/packet.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/py.typed +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/simulation/__init__.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/simulation/simCameraProperties.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/simulation/videoSimUtil.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/simulation/visionSystemSim.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/simulation/visionTargetSim.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/targeting/TargetCorner.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/targeting/__init__.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/targeting/multiTargetPNPResult.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/targeting/photonTrackedTarget.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/timesync/__init__.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/timesync/timeSyncServer.py +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy.egg-info/dependency_links.txt +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy.egg-info/requires.txt +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/pyproject.toml +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/setup.cfg +0 -0
- {photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/setup.py +0 -0
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: photonlibpy
|
3
|
-
Version:
|
4
|
-
Summary: Pure-python implementation of PhotonLib for interfacing with PhotonVision on coprocessors. Implemented with PhotonVision version
|
3
|
+
Version: 2026.0.1a1
|
4
|
+
Summary: Pure-python implementation of PhotonLib for interfacing with PhotonVision on coprocessors. Implemented with PhotonVision version v2026.0.1-alpha-1 .
|
5
5
|
Home-page: https://photonvision.org
|
6
6
|
Author: Photonvision Development Team
|
7
7
|
Classifier: License :: OSI Approved :: MIT License
|
@@ -18,6 +18,7 @@
|
|
18
18
|
from enum import Enum
|
19
19
|
from typing import List
|
20
20
|
|
21
|
+
import hal
|
21
22
|
import ntcore
|
22
23
|
|
23
24
|
# magical import to make serde stuff work
|
@@ -48,6 +49,8 @@ def setVersionCheckEnabled(enabled: bool):
|
|
48
49
|
|
49
50
|
|
50
51
|
class PhotonCamera:
|
52
|
+
instance_count = 1
|
53
|
+
|
51
54
|
def __init__(self, cameraName: str):
|
52
55
|
"""Constructs a PhotonCamera from the name of the camera.
|
53
56
|
|
@@ -108,6 +111,13 @@ class PhotonCamera:
|
|
108
111
|
# Start the time sync server
|
109
112
|
inst.start()
|
110
113
|
|
114
|
+
# Usage reporting
|
115
|
+
hal.report(
|
116
|
+
hal.tResourceType.kResourceType_PhotonCamera.value,
|
117
|
+
PhotonCamera.instance_count,
|
118
|
+
)
|
119
|
+
PhotonCamera.instance_count += 1
|
120
|
+
|
111
121
|
def getAllUnreadResults(self) -> List[PhotonPipelineResult]:
|
112
122
|
"""
|
113
123
|
The list of pipeline results sent by PhotonVision since the last call to getAllUnreadResults().
|
@@ -18,9 +18,20 @@
|
|
18
18
|
import enum
|
19
19
|
from typing import Optional
|
20
20
|
|
21
|
+
import hal
|
21
22
|
import wpilib
|
23
|
+
import wpimath.units
|
22
24
|
from robotpy_apriltag import AprilTagFieldLayout
|
23
|
-
from wpimath.geometry import
|
25
|
+
from wpimath.geometry import (
|
26
|
+
Pose2d,
|
27
|
+
Pose3d,
|
28
|
+
Rotation2d,
|
29
|
+
Rotation3d,
|
30
|
+
Transform3d,
|
31
|
+
Translation2d,
|
32
|
+
Translation3d,
|
33
|
+
)
|
34
|
+
from wpimath.interpolation import TimeInterpolatableRotation2dBuffer
|
24
35
|
|
25
36
|
from .estimatedRobotPose import EstimatedRobotPose
|
26
37
|
from .photonCamera import PhotonCamera
|
@@ -59,8 +70,21 @@ class PoseStrategy(enum.Enum):
|
|
59
70
|
This runs on the RoboRIO, and can take a lot of time.
|
60
71
|
"""
|
61
72
|
|
73
|
+
PNP_DISTANCE_TRIG_SOLVE = enum.auto()
|
74
|
+
"""
|
75
|
+
Use distance data from best visible tag to compute a Pose. This runs on
|
76
|
+
the RoboRIO in order to access the robot's yaw heading, and MUST have
|
77
|
+
addHeadingData called every frame so heading data is up-to-date.
|
78
|
+
|
79
|
+
Produces a Pose2d in estimatedRobotPose (0 for z, roll, pitch).
|
80
|
+
|
81
|
+
See https://www.chiefdelphi.com/t/frc-6328-mechanical-advantage-2025-build-thread/477314/98
|
82
|
+
"""
|
83
|
+
|
62
84
|
|
63
85
|
class PhotonPoseEstimator:
|
86
|
+
instance_count = 1
|
87
|
+
|
64
88
|
"""
|
65
89
|
The PhotonPoseEstimator class filters or combines readings from all the AprilTags visible at a
|
66
90
|
given timestamp on the field to produce a single robot in field pose, using the strategy set
|
@@ -95,8 +119,14 @@ class PhotonPoseEstimator:
|
|
95
119
|
self._poseCacheTimestampSeconds = -1.0
|
96
120
|
self._lastPose: Optional[Pose3d] = None
|
97
121
|
self._referencePose: Optional[Pose3d] = None
|
122
|
+
self._headingBuffer = TimeInterpolatableRotation2dBuffer(1)
|
98
123
|
|
99
|
-
#
|
124
|
+
# Usage reporting
|
125
|
+
hal.report(
|
126
|
+
hal.tResourceType.kResourceType_PhotonPoseEstimator.value,
|
127
|
+
PhotonPoseEstimator.instance_count,
|
128
|
+
)
|
129
|
+
PhotonPoseEstimator.instance_count += 1
|
100
130
|
|
101
131
|
@property
|
102
132
|
def fieldTags(self) -> AprilTagFieldLayout:
|
@@ -199,9 +229,35 @@ class PhotonPoseEstimator:
|
|
199
229
|
self._poseCacheTimestampSeconds = -1.0
|
200
230
|
|
201
231
|
def _checkUpdate(self, oldObj, newObj) -> None:
|
202
|
-
if oldObj != newObj
|
232
|
+
if oldObj != newObj:
|
203
233
|
self._invalidatePoseCache()
|
204
234
|
|
235
|
+
def addHeadingData(
|
236
|
+
self, timestampSeconds: wpimath.units.seconds, heading: Rotation2d | Rotation3d
|
237
|
+
) -> None:
|
238
|
+
"""
|
239
|
+
Add robot heading data to buffer. Must be called periodically for the **PNP_DISTANCE_TRIG_SOLVE** strategy.
|
240
|
+
|
241
|
+
:param timestampSeconds :timestamp of the robot heading data
|
242
|
+
:param heading: field-relative robot heading at given timestamp
|
243
|
+
"""
|
244
|
+
if isinstance(heading, Rotation3d):
|
245
|
+
heading = heading.toRotation2d()
|
246
|
+
self._headingBuffer.addSample(timestampSeconds, heading)
|
247
|
+
|
248
|
+
def resetHeadingData(
|
249
|
+
self, timestampSeconds: wpimath.units.seconds, heading: Rotation2d | Rotation3d
|
250
|
+
) -> None:
|
251
|
+
"""
|
252
|
+
Clears all heading data in the buffer, and adds a new seed. Useful for preventing estimates
|
253
|
+
from utilizing heading data provided prior to a pose or rotation reset.
|
254
|
+
|
255
|
+
:param timestampSeconds: timestamp of the robot heading data
|
256
|
+
:param heading: field-relative robot heading at given timestamp
|
257
|
+
"""
|
258
|
+
self._headingBuffer.clear()
|
259
|
+
self.addHeadingData(timestampSeconds, heading)
|
260
|
+
|
205
261
|
def update(
|
206
262
|
self, cameraResult: Optional[PhotonPipelineResult] = None
|
207
263
|
) -> Optional[EstimatedRobotPose]:
|
@@ -255,6 +311,8 @@ class PhotonPoseEstimator:
|
|
255
311
|
estimatedPose = self._lowestAmbiguityStrategy(cameraResult)
|
256
312
|
elif strat is PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR:
|
257
313
|
estimatedPose = self._multiTagOnCoprocStrategy(cameraResult)
|
314
|
+
elif strat is PoseStrategy.PNP_DISTANCE_TRIG_SOLVE:
|
315
|
+
estimatedPose = self._pnpDistanceTrigSolveStrategy(cameraResult)
|
258
316
|
else:
|
259
317
|
wpilib.reportError(
|
260
318
|
"[PhotonPoseEstimator] Unknown Position Estimation Strategy!", False
|
@@ -266,6 +324,52 @@ class PhotonPoseEstimator:
|
|
266
324
|
|
267
325
|
return estimatedPose
|
268
326
|
|
327
|
+
def _pnpDistanceTrigSolveStrategy(
|
328
|
+
self, result: PhotonPipelineResult
|
329
|
+
) -> Optional[EstimatedRobotPose]:
|
330
|
+
if (bestTarget := result.getBestTarget()) is None:
|
331
|
+
return None
|
332
|
+
|
333
|
+
if (
|
334
|
+
headingSample := self._headingBuffer.sample(result.getTimestampSeconds())
|
335
|
+
) is None:
|
336
|
+
return None
|
337
|
+
|
338
|
+
if (tagPose := self._fieldTags.getTagPose(bestTarget.fiducialId)) is None:
|
339
|
+
return None
|
340
|
+
|
341
|
+
camToTagTranslation = (
|
342
|
+
Translation3d(
|
343
|
+
bestTarget.getBestCameraToTarget().translation().norm(),
|
344
|
+
Rotation3d(
|
345
|
+
0,
|
346
|
+
-wpimath.units.degreesToRadians(bestTarget.getPitch()),
|
347
|
+
-wpimath.units.degreesToRadians(bestTarget.getYaw()),
|
348
|
+
),
|
349
|
+
)
|
350
|
+
.rotateBy(self.robotToCamera.rotation())
|
351
|
+
.toTranslation2d()
|
352
|
+
.rotateBy(headingSample)
|
353
|
+
)
|
354
|
+
|
355
|
+
fieldToCameraTranslation = (
|
356
|
+
tagPose.toPose2d().translation() - camToTagTranslation
|
357
|
+
)
|
358
|
+
camToRobotTranslation: Translation2d = -(
|
359
|
+
self.robotToCamera.translation().toTranslation2d()
|
360
|
+
)
|
361
|
+
camToRobotTranslation = camToRobotTranslation.rotateBy(headingSample)
|
362
|
+
robotPose = Pose2d(
|
363
|
+
fieldToCameraTranslation + camToRobotTranslation, headingSample
|
364
|
+
)
|
365
|
+
|
366
|
+
return EstimatedRobotPose(
|
367
|
+
Pose3d(robotPose),
|
368
|
+
result.getTimestampSeconds(),
|
369
|
+
result.getTargets(),
|
370
|
+
PoseStrategy.PNP_DISTANCE_TRIG_SOLVE,
|
371
|
+
)
|
372
|
+
|
269
373
|
def _multiTagOnCoprocStrategy(
|
270
374
|
self, result: PhotonPipelineResult
|
271
375
|
) -> Optional[EstimatedRobotPose]:
|
@@ -420,14 +420,15 @@ class PhotonCameraSim:
|
|
420
420
|
|
421
421
|
# put this simulated data to NT
|
422
422
|
self.heartbeatCounter += 1
|
423
|
-
|
423
|
+
publishTimestampMicros = wpilib.Timer.getFPGATimestamp() * 1e6
|
424
424
|
return PhotonPipelineResult(
|
425
|
+
ntReceiveTimestampMicros=int(publishTimestampMicros + 10),
|
425
426
|
metadata=PhotonPipelineMetadata(
|
426
|
-
int(
|
427
|
-
int(
|
428
|
-
self.heartbeatCounter,
|
427
|
+
captureTimestampMicros=int(publishTimestampMicros - latency * 1e6),
|
428
|
+
publishTimestampMicros=int(publishTimestampMicros),
|
429
|
+
sequenceID=self.heartbeatCounter,
|
429
430
|
# Pretend like we heard a pong recently
|
430
|
-
int(np.random.uniform(950, 1050)),
|
431
|
+
timeSinceLastPong=int(np.random.uniform(950, 1050)),
|
431
432
|
),
|
432
433
|
targets=detectableTgts,
|
433
434
|
multitagResult=multiTagResults,
|
{photonlibpy-2025.3.2 → photonlibpy-2026.0.1a1}/photonlibpy/targeting/photonPipelineResult.py
RENAMED
@@ -47,13 +47,10 @@ class PhotonPipelineResult:
|
|
47
47
|
timestamp, coproc timebase))
|
48
48
|
"""
|
49
49
|
# TODO - we don't trust NT4 to correctly latency-compensate ntReceiveTimestampMicros
|
50
|
-
|
51
|
-
self.
|
52
|
-
|
53
|
-
|
54
|
-
- self.metadata.captureTimestampMicros
|
55
|
-
)
|
56
|
-
) / 1e6
|
50
|
+
latency = (
|
51
|
+
self.metadata.publishTimestampMicros - self.metadata.captureTimestampMicros
|
52
|
+
)
|
53
|
+
return (self.ntReceiveTimestampMicros - latency) / 1e6
|
57
54
|
|
58
55
|
def getTargets(self) -> list[PhotonTrackedTarget]:
|
59
56
|
return self.targets
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: photonlibpy
|
3
|
-
Version:
|
4
|
-
Summary: Pure-python implementation of PhotonLib for interfacing with PhotonVision on coprocessors. Implemented with PhotonVision version
|
3
|
+
Version: 2026.0.1a1
|
4
|
+
Summary: Pure-python implementation of PhotonLib for interfacing with PhotonVision on coprocessors. Implemented with PhotonVision version v2026.0.1-alpha-1 .
|
5
5
|
Home-page: https://photonvision.org
|
6
6
|
Author: Photonvision Development Team
|
7
7
|
Classifier: License :: OSI Approved :: MIT License
|
@@ -39,4 +39,12 @@ photonlibpy/targeting/multiTargetPNPResult.py
|
|
39
39
|
photonlibpy/targeting/photonPipelineResult.py
|
40
40
|
photonlibpy/targeting/photonTrackedTarget.py
|
41
41
|
photonlibpy/timesync/__init__.py
|
42
|
-
photonlibpy/timesync/timeSyncServer.py
|
42
|
+
photonlibpy/timesync/timeSyncServer.py
|
43
|
+
test/__init__.py
|
44
|
+
test/openCVHelp_test.py
|
45
|
+
test/photonCamera_test.py
|
46
|
+
test/photonPoseEstimator_test.py
|
47
|
+
test/photonlibpy_test.py
|
48
|
+
test/simCameraProperties_test.py
|
49
|
+
test/testUtil.py
|
50
|
+
test/visionSystemSim_test.py
|
File without changes
|
@@ -0,0 +1,205 @@
|
|
1
|
+
import math
|
2
|
+
|
3
|
+
import ntcore as nt
|
4
|
+
import pytest
|
5
|
+
from photonlibpy.estimation import RotTrlTransform3d, TargetModel
|
6
|
+
from photonlibpy.estimation.openCVHelp import OpenCVHelp
|
7
|
+
from photonlibpy.photonCamera import setVersionCheckEnabled
|
8
|
+
from photonlibpy.simulation import SimCameraProperties, VisionTargetSim
|
9
|
+
from wpimath.geometry import Pose3d, Rotation3d, Translation3d
|
10
|
+
|
11
|
+
|
12
|
+
@pytest.fixture(autouse=True)
|
13
|
+
def setupCommon() -> None:
|
14
|
+
nt.NetworkTableInstance.getDefault().startServer()
|
15
|
+
setVersionCheckEnabled(False)
|
16
|
+
|
17
|
+
|
18
|
+
def test_TrlConvert():
|
19
|
+
trl = Translation3d(0.75, -0.4, 0.1)
|
20
|
+
tvec = OpenCVHelp.translationToTVec([trl])
|
21
|
+
result = OpenCVHelp.tVecToTranslation(tvec[0])
|
22
|
+
|
23
|
+
assert result.X() == pytest.approx(trl.X(), 0.005)
|
24
|
+
assert result.Y() == pytest.approx(trl.Y(), 0.005)
|
25
|
+
assert result.Z() == pytest.approx(trl.Z(), 0.005)
|
26
|
+
|
27
|
+
|
28
|
+
def test_RotConvert():
|
29
|
+
rot = Rotation3d(0.5, 1, -1)
|
30
|
+
rvec = OpenCVHelp.rotationToRVec(rot)
|
31
|
+
result = OpenCVHelp.rVecToRotation(rvec[0])
|
32
|
+
|
33
|
+
assert result.X() == pytest.approx(rot.X(), 0.25)
|
34
|
+
assert result.Y() == pytest.approx(rot.Y(), 0.25)
|
35
|
+
assert result.Z() == pytest.approx(rot.Z(), 0.25)
|
36
|
+
|
37
|
+
|
38
|
+
def test_Projection():
|
39
|
+
prop = SimCameraProperties()
|
40
|
+
|
41
|
+
target = VisionTargetSim(
|
42
|
+
Pose3d(Translation3d(1.0, 0.0, 0.0), Rotation3d(0.0, 0.0, math.pi)),
|
43
|
+
TargetModel.AprilTag16h5(),
|
44
|
+
4774,
|
45
|
+
)
|
46
|
+
|
47
|
+
cameraPose = Pose3d(Translation3d(), Rotation3d())
|
48
|
+
|
49
|
+
camRt = RotTrlTransform3d.makeRelativeTo(cameraPose)
|
50
|
+
imagePoints = OpenCVHelp.projectPoints(
|
51
|
+
prop.getIntrinsics(), prop.getDistCoeffs(), camRt, target.getFieldVertices()
|
52
|
+
)
|
53
|
+
|
54
|
+
# find circulation (counter/clockwise-ness)
|
55
|
+
circulation = 0.0
|
56
|
+
for i in range(0, len(imagePoints)):
|
57
|
+
xDiff = imagePoints[(i + 1) % 4][0][0] - imagePoints[i][0][0]
|
58
|
+
ySum = imagePoints[(i + 1) % 4][0][1] + imagePoints[i][0][1]
|
59
|
+
circulation += xDiff * ySum
|
60
|
+
|
61
|
+
assert circulation > 0, "2d fiducial points aren't counter-clockwise"
|
62
|
+
|
63
|
+
# TODO Uncomment after OpenCVHelp.undistortPoints is implemented
|
64
|
+
# # undo projection distortion
|
65
|
+
# imagePoints = OpenCVHelp.undistortPoints(
|
66
|
+
# prop.getIntrinsics(), prop.getDistCoeffs(), imagePoints
|
67
|
+
# )
|
68
|
+
# # test projection results after moving camera
|
69
|
+
# avgCenterRot1 = prop.getPixelRot(OpenCVHelp.avgPoint(imagePoints))
|
70
|
+
# cameraPose = cameraPose + Transform3d(Translation3d(), Rotation3d(0.0, 0.25, 0.25))
|
71
|
+
# camRt = RotTrlTransform3d.makeRelativeTo(cameraPose)
|
72
|
+
# imagePoints = OpenCVHelp.projectPoints(
|
73
|
+
# prop.getIntrinsics(), prop.getDistCoeffs(), camRt, target.getFieldVertices()
|
74
|
+
# )
|
75
|
+
# avgCenterRot2 = prop.getPixelRot(OpenCVHelp.avgPoint(imagePoints))
|
76
|
+
|
77
|
+
# yaw2d = Rotation2d(avgCenterRot2.Z())
|
78
|
+
# pitch2d = Rotation2d(avgCenterRot2.Y())
|
79
|
+
# yawDiff = yaw2d - Rotation2d(avgCenterRot1.Z())
|
80
|
+
# pitchDiff = pitch2d - Rotation2d(avgCenterRot2.Y())
|
81
|
+
# assert yawDiff.radians() < 0.0, "2d points don't follow yaw"
|
82
|
+
# assert pitchDiff.radians() < 0.0, "2d points don't follow pitch"
|
83
|
+
|
84
|
+
# actualRelation = CameraTargetRelation(cameraPose, target.getPose())
|
85
|
+
|
86
|
+
# assert actualRelation.camToTargPitch.degrees() == pytest.approx(
|
87
|
+
# pitchDiff.degrees()
|
88
|
+
# * math.cos(yaw2d.radians()), # adjust for unaccounted perspective distortion
|
89
|
+
# abs=0.25,
|
90
|
+
# ), "2d pitch doesn't match 3d"
|
91
|
+
# assert actualRelation.camToTargYaw.degrees() == pytest.approx(
|
92
|
+
# yawDiff.degrees(), abs=0.25
|
93
|
+
# ), "2d yaw doesn't match 3d"
|
94
|
+
|
95
|
+
|
96
|
+
def test_SolvePNP_SQUARE():
|
97
|
+
prop = SimCameraProperties()
|
98
|
+
|
99
|
+
# square AprilTag target
|
100
|
+
target = VisionTargetSim(
|
101
|
+
Pose3d(Translation3d(5.0, 0.5, 1.0), Rotation3d(0.0, 0.0, math.pi)),
|
102
|
+
TargetModel.AprilTag16h5(),
|
103
|
+
4774,
|
104
|
+
)
|
105
|
+
cameraPose = Pose3d(Translation3d(), Rotation3d())
|
106
|
+
camRt = RotTrlTransform3d.makeRelativeTo(cameraPose)
|
107
|
+
|
108
|
+
# target relative to camera
|
109
|
+
relTarget = camRt.applyPose(target.getPose())
|
110
|
+
|
111
|
+
# simulate solvePNP estimation
|
112
|
+
targetCorners = OpenCVHelp.projectPoints(
|
113
|
+
prop.getIntrinsics(), prop.getDistCoeffs(), camRt, target.getFieldVertices()
|
114
|
+
)
|
115
|
+
|
116
|
+
pnpSim = OpenCVHelp.solvePNP_Square(
|
117
|
+
prop.getIntrinsics(),
|
118
|
+
prop.getDistCoeffs(),
|
119
|
+
target.getModel().vertices,
|
120
|
+
targetCorners,
|
121
|
+
)
|
122
|
+
|
123
|
+
assert pnpSim is not None
|
124
|
+
|
125
|
+
# check solvePNP estimation accuracy
|
126
|
+
assert relTarget.rotation().X() == pytest.approx(
|
127
|
+
pnpSim.best.rotation().X(), abs=0.25
|
128
|
+
)
|
129
|
+
assert relTarget.rotation().Y() == pytest.approx(
|
130
|
+
pnpSim.best.rotation().Y(), abs=0.25
|
131
|
+
)
|
132
|
+
assert relTarget.rotation().Z() == pytest.approx(
|
133
|
+
pnpSim.best.rotation().Z(), abs=0.25
|
134
|
+
)
|
135
|
+
|
136
|
+
assert relTarget.translation().X() == pytest.approx(
|
137
|
+
pnpSim.best.translation().X(), abs=0.005
|
138
|
+
)
|
139
|
+
assert relTarget.translation().Y() == pytest.approx(
|
140
|
+
pnpSim.best.translation().Y(), abs=0.005
|
141
|
+
)
|
142
|
+
assert relTarget.translation().Z() == pytest.approx(
|
143
|
+
pnpSim.best.translation().Z(), abs=0.005
|
144
|
+
)
|
145
|
+
|
146
|
+
|
147
|
+
def test_SolvePNP_SQPNP():
|
148
|
+
prop = SimCameraProperties()
|
149
|
+
|
150
|
+
# (for targets with arbitrary number of non-colinear points > 2)
|
151
|
+
target = VisionTargetSim(
|
152
|
+
Pose3d(Translation3d(5.0, 0.5, 1.0), Rotation3d(0.0, 0.0, math.pi)),
|
153
|
+
TargetModel.createArbitrary(
|
154
|
+
verts=[
|
155
|
+
Translation3d(0.0, 0.0, 0.0),
|
156
|
+
Translation3d(1.0, 0.0, 0.0),
|
157
|
+
Translation3d(0.0, 1.0, 0.0),
|
158
|
+
Translation3d(0.0, 0.0, 1.0),
|
159
|
+
Translation3d(0.125, 0.25, 0.5),
|
160
|
+
Translation3d(0.0, 0.0, -1.0),
|
161
|
+
Translation3d(0.0, -1.0, 0.0),
|
162
|
+
Translation3d(-1.0, 0.0, 0.0),
|
163
|
+
]
|
164
|
+
),
|
165
|
+
4774,
|
166
|
+
)
|
167
|
+
cameraPose = Pose3d(Translation3d(), Rotation3d())
|
168
|
+
camRt = RotTrlTransform3d.makeRelativeTo(cameraPose)
|
169
|
+
# target relative to camera
|
170
|
+
relTarget = camRt.applyPose(target.getPose())
|
171
|
+
|
172
|
+
# simulate solvePNP estimation
|
173
|
+
targetCorners = OpenCVHelp.projectPoints(
|
174
|
+
prop.getIntrinsics(), prop.getDistCoeffs(), camRt, target.getFieldVertices()
|
175
|
+
)
|
176
|
+
|
177
|
+
pnpSim = OpenCVHelp.solvePNP_SQPNP(
|
178
|
+
prop.getIntrinsics(),
|
179
|
+
prop.getDistCoeffs(),
|
180
|
+
target.getModel().vertices,
|
181
|
+
targetCorners,
|
182
|
+
)
|
183
|
+
|
184
|
+
assert pnpSim is not None
|
185
|
+
|
186
|
+
# check solvePNP estimation accuracy
|
187
|
+
assert relTarget.rotation().X() == pytest.approx(
|
188
|
+
pnpSim.best.rotation().X(), abs=0.25
|
189
|
+
)
|
190
|
+
assert relTarget.rotation().Y() == pytest.approx(
|
191
|
+
pnpSim.best.rotation().Y(), abs=0.25
|
192
|
+
)
|
193
|
+
assert relTarget.rotation().Z() == pytest.approx(
|
194
|
+
pnpSim.best.rotation().Z(), abs=0.25
|
195
|
+
)
|
196
|
+
|
197
|
+
assert relTarget.translation().X() == pytest.approx(
|
198
|
+
pnpSim.best.translation().X(), abs=0.005
|
199
|
+
)
|
200
|
+
assert relTarget.translation().Y() == pytest.approx(
|
201
|
+
pnpSim.best.translation().Y(), abs=0.005
|
202
|
+
)
|
203
|
+
assert relTarget.translation().Z() == pytest.approx(
|
204
|
+
pnpSim.best.translation().Z(), abs=0.005
|
205
|
+
)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import pytest
|
2
|
+
from photonlibpy import Packet
|
3
|
+
from photonlibpy.targeting import PhotonPipelineResult
|
4
|
+
|
5
|
+
|
6
|
+
@pytest.fixture(autouse=True)
|
7
|
+
def setupCommon() -> None:
|
8
|
+
pass
|
9
|
+
|
10
|
+
|
11
|
+
def test_Empty() -> None:
|
12
|
+
packet = Packet(b"1")
|
13
|
+
PhotonPipelineResult()
|
14
|
+
packet.setData(bytes(0))
|
15
|
+
PhotonPipelineResult.photonStruct.unpack(packet)
|
16
|
+
# There is no need for an assert as we are checking
|
17
|
+
# if this throws an exception (it should not)
|
18
|
+
|
19
|
+
|
20
|
+
@pytest.mark.parametrize(
|
21
|
+
"robotStart, coprocStart, robotRestart, coprocRestart",
|
22
|
+
[
|
23
|
+
[1, 10, 30, 30],
|
24
|
+
[10, 2, 30, 30],
|
25
|
+
[10, 10, 30, 30],
|
26
|
+
# Reboot just the robot
|
27
|
+
[1, 1, 10, 30],
|
28
|
+
# Reboot just the coproc
|
29
|
+
[1, 1, 30, 10],
|
30
|
+
],
|
31
|
+
)
|
32
|
+
def test_RestartingRobotandCoproc(
|
33
|
+
robotStart: int, coprocStart: int, robotRestart: int, coprocRestart: int
|
34
|
+
):
|
35
|
+
# Python doesn't have a TimeSyncClient so we can't run this yet
|
36
|
+
pass
|