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.
- photonlibpy/__init__.py +2 -2
- photonlibpy/estimation/__init__.py +5 -0
- photonlibpy/estimation/cameraTargetRelation.py +25 -0
- photonlibpy/estimation/openCVHelp.py +200 -0
- photonlibpy/estimation/rotTrlTransform3d.py +32 -0
- photonlibpy/estimation/targetModel.py +137 -0
- photonlibpy/estimation/visionEstimation.py +91 -0
- photonlibpy/generated/MultiTargetPNPResultSerde.py +12 -0
- photonlibpy/generated/PhotonPipelineMetadataSerde.py +23 -4
- photonlibpy/generated/PhotonPipelineResultSerde.py +19 -2
- photonlibpy/generated/PhotonTrackedTargetSerde.py +40 -0
- photonlibpy/generated/PnpResultSerde.py +19 -0
- photonlibpy/generated/TargetCornerSerde.py +12 -0
- photonlibpy/generated/__init__.py +0 -1
- photonlibpy/networktables/NTTopicSet.py +64 -0
- photonlibpy/networktables/__init__.py +1 -0
- photonlibpy/packet.py +123 -8
- photonlibpy/photonCamera.py +10 -7
- photonlibpy/photonPoseEstimator.py +5 -5
- photonlibpy/simulation/__init__.py +5 -0
- photonlibpy/simulation/photonCameraSim.py +408 -0
- photonlibpy/simulation/simCameraProperties.py +661 -0
- photonlibpy/simulation/videoSimUtil.py +2 -0
- photonlibpy/simulation/visionSystemSim.py +237 -0
- photonlibpy/simulation/visionTargetSim.py +50 -0
- photonlibpy/targeting/TargetCorner.py +5 -1
- photonlibpy/targeting/__init__.py +1 -1
- photonlibpy/targeting/multiTargetPNPResult.py +10 -4
- photonlibpy/targeting/photonPipelineResult.py +12 -5
- photonlibpy/targeting/photonTrackedTarget.py +13 -5
- photonlibpy/version.py +2 -2
- {photonlibpy-2025.0.0a0.dist-info → photonlibpy-2025.0.0b2.dist-info}/METADATA +6 -2
- photonlibpy-2025.0.0b2.dist-info/RECORD +36 -0
- {photonlibpy-2025.0.0a0.dist-info → photonlibpy-2025.0.0b2.dist-info}/WHEEL +1 -1
- photonlibpy-2025.0.0a0.dist-info/RECORD +0 -22
- {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
|
20
|
-
|
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 =
|
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[
|
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:
|
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:
|
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)
|
photonlibpy/photonCamera.py
CHANGED
@@ -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
|
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
|
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
|
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.
|
273
|
-
best_tf = result.
|
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
|