photonlibpy 2025.0.0a0__py3-none-any.whl → 2025.0.0b2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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