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.
- 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
|