mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-19 00:41:41 +00:00
238 lines
8.4 KiB
Python
238 lines
8.4 KiB
Python
|
|
import typing
|
||
|
|
|
||
|
|
import wpilib
|
||
|
|
from robotpy_apriltag import AprilTagFieldLayout
|
||
|
|
from wpilib import Field2d
|
||
|
|
from wpimath.geometry import Pose2d, Pose3d, Transform3d
|
||
|
|
|
||
|
|
# TODO(auscompgeek): update import path when RobotPy re-exports are fixed
|
||
|
|
from wpimath.interpolation._interpolation import TimeInterpolatablePose3dBuffer
|
||
|
|
from wpimath.units import seconds
|
||
|
|
|
||
|
|
from ..estimation import TargetModel
|
||
|
|
from .photonCameraSim import PhotonCameraSim
|
||
|
|
from .visionTargetSim import VisionTargetSim
|
||
|
|
|
||
|
|
|
||
|
|
class VisionSystemSim:
|
||
|
|
def __init__(self, visionSystemName: str):
|
||
|
|
self.dbgField: Field2d = Field2d()
|
||
|
|
self.bufferLength: seconds = 1.5
|
||
|
|
|
||
|
|
self.camSimMap: typing.Dict[str, PhotonCameraSim] = {}
|
||
|
|
self.camTrfMap: typing.Dict[PhotonCameraSim, TimeInterpolatablePose3dBuffer] = (
|
||
|
|
{}
|
||
|
|
)
|
||
|
|
self.robotPoseBuffer: TimeInterpolatablePose3dBuffer = (
|
||
|
|
TimeInterpolatablePose3dBuffer(self.bufferLength)
|
||
|
|
)
|
||
|
|
self.targetSets: typing.Dict[str, list[VisionTargetSim]] = {}
|
||
|
|
|
||
|
|
self.tableName: str = "VisionSystemSim-" + visionSystemName
|
||
|
|
wpilib.SmartDashboard.putData(self.tableName + "/Sim Field", self.dbgField)
|
||
|
|
|
||
|
|
def getCameraSim(self, name: str) -> PhotonCameraSim | None:
|
||
|
|
return self.camSimMap.get(name, None)
|
||
|
|
|
||
|
|
def getCameraSims(self) -> list[PhotonCameraSim]:
|
||
|
|
return [*self.camSimMap.values()]
|
||
|
|
|
||
|
|
def addCamera(self, cameraSim: PhotonCameraSim, robotToCamera: Transform3d) -> None:
|
||
|
|
name = cameraSim.getCamera().getName()
|
||
|
|
if name not in self.camSimMap:
|
||
|
|
self.camSimMap[name] = cameraSim
|
||
|
|
self.camTrfMap[cameraSim] = TimeInterpolatablePose3dBuffer(
|
||
|
|
self.bufferLength
|
||
|
|
)
|
||
|
|
self.camTrfMap[cameraSim].addSample(
|
||
|
|
wpilib.Timer.getFPGATimestamp(), Pose3d() + robotToCamera
|
||
|
|
)
|
||
|
|
|
||
|
|
def clearCameras(self) -> None:
|
||
|
|
self.camSimMap.clear()
|
||
|
|
self.camTrfMap.clear()
|
||
|
|
|
||
|
|
def removeCamera(self, cameraSim: PhotonCameraSim) -> bool:
|
||
|
|
name = cameraSim.getCamera().getName()
|
||
|
|
if name in self.camSimMap:
|
||
|
|
del self.camSimMap[name]
|
||
|
|
return True
|
||
|
|
else:
|
||
|
|
return False
|
||
|
|
|
||
|
|
def getRobotToCamera(
|
||
|
|
self,
|
||
|
|
cameraSim: PhotonCameraSim,
|
||
|
|
time: seconds = wpilib.Timer.getFPGATimestamp(),
|
||
|
|
) -> Transform3d | None:
|
||
|
|
if cameraSim in self.camTrfMap:
|
||
|
|
trfBuffer = self.camTrfMap[cameraSim]
|
||
|
|
sample = trfBuffer.sample(time)
|
||
|
|
if sample is None:
|
||
|
|
return None
|
||
|
|
else:
|
||
|
|
return Transform3d(Pose3d(), sample)
|
||
|
|
else:
|
||
|
|
return None
|
||
|
|
|
||
|
|
def getCameraPose(
|
||
|
|
self,
|
||
|
|
cameraSim: PhotonCameraSim,
|
||
|
|
time: seconds = wpilib.Timer.getFPGATimestamp(),
|
||
|
|
) -> Pose3d | None:
|
||
|
|
robotToCamera = self.getRobotToCamera(cameraSim, time)
|
||
|
|
if robotToCamera is None:
|
||
|
|
return None
|
||
|
|
else:
|
||
|
|
return self.getRobotPose(time) + robotToCamera
|
||
|
|
|
||
|
|
def adjustCamera(
|
||
|
|
self, cameraSim: PhotonCameraSim, robotToCamera: Transform3d
|
||
|
|
) -> bool:
|
||
|
|
if cameraSim in self.camTrfMap:
|
||
|
|
self.camTrfMap[cameraSim].addSample(
|
||
|
|
wpilib.Timer.getFPGATimestamp(), Pose3d() + robotToCamera
|
||
|
|
)
|
||
|
|
return True
|
||
|
|
else:
|
||
|
|
return False
|
||
|
|
|
||
|
|
def resetCameraTransforms(self, cameraSim: PhotonCameraSim | None = None) -> None:
|
||
|
|
now = wpilib.Timer.getFPGATimestamp()
|
||
|
|
|
||
|
|
def resetSingleCamera(self, cameraSim: PhotonCameraSim) -> bool:
|
||
|
|
if cameraSim in self.camTrfMap:
|
||
|
|
trfBuffer = self.camTrfMap[cameraSim]
|
||
|
|
lastTrf = Transform3d(Pose3d(), trfBuffer.sample(now))
|
||
|
|
trfBuffer.clear()
|
||
|
|
self.adjustCamera(cameraSim, lastTrf)
|
||
|
|
return True
|
||
|
|
else:
|
||
|
|
return False
|
||
|
|
|
||
|
|
if cameraSim is None:
|
||
|
|
for camera in self.camTrfMap.keys():
|
||
|
|
resetSingleCamera(self, camera)
|
||
|
|
else:
|
||
|
|
resetSingleCamera(self, cameraSim)
|
||
|
|
|
||
|
|
def getVisionTargets(self, targetType: str | None = None) -> list[VisionTargetSim]:
|
||
|
|
if targetType is None:
|
||
|
|
all: list[VisionTargetSim] = []
|
||
|
|
for targets in self.targetSets.values():
|
||
|
|
for target in targets:
|
||
|
|
all.append(target)
|
||
|
|
return all
|
||
|
|
else:
|
||
|
|
return self.targetSets[targetType]
|
||
|
|
|
||
|
|
def addVisionTargets(
|
||
|
|
self, targets: list[VisionTargetSim], targetType: str = "targets"
|
||
|
|
) -> None:
|
||
|
|
if targetType not in self.targetSets:
|
||
|
|
self.targetSets[targetType] = targets
|
||
|
|
else:
|
||
|
|
self.targetSets[targetType] += targets
|
||
|
|
|
||
|
|
def addAprilTags(self, layout: AprilTagFieldLayout) -> None:
|
||
|
|
targets: list[VisionTargetSim] = []
|
||
|
|
for tag in layout.getTags():
|
||
|
|
tag_pose = layout.getTagPose(tag.ID)
|
||
|
|
# TODO this was done to make the python gods happy. Confirm that this is desired or if types dont matter
|
||
|
|
assert tag_pose is not None
|
||
|
|
targets.append(
|
||
|
|
VisionTargetSim(tag_pose, TargetModel.AprilTag36h11(), tag.ID)
|
||
|
|
)
|
||
|
|
self.addVisionTargets(targets, "apriltag")
|
||
|
|
|
||
|
|
def clearVisionTargets(self) -> None:
|
||
|
|
self.targetSets.clear()
|
||
|
|
|
||
|
|
def clearAprilTags(self) -> None:
|
||
|
|
self.removeVisionTargetType("apriltag")
|
||
|
|
|
||
|
|
def removeVisionTargetType(self, targetType: str) -> None:
|
||
|
|
del self.targetSets[targetType]
|
||
|
|
|
||
|
|
def removeVisionTargets(
|
||
|
|
self, targets: list[VisionTargetSim]
|
||
|
|
) -> list[VisionTargetSim]:
|
||
|
|
removedList: list[VisionTargetSim] = []
|
||
|
|
for target in targets:
|
||
|
|
for _, currentTargets in self.targetSets.items():
|
||
|
|
if target in currentTargets:
|
||
|
|
removedList.append(target)
|
||
|
|
currentTargets.remove(target)
|
||
|
|
return removedList
|
||
|
|
|
||
|
|
def getRobotPose(
|
||
|
|
self, timestamp: seconds = wpilib.Timer.getFPGATimestamp()
|
||
|
|
) -> Pose3d:
|
||
|
|
return self.robotPoseBuffer.sample(timestamp)
|
||
|
|
|
||
|
|
def resetRobotPose(self, robotPose: Pose2d | Pose3d) -> None:
|
||
|
|
if type(robotPose) is Pose2d:
|
||
|
|
robotPose = Pose3d(robotPose)
|
||
|
|
assert type(robotPose) is Pose3d
|
||
|
|
|
||
|
|
self.robotPoseBuffer.clear()
|
||
|
|
self.robotPoseBuffer.addSample(wpilib.Timer.getFPGATimestamp(), robotPose)
|
||
|
|
|
||
|
|
def getDebugField(self) -> Field2d:
|
||
|
|
return self.dbgField
|
||
|
|
|
||
|
|
def update(self, robotPose: Pose2d | Pose3d) -> None:
|
||
|
|
if type(robotPose) is Pose2d:
|
||
|
|
robotPose = Pose3d(robotPose)
|
||
|
|
assert type(robotPose) is Pose3d
|
||
|
|
|
||
|
|
for targetType, targets in self.targetSets.items():
|
||
|
|
posesToAdd: list[Pose2d] = []
|
||
|
|
for target in targets:
|
||
|
|
posesToAdd.append(target.getPose().toPose2d())
|
||
|
|
self.dbgField.getObject(targetType).setPoses(posesToAdd)
|
||
|
|
|
||
|
|
now = wpilib.Timer.getFPGATimestamp()
|
||
|
|
self.robotPoseBuffer.addSample(now, robotPose)
|
||
|
|
self.dbgField.setRobotPose(robotPose.toPose2d())
|
||
|
|
|
||
|
|
allTargets: list[VisionTargetSim] = []
|
||
|
|
for targets in self.targetSets.values():
|
||
|
|
for target in targets:
|
||
|
|
allTargets.append(target)
|
||
|
|
|
||
|
|
visTgtPoses2d: list[Pose2d] = []
|
||
|
|
cameraPoses2d: list[Pose2d] = []
|
||
|
|
processed = False
|
||
|
|
for camSim in self.camSimMap.values():
|
||
|
|
optTimestamp = camSim.consumeNextEntryTime()
|
||
|
|
if optTimestamp is None:
|
||
|
|
continue
|
||
|
|
else:
|
||
|
|
processed = True
|
||
|
|
|
||
|
|
timestampNt = optTimestamp
|
||
|
|
latency = camSim.prop.estLatency()
|
||
|
|
timestampCapture = timestampNt * 1.0e-6 - latency
|
||
|
|
|
||
|
|
lateRobotPose = self.getRobotPose(timestampCapture)
|
||
|
|
lateCameraPose = lateRobotPose + self.getRobotToCamera(
|
||
|
|
camSim, timestampCapture
|
||
|
|
)
|
||
|
|
cameraPoses2d.append(lateCameraPose.toPose2d())
|
||
|
|
|
||
|
|
camResult = camSim.process(latency, lateCameraPose, allTargets)
|
||
|
|
camSim.submitProcessedFrame(camResult, timestampNt)
|
||
|
|
for target in camResult.getTargets():
|
||
|
|
trf = target.getBestCameraToTarget()
|
||
|
|
if trf == Transform3d():
|
||
|
|
continue
|
||
|
|
|
||
|
|
visTgtPoses2d.append(lateCameraPose.transformBy(trf).toPose2d())
|
||
|
|
|
||
|
|
if processed:
|
||
|
|
self.dbgField.getObject("visibleTargetPoses").setPoses(visTgtPoses2d)
|
||
|
|
|
||
|
|
if len(cameraPoses2d) != 0:
|
||
|
|
self.dbgField.getObject("cameras").setPoses(cameraPoses2d)
|