Skip to content

Commit e5bf7dd

Browse files
Merge pull request #139 from pollen-robotics/138-provide-raw-images-to-teleop
138 provide raw images to teleop
2 parents 7d0d350 + 74cab25 commit e5bf7dd

6 files changed

Lines changed: 127 additions & 19 deletions

File tree

.github/workflows/pytest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
echo "total=$TOTAL" >> $GITHUB_ENV
4343
echo "### Total coverage: ${TOTAL}%" >> $GITHUB_STEP_SUMMARY
4444
- name: Make badge
45-
uses: schneegans/dynamic-badges-action@v1.6.0
45+
uses: schneegans/dynamic-badges-action@v1.7.0
4646
with:
4747
# GIST_TOKEN is a GitHub personal access token with scope "gist".
4848
auth: ${{ secrets.GIST_TOKEN }}

examples/camera_wrappers_examples/teleopWrapper_example.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828

2929
def spawn_procs(names: List[str]) -> Dict[str, sp.Popen]: # type: ignore [type-arg]
30-
width, height = 1280, 720
30+
width, height = 960, 720
3131
command = [
3232
"ffplay",
3333
"-i",
@@ -60,9 +60,11 @@ def spawn_procs(names: List[str]) -> Dict[str, sp.Popen]: # type: ignore [type-
6060
procs = spawn_procs(["left", "right"])
6161

6262
while True:
63-
data, lat, _ = w.get_data()
63+
data, lat, _ = w.get_data_h264()
6464
logging.info(lat)
6565
for name, packets in data.items():
66+
# if name == "left_raw" or name == "right_raw":
67+
# continue
6668
io = procs[name].stdin
6769
if io is not None:
6870
io.write(packets)

pollen_vision/pollen_vision/camera_wrappers/depthai/cam_config.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""Camera configuration class for depthai cameras."""
22

33
import json
4-
from typing import Dict, Optional, Tuple
4+
from typing import Dict, List, Optional, Tuple
55

6+
import cv2
67
import depthai as dai
78
import numpy as np
89
import numpy.typing as npt
@@ -66,6 +67,10 @@ def __init__(
6667
}
6768
self.calib: dai.CalibrationHandler = dai.CalibrationHandler()
6869

70+
# lazy init, camera needs to be connected to
71+
self.P_left: Optional[cv2.UMat] = None
72+
self.P_right: Optional[cv2.UMat] = None
73+
6974
def get_device_info(self) -> dai.DeviceInfo:
7075
"""Returns a dai.DeviceInfo object with the mx_id.
7176
This allows connecting to multiple devices plugged in the host machine at the same time,
@@ -146,3 +151,54 @@ def to_string(self) -> str:
146151
ret_string += "Undistort maps are: " + "set" if self.undistort_maps["left"] is not None else "not set"
147152

148153
return ret_string
154+
155+
def compute_projection_matrices(self) -> Tuple[cv2.UMat, cv2.UMat]:
156+
left_socket = get_socket_from_name("left", self.name_to_socket)
157+
right_socket = get_socket_from_name("right", self.name_to_socket)
158+
159+
left_D = np.array(self.calib.getDistortionCoefficients(left_socket))
160+
right_D = np.array(self.calib.getDistortionCoefficients(right_socket))
161+
162+
R = np.array(self.calib.getStereoRightRectificationRotation())
163+
164+
T = np.array(self.calib.getCameraTranslationVector(left_socket, right_socket))
165+
T *= 0.01 # to meter for ROS
166+
167+
R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify(
168+
self.get_K_left(),
169+
left_D,
170+
self.get_K_right(),
171+
right_D,
172+
self.undistort_resolution,
173+
R,
174+
T,
175+
flags=0,
176+
)
177+
return P1, P2
178+
179+
def to_ROS_msg(
180+
self, side: str = "left"
181+
) -> Tuple[int, int, str, List[float], npt.NDArray[np.float32], npt.NDArray[np.float32], npt.NDArray[np.float32]]:
182+
# as defined in https://docs.ros.org/en/melodic/api/sensor_msgs/html/msg/CameraInfo.html
183+
184+
height = self.resize_resolution[1]
185+
width = self.resize_resolution[0]
186+
distortion_model = "plumb_bob"
187+
if self.calib.getDistortionModel(get_socket_from_name(side, self.name_to_socket)) == dai.CameraModel.Fisheye:
188+
distortion_model = "equidistant"
189+
D = self.calib.getDistortionCoefficients(get_socket_from_name(side, self.name_to_socket))
190+
191+
if self.P_left is None or self.P_right is None:
192+
self.P_left, self.P_right = self.compute_projection_matrices()
193+
194+
if side == "left":
195+
K = self.get_K_left().flatten()
196+
R = np.array(self.calib.getStereoLeftRectificationRotation()).flatten()
197+
P = np.array(self.P_left).flatten()
198+
199+
else:
200+
K = self.get_K_right().flatten()
201+
R = np.array(self.calib.getStereoRightRectificationRotation()).flatten()
202+
P = np.array(self.P_right).flatten()
203+
204+
return height, width, distortion_model, D, K, R, P

pollen_vision/pollen_vision/camera_wrappers/depthai/teleop.py

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,28 @@ def __init__(
3030
exposure_params: Optional[Tuple[int, int]] = None,
3131
mx_id: str = "",
3232
) -> None:
33+
self._data_h264: Dict[str, npt.NDArray[np.uint8]] = {}
34+
self._latency_h264: Dict[str, float] = {}
35+
self._ts_h264: Dict[str, timedelta] = {}
36+
37+
self._data_mjpeg: Dict[str, npt.NDArray[np.uint8]] = {}
38+
self._latency_mjpeg: Dict[str, float] = {}
39+
self._ts_mjpeg: Dict[str, timedelta] = {}
40+
41+
self._queues_mjpeg: Dict[str, dai.DataOutputQueue] = {}
42+
3343
super().__init__(
3444
cam_config_json,
3545
fps,
3646
force_usb2=force_usb2,
37-
resize=(1280, 720),
47+
resize=(960, 720),
3848
rectify=rectify,
3949
exposure_params=exposure_params,
4050
mx_id=mx_id,
4151
isp_scale=(2, 3),
4252
)
4353

44-
def get_data(
54+
def get_data_h264(
4555
self,
4656
) -> Tuple[Dict[str, npt.NDArray[np.uint8]], Dict[str, float], Dict[str, timedelta]]:
4757
"""Extends the get_data method of the Wrapper class to return the h264 encoded left and right images.
@@ -51,32 +61,57 @@ def get_data(
5161
latencies and timestamps for each camera.
5262
"""
5363

54-
data, latency, ts = super().get_data()
64+
for name, queue in self.queues.items():
65+
pkt = queue.get()
66+
self._data_h264[name] = pkt.getData()
67+
self._latency_h264[name] = dai.Clock.now() - pkt.getTimestamp() # type: ignore[call-arg]
68+
self._ts_h264[name] = pkt.getTimestamp()
69+
70+
return self._data_h264, self._latency_h264, self._ts_h264
71+
72+
def get_data_mjpeg(self) -> Tuple[Dict[str, npt.NDArray[np.uint8]], Dict[str, float], Dict[str, timedelta]]:
73+
for name, queue in self._queues_mjpeg.items():
74+
pkt = queue.get()
75+
self._data_mjpeg[name] = pkt.getData() # type: ignore[attr-defined]
76+
self._latency_mjpeg[name] = dai.Clock.now() - pkt.getTimestamp() # type: ignore[attr-defined, call-arg]
77+
self._ts_mjpeg[name] = pkt.getTimestamp() # type: ignore[attr-defined]
5578

56-
for name, pkt in data.items():
57-
data[name] = pkt.getData()
79+
return self._data_mjpeg, self._latency_mjpeg, self._ts_mjpeg
5880

59-
return data, latency, ts
81+
def _create_output_streams(self, pipeline: dai.Pipeline) -> dai.Pipeline:
82+
super()._create_output_streams(pipeline)
83+
84+
self.xout_left_mjpeg = pipeline.createXLinkOut()
85+
self.xout_left_mjpeg.setStreamName("left_mjpeg")
86+
87+
self.xout_right_mjpeg = pipeline.createXLinkOut()
88+
self.xout_right_mjpeg.setStreamName("right_mjpeg")
89+
90+
return pipeline
6091

6192
def _link_pipeline(self, pipeline: dai.Pipeline) -> dai.Pipeline:
6293
"""Overloads the base class abstract method to link the pipeline with the nodes together."""
6394

6495
self.left.isp.link(self.left_manip.inputImage)
6596
self.left_manip.out.link(self.left_encoder.input)
66-
self.right.isp.link(self.right_manip.inputImage)
67-
self.right_manip.out.link(self.right_encoder.input)
68-
97+
self.left_manip.out.link(self.left_encoder_mjpeg.input)
6998
self.left_encoder.bitstream.link(self.xout_left.input)
7099
self.right_encoder.bitstream.link(self.xout_right.input)
71100

101+
self.right.isp.link(self.right_manip.inputImage)
102+
self.right_manip.out.link(self.right_encoder.input)
103+
self.right_manip.out.link(self.right_encoder_mjpeg.input)
104+
self.left_encoder_mjpeg.bitstream.link(self.xout_left_mjpeg.input)
105+
self.right_encoder_mjpeg.bitstream.link(self.xout_right_mjpeg.input)
106+
72107
return pipeline
73108

74109
def _create_encoders(self, pipeline: dai.Pipeline) -> dai.Pipeline:
75110
"""Creates the h264 encoders for the left and right images."""
76111

77112
profile = dai.VideoEncoderProperties.Profile.H264_BASELINE
78113
bitrate = 4000
79-
numBFrames = 0 # gstreamer recommends 0 B frames
114+
numBFrames = 0 # no B frames for streaming
80115
self.left_encoder = pipeline.create(dai.node.VideoEncoder)
81116
self.left_encoder.setDefaultProfilePreset(self.cam_config.fps, profile)
82117
self.left_encoder.setKeyframeFrequency(self.cam_config.fps) # every 1s
@@ -91,6 +126,16 @@ def _create_encoders(self, pipeline: dai.Pipeline) -> dai.Pipeline:
91126
self.right_encoder.setBitrateKbps(bitrate)
92127
# self.right_encoder.setQuality(self.cam_config.encoder_quality)
93128

129+
profile = dai.VideoEncoderProperties.Profile.MJPEG
130+
131+
self.left_encoder_mjpeg = pipeline.create(dai.node.VideoEncoder)
132+
self.left_encoder_mjpeg.setDefaultProfilePreset(self.cam_config.fps, profile)
133+
# self.left_encoder_mjpeg.setLossless(True)
134+
135+
self.right_encoder_mjpeg = pipeline.create(dai.node.VideoEncoder)
136+
self.right_encoder_mjpeg.setDefaultProfilePreset(self.cam_config.fps, profile)
137+
# self.right_encoder_mjpeg.setLossless(True)
138+
94139
return pipeline
95140

96141
def _create_pipeline(self) -> dai.Pipeline:
@@ -111,7 +156,11 @@ def _create_queues(self) -> Dict[str, dai.DataOutputQueue]:
111156
"""Extends the base class method _create_queues() to add the h264 encoded left and right images queues."""
112157

113158
# config for video: https://docs.luxonis.com/projects/api/en/latest/components/device/#output-queue-maxsize-and-blocking
114-
queues: Dict[str, dai.DataOutputQueue] = {}
159+
queues_h264: Dict[str, dai.DataOutputQueue] = {}
115160
for name in ["left", "right"]:
116-
queues[name] = self._device.getOutputQueue(name, maxSize=10, blocking=True)
117-
return queues
161+
queues_h264[name] = self._device.getOutputQueue(name, maxSize=30, blocking=True)
162+
163+
for name in ["left_mjpeg", "right_mjpeg"]:
164+
self._queues_mjpeg[name] = self._device.getOutputQueue(name, maxSize=1, blocking=False)
165+
166+
return queues_h264

pollen_vision/pollen_vision/camera_wrappers/depthai/wrapper.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def _prepare(self) -> None:
9999
self._set_undistort_maps()
100100

101101
self.pipeline = self._create_pipeline()
102+
self.pipeline.setXLinkChunkSize(0) # better usb performance
102103

103104
self._device.startPipeline(self.pipeline)
104105
self.queues = self._create_queues()

setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ checkpoints =
3636
all = mobile-sam @ git+https://github.com/pollen-robotics/MobileSAM
3737
ram @ git+https://github.com/pollen-robotics/recognize-anything
3838
datasets==2.18.0
39-
depthai==2.25.0.0
39+
depthai==2.27.0.0
4040
datasets==2.18.0
4141
supervision==0.20.0
4242
inference-gpu[yolo-world]==0.9.13
@@ -52,7 +52,7 @@ vision = mobile-sam @ git+https://github.com/pollen-robotics/MobileSAM
5252
scikit-learn==1.2.2
5353
transformers==4.40.2
5454
FramesViewer==1.0.2
55-
depthai_wrapper = depthai==2.25.0.0
55+
depthai_wrapper = depthai==2.27.0.0
5656
realsense_wrapper = pyrealsense2==2.55.1.6486
5757
gradio = gradio==4.21.0
5858
open3d = open3d==0.17.0

0 commit comments

Comments
 (0)