photonlibpy 2025.0.0a0__tar.gz → 2025.0.0b2__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/PKG-INFO +2 -2
  2. photonlibpy-2025.0.0b2/photonlibpy/estimation/__init__.py +5 -0
  3. photonlibpy-2025.0.0b2/photonlibpy/estimation/cameraTargetRelation.py +25 -0
  4. photonlibpy-2025.0.0b2/photonlibpy/estimation/openCVHelp.py +200 -0
  5. photonlibpy-2025.0.0b2/photonlibpy/estimation/rotTrlTransform3d.py +32 -0
  6. photonlibpy-2025.0.0b2/photonlibpy/estimation/targetModel.py +137 -0
  7. photonlibpy-2025.0.0b2/photonlibpy/estimation/visionEstimation.py +91 -0
  8. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/generated/MultiTargetPNPResultSerde.py +12 -0
  9. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/generated/PhotonPipelineMetadataSerde.py +23 -4
  10. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/generated/PhotonPipelineResultSerde.py +19 -2
  11. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/generated/PhotonTrackedTargetSerde.py +40 -0
  12. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/generated/PnpResultSerde.py +19 -0
  13. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/generated/TargetCornerSerde.py +12 -0
  14. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/generated/__init__.py +0 -1
  15. photonlibpy-2025.0.0b2/photonlibpy/networktables/NTTopicSet.py +64 -0
  16. photonlibpy-2025.0.0b2/photonlibpy/networktables/__init__.py +1 -0
  17. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/packet.py +123 -8
  18. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/photonCamera.py +10 -7
  19. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/photonPoseEstimator.py +5 -5
  20. photonlibpy-2025.0.0b2/photonlibpy/simulation/__init__.py +5 -0
  21. photonlibpy-2025.0.0b2/photonlibpy/simulation/photonCameraSim.py +408 -0
  22. photonlibpy-2025.0.0b2/photonlibpy/simulation/simCameraProperties.py +661 -0
  23. photonlibpy-2025.0.0b2/photonlibpy/simulation/videoSimUtil.py +2 -0
  24. photonlibpy-2025.0.0b2/photonlibpy/simulation/visionSystemSim.py +237 -0
  25. photonlibpy-2025.0.0b2/photonlibpy/simulation/visionTargetSim.py +50 -0
  26. photonlibpy-2025.0.0b2/photonlibpy/targeting/TargetCorner.py +13 -0
  27. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/targeting/multiTargetPNPResult.py +10 -4
  28. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/targeting/photonPipelineResult.py +12 -5
  29. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/targeting/photonTrackedTarget.py +13 -5
  30. photonlibpy-2025.0.0b2/photonlibpy/version.py +2 -0
  31. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy.egg-info/PKG-INFO +2 -2
  32. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy.egg-info/SOURCES.txt +14 -0
  33. photonlibpy-2025.0.0b2/photonlibpy.egg-info/requires.txt +12 -0
  34. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/setup.py +8 -2
  35. photonlibpy-2025.0.0a0/photonlibpy/targeting/TargetCorner.py +0 -9
  36. photonlibpy-2025.0.0a0/photonlibpy/version.py +0 -2
  37. photonlibpy-2025.0.0a0/photonlibpy.egg-info/requires.txt +0 -4
  38. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/__init__.py +2 -2
  39. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/estimatedRobotPose.py +0 -0
  40. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy/targeting/__init__.py +1 -1
  41. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy.egg-info/dependency_links.txt +0 -0
  42. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/photonlibpy.egg-info/top_level.txt +0 -0
  43. {photonlibpy-2025.0.0a0 → photonlibpy-2025.0.0b2}/setup.cfg +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: photonlibpy
3
- Version: 2025.0.0a0
4
- Summary: Pure-python implementation of PhotonLib for interfacing with PhotonVision on coprocessors. Implemented with PhotonVision version v2025.0.0-alpha-0 .
3
+ Version: 2025.0.0b2
4
+ Summary: Pure-python implementation of PhotonLib for interfacing with PhotonVision on coprocessors. Implemented with PhotonVision version v2025.0.0-beta-2 .
5
5
  Home-page: https://photonvision.org
6
6
  Author: Photonvision Development Team
7
7
  Description-Content-Type: text/markdown
@@ -0,0 +1,5 @@
1
+ from .cameraTargetRelation import CameraTargetRelation
2
+ from .openCVHelp import OpenCVHelp
3
+ from .rotTrlTransform3d import RotTrlTransform3d
4
+ from .targetModel import TargetModel
5
+ from .visionEstimation import VisionEstimation
@@ -0,0 +1,25 @@
1
+ import math
2
+
3
+ from wpimath.geometry import Pose3d, Rotation2d, Transform3d
4
+ from wpimath.units import meters
5
+
6
+
7
+ class CameraTargetRelation:
8
+ def __init__(self, cameraPose: Pose3d, targetPose: Pose3d):
9
+ self.camPose = cameraPose
10
+ self.camToTarg = Transform3d(cameraPose, targetPose)
11
+ self.camToTargDist = self.camToTarg.translation().norm()
12
+ self.camToTargDistXY: meters = math.hypot(
13
+ self.camToTarg.translation().X(), self.camToTarg.translation().Y()
14
+ )
15
+ self.camToTargYaw = Rotation2d(self.camToTarg.X(), self.camToTarg.Y())
16
+ self.camToTargPitch = Rotation2d(self.camToTargDistXY, -self.camToTarg.Z())
17
+ self.camToTargAngle = Rotation2d(
18
+ math.hypot(self.camToTargYaw.radians(), self.camToTargPitch.radians())
19
+ )
20
+ self.targToCam = Transform3d(targetPose, cameraPose)
21
+ self.targToCamYaw = Rotation2d(self.targToCam.X(), self.targToCam.Y())
22
+ self.targToCamPitch = Rotation2d(self.camToTargDistXY, -self.targToCam.Z())
23
+ self.targtoCamAngle = Rotation2d(
24
+ math.hypot(self.targToCamYaw.radians(), self.targToCamPitch.radians())
25
+ )
@@ -0,0 +1,200 @@
1
+ import math
2
+ from typing import Any, Tuple
3
+
4
+ import cv2 as cv
5
+ import numpy as np
6
+ from wpimath.geometry import Rotation3d, Transform3d, Translation3d
7
+
8
+ from ..targeting import PnpResult, TargetCorner
9
+ from .rotTrlTransform3d import RotTrlTransform3d
10
+
11
+ NWU_TO_EDN = Rotation3d(np.array([[0, -1, 0], [0, 0, -1], [1, 0, 0]]))
12
+ EDN_TO_NWU = Rotation3d(np.array([[0, 0, 1], [-1, 0, 0], [0, -1, 0]]))
13
+
14
+
15
+ class OpenCVHelp:
16
+ @staticmethod
17
+ def getMinAreaRect(points: np.ndarray) -> cv.RotatedRect:
18
+ return cv.RotatedRect(*cv.minAreaRect(points))
19
+
20
+ @staticmethod
21
+ def translationNWUtoEDN(trl: Translation3d) -> Translation3d:
22
+ return trl.rotateBy(NWU_TO_EDN)
23
+
24
+ @staticmethod
25
+ def rotationNWUtoEDN(rot: Rotation3d) -> Rotation3d:
26
+ return -NWU_TO_EDN + (rot + NWU_TO_EDN)
27
+
28
+ @staticmethod
29
+ def translationToTVec(translations: list[Translation3d]) -> np.ndarray:
30
+ retVal: list[list] = []
31
+ for translation in translations:
32
+ trl = OpenCVHelp.translationNWUtoEDN(translation)
33
+ retVal.append([trl.X(), trl.Y(), trl.Z()])
34
+ return np.array(
35
+ retVal,
36
+ dtype=np.float32,
37
+ )
38
+
39
+ @staticmethod
40
+ def rotationToRVec(rotation: Rotation3d) -> np.ndarray:
41
+ retVal: list[np.ndarray] = []
42
+ rot = OpenCVHelp.rotationNWUtoEDN(rotation)
43
+ rotVec = rot.getQuaternion().toRotationVector()
44
+ retVal.append(rotVec)
45
+ return np.array(
46
+ retVal,
47
+ dtype=np.float32,
48
+ )
49
+
50
+ @staticmethod
51
+ def avgPoint(points: list[Tuple[float, float]]) -> Tuple[float, float]:
52
+ x = 0.0
53
+ y = 0.0
54
+ for p in points:
55
+ x += p[0]
56
+ y += p[1]
57
+ return (x / len(points), y / len(points))
58
+
59
+ @staticmethod
60
+ def pointsToTargetCorners(points: np.ndarray) -> list[TargetCorner]:
61
+ corners = [TargetCorner(p[0, 0], p[0, 1]) for p in points]
62
+ return corners
63
+
64
+ @staticmethod
65
+ def cornersToPoints(corners: list[TargetCorner]) -> np.ndarray:
66
+ points = [[[c.x, c.y]] for c in corners]
67
+ return np.array(points)
68
+
69
+ @staticmethod
70
+ def projectPoints(
71
+ cameraMatrix: np.ndarray,
72
+ distCoeffs: np.ndarray,
73
+ camRt: RotTrlTransform3d,
74
+ objectTranslations: list[Translation3d],
75
+ ) -> np.ndarray:
76
+ objectPoints = OpenCVHelp.translationToTVec(objectTranslations)
77
+ rvec = OpenCVHelp.rotationToRVec(camRt.getRotation())
78
+ tvec = OpenCVHelp.translationToTVec(
79
+ [
80
+ camRt.getTranslation(),
81
+ ]
82
+ )
83
+
84
+ pts, _ = cv.projectPoints(objectPoints, rvec, tvec, cameraMatrix, distCoeffs)
85
+ return pts
86
+
87
+ @staticmethod
88
+ def reorderCircular(
89
+ elements: list[Any] | np.ndarray, backwards: bool, shiftStart: int
90
+ ) -> list[Any]:
91
+ size = len(elements)
92
+ reordered = []
93
+ dir = -1 if backwards else 1
94
+ for i in range(size):
95
+ index = (i * dir + shiftStart * dir) % size
96
+ if index < 0:
97
+ index += size
98
+ reordered.append(elements[index])
99
+ return reordered
100
+
101
+ @staticmethod
102
+ def translationEDNToNWU(trl: Translation3d) -> Translation3d:
103
+ return trl.rotateBy(EDN_TO_NWU)
104
+
105
+ @staticmethod
106
+ def rotationEDNToNWU(rot: Rotation3d) -> Rotation3d:
107
+ return -EDN_TO_NWU + (rot + EDN_TO_NWU)
108
+
109
+ @staticmethod
110
+ def tVecToTranslation(tvecInput: np.ndarray) -> Translation3d:
111
+ return OpenCVHelp.translationEDNToNWU(Translation3d(tvecInput))
112
+
113
+ @staticmethod
114
+ def rVecToRotation(rvecInput: np.ndarray) -> Rotation3d:
115
+ return OpenCVHelp.rotationEDNToNWU(Rotation3d(rvecInput))
116
+
117
+ @staticmethod
118
+ def solvePNP_Square(
119
+ cameraMatrix: np.ndarray,
120
+ distCoeffs: np.ndarray,
121
+ modelTrls: list[Translation3d],
122
+ imagePoints: np.ndarray,
123
+ ) -> PnpResult | None:
124
+ modelTrls = OpenCVHelp.reorderCircular(modelTrls, True, -1)
125
+ imagePoints = np.array(OpenCVHelp.reorderCircular(imagePoints, True, -1))
126
+ objectMat = np.array(OpenCVHelp.translationToTVec(modelTrls))
127
+
128
+ alt: Transform3d | None = None
129
+ for tries in range(2):
130
+ retval, rvecs, tvecs, reprojectionError = cv.solvePnPGeneric(
131
+ objectMat,
132
+ imagePoints,
133
+ cameraMatrix,
134
+ distCoeffs,
135
+ flags=cv.SOLVEPNP_IPPE_SQUARE,
136
+ )
137
+
138
+ best = Transform3d(
139
+ OpenCVHelp.tVecToTranslation(tvecs[0]),
140
+ OpenCVHelp.rVecToRotation(rvecs[0]),
141
+ )
142
+ if len(tvecs) > 1:
143
+ alt = Transform3d(
144
+ OpenCVHelp.tVecToTranslation(tvecs[1]),
145
+ OpenCVHelp.rVecToRotation(rvecs[1]),
146
+ )
147
+
148
+ if not math.isnan(reprojectionError[0, 0]):
149
+ break
150
+ else:
151
+ pt = imagePoints[0]
152
+ pt[0, 0] -= 0.001
153
+ pt[0, 1] -= 0.001
154
+ imagePoints[0] = pt
155
+
156
+ if math.isnan(reprojectionError[0, 0]):
157
+ print("SolvePNP_Square failed!")
158
+ return None
159
+
160
+ if alt:
161
+ return PnpResult(
162
+ best=best,
163
+ bestReprojErr=reprojectionError[0, 0],
164
+ alt=alt,
165
+ altReprojErr=reprojectionError[1, 0],
166
+ ambiguity=reprojectionError[0, 0] / reprojectionError[1, 0],
167
+ )
168
+ else:
169
+ # We have no alternative so set it to best as well
170
+ return PnpResult(
171
+ best=best,
172
+ bestReprojErr=reprojectionError[0],
173
+ alt=best,
174
+ altReprojErr=reprojectionError[0],
175
+ )
176
+
177
+ @staticmethod
178
+ def solvePNP_SQPNP(
179
+ cameraMatrix: np.ndarray,
180
+ distCoeffs: np.ndarray,
181
+ modelTrls: list[Translation3d],
182
+ imagePoints: np.ndarray,
183
+ ) -> PnpResult | None:
184
+ objectMat = np.array(OpenCVHelp.translationToTVec(modelTrls))
185
+
186
+ retval, rvecs, tvecs, reprojectionError = cv.solvePnPGeneric(
187
+ objectMat, imagePoints, cameraMatrix, distCoeffs, flags=cv.SOLVEPNP_SQPNP
188
+ )
189
+
190
+ error = reprojectionError[0, 0]
191
+ best = Transform3d(
192
+ OpenCVHelp.tVecToTranslation(tvecs[0]), OpenCVHelp.rVecToRotation(rvecs[0])
193
+ )
194
+
195
+ if math.isnan(error):
196
+ return None
197
+
198
+ # We have no alternative so set it to best as well
199
+ result = PnpResult(best=best, bestReprojErr=error, alt=best, altReprojErr=error)
200
+ return result
@@ -0,0 +1,32 @@
1
+ from typing import Self
2
+
3
+ from wpimath.geometry import Pose3d, Rotation3d, Transform3d, Translation3d
4
+
5
+
6
+ class RotTrlTransform3d:
7
+ def __init__(
8
+ self, rot: Rotation3d = Rotation3d(), trl: Translation3d = Translation3d()
9
+ ):
10
+ self.rot = rot
11
+ self.trl = trl
12
+
13
+ def inverse(self) -> Self:
14
+ invRot = -self.rot
15
+ invTrl = -(self.trl.rotateBy(invRot))
16
+ return type(self)(invRot, invTrl)
17
+
18
+ def getTransform(self) -> Transform3d:
19
+ return Transform3d(self.trl, self.rot)
20
+
21
+ def getTranslation(self) -> Translation3d:
22
+ return self.trl
23
+
24
+ def getRotation(self) -> Rotation3d:
25
+ return self.rot
26
+
27
+ def apply(self, trlToApply: Translation3d) -> Translation3d:
28
+ return trlToApply.rotateBy(self.rot) + self.trl
29
+
30
+ @classmethod
31
+ def makeRelativeTo(cls, pose: Pose3d) -> Self:
32
+ return cls(pose.rotation(), pose.translation()).inverse()
@@ -0,0 +1,137 @@
1
+ import math
2
+ from typing import List, Self
3
+
4
+ from wpimath.geometry import Pose3d, Rotation2d, Rotation3d, Translation3d
5
+ from wpimath.units import meters
6
+
7
+ from . import RotTrlTransform3d
8
+
9
+
10
+ class TargetModel:
11
+ def __init__(
12
+ self,
13
+ *,
14
+ width: meters | None = None,
15
+ height: meters | None = None,
16
+ length: meters | None = None,
17
+ diameter: meters | None = None,
18
+ verts: List[Translation3d] | None = None
19
+ ):
20
+
21
+ if (
22
+ width is not None
23
+ and height is not None
24
+ and length is None
25
+ and diameter is None
26
+ and verts is None
27
+ ):
28
+ self.isPlanar = True
29
+ self.isSpherical = False
30
+ self.vertices = [
31
+ Translation3d(0.0, -width / 2.0, -height / 2.0),
32
+ Translation3d(0.0, width / 2.0, -height / 2.0),
33
+ Translation3d(0.0, width / 2.0, height / 2.0),
34
+ Translation3d(0.0, -width / 2.0, height / 2.0),
35
+ ]
36
+
37
+ return
38
+
39
+ elif (
40
+ length is not None
41
+ and width is not None
42
+ and height is not None
43
+ and diameter is None
44
+ and verts is None
45
+ ):
46
+ verts = [
47
+ Translation3d(length / 2.0, -width / 2.0, -height / 2.0),
48
+ Translation3d(length / 2.0, width / 2.0, -height / 2.0),
49
+ Translation3d(length / 2.0, width / 2.0, height / 2.0),
50
+ Translation3d(length / 2.0, -width / 2.0, height / 2.0),
51
+ Translation3d(-length / 2.0, -width / 2.0, height / 2.0),
52
+ Translation3d(-length / 2.0, width / 2.0, height / 2.0),
53
+ Translation3d(-length / 2.0, width / 2.0, -height / 2.0),
54
+ Translation3d(-length / 2.0, -width / 2.0, -height / 2.0),
55
+ ]
56
+ # Handle the rest of this in the "default" case
57
+ elif (
58
+ diameter is not None
59
+ and width is None
60
+ and height is None
61
+ and length is None
62
+ and verts is None
63
+ ):
64
+ self.isPlanar = False
65
+ self.isSpherical = True
66
+ self.vertices = [
67
+ Translation3d(0.0, -diameter / 2.0, 0.0),
68
+ Translation3d(0.0, 0.0, -diameter / 2.0),
69
+ Translation3d(0.0, diameter / 2.0, 0.0),
70
+ Translation3d(0.0, 0.0, diameter / 2.0),
71
+ ]
72
+ return
73
+ elif (
74
+ verts is not None
75
+ and width is None
76
+ and height is None
77
+ and length is None
78
+ and diameter is None
79
+ ):
80
+ # Handle this in the "default" case
81
+ pass
82
+ else:
83
+ raise Exception("Not a valid overload")
84
+
85
+ # TODO maybe remove this if there is a better/preferred way
86
+ # make the python type checking gods happy
87
+ assert verts is not None
88
+
89
+ self.isSpherical = False
90
+ if len(verts) <= 2:
91
+ self.vertices: List[Translation3d] = []
92
+ self.isPlanar = False
93
+ else:
94
+ cornersPlaner = True
95
+ for corner in verts:
96
+ if abs(corner.X() < 1e-4):
97
+ cornersPlaner = False
98
+ self.isPlanar = cornersPlaner
99
+
100
+ self.vertices = verts
101
+
102
+ def getFieldVertices(self, targetPose: Pose3d) -> List[Translation3d]:
103
+ basisChange = RotTrlTransform3d(targetPose.rotation(), targetPose.translation())
104
+
105
+ retVal = []
106
+
107
+ for vert in self.vertices:
108
+ retVal.append(basisChange.apply(vert))
109
+
110
+ return retVal
111
+
112
+ @classmethod
113
+ def getOrientedPose(cls, tgtTrl: Translation3d, cameraTrl: Translation3d):
114
+ relCam = cameraTrl - tgtTrl
115
+ orientToCam = Rotation3d(
116
+ 0.0,
117
+ Rotation2d(math.hypot(relCam.X(), relCam.Y()), relCam.Z()).radians(),
118
+ Rotation2d(relCam.X(), relCam.Y()).radians(),
119
+ )
120
+ return Pose3d(tgtTrl, orientToCam)
121
+
122
+ def getVertices(self) -> List[Translation3d]:
123
+ return self.vertices
124
+
125
+ def getIsPlanar(self) -> bool:
126
+ return self.isPlanar
127
+
128
+ def getIsSpherical(self) -> bool:
129
+ return self.isSpherical
130
+
131
+ @classmethod
132
+ def AprilTag36h11(cls) -> Self:
133
+ return cls(width=6.5 * 0.0254, height=6.5 * 0.0254)
134
+
135
+ @classmethod
136
+ def AprilTag16h5(cls) -> Self:
137
+ return cls(width=6.0 * 0.0254, height=6.0 * 0.0254)
@@ -0,0 +1,91 @@
1
+ import numpy as np
2
+ from robotpy_apriltag import AprilTag, AprilTagFieldLayout
3
+ from wpimath.geometry import Pose3d, Transform3d, Translation3d
4
+
5
+ from ..targeting import PhotonTrackedTarget, PnpResult, TargetCorner
6
+ from . import OpenCVHelp, TargetModel
7
+
8
+
9
+ class VisionEstimation:
10
+ @staticmethod
11
+ def getVisibleLayoutTags(
12
+ visTags: list[PhotonTrackedTarget], layout: AprilTagFieldLayout
13
+ ) -> list[AprilTag]:
14
+ retVal: list[AprilTag] = []
15
+ for tag in visTags:
16
+ id = tag.getFiducialId()
17
+ maybePose = layout.getTagPose(id)
18
+ if maybePose:
19
+ tag = AprilTag()
20
+ tag.ID = id
21
+ tag.pose = maybePose
22
+ retVal.append(tag)
23
+ return retVal
24
+
25
+ @staticmethod
26
+ def estimateCamPosePNP(
27
+ cameraMatrix: np.ndarray,
28
+ distCoeffs: np.ndarray,
29
+ visTags: list[PhotonTrackedTarget],
30
+ layout: AprilTagFieldLayout,
31
+ tagModel: TargetModel,
32
+ ) -> PnpResult | None:
33
+ if len(visTags) == 0:
34
+ return None
35
+
36
+ corners: list[TargetCorner] = []
37
+ knownTags: list[AprilTag] = []
38
+
39
+ for tgt in visTags:
40
+ id = tgt.getFiducialId()
41
+ maybePose = layout.getTagPose(id)
42
+ if maybePose:
43
+ tag = AprilTag()
44
+ tag.ID = id
45
+ tag.pose = maybePose
46
+ knownTags.append(tag)
47
+ currentCorners = tgt.getDetectedCorners()
48
+ if currentCorners:
49
+ corners += currentCorners
50
+
51
+ if len(knownTags) == 0 or len(corners) == 0 or len(corners) % 4 != 0:
52
+ return None
53
+
54
+ points = OpenCVHelp.cornersToPoints(corners)
55
+
56
+ if len(knownTags) == 1:
57
+ camToTag = OpenCVHelp.solvePNP_Square(
58
+ cameraMatrix, distCoeffs, tagModel.getVertices(), points
59
+ )
60
+ if not camToTag:
61
+ return None
62
+
63
+ bestPose = knownTags[0].pose.transformBy(camToTag.best.inverse())
64
+ altPose = Pose3d()
65
+ if camToTag.ambiguity != 0:
66
+ altPose = knownTags[0].pose.transformBy(camToTag.alt.inverse())
67
+
68
+ o = Pose3d()
69
+ result = PnpResult(
70
+ best=Transform3d(o, bestPose),
71
+ alt=Transform3d(o, altPose),
72
+ ambiguity=camToTag.ambiguity,
73
+ bestReprojErr=camToTag.bestReprojErr,
74
+ altReprojErr=camToTag.altReprojErr,
75
+ )
76
+ return result
77
+ else:
78
+ objectTrls: list[Translation3d] = []
79
+ for tag in knownTags:
80
+ verts = tagModel.getFieldVertices(tag.pose)
81
+ objectTrls += verts
82
+
83
+ ret = OpenCVHelp.solvePNP_SQPNP(
84
+ cameraMatrix, distCoeffs, objectTrls, points
85
+ )
86
+ if ret:
87
+ # Invert best/alt transforms
88
+ ret.best = ret.best.inverse()
89
+ ret.alt = ret.alt.inverse()
90
+
91
+ return ret
@@ -20,6 +20,7 @@
20
20
  ## --> DO NOT MODIFY <--
21
21
  ###############################################################################
22
22
 
23
+ from ..packet import Packet
23
24
  from ..targeting import *
24
25
 
25
26
 
@@ -28,6 +29,17 @@ class MultiTargetPNPResultSerde:
28
29
  MESSAGE_VERSION = "541096947e9f3ca2d3f425ff7b04aa7b"
29
30
  MESSAGE_FORMAT = "PnpResult:ae4d655c0a3104d88df4f5db144c1e86 estimatedPose;int16 fiducialIDsUsed[?];"
30
31
 
32
+ @staticmethod
33
+ def pack(value: "MultiTargetPNPResult") -> "Packet":
34
+ ret = Packet()
35
+
36
+ # estimatedPose is of non-intrinsic type PnpResult
37
+ ret.encodeBytes(PnpResult.photonStruct.pack(value.estimatedPose).getData())
38
+
39
+ # fiducialIDsUsed is a custom VLA!
40
+ ret.encodeShortList(value.fiducialIDsUsed)
41
+ return ret
42
+
31
43
  @staticmethod
32
44
  def unpack(packet: "Packet") -> "MultiTargetPNPResult":
33
45
  ret = MultiTargetPNPResult()
@@ -20,15 +20,31 @@
20
20
  ## --> DO NOT MODIFY <--
21
21
  ###############################################################################
22
22
 
23
+ from ..packet import Packet
23
24
  from ..targeting import *
24
25
 
25
26
 
26
27
  class PhotonPipelineMetadataSerde:
27
28
  # Message definition md5sum. See photon_packet.adoc for details
28
- MESSAGE_VERSION = "626e70461cbdb274fb43ead09c255f4e"
29
- MESSAGE_FORMAT = (
30
- "int64 sequenceID;int64 captureTimestampMicros;int64 publishTimestampMicros;"
31
- )
29
+ MESSAGE_VERSION = "ac0a45f686457856fb30af77699ea356"
30
+ MESSAGE_FORMAT = "int64 sequenceID;int64 captureTimestampMicros;int64 publishTimestampMicros;int64 timeSinceLastPong;"
31
+
32
+ @staticmethod
33
+ def pack(value: "PhotonPipelineMetadata") -> "Packet":
34
+ ret = Packet()
35
+
36
+ # sequenceID is of intrinsic type int64
37
+ ret.encodeLong(value.sequenceID)
38
+
39
+ # captureTimestampMicros is of intrinsic type int64
40
+ ret.encodeLong(value.captureTimestampMicros)
41
+
42
+ # publishTimestampMicros is of intrinsic type int64
43
+ ret.encodeLong(value.publishTimestampMicros)
44
+
45
+ # timeSinceLastPong is of intrinsic type int64
46
+ ret.encodeLong(value.timeSinceLastPong)
47
+ return ret
32
48
 
33
49
  @staticmethod
34
50
  def unpack(packet: "Packet") -> "PhotonPipelineMetadata":
@@ -43,6 +59,9 @@ class PhotonPipelineMetadataSerde:
43
59
  # publishTimestampMicros is of intrinsic type int64
44
60
  ret.publishTimestampMicros = packet.decodeLong()
45
61
 
62
+ # timeSinceLastPong is of intrinsic type int64
63
+ ret.timeSinceLastPong = packet.decodeLong()
64
+
46
65
  return ret
47
66
 
48
67
 
@@ -20,13 +20,30 @@
20
20
  ## --> DO NOT MODIFY <--
21
21
  ###############################################################################
22
22
 
23
+ from ..packet import Packet
23
24
  from ..targeting import *
24
25
 
25
26
 
26
27
  class PhotonPipelineResultSerde:
27
28
  # Message definition md5sum. See photon_packet.adoc for details
28
- MESSAGE_VERSION = "5eeaa293d0c69aea90eaddea786a2b3b"
29
- MESSAGE_FORMAT = "PhotonPipelineMetadata:626e70461cbdb274fb43ead09c255f4e metadata;PhotonTrackedTarget:cc6dbb5c5c1e0fa808108019b20863f1 targets[?];optional MultiTargetPNPResult:541096947e9f3ca2d3f425ff7b04aa7b multitagResult;"
29
+ MESSAGE_VERSION = "4b2ff16a964b5e2bf04be0c1454d91c4"
30
+ MESSAGE_FORMAT = "PhotonPipelineMetadata:ac0a45f686457856fb30af77699ea356 metadata;PhotonTrackedTarget:cc6dbb5c5c1e0fa808108019b20863f1 targets[?];optional MultiTargetPNPResult:541096947e9f3ca2d3f425ff7b04aa7b multitagResult;"
31
+
32
+ @staticmethod
33
+ def pack(value: "PhotonPipelineResult") -> "Packet":
34
+ ret = Packet()
35
+
36
+ # metadata is of non-intrinsic type PhotonPipelineMetadata
37
+ ret.encodeBytes(
38
+ PhotonPipelineMetadata.photonStruct.pack(value.metadata).getData()
39
+ )
40
+
41
+ # targets is a custom VLA!
42
+ ret.encodeList(value.targets, PhotonTrackedTarget.photonStruct)
43
+
44
+ # multitagResult is optional! it better not be a VLA too
45
+ ret.encodeOptional(value.multitagResult, MultiTargetPNPResult.photonStruct)
46
+ return ret
30
47
 
31
48
  @staticmethod
32
49
  def unpack(packet: "Packet") -> "PhotonPipelineResult":
@@ -20,6 +20,7 @@
20
20
  ## --> DO NOT MODIFY <--
21
21
  ###############################################################################
22
22
 
23
+ from ..packet import Packet
23
24
  from ..targeting import *
24
25
 
25
26
 
@@ -28,6 +29,45 @@ class PhotonTrackedTargetSerde:
28
29
  MESSAGE_VERSION = "cc6dbb5c5c1e0fa808108019b20863f1"
29
30
  MESSAGE_FORMAT = "float64 yaw;float64 pitch;float64 area;float64 skew;int32 fiducialId;int32 objDetectId;float32 objDetectConf;Transform3d bestCameraToTarget;Transform3d altCameraToTarget;float64 poseAmbiguity;TargetCorner:16f6ac0dedc8eaccb951f4895d9e18b6 minAreaRectCorners[?];TargetCorner:16f6ac0dedc8eaccb951f4895d9e18b6 detectedCorners[?];"
30
31
 
32
+ @staticmethod
33
+ def pack(value: "PhotonTrackedTarget") -> "Packet":
34
+ ret = Packet()
35
+
36
+ # yaw is of intrinsic type float64
37
+ ret.encodeDouble(value.yaw)
38
+
39
+ # pitch is of intrinsic type float64
40
+ ret.encodeDouble(value.pitch)
41
+
42
+ # area is of intrinsic type float64
43
+ ret.encodeDouble(value.area)
44
+
45
+ # skew is of intrinsic type float64
46
+ ret.encodeDouble(value.skew)
47
+
48
+ # fiducialId is of intrinsic type int32
49
+ ret.encodeInt(value.fiducialId)
50
+
51
+ # objDetectId is of intrinsic type int32
52
+ ret.encodeInt(value.objDetectId)
53
+
54
+ # objDetectConf is of intrinsic type float32
55
+ ret.encodeFloat(value.objDetectConf)
56
+
57
+ ret.encodeTransform(value.bestCameraToTarget)
58
+
59
+ ret.encodeTransform(value.altCameraToTarget)
60
+
61
+ # poseAmbiguity is of intrinsic type float64
62
+ ret.encodeDouble(value.poseAmbiguity)
63
+
64
+ # minAreaRectCorners is a custom VLA!
65
+ ret.encodeList(value.minAreaRectCorners, TargetCorner.photonStruct)
66
+
67
+ # detectedCorners is a custom VLA!
68
+ ret.encodeList(value.detectedCorners, TargetCorner.photonStruct)
69
+ return ret
70
+
31
71
  @staticmethod
32
72
  def unpack(packet: "Packet") -> "PhotonTrackedTarget":
33
73
  ret = PhotonTrackedTarget()