Skip to main content

cu_zed/
lib.rs

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
66/// Standard dense point cloud capacity for the default ZED `HD720` resolution.
67pub type ZedPointCloudHd720 = PointCloudSoa<{ 1280 * 720 }>;
68
69/// Pure projection task sized for the default ZED `HD720` resolution.
70pub type ZedDepthToPointCloudHd720 = ZedDepthToPointCloud<{ 1280 * 720 }>;
71
72/// Projects a `ZedDepthMap` into a standard `PointCloudSoa`.
73///
74/// The task consumes the latest latched calibration bundle and currently supports
75/// `IMAGE` and `LEFT_HANDED_Y_UP` ZED coordinate systems. Use a capacity large
76/// enough for the expected number of valid depth samples. The `Hd720` alias
77/// matches the source crate's default resolution.
78#[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    // SAFETY: `Zed` owns opaque SDK handles and Copper drives each task instance through
367    // exclusive `&mut self` process calls. We never expose aliased mutable access to the
368    // underlying SDK resources through this wrapper.
369    unsafe impl Send for Zed {}
370    // SAFETY: shared references do not permit mutation of the owned SDK handles through this
371    // wrapper. Copper requires `Reflect` tasks to be `Sync`, so the task advertises that bound.
372    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}