1mod payloads;
2
3pub use payloads::*;
4
5use bincode::de::Decoder;
6use bincode::error::DecodeError;
7use bincode::{Decode, Encode};
8use cu_sensor_payloads::{
9 BarometerPayload, CuDepthMapFormat, CuImage, Distance, ImuPayload, MagnetometerPayload,
10 PointCloudSoa, Reflectivity,
11};
12use cu29::prelude::*;
13use cu29::units::si::length::meter;
14use cu29::units::si::ratio::percent;
15use serde::{Deserialize, Serialize};
16
17#[derive(Default, Debug, Clone, Encode, Serialize, Deserialize, Reflect)]
18#[reflect(from_reflect = false, no_field_bounds, type_path = false)]
19pub struct ZedStereoImages {
20 pub left: CuImage<Vec<u8>>,
21 pub right: CuImage<Vec<u8>>,
22}
23
24impl TypePath for ZedStereoImages {
25 fn type_path() -> &'static str {
26 "cu_zed::ZedStereoImages"
27 }
28
29 fn short_type_path() -> &'static str {
30 "ZedStereoImages"
31 }
32
33 fn type_ident() -> Option<&'static str> {
34 Some("ZedStereoImages")
35 }
36
37 fn crate_name() -> Option<&'static str> {
38 Some("cu_zed")
39 }
40
41 fn module_path() -> Option<&'static str> {
42 Some("cu_zed")
43 }
44}
45
46impl Decode<()> for ZedStereoImages {
47 fn decode<D: Decoder<Context = ()>>(decoder: &mut D) -> Result<Self, DecodeError> {
48 let left: CuImage<Vec<u8>> = Decode::decode(decoder)?;
49 let right: CuImage<Vec<u8>> = Decode::decode(decoder)?;
50 Ok(Self { left, right })
51 }
52}
53
54pub type ZedSourceOutputs = (
55 CuMsg<ZedStereoImages>,
56 CuMsg<ZedDepthMap>,
57 CuMsg<ZedConfidenceMap<Vec<f32>>>,
58 CuMsg<CuLatchedStateUpdate<ZedCalibrationBundle>>,
59 CuMsg<CuLatchedStateUpdate<ZedRigTransforms>>,
60 CuMsg<ImuPayload>,
61 CuMsg<MagnetometerPayload>,
62 CuMsg<BarometerPayload>,
63 CuMsg<ZedFrameMeta>,
64);
65
66pub type ZedPointCloudHd720 = PointCloudSoa<{ 1280 * 720 }>;
68
69pub type ZedDepthToPointCloudHd720 = ZedDepthToPointCloud<{ 1280 * 720 }>;
71
72#[derive(Default, Reflect)]
79#[reflect(from_reflect = false)]
80pub struct ZedDepthToPointCloud<const MAX_POINTS: usize> {
81 calibration: CuLatchedState<ZedCalibrationBundle>,
82}
83
84impl<const MAX_POINTS: usize> Freezable for ZedDepthToPointCloud<MAX_POINTS> {}
85
86impl<const MAX_POINTS: usize> CuTask for ZedDepthToPointCloud<MAX_POINTS> {
87 type Resources<'r> = ();
88 type Input<'m> = input_msg!('m, ZedDepthMap, CuLatchedStateUpdate<ZedCalibrationBundle>);
89 type Output<'m> = output_msg!(PointCloudSoa<MAX_POINTS>);
90
91 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
92 where
93 Self: Sized,
94 {
95 Ok(Self::default())
96 }
97
98 fn process(
99 &mut self,
100 _ctx: &CuContext,
101 input: &Self::Input<'_>,
102 output: &mut Self::Output<'_>,
103 ) -> CuResult<()> {
104 let (depth_msg, calibration_msg) = *input;
105 self.apply_calibration_update(calibration_msg.payload());
106
107 let Some(depth) = depth_msg.payload() else {
108 output.clear_payload();
109 output.tov = depth_msg.tov;
110 output.metadata.set_status("no depth");
111 return Ok(());
112 };
113
114 let Some(calibration) = self.calibration.as_ref() else {
115 output.clear_payload();
116 output.tov = depth_msg.tov;
117 output.metadata.set_status("no calib");
118 return Ok(());
119 };
120
121 let projection = ProjectionIntrinsics::from_bundle(depth.format, calibration)?;
122 let point_tov = representative_tov(depth_msg.tov);
123 let projected_points = {
124 let pointcloud = output.payload_mut().get_or_insert_with(Default::default);
125 pointcloud.len = 0;
126
127 depth.with_samples(|samples, format| {
128 for row in 0..format.height as usize {
129 let row_offset = row * format.stride as usize;
130 for col in 0..format.width as usize {
131 let Some(depth_value) =
132 ZedDepthMap::decode_sample(samples[row_offset + col])
133 else {
134 continue;
135 };
136
137 if pointcloud.len == MAX_POINTS {
138 return Err(CuError::from(format!(
139 "ZED point cloud capacity {MAX_POINTS} exceeded while projecting {}x{} depth map",
140 format.width, format.height
141 )));
142 }
143
144 let (x, y, z) = projection.project(
145 col as f32,
146 row as f32,
147 depth_value.get::<meter>(),
148 )?;
149 let idx = pointcloud.len;
150 pointcloud.tov[idx] = point_tov;
151 pointcloud.x[idx] = Distance::new::<meter>(x);
152 pointcloud.y[idx] = Distance::new::<meter>(y);
153 pointcloud.z[idx] = Distance::new::<meter>(z);
154 pointcloud.i[idx] = Reflectivity::new::<percent>(0.0);
155 pointcloud.return_order[idx] = 0;
156 pointcloud.len += 1;
157 }
158 }
159
160 Ok(pointcloud.len)
161 })?
162 };
163
164 output.tov = depth_msg.tov;
165 output.metadata.set_status(projected_points);
166 Ok(())
167 }
168}
169
170impl<const MAX_POINTS: usize> ZedDepthToPointCloud<MAX_POINTS> {
171 fn apply_calibration_update(
172 &mut self,
173 update: Option<&CuLatchedStateUpdate<ZedCalibrationBundle>>,
174 ) {
175 match update {
176 Some(CuLatchedStateUpdate::Set(bundle)) => {
177 self.calibration = CuLatchedState::Set(bundle.clone());
178 }
179 Some(CuLatchedStateUpdate::Clear) => {
180 self.calibration = CuLatchedState::Unset;
181 }
182 Some(CuLatchedStateUpdate::NoChange) | None => {}
183 }
184 }
185}
186
187#[derive(Clone, Copy, Debug)]
188struct ProjectionIntrinsics {
189 fx: f32,
190 fy: f32,
191 cx: f32,
192 cy: f32,
193 y_sign: f32,
194}
195
196impl ProjectionIntrinsics {
197 fn from_bundle(format: CuDepthMapFormat, calibration: &ZedCalibrationBundle) -> CuResult<Self> {
198 if calibration.left.width == 0 || calibration.left.height == 0 {
199 return Err(CuError::from("ZED calibration reported a zero-sized image"));
200 }
201 if calibration.left.fx.abs() <= f32::EPSILON || calibration.left.fy.abs() <= f32::EPSILON {
202 return Err(CuError::from("ZED calibration reported zero focal length"));
203 }
204
205 let scale_x = format.width as f32 / calibration.left.width as f32;
206 let scale_y = format.height as f32 / calibration.left.height as f32;
207 let y_sign = match calibration.coordinate_system {
208 ZedCoordinateSystem::Image => 1.0,
209 ZedCoordinateSystem::LeftHandedYUp => -1.0,
210 other => {
211 return Err(CuError::from(format!(
212 "ZedDepthToPointCloud only supports IMAGE and LEFT_HANDED_Y_UP depth projections, got {other:?}"
213 )));
214 }
215 };
216
217 Ok(Self {
218 fx: calibration.left.fx * scale_x,
219 fy: calibration.left.fy * scale_y,
220 cx: calibration.left.cx * scale_x,
221 cy: calibration.left.cy * scale_y,
222 y_sign,
223 })
224 }
225
226 fn project(&self, pixel_x: f32, pixel_y: f32, depth_m: f32) -> CuResult<(f32, f32, f32)> {
227 let z = depth_m;
228 let x = (pixel_x - self.cx) * z / self.fx;
229 let y = self.y_sign * (pixel_y - self.cy) * z / self.fy;
230 if !x.is_finite() || !y.is_finite() || !z.is_finite() {
231 return Err(CuError::from(
232 "ZED depth projection produced a non-finite point",
233 ));
234 }
235 Ok((x, y, z))
236 }
237}
238
239fn representative_tov(tov: Tov) -> CuTime {
240 match tov {
241 Tov::Time(time) => time,
242 Tov::Range(range) => range.start,
243 Tov::None => CuTime::default(),
244 }
245}
246
247#[cfg(not(target_os = "linux"))]
248mod empty_impl {
249 use super::*;
250
251 #[derive(Reflect)]
252 #[reflect(from_reflect = false)]
253 pub struct Zed;
254
255 impl Freezable for Zed {}
256
257 impl CuSrcTask for Zed {
258 type Resources<'r> = ();
259 type Output<'m> = ZedSourceOutputs;
260
261 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
262 where
263 Self: Sized,
264 {
265 Ok(Self)
266 }
267
268 fn process(&mut self, _ctx: &CuContext, output: &mut Self::Output<'_>) -> CuResult<()> {
269 let (stereo, depth, confidence, calib, transforms, imu, mag, baro, meta) = output;
270 stereo.clear_payload();
271 depth.clear_payload();
272 confidence.clear_payload();
273 calib.set_payload(CuLatchedStateUpdate::NoChange);
274 transforms.set_payload(CuLatchedStateUpdate::NoChange);
275 imu.clear_payload();
276 mag.clear_payload();
277 baro.clear_payload();
278 meta.clear_payload();
279 Ok(())
280 }
281 }
282}
283
284#[cfg(not(target_os = "linux"))]
285pub use empty_impl::Zed;
286
287#[cfg(target_os = "linux")]
288mod linux_impl {
289 use super::*;
290 use core::mem::size_of;
291 use cu_sensor_payloads::CuImageBufferFormat;
292 use cu_transform::FrameTransform;
293 use std::path::PathBuf;
294 use zed_sdk::{
295 CalibrationParameters, Camera, CameraImuTransform, CameraInformation, CameraParameters,
296 CoordinateSystem, DepthMode, ErrorCode, InputSource, OpenOptions, ReferenceFrame,
297 Resolution, ResolutionPreset, RuntimeParameters, SensorsConfiguration, SensorsData, Unit,
298 };
299 use zed_sdk::{Mat, Rgba8};
300
301 struct ImageSlot {
302 handle: CuHandle<Vec<u8>>,
303 mat: Mat<Rgba8>,
304 }
305
306 struct DepthSlot {
307 handle: CuHandle<Vec<u16>>,
308 mat: Mat<u16>,
309 }
310
311 struct RasterSlot {
312 handle: CuHandle<Vec<f32>>,
313 mat: Mat<f32>,
314 }
315
316 struct OutputSlot {
317 left: ImageSlot,
318 right: ImageSlot,
319 depth: DepthSlot,
320 confidence: Option<RasterSlot>,
321 }
322
323 impl OutputSlot {
324 fn is_available(&self) -> bool {
325 handle_is_available(&self.left.handle)
326 && handle_is_available(&self.right.handle)
327 && handle_is_available(&self.depth.handle)
328 && self
329 .confidence
330 .as_ref()
331 .is_none_or(|slot| handle_is_available(&slot.handle))
332 }
333 }
334
335 #[derive(Reflect)]
336 #[reflect(from_reflect = false)]
337 pub struct Zed {
338 #[reflect(ignore)]
339 camera: Camera,
340 #[reflect(ignore)]
341 runtime: RuntimeParameters,
342 #[reflect(ignore)]
343 slots: Vec<OutputSlot>,
344 next_slot: usize,
345 #[reflect(ignore)]
346 left_format: CuImageBufferFormat,
347 #[reflect(ignore)]
348 right_format: CuImageBufferFormat,
349 #[reflect(ignore)]
350 depth_format: CuDepthMapFormat,
351 #[reflect(ignore)]
352 confidence_format: Option<ZedRasterFormat>,
353 emit_confidence: bool,
354 emit_imu: bool,
355 emit_mag: bool,
356 emit_baro: bool,
357 seq: u64,
358 #[reflect(ignore)]
359 pending_calibration: Option<ZedCalibrationBundle>,
360 #[reflect(ignore)]
361 pending_transforms: Option<ZedRigTransforms>,
362 }
363
364 impl Freezable for Zed {}
365
366 unsafe impl Send for Zed {}
370 unsafe impl Sync for Zed {}
373
374 impl CuSrcTask for Zed {
375 type Resources<'r> = ();
376 type Output<'m> = ZedSourceOutputs;
377
378 fn new(config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
379 where
380 Self: Sized,
381 {
382 let coordinate_system = config_zed_coordinate_system(config)?;
383 let coordinate_unit = config_zed_coordinate_unit(config)?;
384 let open = build_open_options(config, coordinate_system, coordinate_unit)?;
385 let runtime = build_runtime_parameters(config)?;
386 let emit_confidence = config_bool(config, "emit_confidence", false)?;
387 let emit_imu = config_bool(config, "emit_imu", true)?;
388 let emit_mag = config_bool(config, "emit_mag", true)?;
389 let emit_baro = config_bool(config, "emit_baro", true)?;
390 let pool_slots = config_u32(config, "pool_slots", 4)?.max(1) as usize;
391 let frame_prefix =
392 config_string_opt(config, "frame_id_prefix")?.unwrap_or_else(|| "zed".to_string());
393
394 let camera = Camera::open(open)
395 .map_err(|e| CuError::new_with_cause("Could not open ZED camera", e))?;
396 let info = camera.info().map_err(|e| {
397 CuError::new_with_cause("Could not fetch ZED camera information", e)
398 })?;
399 let calibration = camera.calibration_parameters(false).map_err(|e| {
400 CuError::new_with_cause("Could not fetch ZED calibration parameters", e)
401 })?;
402 let sensors_configuration = camera.sensors_configuration().ok();
403 let camera_imu = sensors_configuration
404 .as_ref()
405 .filter(|config| has_motion_sensors(config))
406 .map(|_| camera.camera_imu_transform())
407 .transpose()
408 .map_err(|e| {
409 CuError::new_with_cause("Could not fetch ZED camera->IMU transform", e)
410 })?;
411
412 let resolution = camera
413 .resolution()
414 .map_err(|e| CuError::new_with_cause("Could not read ZED camera resolution", e))?;
415
416 let left_format = rgba_format(resolution);
417 let right_format = rgba_format(resolution);
418 let depth_format = depth_map_format(resolution);
419 let confidence_format = emit_confidence.then(|| raster_format(resolution));
420 let slots = build_output_slots(
421 pool_slots,
422 resolution,
423 left_format,
424 right_format,
425 depth_format,
426 confidence_format,
427 )?;
428
429 let pending_calibration = Some(build_calibration_bundle(
430 &info,
431 &calibration,
432 camera_imu.as_ref(),
433 coordinate_system,
434 coordinate_unit,
435 ));
436 let pending_left_to_right = build_left_to_right_transform(&frame_prefix, &calibration);
437 let pending_camera_to_imu = camera_imu
438 .as_ref()
439 .map(|transform| build_camera_to_imu_transform(&frame_prefix, transform));
440 let pending_transforms = Some(ZedRigTransforms {
441 left_to_right: ZedNamedTransform::from_frame_transform(&pending_left_to_right),
442 camera_to_imu: pending_camera_to_imu
443 .as_ref()
444 .map(ZedNamedTransform::from_frame_transform)
445 .unwrap_or_default(),
446 has_camera_to_imu: pending_camera_to_imu.is_some(),
447 });
448
449 Ok(Self {
450 camera,
451 runtime,
452 slots,
453 next_slot: 0,
454 left_format,
455 right_format,
456 depth_format,
457 confidence_format,
458 emit_confidence,
459 emit_imu,
460 emit_mag,
461 emit_baro,
462 seq: 0,
463 pending_calibration,
464 pending_transforms,
465 })
466 }
467
468 fn process(&mut self, _ctx: &CuContext, output: &mut Self::Output<'_>) -> CuResult<()> {
469 let (stereo, depth, confidence, calibration, transforms, imu, mag, baro, meta) = output;
470
471 self.camera
472 .grab(&self.runtime)
473 .map_err(|e| CuError::new_with_cause("ZED grab failed", e))?;
474
475 let frame_tov: CuTime = self.camera.image_timestamp().into();
476 let seq = self.seq;
477 self.seq = self.seq.wrapping_add(1);
478 let slot_index = acquire_output_slot_index(&self.slots, &mut self.next_slot)
479 .ok_or_else(|| {
480 CuError::from(
481 "No reusable ZED output slot available; increase pool_slots or release downstream handles sooner",
482 )
483 })?;
484 let slot = &mut self.slots[slot_index];
485
486 self.camera
487 .retrieve_left(&mut slot.left.mat)
488 .map_err(|e| CuError::new_with_cause("Could not retrieve ZED left image", e))?;
489 self.camera
490 .retrieve_right(&mut slot.right.mat)
491 .map_err(|e| CuError::new_with_cause("Could not retrieve ZED right image", e))?;
492 self.camera
493 .retrieve_depth_u16_mm(&mut slot.depth.mat)
494 .map_err(|e| CuError::new_with_cause("Could not retrieve ZED depth map", e))?;
495
496 stereo.set_payload(ZedStereoImages {
497 left: image_payload_from_handle(seq, &slot.left.handle, self.left_format),
498 right: image_payload_from_handle(seq, &slot.right.handle, self.right_format),
499 });
500 stereo.tov = Tov::Time(frame_tov);
501
502 depth.set_payload(ZedDepthMap::from_integer(
503 self.depth_format,
504 slot.depth.handle.clone(),
505 ));
506 depth.tov = Tov::Time(frame_tov);
507
508 if self.emit_confidence {
509 let confidence_slot = slot.confidence.as_mut().ok_or_else(|| {
510 CuError::from("confidence output requested without a backing mat")
511 })?;
512 self.camera
513 .retrieve_confidence(&mut confidence_slot.mat)
514 .map_err(|e| {
515 CuError::new_with_cause("Could not retrieve ZED confidence map", e)
516 })?;
517 let confidence_format = self
518 .confidence_format
519 .ok_or_else(|| CuError::from("confidence output requested without a format"))?;
520 confidence.set_payload(raster_payload_from_handle(
521 seq,
522 &confidence_slot.handle,
523 confidence_format,
524 ZedConfidenceMap::new,
525 ));
526 confidence.tov = Tov::Time(frame_tov);
527 } else {
528 confidence.clear_payload();
529 }
530
531 emit_latched(calibration, frame_tov, self.pending_calibration.take());
532 emit_latched(transforms, frame_tov, self.pending_transforms.take());
533
534 let sensor_result = self.camera.sensors_data();
535 let sensors = match sensor_result {
536 Ok(sensors) => Some(sensors),
537 Err(err) if is_optional_sensor_error(&err) => None,
538 Err(err) => {
539 return Err(CuError::new_with_cause(
540 "Could not retrieve ZED sensors data",
541 err,
542 ));
543 }
544 };
545
546 if self.emit_imu {
547 if let Some(imu_data) = sensors.as_ref().and_then(|data| data.imu) {
548 let temperature_c = sensors
549 .as_ref()
550 .map(|data| data.temperature.imu_temp_c)
551 .unwrap_or_default();
552 imu.set_payload(ImuPayload::from_raw(
553 [
554 imu_data.linear_acceleration.x,
555 imu_data.linear_acceleration.y,
556 imu_data.linear_acceleration.z,
557 ],
558 [
559 imu_data.angular_velocity.x,
560 imu_data.angular_velocity.y,
561 imu_data.angular_velocity.z,
562 ],
563 temperature_c,
564 ));
565 imu.tov = Tov::Time(imu_data.timestamp_ns.into());
566 } else {
567 imu.clear_payload();
568 }
569 } else {
570 imu.clear_payload();
571 }
572
573 if self.emit_mag {
574 if let Some(mag_data) = sensors.as_ref().and_then(|data| data.magnetometer) {
575 mag.set_payload(MagnetometerPayload::from_raw([
576 mag_data.magnetic_field_ut.x,
577 mag_data.magnetic_field_ut.y,
578 mag_data.magnetic_field_ut.z,
579 ]));
580 mag.tov = Tov::Time(mag_data.timestamp_ns.into());
581 } else {
582 mag.clear_payload();
583 }
584 } else {
585 mag.clear_payload();
586 }
587
588 if self.emit_baro {
589 if let Some(baro_data) = sensors.as_ref().and_then(|data| data.barometer) {
590 let temperature_c = sensors
591 .as_ref()
592 .map(|data| data.temperature.barometer_temp_c)
593 .unwrap_or_default();
594 baro.set_payload(BarometerPayload::from_raw(
595 baro_data.pressure_pa,
596 temperature_c,
597 ));
598 baro.tov = Tov::Time(baro_data.timestamp_ns.into());
599 } else {
600 baro.clear_payload();
601 }
602 } else {
603 baro.clear_payload();
604 }
605
606 meta.set_payload(build_frame_meta(seq, &self.camera, sensors.as_ref()));
607 meta.tov = Tov::Time(frame_tov);
608
609 Ok(())
610 }
611 }
612
613 fn config_bool(config: Option<&ComponentConfig>, key: &str, default: bool) -> CuResult<bool> {
614 Ok(match config {
615 Some(config) => config.get::<bool>(key)?.unwrap_or(default),
616 None => default,
617 })
618 }
619
620 fn config_u32(config: Option<&ComponentConfig>, key: &str, default: u32) -> CuResult<u32> {
621 Ok(match config {
622 Some(config) => config.get::<u32>(key)?.unwrap_or(default),
623 None => default,
624 })
625 }
626
627 fn config_string_opt(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<String>> {
628 match config {
629 Some(config) => config.get::<String>(key).map_err(Into::into),
630 None => Ok(None),
631 }
632 }
633
634 fn build_open_options(
635 config: Option<&ComponentConfig>,
636 coordinate_system: ZedCoordinateSystem,
637 coordinate_unit: ZedCoordinateUnit,
638 ) -> CuResult<OpenOptions> {
639 let mut options = OpenOptions::default();
640
641 if let (Some(serial_number), Some(port)) = (
642 config_u32_opt(config, "gmsl_serial_number")?,
643 config_i32_opt(config, "gmsl_port")?,
644 ) {
645 options = options.source(InputSource::Gmsl {
646 serial_number,
647 port,
648 });
649 } else if let Some(serial_number) = config_u32_opt(config, "serial_number")? {
650 options = options.source(InputSource::SerialNumber(serial_number));
651 } else if let Some(svo_file) = config_string_opt(config, "svo_file")? {
652 options = options.source(InputSource::SvoFile(PathBuf::from(svo_file)));
653 } else if let Some(stream_ip) = config_string_opt(config, "stream_ip")? {
654 let stream_port = config_i32_opt(config, "stream_port")?.unwrap_or(30000);
655 options = options.source(InputSource::Stream {
656 ip: stream_ip,
657 port: stream_port,
658 });
659 } else {
660 let device_id = config_i32_opt(config, "device_id")?.unwrap_or(0);
661 options = options.camera_device_id(device_id);
662 }
663
664 if let Some(resolution) = config_string_opt(config, "resolution")? {
665 let parsed = resolution
666 .to_ascii_uppercase()
667 .parse::<ResolutionPreset>()
668 .map_err(|err| CuError::from(format!("Invalid ZED resolution preset: {err}")))?;
669 options = options.resolution(parsed);
670 }
671 if let Some(fps) = config_i32_opt(config, "fps")? {
672 options = options.fps(fps);
673 }
674 if let Some(depth_mode) = config_string_opt(config, "depth_mode")? {
675 let parsed = depth_mode
676 .to_ascii_uppercase()
677 .parse::<DepthMode>()
678 .map_err(|err| CuError::from(format!("Invalid ZED depth mode: {err}")))?;
679 options = options.depth_mode(parsed);
680 }
681 options = options.coordinate_system(to_sdk_coordinate_system(coordinate_system));
682 options = options.coordinate_unit(to_sdk_coordinate_unit(coordinate_unit));
683
684 let depth_min = config_f32_opt(config, "depth_minimum_distance_m")?;
685 let depth_max = config_f32_opt(config, "depth_maximum_distance_m")?.unwrap_or(40.0);
686 options = options.depth_range_m(depth_min, depth_max);
687
688 if let Some(timeout_ms) = config_u32_opt(config, "open_timeout_ms")? {
689 options = options.open_timeout(core::time::Duration::from_millis(timeout_ms as u64));
690 }
691 if let Some(sensors_required) = config_bool_opt(config, "sensors_required")? {
692 options = options.sensors_required(sensors_required);
693 }
694 if let Some(verbose) = config_i32_opt(config, "sdk_verbose")? {
695 options = options.sdk_verbose(verbose);
696 }
697 if let Some(settings_path) = config_string_opt(config, "settings_path")? {
698 options = options.settings_path(settings_path);
699 }
700 if let Some(calibration_path) = config_string_opt(config, "opencv_calibration_path")? {
701 options = options.opencv_calibration_path(calibration_path);
702 }
703
704 Ok(options)
705 }
706
707 fn config_zed_coordinate_system(
708 config: Option<&ComponentConfig>,
709 ) -> CuResult<ZedCoordinateSystem> {
710 match config_string_opt(config, "coordinate_system")? {
711 Some(value) => parse_zed_coordinate_system(&value),
712 None => Ok(ZedCoordinateSystem::default()),
713 }
714 }
715
716 fn config_zed_coordinate_unit(config: Option<&ComponentConfig>) -> CuResult<ZedCoordinateUnit> {
717 match config_string_opt(config, "coordinate_unit")? {
718 Some(value) => parse_zed_coordinate_unit(&value),
719 None => Ok(ZedCoordinateUnit::default()),
720 }
721 }
722
723 fn build_runtime_parameters(config: Option<&ComponentConfig>) -> CuResult<RuntimeParameters> {
724 let mut runtime = RuntimeParameters::default();
725 if let Some(reference_frame) = config_string_opt(config, "reference_frame")? {
726 let parsed = parse_reference_frame(&reference_frame)?;
727 runtime = runtime.reference_frame(parsed);
728 }
729 if let Some(enable_fill_mode) = config_bool_opt(config, "enable_fill_mode")? {
730 runtime = runtime.enable_fill_mode(enable_fill_mode);
731 }
732 if let Some(confidence_threshold) = config_i32_opt(config, "confidence_threshold")? {
733 runtime = runtime.confidence_threshold(confidence_threshold);
734 }
735 if let Some(texture_confidence_threshold) =
736 config_i32_opt(config, "texture_confidence_threshold")?
737 {
738 runtime = runtime.texture_confidence_threshold(texture_confidence_threshold);
739 }
740 Ok(runtime)
741 }
742
743 fn config_bool_opt(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<bool>> {
744 match config {
745 Some(config) => config.get::<bool>(key).map_err(Into::into),
746 None => Ok(None),
747 }
748 }
749
750 fn config_u32_opt(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<u32>> {
751 match config {
752 Some(config) => config.get::<u32>(key).map_err(Into::into),
753 None => Ok(None),
754 }
755 }
756
757 fn config_i32_opt(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<i32>> {
758 match config {
759 Some(config) => config.get::<i32>(key).map_err(Into::into),
760 None => Ok(None),
761 }
762 }
763
764 fn config_f32_opt(config: Option<&ComponentConfig>, key: &str) -> CuResult<Option<f32>> {
765 match config {
766 Some(config) => config
767 .get::<f64>(key)
768 .map(|value| value.map(|value| value as f32))
769 .map_err(Into::into),
770 None => Ok(None),
771 }
772 }
773
774 fn parse_zed_coordinate_system(value: &str) -> CuResult<ZedCoordinateSystem> {
775 match value.to_ascii_uppercase().as_str() {
776 "IMAGE" => Ok(ZedCoordinateSystem::Image),
777 "LEFT_HANDED_Y_UP" => Ok(ZedCoordinateSystem::LeftHandedYUp),
778 "RIGHT_HANDED_Y_UP" => Ok(ZedCoordinateSystem::RightHandedYUp),
779 "RIGHT_HANDED_Z_UP" => Ok(ZedCoordinateSystem::RightHandedZUp),
780 "LEFT_HANDED_Z_UP" => Ok(ZedCoordinateSystem::LeftHandedZUp),
781 "RIGHT_HANDED_Z_UP_X_FWD" | "RIGHT_HANDED_Z_UP_X_FORWARD" => {
782 Ok(ZedCoordinateSystem::RightHandedZUpXForward)
783 }
784 _ => Err(CuError::from(format!(
785 "Invalid ZED coordinate_system: {value}"
786 ))),
787 }
788 }
789
790 fn parse_zed_coordinate_unit(value: &str) -> CuResult<ZedCoordinateUnit> {
791 match value.to_ascii_uppercase().as_str() {
792 "MILLIMETER" | "MILLIMETERS" => Ok(ZedCoordinateUnit::Millimeter),
793 "CENTIMETER" | "CENTIMETERS" => Ok(ZedCoordinateUnit::Centimeter),
794 "METER" | "METERS" => Ok(ZedCoordinateUnit::Meter),
795 "INCH" | "INCHES" => Ok(ZedCoordinateUnit::Inch),
796 "FOOT" | "FEET" => Ok(ZedCoordinateUnit::Foot),
797 _ => Err(CuError::from(format!(
798 "Invalid ZED coordinate_unit: {value}"
799 ))),
800 }
801 }
802
803 fn to_sdk_coordinate_system(value: ZedCoordinateSystem) -> CoordinateSystem {
804 match value {
805 ZedCoordinateSystem::Image => CoordinateSystem::Image,
806 ZedCoordinateSystem::LeftHandedYUp => CoordinateSystem::LeftHandedYUp,
807 ZedCoordinateSystem::RightHandedYUp => CoordinateSystem::RightHandedYUp,
808 ZedCoordinateSystem::RightHandedZUp => CoordinateSystem::RightHandedZUp,
809 ZedCoordinateSystem::LeftHandedZUp => CoordinateSystem::LeftHandedZUp,
810 ZedCoordinateSystem::RightHandedZUpXForward => CoordinateSystem::RightHandedZUpXForward,
811 }
812 }
813
814 fn to_sdk_coordinate_unit(value: ZedCoordinateUnit) -> Unit {
815 match value {
816 ZedCoordinateUnit::Millimeter => Unit::Millimeter,
817 ZedCoordinateUnit::Centimeter => Unit::Centimeter,
818 ZedCoordinateUnit::Meter => Unit::Meter,
819 ZedCoordinateUnit::Inch => Unit::Inch,
820 ZedCoordinateUnit::Foot => Unit::Foot,
821 }
822 }
823
824 fn parse_reference_frame(value: &str) -> CuResult<ReferenceFrame> {
825 match value.to_ascii_uppercase().as_str() {
826 "WORLD" => Ok(ReferenceFrame::World),
827 "CAMERA" => Ok(ReferenceFrame::Camera),
828 _ => Err(CuError::from(format!(
829 "Invalid ZED reference_frame: {value}"
830 ))),
831 }
832 }
833
834 fn has_motion_sensors(config: &SensorsConfiguration) -> bool {
835 config.accelerometer.is_available || config.gyroscope.is_available
836 }
837
838 fn rgba_format(resolution: Resolution) -> CuImageBufferFormat {
839 CuImageBufferFormat {
840 width: resolution.width(),
841 height: resolution.height(),
842 stride: (resolution.width() as usize * size_of::<Rgba8>()) as u32,
843 pixel_format: *b"RGBA",
844 }
845 }
846
847 fn raster_format(resolution: Resolution) -> ZedRasterFormat {
848 ZedRasterFormat {
849 width: resolution.width(),
850 height: resolution.height(),
851 stride: resolution.width(),
852 }
853 }
854
855 fn depth_map_format(resolution: Resolution) -> CuDepthMapFormat {
856 CuDepthMapFormat {
857 width: resolution.width(),
858 height: resolution.height(),
859 stride: resolution.width(),
860 }
861 }
862
863 fn build_output_slots(
864 slot_count: usize,
865 resolution: Resolution,
866 left_format: CuImageBufferFormat,
867 right_format: CuImageBufferFormat,
868 depth_format: CuDepthMapFormat,
869 confidence_format: Option<ZedRasterFormat>,
870 ) -> CuResult<Vec<OutputSlot>> {
871 (0..slot_count)
872 .map(|_| {
873 Ok(OutputSlot {
874 left: build_image_slot(resolution, left_format)?,
875 right: build_image_slot(resolution, right_format)?,
876 depth: build_depth_slot(resolution, depth_format)?,
877 confidence: confidence_format
878 .map(|format| build_raster_slot(resolution, format))
879 .transpose()?,
880 })
881 })
882 .collect()
883 }
884
885 fn build_image_slot(
886 resolution: Resolution,
887 format: CuImageBufferFormat,
888 ) -> CuResult<ImageSlot> {
889 let handle = CuHandle::new_detached(vec![0u8; format.byte_size()]);
890 let stride_bytes = format.stride as usize;
891 let stride_elems = stride_bytes / size_of::<Rgba8>();
892 if stride_elems * size_of::<Rgba8>() != stride_bytes {
893 return Err(CuError::from(
894 "ZED image stride is not aligned to the RGBA pixel size",
895 ));
896 }
897 let (ptr, len_bytes) = handle.with_inner_mut(|inner| (inner.as_mut_ptr(), inner.len()));
898 if len_bytes % size_of::<Rgba8>() != 0 {
899 return Err(CuError::from(
900 "ZED image buffer length is not aligned to the RGBA pixel size",
901 ));
902 }
903
904 let mat = unsafe {
905 Mat::from_external_cpu_buffer(
906 resolution,
907 stride_elems,
908 len_bytes / size_of::<Rgba8>(),
909 ptr.cast::<Rgba8>(),
910 )
911 }
912 .map_err(|e| CuError::new_with_cause("Could not alias ZED image buffer as sl::Mat", e))?;
913
914 Ok(ImageSlot { handle, mat })
915 }
916
917 fn build_raster_slot(resolution: Resolution, format: ZedRasterFormat) -> CuResult<RasterSlot> {
918 let handle = CuHandle::new_detached(vec![0f32; format.len_elements()]);
919 let (ptr, len_elements) = handle.with_inner_mut(|inner| (inner.as_mut_ptr(), inner.len()));
920 let mat = unsafe {
921 Mat::from_external_cpu_buffer(resolution, format.stride as usize, len_elements, ptr)
922 }
923 .map_err(|e| CuError::new_with_cause("Could not alias ZED raster buffer as sl::Mat", e))?;
924
925 Ok(RasterSlot { handle, mat })
926 }
927
928 fn build_depth_slot(resolution: Resolution, format: CuDepthMapFormat) -> CuResult<DepthSlot> {
929 let handle = CuHandle::new_detached(vec![0u16; format.required_elements()]);
930 let (ptr, len_elements) = handle.with_inner_mut(|inner| (inner.as_mut_ptr(), inner.len()));
931 let mat = unsafe {
932 Mat::from_external_cpu_buffer(resolution, format.stride as usize, len_elements, ptr)
933 }
934 .map_err(|e| CuError::new_with_cause("Could not alias ZED depth buffer as sl::Mat", e))?;
935
936 Ok(DepthSlot { handle, mat })
937 }
938
939 fn handle_is_available<T>(handle: &CuHandle<T>) -> bool
940 where
941 T: ArrayLike,
942 {
943 handle.is_unique()
944 }
945
946 fn acquire_output_slot_index(slots: &[OutputSlot], next_slot: &mut usize) -> Option<usize> {
947 if slots.is_empty() {
948 return None;
949 }
950
951 for offset in 0..slots.len() {
952 let index = (*next_slot + offset) % slots.len();
953 if slots[index].is_available() {
954 *next_slot = (index + 1) % slots.len();
955 return Some(index);
956 }
957 }
958
959 None
960 }
961
962 fn image_payload_from_handle(
963 seq: u64,
964 handle: &CuHandle<Vec<u8>>,
965 format: CuImageBufferFormat,
966 ) -> CuImage<Vec<u8>> {
967 let mut image = CuImage::new(format, handle.clone());
968 image.seq = seq;
969 image
970 }
971
972 fn raster_payload_from_handle<P>(
973 seq: u64,
974 handle: &CuHandle<Vec<f32>>,
975 format: ZedRasterFormat,
976 ctor: impl FnOnce(ZedRasterFormat, CuHandle<Vec<f32>>) -> P,
977 ) -> P
978 where
979 P: RasterSeq,
980 {
981 let mut payload = ctor(format, handle.clone());
982 payload.set_seq(seq);
983 payload
984 }
985
986 trait RasterSeq {
987 fn set_seq(&mut self, seq: u64);
988 }
989
990 impl RasterSeq for ZedConfidenceMap<Vec<f32>> {
991 fn set_seq(&mut self, seq: u64) {
992 self.seq = seq;
993 }
994 }
995
996 fn emit_latched<T: CuMsgPayload>(
997 msg: &mut CuMsg<CuLatchedStateUpdate<T>>,
998 tov: CuTime,
999 pending: Option<T>,
1000 ) {
1001 msg.tov = Tov::Time(tov);
1002 match pending {
1003 Some(value) => msg.set_payload(CuLatchedStateUpdate::Set(value)),
1004 None => msg.set_payload(CuLatchedStateUpdate::NoChange),
1005 }
1006 }
1007
1008 fn build_calibration_bundle(
1009 info: &CameraInformation,
1010 calibration: &CalibrationParameters,
1011 camera_imu: Option<&CameraImuTransform>,
1012 coordinate_system: ZedCoordinateSystem,
1013 coordinate_unit: ZedCoordinateUnit,
1014 ) -> ZedCalibrationBundle {
1015 ZedCalibrationBundle {
1016 serial_number: info.serial_number,
1017 width: info.resolution.width(),
1018 height: info.resolution.height(),
1019 fps: info.fps,
1020 coordinate_system,
1021 coordinate_unit,
1022 left: intrinsics_from_sdk(&calibration.left_cam),
1023 right: intrinsics_from_sdk(&calibration.right_cam),
1024 stereo_rotation_rodrigues: calibration.rotation_rodrigues,
1025 stereo_translation_m: [
1026 calibration.translation.x,
1027 calibration.translation.y,
1028 calibration.translation.z,
1029 ],
1030 camera_to_imu_translation_m: camera_imu.map(|transform| {
1031 [
1032 transform.translation.x,
1033 transform.translation.y,
1034 transform.translation.z,
1035 ]
1036 }),
1037 camera_to_imu_quaternion_xyzw: camera_imu.map(|transform| transform.rotation_xyzw),
1038 }
1039 }
1040
1041 fn intrinsics_from_sdk(parameters: &CameraParameters) -> ZedCameraIntrinsics {
1042 ZedCameraIntrinsics {
1043 fx: parameters.fx,
1044 fy: parameters.fy,
1045 cx: parameters.cx,
1046 cy: parameters.cy,
1047 disto: parameters.disto,
1048 v_fov: parameters.v_fov,
1049 h_fov: parameters.h_fov,
1050 d_fov: parameters.d_fov,
1051 width: parameters.image_size.width(),
1052 height: parameters.image_size.height(),
1053 focal_length_metric: parameters.focal_length_metric,
1054 }
1055 }
1056
1057 fn build_left_to_right_transform(
1058 frame_prefix: &str,
1059 calibration: &CalibrationParameters,
1060 ) -> FrameTransform<f32> {
1061 let transform = transform_from_rodrigues_translation(
1062 calibration.rotation_rodrigues,
1063 [
1064 calibration.translation.x,
1065 calibration.translation.y,
1066 calibration.translation.z,
1067 ],
1068 );
1069 FrameTransform::new(
1070 transform,
1071 left_frame_id(frame_prefix),
1072 right_frame_id(frame_prefix),
1073 )
1074 }
1075
1076 fn build_camera_to_imu_transform(
1077 frame_prefix: &str,
1078 camera_imu: &CameraImuTransform,
1079 ) -> FrameTransform<f32> {
1080 let transform = transform_from_quaternion_translation(
1081 camera_imu.rotation_xyzw,
1082 [
1083 camera_imu.translation.x,
1084 camera_imu.translation.y,
1085 camera_imu.translation.z,
1086 ],
1087 );
1088 FrameTransform::new(
1089 transform,
1090 left_frame_id(frame_prefix),
1091 imu_frame_id(frame_prefix),
1092 )
1093 }
1094
1095 fn left_frame_id(frame_prefix: &str) -> String {
1096 format!("{frame_prefix}_left")
1097 }
1098
1099 fn right_frame_id(frame_prefix: &str) -> String {
1100 format!("{frame_prefix}_right")
1101 }
1102
1103 fn imu_frame_id(frame_prefix: &str) -> String {
1104 format!("{frame_prefix}_imu")
1105 }
1106
1107 fn build_frame_meta(seq: u64, camera: &Camera, sensors: Option<&SensorsData>) -> ZedFrameMeta {
1108 let temps = sensors.map(|data| data.temperature);
1109 ZedFrameMeta {
1110 seq,
1111 image_timestamp_ns: camera.image_timestamp(),
1112 current_timestamp_ns: camera.current_timestamp(),
1113 current_fps: camera.current_fps(),
1114 camera_moving_state: sensors.map(|data| data.camera_moving_state),
1115 image_sync_trigger: sensors.map(|data| data.image_sync_trigger),
1116 imu_temp_c: temps.map(|data| data.imu_temp_c),
1117 barometer_temp_c: temps.map(|data| data.barometer_temp_c),
1118 onboard_left_temp_c: temps.map(|data| data.onboard_left_temp_c),
1119 onboard_right_temp_c: temps.map(|data| data.onboard_right_temp_c),
1120 }
1121 }
1122
1123 fn is_optional_sensor_error(err: &zed_sdk::Error) -> bool {
1124 matches!(
1125 err.sdk_code(),
1126 Some(
1127 ErrorCode::SensorsDataRequired
1128 | ErrorCode::SensorsNotAvailable
1129 | ErrorCode::SensorsNotInitialized
1130 | ErrorCode::MotionSensorsRequired
1131 )
1132 )
1133 }
1134
1135 fn transform_from_rodrigues_translation(
1136 rotation_rodrigues: [f32; 3],
1137 translation: [f32; 3],
1138 ) -> cu_transform::Transform3D<f32> {
1139 let theta = (rotation_rodrigues[0] * rotation_rodrigues[0]
1140 + rotation_rodrigues[1] * rotation_rodrigues[1]
1141 + rotation_rodrigues[2] * rotation_rodrigues[2])
1142 .sqrt();
1143
1144 let mut matrix = [
1145 [1.0f32, 0.0, 0.0, translation[0]],
1146 [0.0, 1.0, 0.0, translation[1]],
1147 [0.0, 0.0, 1.0, translation[2]],
1148 [0.0, 0.0, 0.0, 1.0],
1149 ];
1150
1151 if theta > 1.0e-6 {
1152 let x = rotation_rodrigues[0] / theta;
1153 let y = rotation_rodrigues[1] / theta;
1154 let z = rotation_rodrigues[2] / theta;
1155 let cos_theta = theta.cos();
1156 let sin_theta = theta.sin();
1157 let one_minus_cos = 1.0 - cos_theta;
1158
1159 matrix[0][0] = cos_theta + x * x * one_minus_cos;
1160 matrix[0][1] = x * y * one_minus_cos - z * sin_theta;
1161 matrix[0][2] = x * z * one_minus_cos + y * sin_theta;
1162
1163 matrix[1][0] = y * x * one_minus_cos + z * sin_theta;
1164 matrix[1][1] = cos_theta + y * y * one_minus_cos;
1165 matrix[1][2] = y * z * one_minus_cos - x * sin_theta;
1166
1167 matrix[2][0] = z * x * one_minus_cos - y * sin_theta;
1168 matrix[2][1] = z * y * one_minus_cos + x * sin_theta;
1169 matrix[2][2] = cos_theta + z * z * one_minus_cos;
1170 }
1171
1172 cu_transform::Transform3D::from_matrix(matrix)
1173 }
1174
1175 fn transform_from_quaternion_translation(
1176 rotation_xyzw: [f32; 4],
1177 translation: [f32; 3],
1178 ) -> cu_transform::Transform3D<f32> {
1179 let [x, y, z, w] = rotation_xyzw;
1180 let norm = (x * x + y * y + z * z + w * w).sqrt();
1181 let (x, y, z, w) = if norm > 1.0e-6 {
1182 (x / norm, y / norm, z / norm, w / norm)
1183 } else {
1184 (0.0, 0.0, 0.0, 1.0)
1185 };
1186
1187 cu_transform::Transform3D::from_matrix([
1188 [
1189 1.0 - 2.0 * (y * y + z * z),
1190 2.0 * (x * y - z * w),
1191 2.0 * (x * z + y * w),
1192 translation[0],
1193 ],
1194 [
1195 2.0 * (x * y + z * w),
1196 1.0 - 2.0 * (x * x + z * z),
1197 2.0 * (y * z - x * w),
1198 translation[1],
1199 ],
1200 [
1201 2.0 * (x * z - y * w),
1202 2.0 * (y * z + x * w),
1203 1.0 - 2.0 * (x * x + y * y),
1204 translation[2],
1205 ],
1206 [0.0, 0.0, 0.0, 1.0],
1207 ])
1208 }
1209}
1210
1211#[cfg(target_os = "linux")]
1212pub use linux_impl::Zed;
1213
1214#[cfg(test)]
1215mod tests {
1216 use super::*;
1217
1218 fn calibration_bundle(
1219 coordinate_system: ZedCoordinateSystem,
1220 width: u32,
1221 height: u32,
1222 ) -> ZedCalibrationBundle {
1223 ZedCalibrationBundle {
1224 serial_number: 1,
1225 width,
1226 height,
1227 fps: 30.0,
1228 coordinate_system,
1229 coordinate_unit: ZedCoordinateUnit::Meter,
1230 left: ZedCameraIntrinsics {
1231 fx: width as f32,
1232 fy: height as f32,
1233 cx: width as f32 / 2.0,
1234 cy: height as f32 / 2.0,
1235 width,
1236 height,
1237 ..Default::default()
1238 },
1239 right: ZedCameraIntrinsics {
1240 width,
1241 height,
1242 ..Default::default()
1243 },
1244 ..Default::default()
1245 }
1246 }
1247
1248 fn depth_msg(width: u32, height: u32, values: Vec<u16>) -> CuMsg<ZedDepthMap> {
1249 let mut msg = CuMsg::new(Some(ZedDepthMap::from_integer(
1250 CuDepthMapFormat {
1251 width,
1252 height,
1253 stride: width,
1254 },
1255 CuHandle::new_detached(values),
1256 )));
1257 msg.tov = Tov::from(CuDuration(42));
1258 msg
1259 }
1260
1261 #[test]
1262 fn depth_to_pointcloud_projects_image_coordinates() {
1263 let ctx = CuContext::new_with_clock();
1264 let mut task = ZedDepthToPointCloud::<4>::new(None, ()).expect("task");
1265 let depth = depth_msg(2, 2, vec![2_000, 2_000, 2_000, 2_000]);
1266 let calibration = CuMsg::new(Some(CuLatchedStateUpdate::Set(calibration_bundle(
1267 ZedCoordinateSystem::Image,
1268 2,
1269 2,
1270 ))));
1271 let input = (&depth, &calibration);
1272 let mut output: <ZedDepthToPointCloud<4> as CuTask>::Output<'_> = Default::default();
1273
1274 task.process(&ctx, &input, &mut output).expect("process");
1275
1276 let payload = output.payload().expect("payload");
1277 assert_eq!(payload.len, 4);
1278 assert_eq!(payload.x[0].value, -1.0);
1279 assert_eq!(payload.y[0].value, -1.0);
1280 assert_eq!(payload.z[0].value, 2.0);
1281 assert_eq!(payload.x[3].value, 0.0);
1282 assert_eq!(payload.y[3].value, 0.0);
1283 assert_eq!(payload.z[3].value, 2.0);
1284 }
1285
1286 #[test]
1287 fn depth_to_pointcloud_flips_y_for_left_handed_y_up() {
1288 let ctx = CuContext::new_with_clock();
1289 let mut task = ZedDepthToPointCloud::<4>::new(None, ()).expect("task");
1290 let depth = depth_msg(2, 2, vec![1_000, 1_000, 1_000, 1_000]);
1291 let calibration = CuMsg::new(Some(CuLatchedStateUpdate::Set(calibration_bundle(
1292 ZedCoordinateSystem::LeftHandedYUp,
1293 2,
1294 2,
1295 ))));
1296 let input = (&depth, &calibration);
1297 let mut output: <ZedDepthToPointCloud<4> as CuTask>::Output<'_> = Default::default();
1298
1299 task.process(&ctx, &input, &mut output).expect("process");
1300
1301 let payload = output.payload().expect("payload");
1302 assert_eq!(payload.len, 4);
1303 assert_eq!(payload.y[0].value, 0.5);
1304 assert_eq!(payload.y[2].value, -0.0);
1305 }
1306
1307 #[test]
1308 fn depth_to_pointcloud_scales_intrinsics_to_raster_size() {
1309 let ctx = CuContext::new_with_clock();
1310 let mut task = ZedDepthToPointCloud::<4>::new(None, ()).expect("task");
1311 let depth = depth_msg(2, 2, vec![4_000, 4_000, 4_000, 4_000]);
1312 let calibration = CuMsg::new(Some(CuLatchedStateUpdate::Set(calibration_bundle(
1313 ZedCoordinateSystem::Image,
1314 4,
1315 4,
1316 ))));
1317 let input = (&depth, &calibration);
1318 let mut output: <ZedDepthToPointCloud<4> as CuTask>::Output<'_> = Default::default();
1319
1320 task.process(&ctx, &input, &mut output).expect("process");
1321
1322 let payload = output.payload().expect("payload");
1323 assert_eq!(payload.len, 4);
1324 assert_eq!(payload.x[3].value, 0.0);
1325 assert_eq!(payload.y[3].value, 0.0);
1326 }
1327
1328 #[test]
1329 fn depth_to_pointcloud_skips_invalid_depth_samples() {
1330 let ctx = CuContext::new_with_clock();
1331 let mut task = ZedDepthToPointCloud::<4>::new(None, ()).expect("task");
1332 let depth = depth_msg(2, 2, vec![1_000, 0, 0, 0]);
1333 let calibration = CuMsg::new(Some(CuLatchedStateUpdate::Set(calibration_bundle(
1334 ZedCoordinateSystem::Image,
1335 2,
1336 2,
1337 ))));
1338 let input = (&depth, &calibration);
1339 let mut output: <ZedDepthToPointCloud<4> as CuTask>::Output<'_> = Default::default();
1340
1341 task.process(&ctx, &input, &mut output).expect("process");
1342
1343 let payload = output.payload().expect("payload");
1344 assert_eq!(payload.len, 1);
1345 assert_eq!(payload.z[0].value, 1.0);
1346 }
1347}