photonlibpy 2025.3.2__py3-none-any.whl → 2026.0.0a1__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.
@@ -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 Pose2d, Pose3d, Transform3d
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
- # TODO: Implement HAL reporting
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 and oldObj is not None and oldObj is not 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
- now_micros = wpilib.Timer.getFPGATimestamp() * 1e6
423
+ publishTimestampMicros = wpilib.Timer.getFPGATimestamp() * 1e6
424
424
  return PhotonPipelineResult(
425
+ ntReceiveTimestampMicros=int(publishTimestampMicros + 10),
425
426
  metadata=PhotonPipelineMetadata(
426
- int(now_micros - latency * 1e6),
427
- int(now_micros),
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,
@@ -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
- return (
51
- self.ntReceiveTimestampMicros
52
- - (
53
- self.metadata.publishTimestampMicros
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
photonlibpy/version.py CHANGED
@@ -1,2 +1,2 @@
1
- PHOTONLIB_VERSION="2025.3.2"
2
- PHOTONVISION_VERSION="v2025.3.2"
1
+ PHOTONLIB_VERSION="v2026.0.0.alpha.1"
2
+ PHOTONVISION_VERSION="v2026.0.0-alpha-1"
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: photonlibpy
3
- Version: 2025.3.2
4
- Summary: Pure-python implementation of PhotonLib for interfacing with PhotonVision on coprocessors. Implemented with PhotonVision version v2025.3.2 .
3
+ Version: 2026.0.0a1
4
+ Summary: Pure-python implementation of PhotonLib for interfacing with PhotonVision on coprocessors. Implemented with PhotonVision version v2026.0.0-alpha-1 .
5
5
  Home-page: https://photonvision.org
6
6
  Author: Photonvision Development Team
7
7
  Classifier: License :: OSI Approved :: MIT License
@@ -1,10 +1,10 @@
1
1
  photonlibpy/__init__.py,sha256=nPUL143Q0VAdgE4L1U-xRkodvoU4a76w5lu_1iDVJjI,1413
2
2
  photonlibpy/estimatedRobotPose.py,sha256=X7wF9xdPXGKSVy0MY0qrWZJOEbuZPd721lYp0KXKlP0,1603
3
3
  photonlibpy/packet.py,sha256=5YomViVFwOljL2FGOetWM9FbPc_yCQ15ylzkYlgLIs8,9724
4
- photonlibpy/photonCamera.py,sha256=r5dH2S4ZcwiBDrpK58aHULrifIA0Tqr4iJp94My75A0,13006
5
- photonlibpy/photonPoseEstimator.py,sha256=2iMqxPFsQHTsq95yv-WCSv1a6wXNcHPqOyMc4Bu6IG0,12584
4
+ photonlibpy/photonCamera.py,sha256=Vkn82MOKbWCLuTU8ljXS-yVQfhoJjCBcaJsbluhlQxQ,13244
5
+ photonlibpy/photonPoseEstimator.py,sha256=OBO5eVqz8UVlpu1R3YHcFLBAPfZfqqAa6pvBdb4oiq8,16298
6
6
  photonlibpy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- photonlibpy/version.py,sha256=SAchXEI4wYx6RIgKL3e_khs4gaDfnKLfyzylwj_RIgk,62
7
+ photonlibpy/version.py,sha256=r9LHo7JkyIMrKEwnwnZ8dzRLsJEufYcwODdFOZnEhiY,79
8
8
  photonlibpy/estimation/__init__.py,sha256=pZ-d6fN1DJvT-lRl4FfIos5HAvlzmetIOrGIinrdv7k,223
9
9
  photonlibpy/estimation/cameraTargetRelation.py,sha256=i7DPBXtkZve4ToXQscEIe-5F1oGQ1Qmf5QBaE__EeMQ,1158
10
10
  photonlibpy/estimation/openCVHelp.py,sha256=O1dV7v7RHSyw7l5L0QXbal6t9K7iyvEG76tG-t4AFVg,12388
@@ -21,7 +21,7 @@ photonlibpy/generated/__init__.py,sha256=mElM8M88---wxTWO-SRqIJ4EfxN0fdIUwZBZ-UI
21
21
  photonlibpy/networktables/NTTopicSet.py,sha256=29wPgXcuqT-u72-YXwSjRHWhECNzU8eDsexcqlA8KQ0,2967
22
22
  photonlibpy/networktables/__init__.py,sha256=o_LxTdyIylAszMy_zhUtTkXHyu6jqxccccj78d44OrI,35
23
23
  photonlibpy/simulation/__init__.py,sha256=HazsBMXg1HT8TnyxYO8QI9NXwZOrtuCSytnTdquLBKw,358
24
- photonlibpy/simulation/photonCameraSim.py,sha256=24AcbAjWEfGBAhuIXuTuISb9uM0lLtgMdqSGZvJGPjg,19992
24
+ photonlibpy/simulation/photonCameraSim.py,sha256=A4cTSZ87PIg24Dim63I_DaP1SJ4h9bDR5LIySoyClr4,20174
25
25
  photonlibpy/simulation/simCameraProperties.py,sha256=ODVxnylF8zw9HZSbw0PzG_OEtUo9ChRo-G_iEgADOCg,27195
26
26
  photonlibpy/simulation/videoSimUtil.py,sha256=xMuTvJ2Jx9IoQqmAJi_zUm06MdEwhVpIz9OyzYQp0k4,29
27
27
  photonlibpy/simulation/visionSystemSim.py,sha256=GmKs0d32WE8B020YEWnj-0dQuCnVv1ScGdcFl1fOsKo,13835
@@ -29,11 +29,19 @@ photonlibpy/simulation/visionTargetSim.py,sha256=FH85fKE4NntowUvssfgZ1KlE-I_3Z-Q
29
29
  photonlibpy/targeting/TargetCorner.py,sha256=ouKj3E5uD76OZSNHHuSDzKOY65a8HqtcOsuejH-MVsU,276
30
30
  photonlibpy/targeting/__init__.py,sha256=OxxkBvBa6sFdjG7T1hO8CwBkRHk6GYdXbVhVgYfW7Gc,402
31
31
  photonlibpy/targeting/multiTargetPNPResult.py,sha256=Y9rweHtMzoCZ6mv6F8CutQi2Thq5pHN0ydBWvTCsOwY,806
32
- photonlibpy/targeting/photonPipelineResult.py,sha256=MbaSyHZTJpoKTtLOZztpSGSt9xWWFqhzgwj8medObVA,2732
32
+ photonlibpy/targeting/photonPipelineResult.py,sha256=H_wsIQfdo41xNT32YqfttH6AYQRZhAMR7XDiysMa3lw,2700
33
33
  photonlibpy/targeting/photonTrackedTarget.py,sha256=zCoFp32hX-3GmBYEmsYBQieBoMzXtP2F_55_q0zPOXA,1956
34
34
  photonlibpy/timesync/__init__.py,sha256=pECFraHRZC2Dl4x4OrmhW8oWkawP68pCb-Y61dZycJs,25
35
35
  photonlibpy/timesync/timeSyncServer.py,sha256=0d76uHBS_Jo3etvougdZHapbRrbb31iu-jlf8h9U_QQ,3194
36
- photonlibpy-2025.3.2.dist-info/METADATA,sha256=I2o8AX_j0PMkvUCz4lawZ2jWyupx2Gjk_86XymhBa3U,721
37
- photonlibpy-2025.3.2.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
38
- photonlibpy-2025.3.2.dist-info/top_level.txt,sha256=T8Xc6U6he2VjKUAca6zawSkHdUZuLanxYIc4nxw2ctc,12
39
- photonlibpy-2025.3.2.dist-info/RECORD,,
36
+ test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
+ test/openCVHelp_test.py,sha256=Lr6yDXB5dcwWNoOf0azTizs3F_uarCwP92lKfMIwKXo,6996
38
+ test/photonCamera_test.py,sha256=pdT7AplfBOuRK8pQVvO2dSCAwEhpaLU1JrIxzRODvpY,921
39
+ test/photonPoseEstimator_test.py,sha256=FUyIF2y-tfQEJqsCXCjqa_YVNU19NsdRVowziwjR5eI,13024
40
+ test/photonlibpy_test.py,sha256=u_Ycke7fCfV2sr7O9RBWD8e1w-XWdMAx01eMJKIcbYM,1565
41
+ test/simCameraProperties_test.py,sha256=vJJjNmKoO_neFIIgRW8KRro75s7VPOvkekex27oW6aU,3763
42
+ test/testUtil.py,sha256=sxACyosw9IjfTlZAjrsSKr0mCBQUjC3tvAQUqndGtkk,2297
43
+ test/visionSystemSim_test.py,sha256=8lBJD6vdqV2LwgNYtEgrJjuizPaRRJ2O4iGKhEeIDkM,21007
44
+ photonlibpy-2026.0.0a1.dist-info/METADATA,sha256=t6pxUB-RbN5PvqxloLYeHhK2S5Dm_CWXHYqNNM4cTVk,731
45
+ photonlibpy-2026.0.0a1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
46
+ photonlibpy-2026.0.0a1.dist-info/top_level.txt,sha256=f7vWroLAnhnbYyix-BbJXaIzvsfWUu7sMlKZrDISjHE,17
47
+ photonlibpy-2026.0.0a1.dist-info/RECORD,,
test/__init__.py ADDED
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