photonlibpy 2025.0.0a0__py3-none-any.whl → 2025.0.0b2__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.
Files changed (36) hide show
  1. photonlibpy/__init__.py +2 -2
  2. photonlibpy/estimation/__init__.py +5 -0
  3. photonlibpy/estimation/cameraTargetRelation.py +25 -0
  4. photonlibpy/estimation/openCVHelp.py +200 -0
  5. photonlibpy/estimation/rotTrlTransform3d.py +32 -0
  6. photonlibpy/estimation/targetModel.py +137 -0
  7. photonlibpy/estimation/visionEstimation.py +91 -0
  8. photonlibpy/generated/MultiTargetPNPResultSerde.py +12 -0
  9. photonlibpy/generated/PhotonPipelineMetadataSerde.py +23 -4
  10. photonlibpy/generated/PhotonPipelineResultSerde.py +19 -2
  11. photonlibpy/generated/PhotonTrackedTargetSerde.py +40 -0
  12. photonlibpy/generated/PnpResultSerde.py +19 -0
  13. photonlibpy/generated/TargetCornerSerde.py +12 -0
  14. photonlibpy/generated/__init__.py +0 -1
  15. photonlibpy/networktables/NTTopicSet.py +64 -0
  16. photonlibpy/networktables/__init__.py +1 -0
  17. photonlibpy/packet.py +123 -8
  18. photonlibpy/photonCamera.py +10 -7
  19. photonlibpy/photonPoseEstimator.py +5 -5
  20. photonlibpy/simulation/__init__.py +5 -0
  21. photonlibpy/simulation/photonCameraSim.py +408 -0
  22. photonlibpy/simulation/simCameraProperties.py +661 -0
  23. photonlibpy/simulation/videoSimUtil.py +2 -0
  24. photonlibpy/simulation/visionSystemSim.py +237 -0
  25. photonlibpy/simulation/visionTargetSim.py +50 -0
  26. photonlibpy/targeting/TargetCorner.py +5 -1
  27. photonlibpy/targeting/__init__.py +1 -1
  28. photonlibpy/targeting/multiTargetPNPResult.py +10 -4
  29. photonlibpy/targeting/photonPipelineResult.py +12 -5
  30. photonlibpy/targeting/photonTrackedTarget.py +13 -5
  31. photonlibpy/version.py +2 -2
  32. {photonlibpy-2025.0.0a0.dist-info → photonlibpy-2025.0.0b2.dist-info}/METADATA +6 -2
  33. photonlibpy-2025.0.0b2.dist-info/RECORD +36 -0
  34. {photonlibpy-2025.0.0a0.dist-info → photonlibpy-2025.0.0b2.dist-info}/WHEEL +1 -1
  35. photonlibpy-2025.0.0a0.dist-info/RECORD +0 -22
  36. {photonlibpy-2025.0.0a0.dist-info → photonlibpy-2025.0.0b2.dist-info}/top_level.txt +0 -0
@@ -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,24 @@ class PnpResultSerde:
28
29
  MESSAGE_VERSION = "ae4d655c0a3104d88df4f5db144c1e86"
29
30
  MESSAGE_FORMAT = "Transform3d best;Transform3d alt;float64 bestReprojErr;float64 altReprojErr;float64 ambiguity;"
30
31
 
32
+ @staticmethod
33
+ def pack(value: "PnpResult") -> "Packet":
34
+ ret = Packet()
35
+
36
+ ret.encodeTransform(value.best)
37
+
38
+ ret.encodeTransform(value.alt)
39
+
40
+ # bestReprojErr is of intrinsic type float64
41
+ ret.encodeDouble(value.bestReprojErr)
42
+
43
+ # altReprojErr is of intrinsic type float64
44
+ ret.encodeDouble(value.altReprojErr)
45
+
46
+ # ambiguity is of intrinsic type float64
47
+ ret.encodeDouble(value.ambiguity)
48
+ return ret
49
+
31
50
  @staticmethod
32
51
  def unpack(packet: "Packet") -> "PnpResult":
33
52
  ret = PnpResult()
@@ -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 TargetCornerSerde:
28
29
  MESSAGE_VERSION = "16f6ac0dedc8eaccb951f4895d9e18b6"
29
30
  MESSAGE_FORMAT = "float64 x;float64 y;"
30
31
 
32
+ @staticmethod
33
+ def pack(value: "TargetCorner") -> "Packet":
34
+ ret = Packet()
35
+
36
+ # x is of intrinsic type float64
37
+ ret.encodeDouble(value.x)
38
+
39
+ # y is of intrinsic type float64
40
+ ret.encodeDouble(value.y)
41
+ return ret
42
+
31
43
  @staticmethod
32
44
  def unpack(packet: "Packet") -> "TargetCorner":
33
45
  ret = TargetCorner()
@@ -2,7 +2,6 @@
2
2
 
3
3
  from .MultiTargetPNPResultSerde import MultiTargetPNPResultSerde # noqa
4
4
  from .PhotonPipelineMetadataSerde import PhotonPipelineMetadataSerde # noqa
5
- from .PhotonPipelineMetadataSerde import PhotonPipelineMetadataSerde # noqa
6
5
  from .PhotonPipelineResultSerde import PhotonPipelineResultSerde # noqa
7
6
  from .PhotonTrackedTargetSerde import PhotonTrackedTargetSerde # noqa
8
7
  from .PnpResultSerde import PnpResultSerde # noqa
@@ -0,0 +1,64 @@
1
+ import ntcore as nt
2
+ from wpimath.geometry import Transform3d
3
+
4
+ from ..generated.PhotonPipelineResultSerde import PhotonPipelineResultSerde
5
+
6
+ PhotonPipelineResult_TYPE_STRING = (
7
+ "photonstruct:PhotonPipelineResult:" + PhotonPipelineResultSerde.MESSAGE_VERSION
8
+ )
9
+
10
+
11
+ class NTTopicSet:
12
+
13
+ def __init__(self) -> None:
14
+ self.subTable = nt.NetworkTableInstance.getDefault()
15
+
16
+ def updateEntries(self) -> None:
17
+ options = nt.PubSubOptions()
18
+ options.periodic = 0.01
19
+ options.sendAll = True
20
+ self.rawBytesEntry = self.subTable.getRawTopic("rawBytes").publish(
21
+ PhotonPipelineResult_TYPE_STRING, options
22
+ )
23
+ self.rawBytesEntry.getTopic().setProperty(
24
+ "message_uuid", PhotonPipelineResultSerde.MESSAGE_VERSION
25
+ )
26
+ self.pipelineIndexPublisher = self.subTable.getIntegerTopic(
27
+ "pipelineIndexState"
28
+ ).publish()
29
+ self.pipelineIndexRequestSub = self.subTable.getIntegerTopic(
30
+ "pipelineIndexRequest"
31
+ ).subscribe(0)
32
+
33
+ self.driverModePublisher = self.subTable.getBooleanTopic("driverMode").publish()
34
+ self.driverModeSubscriber = self.subTable.getBooleanTopic(
35
+ "driverModeRequest"
36
+ ).subscribe(False)
37
+
38
+ self.driverModeSubscriber.getTopic().publish().setDefault(False)
39
+
40
+ self.latencyMillisEntry = self.subTable.getDoubleTopic(
41
+ "latencyMillis"
42
+ ).publish()
43
+ self.hasTargetEntry = self.subTable.getBooleanTopic("hasTargets").publish()
44
+
45
+ self.targetPitchEntry = self.subTable.getDoubleTopic("targetPitch").publish()
46
+ self.targetAreaEntry = self.subTable.getDoubleTopic("targetArea").publish()
47
+ self.targetYawEntry = self.subTable.getDoubleTopic("targetYaw").publish()
48
+ self.targetPoseEntry = self.subTable.getStructTopic(
49
+ "targetPose", Transform3d
50
+ ).publish()
51
+ self.targetSkewEntry = self.subTable.getDoubleTopic("targetSkew").publish()
52
+
53
+ self.bestTargetPosX = self.subTable.getDoubleTopic("targetPixelsX").publish()
54
+ self.bestTargetPosY = self.subTable.getDoubleTopic("targetPixelsY").publish()
55
+
56
+ self.heartbeatTopic = self.subTable.getIntegerTopic("heartbeat")
57
+ self.heartbeatPublisher = self.heartbeatTopic.publish()
58
+
59
+ self.cameraIntrinsicsPublisher = self.subTable.getDoubleArrayTopic(
60
+ "cameraIntrinsics"
61
+ ).publish()
62
+ self.cameraDistortionPublisher = self.subTable.getDoubleArrayTopic(
63
+ "cameraDistortion"
64
+ ).publish()
@@ -0,0 +1 @@
1
+ from .NTTopicSet import NTTopicSet
photonlibpy/packet.py CHANGED
@@ -16,13 +16,21 @@
16
16
  ###############################################################################
17
17
 
18
18
  import struct
19
- from typing import Any, Optional, Type
20
- from wpimath.geometry import Transform3d, Translation3d, Rotation3d, Quaternion
19
+ from typing import Generic, Optional, Protocol, TypeVar
20
+
21
21
  import wpilib
22
+ from wpimath.geometry import Quaternion, Rotation3d, Transform3d, Translation3d
23
+
24
+ T = TypeVar("T")
25
+
26
+
27
+ class Serde(Generic[T], Protocol):
28
+ def pack(self, value: T) -> "Packet": ...
29
+ def unpack(self, packet: "Packet") -> T: ...
22
30
 
23
31
 
24
32
  class Packet:
25
- def __init__(self, data: bytes):
33
+ def __init__(self, data: bytes = b""):
26
34
  """
27
35
  * Constructs an empty packet.
28
36
  *
@@ -33,9 +41,9 @@ class Packet:
33
41
  self.readPos = 0
34
42
  self.outOfBytes = False
35
43
 
36
- def clear(self):
44
+ def clear(self) -> None:
37
45
  """Clears the packet and resets the read and write positions."""
38
- self.packetData = [0] * self.size
46
+ self.packetData = bytes(self.size)
39
47
  self.readPos = 0
40
48
  self.outOfBytes = False
41
49
 
@@ -157,7 +165,7 @@ class Packet:
157
165
  ret.append(self.decodeDouble())
158
166
  return ret
159
167
 
160
- def decodeShortList(self) -> list[float]:
168
+ def decodeShortList(self) -> list[int]:
161
169
  """
162
170
  * Returns a decoded array of shorts from the packet.
163
171
  """
@@ -186,15 +194,122 @@ class Packet:
186
194
 
187
195
  return Transform3d(translation, rotation)
188
196
 
189
- def decodeList(self, serde: Type):
197
+ def decodeList(self, serde: Serde[T]) -> list[T]:
190
198
  retList = []
191
199
  arr_len = self.decode8()
192
200
  for _ in range(arr_len):
193
201
  retList.append(serde.unpack(self))
194
202
  return retList
195
203
 
196
- def decodeOptional(self, serde: Type) -> Optional[Any]:
204
+ def decodeOptional(self, serde: Serde[T]) -> Optional[T]:
197
205
  if self.decodeBoolean():
198
206
  return serde.unpack(self)
199
207
  else:
200
208
  return None
209
+
210
+ def _encodeGeneric(self, packFormat, value):
211
+ """
212
+ Append bytes to the packet data buffer.
213
+ """
214
+ self.packetData = self.packetData + struct.pack(packFormat, value)
215
+ self.size = len(self.packetData)
216
+
217
+ def encode8(self, value: int):
218
+ """
219
+ Encodes a single byte and appends it to the packet.
220
+ """
221
+ self._encodeGeneric("<b", value)
222
+
223
+ def encode16(self, value: int):
224
+ """
225
+ Encodes a short (2 bytes) and appends it to the packet.
226
+ """
227
+ self._encodeGeneric("<h", value)
228
+
229
+ def encodeInt(self, value: int):
230
+ """
231
+ Encodes an int (4 bytes) and appends it to the packet.
232
+ """
233
+ self._encodeGeneric("<l", value)
234
+
235
+ def encodeFloat(self, value: float):
236
+ """
237
+ Encodes a float (4 bytes) and appends it to the packet.
238
+ """
239
+ self._encodeGeneric("<f", value)
240
+
241
+ def encodeLong(self, value: int):
242
+ """
243
+ Encodes a long (8 bytes) and appends it to the packet.
244
+ """
245
+ self._encodeGeneric("<q", value)
246
+
247
+ def encodeDouble(self, value: float):
248
+ """
249
+ Encodes a double (8 bytes) and appends it to the packet.
250
+ """
251
+ self._encodeGeneric("<d", value)
252
+
253
+ def encodeBoolean(self, value: bool):
254
+ """
255
+ Encodes a boolean as a single byte and appends it to the packet.
256
+ """
257
+ self.encode8(1 if value else 0)
258
+
259
+ def encodeDoubleArray(self, values: list[float]):
260
+ """
261
+ Encodes an array of doubles and appends it to the packet.
262
+ """
263
+ self.encode8(len(values))
264
+ for value in values:
265
+ self.encodeDouble(value)
266
+
267
+ def encodeShortList(self, values: list[int]):
268
+ """
269
+ Encodes a list of shorts, with length prefixed as a single byte.
270
+ """
271
+ self.encode8(len(values))
272
+ for value in values:
273
+ self.encode16(value)
274
+
275
+ def encodeTransform(self, transform: Transform3d):
276
+ """
277
+ Encodes a Transform3d (translation and rotation) and appends it to the packet.
278
+ """
279
+ # Encode Translation3d part (x, y, z)
280
+ self.encodeDouble(transform.translation().x)
281
+ self.encodeDouble(transform.translation().y)
282
+ self.encodeDouble(transform.translation().z)
283
+
284
+ # Encode Rotation3d as Quaternion (w, x, y, z)
285
+ quaternion = transform.rotation().getQuaternion()
286
+ self.encodeDouble(quaternion.W())
287
+ self.encodeDouble(quaternion.X())
288
+ self.encodeDouble(quaternion.Y())
289
+ self.encodeDouble(quaternion.Z())
290
+
291
+ def encodeList(self, values: list[T], serde: Serde[T]):
292
+ """
293
+ Encodes a list of items using a specific serializer and appends it to the packet.
294
+ """
295
+ self.encode8(len(values))
296
+ for item in values:
297
+ packed = serde.pack(item)
298
+ self.packetData = self.packetData + packed.getData()
299
+ self.size = len(self.packetData)
300
+
301
+ def encodeOptional(self, value: Optional[T], serde: Serde[T]):
302
+ """
303
+ Encodes an optional value using a specific serializer.
304
+ """
305
+ if value is None:
306
+ self.encodeBoolean(False)
307
+ else:
308
+ self.encodeBoolean(True)
309
+ packed = serde.pack(value)
310
+ self.packetData = self.packetData + packed.getData()
311
+ self.size = len(self.packetData)
312
+
313
+ def encodeBytes(self, value: bytes):
314
+ self.packetData = self.packetData + value
315
+ self.size = len(self.packetData)
@@ -17,15 +17,17 @@
17
17
 
18
18
  from enum import Enum
19
19
  from typing import List
20
+
20
21
  import ntcore
21
- from wpilib import RobotController, Timer
22
- import wpilib
23
- from .packet import Packet
24
- from .targeting.photonPipelineResult import PhotonPipelineResult
25
- from .version import PHOTONVISION_VERSION, PHOTONLIB_VERSION # type: ignore[import-untyped]
26
22
 
27
23
  # magical import to make serde stuff work
28
24
  import photonlibpy.generated # noqa
25
+ import wpilib
26
+ from wpilib import RobotController, Timer
27
+
28
+ from .packet import Packet
29
+ from .targeting.photonPipelineResult import PhotonPipelineResult
30
+ from .version import PHOTONLIB_VERSION # type: ignore[import-untyped]
29
31
 
30
32
 
31
33
  class VisionLEDMode(Enum):
@@ -124,7 +126,7 @@ class PhotonCamera:
124
126
  pkt = Packet(byteList)
125
127
  newResult = PhotonPipelineResult.photonStruct.unpack(pkt)
126
128
  # NT4 allows us to correct the timestamp based on when the message was sent
127
- newResult.ntReceiveTimestampMicros = timestamp / 1e6
129
+ newResult.ntReceiveTimestampMicros = timestamp
128
130
  ret.append(newResult)
129
131
 
130
132
  return ret
@@ -231,12 +233,13 @@ class PhotonCamera:
231
233
 
232
234
  remoteUUID = self._rawBytesEntry.getTopic().getProperty("message_uuid")
233
235
 
234
- if remoteUUID is None or len(remoteUUID) == 0:
236
+ if not remoteUUID:
235
237
  wpilib.reportWarning(
236
238
  f"PhotonVision coprocessor at path {self._path} has not reported a message interface UUID - is your coprocessor's camera started?",
237
239
  True,
238
240
  )
239
241
 
242
+ assert isinstance(remoteUUID, str)
240
243
  # ntcore hands us a JSON string with leading/trailing quotes - remove those
241
244
  remoteUUID = remoteUUID.replace('"', "")
242
245
 
@@ -20,11 +20,11 @@ from typing import Optional
20
20
 
21
21
  import wpilib
22
22
  from robotpy_apriltag import AprilTagFieldLayout
23
- from wpimath.geometry import Transform3d, Pose3d, Pose2d
23
+ from wpimath.geometry import Pose2d, Pose3d, Transform3d
24
24
 
25
- from .targeting.photonPipelineResult import PhotonPipelineResult
26
- from .photonCamera import PhotonCamera
27
25
  from .estimatedRobotPose import EstimatedRobotPose
26
+ from .photonCamera import PhotonCamera
27
+ from .targeting.photonPipelineResult import PhotonPipelineResult
28
28
 
29
29
 
30
30
  class PoseStrategy(enum.Enum):
@@ -269,8 +269,8 @@ class PhotonPoseEstimator:
269
269
  def _multiTagOnCoprocStrategy(
270
270
  self, result: PhotonPipelineResult
271
271
  ) -> Optional[EstimatedRobotPose]:
272
- if result.multiTagResult.estimatedPose.isPresent:
273
- best_tf = result.multiTagResult.estimatedPose.best
272
+ if result.multitagResult is not None:
273
+ best_tf = result.multitagResult.estimatedPose.best
274
274
  best = (
275
275
  Pose3d()
276
276
  .transformBy(best_tf) # field-to-camera
@@ -0,0 +1,5 @@
1
+ from .photonCameraSim import PhotonCameraSim
2
+ from .simCameraProperties import SimCameraProperties
3
+ from .videoSimUtil import VideoSimUtil
4
+ from .visionSystemSim import VisionSystemSim
5
+ from .visionTargetSim import VisionTargetSim