photonlibpy 2025.3.2__py3-none-any.whl → 2026.0.1a1__py3-none-any.whl
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/photonCamera.py +10 -0
- photonlibpy/photonPoseEstimator.py +107 -3
- photonlibpy/simulation/photonCameraSim.py +6 -5
- photonlibpy/targeting/photonPipelineResult.py +4 -7
- photonlibpy/version.py +2 -2
- {photonlibpy-2025.3.2.dist-info → photonlibpy-2026.0.1a1.dist-info}/METADATA +2 -2
- {photonlibpy-2025.3.2.dist-info → photonlibpy-2026.0.1a1.dist-info}/RECORD +17 -9
- {photonlibpy-2025.3.2.dist-info → photonlibpy-2026.0.1a1.dist-info}/top_level.txt +1 -0
- test/__init__.py +0 -0
- test/openCVHelp_test.py +205 -0
- test/photonCamera_test.py +36 -0
- test/photonPoseEstimator_test.py +384 -0
- test/photonlibpy_test.py +42 -0
- test/simCameraProperties_test.py +99 -0
- test/testUtil.py +65 -0
- test/visionSystemSim_test.py +617 -0
- {photonlibpy-2025.3.2.dist-info → photonlibpy-2026.0.1a1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,384 @@
|
|
1
|
+
###############################################################################
|
2
|
+
## Copyright (C) Photon Vision.
|
3
|
+
###############################################################################
|
4
|
+
## This program is free software: you can redistribute it and/or modify
|
5
|
+
## it under the terms of the GNU General Public License as published by
|
6
|
+
## the Free Software Foundation, either version 3 of the License, or
|
7
|
+
## (at your option) any later version.
|
8
|
+
##
|
9
|
+
## This program is distributed in the hope that it will be useful,
|
10
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
## GNU General Public License for more details.
|
13
|
+
##
|
14
|
+
## You should have received a copy of the GNU General Public License
|
15
|
+
## along with this program. If not, see <https://www.gnu.org/licenses/>.
|
16
|
+
###############################################################################
|
17
|
+
|
18
|
+
from test import testUtil
|
19
|
+
|
20
|
+
import wpimath.units
|
21
|
+
from photonlibpy import PhotonCamera, PhotonPoseEstimator, PoseStrategy
|
22
|
+
from photonlibpy.estimation import TargetModel
|
23
|
+
from photonlibpy.simulation import PhotonCameraSim, SimCameraProperties, VisionTargetSim
|
24
|
+
from photonlibpy.targeting import (
|
25
|
+
PhotonPipelineMetadata,
|
26
|
+
PhotonTrackedTarget,
|
27
|
+
TargetCorner,
|
28
|
+
)
|
29
|
+
from photonlibpy.targeting.multiTargetPNPResult import MultiTargetPNPResult, PnpResult
|
30
|
+
from photonlibpy.targeting.photonPipelineResult import PhotonPipelineResult
|
31
|
+
from robotpy_apriltag import AprilTag, AprilTagFieldLayout
|
32
|
+
from wpimath.geometry import Pose3d, Rotation3d, Transform3d, Translation3d
|
33
|
+
|
34
|
+
|
35
|
+
class PhotonCameraInjector(PhotonCamera):
|
36
|
+
result: PhotonPipelineResult
|
37
|
+
|
38
|
+
def __init__(self, cameraName="camera"):
|
39
|
+
super().__init__(cameraName)
|
40
|
+
|
41
|
+
def getLatestResult(self) -> PhotonPipelineResult:
|
42
|
+
return self.result
|
43
|
+
|
44
|
+
|
45
|
+
def fakeAprilTagFieldLayout() -> AprilTagFieldLayout:
|
46
|
+
tagList = []
|
47
|
+
tagPoses = (
|
48
|
+
Pose3d(3, 3, 3, Rotation3d()),
|
49
|
+
Pose3d(5, 5, 5, Rotation3d()),
|
50
|
+
)
|
51
|
+
for id_, pose in enumerate(tagPoses):
|
52
|
+
aprilTag = AprilTag()
|
53
|
+
aprilTag.ID = id_
|
54
|
+
aprilTag.pose = pose
|
55
|
+
tagList.append(aprilTag)
|
56
|
+
|
57
|
+
fieldLength = 54 / 3.281 # 54 ft -> meters
|
58
|
+
fieldWidth = 27 / 3.281 # 24 ft -> meters
|
59
|
+
|
60
|
+
return AprilTagFieldLayout(tagList, fieldLength, fieldWidth)
|
61
|
+
|
62
|
+
|
63
|
+
def test_lowestAmbiguityStrategy():
|
64
|
+
aprilTags = fakeAprilTagFieldLayout()
|
65
|
+
cameraOne = PhotonCameraInjector()
|
66
|
+
cameraOne.result = PhotonPipelineResult(
|
67
|
+
int(11 * 1e6),
|
68
|
+
[
|
69
|
+
PhotonTrackedTarget(
|
70
|
+
3.0,
|
71
|
+
-4.0,
|
72
|
+
9.0,
|
73
|
+
4.0,
|
74
|
+
0,
|
75
|
+
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
76
|
+
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
77
|
+
[
|
78
|
+
TargetCorner(1, 2),
|
79
|
+
TargetCorner(3, 4),
|
80
|
+
TargetCorner(5, 6),
|
81
|
+
TargetCorner(7, 8),
|
82
|
+
],
|
83
|
+
[
|
84
|
+
TargetCorner(1, 2),
|
85
|
+
TargetCorner(3, 4),
|
86
|
+
TargetCorner(5, 6),
|
87
|
+
TargetCorner(7, 8),
|
88
|
+
],
|
89
|
+
0.7,
|
90
|
+
),
|
91
|
+
PhotonTrackedTarget(
|
92
|
+
3.0,
|
93
|
+
-4.0,
|
94
|
+
9.1,
|
95
|
+
6.7,
|
96
|
+
1,
|
97
|
+
Transform3d(Translation3d(4, 2, 3), Rotation3d(0, 0, 0)),
|
98
|
+
Transform3d(Translation3d(4, 2, 3), Rotation3d(1, 5, 3)),
|
99
|
+
[
|
100
|
+
TargetCorner(1, 2),
|
101
|
+
TargetCorner(3, 4),
|
102
|
+
TargetCorner(5, 6),
|
103
|
+
TargetCorner(7, 8),
|
104
|
+
],
|
105
|
+
[
|
106
|
+
TargetCorner(1, 2),
|
107
|
+
TargetCorner(3, 4),
|
108
|
+
TargetCorner(5, 6),
|
109
|
+
TargetCorner(7, 8),
|
110
|
+
],
|
111
|
+
0.3,
|
112
|
+
),
|
113
|
+
PhotonTrackedTarget(
|
114
|
+
9.0,
|
115
|
+
-2.0,
|
116
|
+
19.0,
|
117
|
+
3.0,
|
118
|
+
0,
|
119
|
+
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
120
|
+
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
121
|
+
[
|
122
|
+
TargetCorner(1, 2),
|
123
|
+
TargetCorner(3, 4),
|
124
|
+
TargetCorner(5, 6),
|
125
|
+
TargetCorner(7, 8),
|
126
|
+
],
|
127
|
+
[
|
128
|
+
TargetCorner(1, 2),
|
129
|
+
TargetCorner(3, 4),
|
130
|
+
TargetCorner(5, 6),
|
131
|
+
TargetCorner(7, 8),
|
132
|
+
],
|
133
|
+
0.4,
|
134
|
+
),
|
135
|
+
],
|
136
|
+
metadata=PhotonPipelineMetadata(0, int(2 * 1e3), 0),
|
137
|
+
multitagResult=None,
|
138
|
+
)
|
139
|
+
|
140
|
+
estimator = PhotonPoseEstimator(
|
141
|
+
aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameraOne, Transform3d()
|
142
|
+
)
|
143
|
+
|
144
|
+
estimatedPose = estimator.update()
|
145
|
+
|
146
|
+
assert estimatedPose is not None
|
147
|
+
|
148
|
+
pose = estimatedPose.estimatedPose
|
149
|
+
|
150
|
+
assertEquals(11 - 0.002, estimatedPose.timestampSeconds, 1e-3)
|
151
|
+
assertEquals(1, pose.x, 0.01)
|
152
|
+
assertEquals(3, pose.y, 0.01)
|
153
|
+
assertEquals(2, pose.z, 0.01)
|
154
|
+
|
155
|
+
|
156
|
+
def test_pnpDistanceTrigSolve():
|
157
|
+
aprilTags = fakeAprilTagFieldLayout()
|
158
|
+
cameraOne = PhotonCameraInjector()
|
159
|
+
latencySecs: wpimath.units.seconds = 1
|
160
|
+
fakeTimestampSecs: wpimath.units.seconds = 9 + latencySecs
|
161
|
+
|
162
|
+
cameraOneSim = PhotonCameraSim(cameraOne, SimCameraProperties.PERFECT_90DEG())
|
163
|
+
simTargets = [
|
164
|
+
VisionTargetSim(tag.pose, TargetModel.AprilTag36h11(), tag.ID)
|
165
|
+
for tag in aprilTags.getTags()
|
166
|
+
]
|
167
|
+
|
168
|
+
# Compound Rolled + Pitched + Yaw
|
169
|
+
compoundTestTransform = Transform3d(
|
170
|
+
-wpimath.units.inchesToMeters(12),
|
171
|
+
-wpimath.units.inchesToMeters(11),
|
172
|
+
3,
|
173
|
+
Rotation3d(
|
174
|
+
wpimath.units.degreesToRadians(37),
|
175
|
+
wpimath.units.degreesToRadians(6),
|
176
|
+
wpimath.units.degreesToRadians(60),
|
177
|
+
),
|
178
|
+
)
|
179
|
+
|
180
|
+
estimator = PhotonPoseEstimator(
|
181
|
+
aprilTags,
|
182
|
+
PoseStrategy.PNP_DISTANCE_TRIG_SOLVE,
|
183
|
+
cameraOne,
|
184
|
+
compoundTestTransform,
|
185
|
+
)
|
186
|
+
|
187
|
+
realPose = Pose3d(7.3, 4.42, 0, Rotation3d(0, 0, 2.197)) # Pose to compare with
|
188
|
+
result = cameraOneSim.process(
|
189
|
+
latencySecs, realPose.transformBy(estimator.robotToCamera), simTargets
|
190
|
+
)
|
191
|
+
bestTarget = result.getBestTarget()
|
192
|
+
assert bestTarget is not None
|
193
|
+
assert bestTarget.fiducialId == 0
|
194
|
+
assert result.ntReceiveTimestampMicros > 0
|
195
|
+
# Make test independent of the FPGA time.
|
196
|
+
result.ntReceiveTimestampMicros = int(fakeTimestampSecs * 1e6)
|
197
|
+
|
198
|
+
estimator.addHeadingData(
|
199
|
+
result.getTimestampSeconds(), realPose.rotation().toRotation2d()
|
200
|
+
)
|
201
|
+
estimatedRobotPose = estimator.update(result)
|
202
|
+
|
203
|
+
assert estimatedRobotPose is not None
|
204
|
+
pose = estimatedRobotPose.estimatedPose
|
205
|
+
assertEquals(realPose.x, pose.x, 0.01)
|
206
|
+
assertEquals(realPose.y, pose.y, 0.01)
|
207
|
+
assertEquals(0.0, pose.z, 0.01)
|
208
|
+
|
209
|
+
# Straight on
|
210
|
+
fakeTimestampSecs += 60
|
211
|
+
straightOnTestTransform = Transform3d(0, 0, 3, Rotation3d())
|
212
|
+
estimator.robotToCamera = straightOnTestTransform
|
213
|
+
realPose = Pose3d(4.81, 2.38, 0, Rotation3d(0, 0, 2.818)) # Pose to compare with
|
214
|
+
result = cameraOneSim.process(
|
215
|
+
latencySecs, realPose.transformBy(estimator.robotToCamera), simTargets
|
216
|
+
)
|
217
|
+
bestTarget = result.getBestTarget()
|
218
|
+
assert bestTarget is not None
|
219
|
+
assert bestTarget.fiducialId == 0
|
220
|
+
assert result.ntReceiveTimestampMicros > 0
|
221
|
+
# Make test independent of the FPGA time.
|
222
|
+
result.ntReceiveTimestampMicros = int(fakeTimestampSecs * 1e6)
|
223
|
+
|
224
|
+
estimator.addHeadingData(
|
225
|
+
result.getTimestampSeconds(), realPose.rotation().toRotation2d()
|
226
|
+
)
|
227
|
+
estimatedRobotPose = estimator.update(result)
|
228
|
+
|
229
|
+
assert estimatedRobotPose is not None
|
230
|
+
pose = estimatedRobotPose.estimatedPose
|
231
|
+
assertEquals(realPose.x, pose.x, 0.01)
|
232
|
+
assertEquals(realPose.y, pose.y, 0.01)
|
233
|
+
assertEquals(0.0, pose.z, 0.01)
|
234
|
+
|
235
|
+
|
236
|
+
def test_multiTagOnCoprocStrategy():
|
237
|
+
cameraOne = PhotonCameraInjector()
|
238
|
+
cameraOne.result = PhotonPipelineResult(
|
239
|
+
int(11 * 1e6),
|
240
|
+
# There needs to be at least one target present for pose estimation to work
|
241
|
+
# Doesn't matter which/how many targets for this test
|
242
|
+
[
|
243
|
+
PhotonTrackedTarget(
|
244
|
+
3.0,
|
245
|
+
-4.0,
|
246
|
+
9.0,
|
247
|
+
4.0,
|
248
|
+
0,
|
249
|
+
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
250
|
+
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
251
|
+
[
|
252
|
+
TargetCorner(1, 2),
|
253
|
+
TargetCorner(3, 4),
|
254
|
+
TargetCorner(5, 6),
|
255
|
+
TargetCorner(7, 8),
|
256
|
+
],
|
257
|
+
[
|
258
|
+
TargetCorner(1, 2),
|
259
|
+
TargetCorner(3, 4),
|
260
|
+
TargetCorner(5, 6),
|
261
|
+
TargetCorner(7, 8),
|
262
|
+
],
|
263
|
+
0.7,
|
264
|
+
)
|
265
|
+
],
|
266
|
+
metadata=PhotonPipelineMetadata(0, int(2 * 1e3), 0),
|
267
|
+
multitagResult=MultiTargetPNPResult(
|
268
|
+
PnpResult(Transform3d(1, 3, 2, Rotation3d()))
|
269
|
+
),
|
270
|
+
)
|
271
|
+
|
272
|
+
estimator = PhotonPoseEstimator(
|
273
|
+
AprilTagFieldLayout(),
|
274
|
+
PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR,
|
275
|
+
cameraOne,
|
276
|
+
Transform3d(),
|
277
|
+
)
|
278
|
+
|
279
|
+
estimatedPose = estimator.update()
|
280
|
+
|
281
|
+
assert estimatedPose is not None
|
282
|
+
|
283
|
+
pose = estimatedPose.estimatedPose
|
284
|
+
|
285
|
+
assertEquals(11 - 2e-3, estimatedPose.timestampSeconds, 1e-3)
|
286
|
+
assertEquals(1, pose.x, 0.01)
|
287
|
+
assertEquals(3, pose.y, 0.01)
|
288
|
+
assertEquals(2, pose.z, 0.01)
|
289
|
+
|
290
|
+
|
291
|
+
def test_cacheIsInvalidated():
|
292
|
+
aprilTags = fakeAprilTagFieldLayout()
|
293
|
+
cameraOne = PhotonCameraInjector()
|
294
|
+
|
295
|
+
estimator = PhotonPoseEstimator(
|
296
|
+
aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameraOne, Transform3d()
|
297
|
+
)
|
298
|
+
|
299
|
+
# Initial state, expect no timestamp.
|
300
|
+
assertEquals(-1, estimator._poseCacheTimestampSeconds)
|
301
|
+
|
302
|
+
# First result is 17s after epoch start.
|
303
|
+
timestamps = testUtil.PipelineTimestamps(captureTimestampMicros=17_000_000)
|
304
|
+
latencySecs = timestamps.pipelineLatencySecs()
|
305
|
+
|
306
|
+
# No targets, expect empty result
|
307
|
+
cameraOne.result = PhotonPipelineResult(
|
308
|
+
timestamps.receiveTimestampMicros(),
|
309
|
+
metadata=timestamps.toPhotonPipelineMetadata(),
|
310
|
+
)
|
311
|
+
estimatedPose = estimator.update()
|
312
|
+
|
313
|
+
assert estimatedPose is None
|
314
|
+
assertEquals(
|
315
|
+
timestamps.receiveTimestampMicros() * 1e-6 - latencySecs,
|
316
|
+
estimator._poseCacheTimestampSeconds,
|
317
|
+
1e-3,
|
318
|
+
)
|
319
|
+
|
320
|
+
# Set actual result
|
321
|
+
timestamps.incrementTimeMicros(2_500_000)
|
322
|
+
result = PhotonPipelineResult(
|
323
|
+
timestamps.receiveTimestampMicros(),
|
324
|
+
[
|
325
|
+
PhotonTrackedTarget(
|
326
|
+
3.0,
|
327
|
+
-4.0,
|
328
|
+
9.0,
|
329
|
+
4.0,
|
330
|
+
0,
|
331
|
+
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
332
|
+
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
333
|
+
[
|
334
|
+
TargetCorner(1, 2),
|
335
|
+
TargetCorner(3, 4),
|
336
|
+
TargetCorner(5, 6),
|
337
|
+
TargetCorner(7, 8),
|
338
|
+
],
|
339
|
+
[
|
340
|
+
TargetCorner(1, 2),
|
341
|
+
TargetCorner(3, 4),
|
342
|
+
TargetCorner(5, 6),
|
343
|
+
TargetCorner(7, 8),
|
344
|
+
],
|
345
|
+
0.7,
|
346
|
+
)
|
347
|
+
],
|
348
|
+
metadata=timestamps.toPhotonPipelineMetadata(),
|
349
|
+
)
|
350
|
+
cameraOne.result = result
|
351
|
+
estimatedPose = estimator.update()
|
352
|
+
assert estimatedPose is not None
|
353
|
+
expectedTimestamp = timestamps.receiveTimestampMicros() * 1e-6 - latencySecs
|
354
|
+
assertEquals(expectedTimestamp, estimatedPose.timestampSeconds, 1e-3)
|
355
|
+
assertEquals(expectedTimestamp, estimator._poseCacheTimestampSeconds, 1e-3)
|
356
|
+
|
357
|
+
# And again -- pose cache should mean this is empty
|
358
|
+
cameraOne.result = result
|
359
|
+
estimatedPose = estimator.update()
|
360
|
+
assert estimatedPose is None
|
361
|
+
# Expect the old timestamp to still be here
|
362
|
+
assertEquals(expectedTimestamp, estimator._poseCacheTimestampSeconds, 1e-3)
|
363
|
+
|
364
|
+
# Set new field layout -- right after, the pose cache timestamp should be -1
|
365
|
+
estimator.fieldTags = AprilTagFieldLayout([AprilTag()], 0, 0)
|
366
|
+
assertEquals(-1, estimator._poseCacheTimestampSeconds)
|
367
|
+
# Update should cache the current timestamp (20) again
|
368
|
+
cameraOne.result = result
|
369
|
+
estimatedPose = estimator.update()
|
370
|
+
|
371
|
+
assert estimatedPose is not None
|
372
|
+
|
373
|
+
assertEquals(expectedTimestamp, estimatedPose.timestampSeconds, 1e-3)
|
374
|
+
assertEquals(expectedTimestamp, estimator._poseCacheTimestampSeconds, 1e-3)
|
375
|
+
|
376
|
+
# Setting a value from None to a non-None should invalidate the cache.
|
377
|
+
assert estimator.referencePose is None
|
378
|
+
estimator.referencePose = Pose3d(3, 3, 3, Rotation3d())
|
379
|
+
|
380
|
+
assertEquals(-1, estimator._poseCacheTimestampSeconds)
|
381
|
+
|
382
|
+
|
383
|
+
def assertEquals(expected, actual, epsilon=0.0):
|
384
|
+
assert abs(expected - actual) <= epsilon
|
test/photonlibpy_test.py
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
###############################################################################
|
2
|
+
## Copyright (C) Photon Vision.
|
3
|
+
###############################################################################
|
4
|
+
## This program is free software: you can redistribute it and/or modify
|
5
|
+
## it under the terms of the GNU General Public License as published by
|
6
|
+
## the Free Software Foundation, either version 3 of the License, or
|
7
|
+
## (at your option) any later version.
|
8
|
+
##
|
9
|
+
## This program is distributed in the hope that it will be useful,
|
10
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
## GNU General Public License for more details.
|
13
|
+
##
|
14
|
+
## You should have received a copy of the GNU General Public License
|
15
|
+
## along with this program. If not, see <https://www.gnu.org/licenses/>.
|
16
|
+
###############################################################################
|
17
|
+
|
18
|
+
from time import sleep
|
19
|
+
|
20
|
+
import ntcore
|
21
|
+
from photonlibpy import PhotonCamera
|
22
|
+
from photonlibpy.photonCamera import setVersionCheckEnabled
|
23
|
+
|
24
|
+
|
25
|
+
def test_roundTrip():
|
26
|
+
ntcore.NetworkTableInstance.getDefault().stopServer()
|
27
|
+
ntcore.NetworkTableInstance.getDefault().setServer("localhost")
|
28
|
+
ntcore.NetworkTableInstance.getDefault().startClient4("meme")
|
29
|
+
|
30
|
+
camera = PhotonCamera("WPI2024")
|
31
|
+
|
32
|
+
setVersionCheckEnabled(False)
|
33
|
+
|
34
|
+
for i in range(5):
|
35
|
+
sleep(0.1)
|
36
|
+
result = camera.getLatestResult()
|
37
|
+
print(result)
|
38
|
+
print(camera._rawBytesEntry.getTopic().getProperties())
|
39
|
+
|
40
|
+
|
41
|
+
if __name__ == "__main__":
|
42
|
+
test_roundTrip()
|
@@ -0,0 +1,99 @@
|
|
1
|
+
import math
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
import pytest
|
5
|
+
from photonlibpy.estimation import RotTrlTransform3d
|
6
|
+
from photonlibpy.simulation import SimCameraProperties
|
7
|
+
from wpimath.geometry import Rotation2d, Translation3d
|
8
|
+
|
9
|
+
|
10
|
+
@pytest.fixture(autouse=True)
|
11
|
+
def scp() -> SimCameraProperties:
|
12
|
+
props = SimCameraProperties()
|
13
|
+
props.setCalibrationFromFOV(1000, 1000, fovDiag=Rotation2d(math.radians(90.0)))
|
14
|
+
return props
|
15
|
+
|
16
|
+
|
17
|
+
def test_GetPixelYaw(scp) -> None:
|
18
|
+
rot = scp.getPixelYaw(scp.getResWidth() / 2)
|
19
|
+
assert rot.degrees() == pytest.approx(0.0, abs=1.0)
|
20
|
+
rot = scp.getPixelYaw(0.0)
|
21
|
+
# FOV is square
|
22
|
+
assert rot.degrees() == pytest.approx(45.0 / math.sqrt(2.0), abs=5.0)
|
23
|
+
rot = scp.getPixelYaw(scp.getResWidth())
|
24
|
+
assert rot.degrees() == pytest.approx(-45.0 / math.sqrt(2.0), abs=5.0)
|
25
|
+
|
26
|
+
|
27
|
+
def test_GetPixelPitch(scp) -> None:
|
28
|
+
rot = scp.getPixelPitch(scp.getResHeight() / 2)
|
29
|
+
assert rot.degrees() == pytest.approx(0.0, abs=1.0)
|
30
|
+
rot = scp.getPixelPitch(0.0)
|
31
|
+
# FOV is square
|
32
|
+
assert rot.degrees() == pytest.approx(-45.0 / math.sqrt(2.0), abs=5.0)
|
33
|
+
rot = scp.getPixelPitch(scp.getResHeight())
|
34
|
+
assert rot.degrees() == pytest.approx(45.0 / math.sqrt(2.0), abs=5.0)
|
35
|
+
|
36
|
+
|
37
|
+
def test_GetPixelRot(scp) -> None:
|
38
|
+
rot = scp.getPixelRot(np.array([scp.getResWidth() / 2.0, scp.getResHeight() / 2.0]))
|
39
|
+
assert rot.x_degrees == pytest.approx(0.0, abs=5)
|
40
|
+
assert rot.y_degrees == pytest.approx(0.0, abs=5)
|
41
|
+
assert rot.z_degrees == pytest.approx(0.0, abs=5)
|
42
|
+
rot = scp.getPixelRot(np.array([0.0, 0.0]))
|
43
|
+
assert rot.x_degrees == pytest.approx(0.0, abs=5)
|
44
|
+
assert rot.y_degrees == pytest.approx(-45.0 / math.sqrt(2.0), abs=5)
|
45
|
+
assert rot.z_degrees == pytest.approx(45.0 / math.sqrt(2.0), abs=5)
|
46
|
+
rot = scp.getPixelRot(np.array([scp.getResWidth(), scp.getResHeight()]))
|
47
|
+
assert rot.x_degrees == pytest.approx(0.0, abs=5)
|
48
|
+
assert rot.y_degrees == pytest.approx(45.0 / math.sqrt(2.0), abs=5)
|
49
|
+
assert rot.z_degrees == pytest.approx(-45.0 / math.sqrt(2.0), abs=5)
|
50
|
+
|
51
|
+
|
52
|
+
def test_GetCorrectedPixelRot(scp) -> None:
|
53
|
+
rot = scp.getCorrectedPixelRot(
|
54
|
+
np.array([scp.getResWidth() / 2.0, scp.getResHeight() / 2.0])
|
55
|
+
)
|
56
|
+
assert rot.x_degrees == pytest.approx(0.0, abs=5)
|
57
|
+
assert rot.y_degrees == pytest.approx(0.0, abs=5)
|
58
|
+
assert rot.z_degrees == pytest.approx(0.0, abs=5)
|
59
|
+
rot = scp.getCorrectedPixelRot(np.array([0.0, 0.0]))
|
60
|
+
assert rot.x_degrees == pytest.approx(0.0, abs=5)
|
61
|
+
assert rot.y_degrees == pytest.approx(-45.0 / math.sqrt(2.0), abs=5)
|
62
|
+
assert rot.z_degrees == pytest.approx(45.0 / math.sqrt(2.0), abs=5)
|
63
|
+
rot = scp.getCorrectedPixelRot(np.array([scp.getResWidth(), scp.getResHeight()]))
|
64
|
+
assert rot.x_degrees == pytest.approx(0.0, abs=5)
|
65
|
+
assert rot.y_degrees == pytest.approx(45.0 / math.sqrt(2.0), abs=5)
|
66
|
+
assert rot.z_degrees == pytest.approx(-45.0 / math.sqrt(2.0), abs=5)
|
67
|
+
|
68
|
+
|
69
|
+
def test_GetVisibleLine(scp) -> None:
|
70
|
+
camRt = RotTrlTransform3d()
|
71
|
+
a = Translation3d()
|
72
|
+
b = Translation3d()
|
73
|
+
retval = scp.getVisibleLine(camRt, a, b)
|
74
|
+
assert retval == (None, None)
|
75
|
+
|
76
|
+
a = Translation3d(-5.0, -0.1, 0)
|
77
|
+
b = Translation3d(5.0, 0.1, 0)
|
78
|
+
retval = scp.getVisibleLine(camRt, a, b)
|
79
|
+
assert retval == (0.5, 0.5)
|
80
|
+
|
81
|
+
|
82
|
+
def test_EstPixelNoise(scp) -> None:
|
83
|
+
with pytest.raises(Exception):
|
84
|
+
scp.test_EstPixelNoise(np.array([0, 0]))
|
85
|
+
with pytest.raises(Exception):
|
86
|
+
scp.test_EstPixelNoise(np.array([[0], [0]]))
|
87
|
+
|
88
|
+
pts = np.array([[[0, 0]], [[0, 0]]])
|
89
|
+
|
90
|
+
# No noise parameters set
|
91
|
+
noisy = scp.estPixelNoise(pts)
|
92
|
+
for n, p in zip(noisy, pts):
|
93
|
+
assert n.all() == p.all()
|
94
|
+
|
95
|
+
# Noise parameters set
|
96
|
+
scp.setCalibError(1.0, 1.0)
|
97
|
+
noisy = scp.estPixelNoise(pts)
|
98
|
+
for n, p in zip(noisy, pts):
|
99
|
+
assert n.any() != p.any()
|
test/testUtil.py
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
"""Test utilities."""
|
2
|
+
|
3
|
+
from photonlibpy.targeting import PhotonPipelineMetadata
|
4
|
+
|
5
|
+
|
6
|
+
class InvalidTestDataException(ValueError):
|
7
|
+
pass
|
8
|
+
|
9
|
+
|
10
|
+
class PipelineTimestamps:
|
11
|
+
"""Helper class to ensure timestamps are positive."""
|
12
|
+
|
13
|
+
def __init__(
|
14
|
+
self,
|
15
|
+
*,
|
16
|
+
captureTimestampMicros: int,
|
17
|
+
pipelineLatencyMicros=2_000,
|
18
|
+
receiveLatencyMicros=1_000,
|
19
|
+
):
|
20
|
+
if captureTimestampMicros < 0:
|
21
|
+
raise InvalidTestDataException("captureTimestampMicros cannot be negative")
|
22
|
+
if pipelineLatencyMicros <= 0:
|
23
|
+
raise InvalidTestDataException("pipelineLatencyMicros must be positive")
|
24
|
+
if receiveLatencyMicros < 0:
|
25
|
+
raise InvalidTestDataException("receiveLatencyMicros cannot be negative")
|
26
|
+
self._captureTimestampMicros = captureTimestampMicros
|
27
|
+
self._pipelineLatencyMicros = pipelineLatencyMicros
|
28
|
+
self._receiveLatencyMicros = receiveLatencyMicros
|
29
|
+
self._sequenceID = 0
|
30
|
+
|
31
|
+
@property
|
32
|
+
def captureTimestampMicros(self) -> int:
|
33
|
+
return self._captureTimestampMicros
|
34
|
+
|
35
|
+
@captureTimestampMicros.setter
|
36
|
+
def captureTimestampMicros(self, micros: int) -> None:
|
37
|
+
if micros < 0:
|
38
|
+
raise InvalidTestDataException("captureTimestampMicros cannot be negative")
|
39
|
+
if micros < self._captureTimestampMicros:
|
40
|
+
raise InvalidTestDataException("time cannot go backwards")
|
41
|
+
self._captureTimestampMicros = micros
|
42
|
+
self._sequenceID += 1
|
43
|
+
|
44
|
+
@property
|
45
|
+
def pipelineLatencyMicros(self) -> int:
|
46
|
+
return self._pipelineLatencyMicros
|
47
|
+
|
48
|
+
def pipelineLatencySecs(self) -> float:
|
49
|
+
return self.pipelineLatencyMicros * 1e-6
|
50
|
+
|
51
|
+
def incrementTimeMicros(self, micros: int) -> None:
|
52
|
+
self.captureTimestampMicros += micros
|
53
|
+
|
54
|
+
def publishTimestampMicros(self) -> int:
|
55
|
+
return self._captureTimestampMicros + self.pipelineLatencyMicros
|
56
|
+
|
57
|
+
def receiveTimestampMicros(self) -> int:
|
58
|
+
return self.publishTimestampMicros() + self._receiveLatencyMicros
|
59
|
+
|
60
|
+
def toPhotonPipelineMetadata(self) -> PhotonPipelineMetadata:
|
61
|
+
return PhotonPipelineMetadata(
|
62
|
+
captureTimestampMicros=self.captureTimestampMicros,
|
63
|
+
publishTimestampMicros=self.publishTimestampMicros(),
|
64
|
+
sequenceID=self._sequenceID,
|
65
|
+
)
|