epoint_core/
point_cloud.rs

1use crate::error::Error;
2use chrono::{DateTime, Utc};
3use std::collections::{HashMap, HashSet};
4
5use crate::{PointCloudInfo, PointDataColumnType, PointDataColumns};
6
7use ecoord::{FrameId, ReferenceFrames};
8use nalgebra;
9use nalgebra::Point3;
10
11use polars::prelude::DataFrame;
12
13use crate::Error::{
14    MultipleFrameIdDefinitions, NoFrameIdDefinition, NoFrameIdDefinitions, NoIdColumn,
15};
16use crate::point_data::PointData;
17use polars::prelude::*;
18
19#[derive(Debug, Clone, PartialEq)]
20pub struct PointCloud {
21    pub point_data: PointData,
22    pub info: PointCloudInfo,
23    pub reference_frames: ReferenceFrames,
24}
25
26impl PointCloud {
27    pub fn new(
28        point_data: PointDataColumns,
29        info: PointCloudInfo,
30        reference_frames: ReferenceFrames,
31    ) -> Result<Self, Error> {
32        let point_data = point_data.get_as_data_frame();
33        if point_data
34            .column(PointDataColumnType::FrameId.as_str())
35            .is_ok()
36            && info.frame_id.is_some()
37        {
38            return Err(MultipleFrameIdDefinitions);
39        }
40
41        let point_data_new = PointData::new(point_data)?; // TODO: simplify
42
43        Ok(Self {
44            point_data: point_data_new,
45            info,
46            reference_frames,
47        })
48    }
49
50    pub fn from_data_frame(
51        point_data: DataFrame,
52        info: PointCloudInfo,
53        reference_frames: ReferenceFrames,
54    ) -> Result<Self, Error> {
55        if point_data
56            .column(PointDataColumnType::FrameId.as_str())
57            .is_ok()
58            && info.frame_id.is_some()
59        {
60            return Err(MultipleFrameIdDefinitions);
61        }
62        let point_data_new = PointData::new(point_data)?; // TODO: simplify
63
64        Ok(Self {
65            point_data: point_data_new,
66            info,
67            reference_frames,
68        })
69    }
70}
71
72impl PointCloud {
73    pub fn point_data(&self) -> &PointData {
74        &self.point_data
75    }
76
77    pub fn info(&self) -> &PointCloudInfo {
78        &self.info
79    }
80
81    pub fn reference_frames(&self) -> &ReferenceFrames {
82        &self.reference_frames
83    }
84
85    pub fn size(&self) -> usize {
86        self.point_data.height()
87    }
88
89    pub fn info_frame_id(&self) -> Option<&FrameId> {
90        self.info.frame_id.as_ref()
91    }
92
93    pub fn get_distinct_frame_ids(&self) -> Option<HashSet<FrameId>> {
94        if let Some(frame_id) = &self.info.frame_id {
95            let frame_ids = HashSet::from([frame_id.clone()]);
96            return Some(frame_ids);
97        }
98
99        if self.point_data.contains_frame_id_column() {
100            let frame_ids = self
101                .point_data
102                .get_distinct_frame_ids()
103                .expect("must be derivable");
104            return Some(frame_ids);
105        }
106
107        None
108    }
109}
110
111impl PointCloud {
112    pub fn contains_ids(&self) -> bool {
113        self.point_data.contains_id_column()
114    }
115
116    pub fn contains_frame_ids(&self) -> bool {
117        self.info.frame_id.is_some() || !self.point_data.contains_frame_id_column()
118    }
119    pub fn contains_timestamps(&self) -> bool {
120        self.point_data.contains_timestamps()
121    }
122
123    pub fn contains_sensor_translation(&self) -> bool {
124        self.point_data.contains_sensor_translation()
125    }
126
127    pub fn contains_colors(&self) -> bool {
128        self.point_data.contains_colors()
129    }
130
131    pub fn set_reference_frames(&mut self, reference_frames: ReferenceFrames) {
132        self.reference_frames = reference_frames;
133    }
134
135    pub fn set_info_frame_id(&mut self, frame_id: Option<FrameId>) {
136        self.info.frame_id = frame_id;
137    }
138
139    pub fn update_points(
140        &mut self,
141        points: Vec<Point3<f64>>,
142        frame_id: Option<FrameId>,
143    ) -> Result<(), Error> {
144        self.point_data.update_points_in_place(points)?;
145        self.info.frame_id = frame_id;
146
147        Ok(())
148    }
149
150    pub fn derive_spherical_points(&mut self) -> Result<(), Error> {
151        self.point_data.derive_spherical_points()?;
152
153        Ok(())
154    }
155
156    pub fn filter_by_id_range(
157        &self,
158        id_min: Option<u64>,
159        id_max: Option<u64>,
160    ) -> Result<PointCloud, Error> {
161        if !self.contains_ids() {
162            return Err(NoIdColumn);
163        }
164
165        let mut filter_predicate = col(PointDataColumnType::Id.as_str());
166        if let Some(id_min) = id_min {
167            filter_predicate = filter_predicate.gt_eq(lit(id_min));
168        }
169        if let Some(id_max) = id_max {
170            filter_predicate =
171                filter_predicate.and(col(PointDataColumnType::Id.as_str()).lt_eq(id_max));
172        }
173
174        let point_data = self
175            .point_data
176            .data_frame
177            .clone()
178            .lazy()
179            .filter(filter_predicate)
180            .collect()?;
181
182        let filtered_point_cloud = PointCloud::from_data_frame(
183            point_data,
184            self.info.clone(),
185            self.reference_frames.clone(),
186        )?;
187        Ok(filtered_point_cloud)
188    }
189
190    pub fn filter_by_frame_id(&self, frame_id: &FrameId) -> Result<PointCloud, Error> {
191        if !self
192            .get_distinct_frame_ids()
193            .ok_or(NoFrameIdDefinitions)?
194            .contains(frame_id)
195        {
196            return Err(NoFrameIdDefinition(frame_id.clone()));
197        }
198
199        let filter_predicate = col(PointDataColumnType::FrameId.as_str())
200            .cast(DataType::String)
201            .eq(lit(frame_id.clone().to_string().as_str()));
202
203        let point_data = self
204            .point_data
205            .data_frame
206            .clone()
207            .lazy()
208            .filter(filter_predicate)
209            .collect()?;
210
211        let filtered_point_cloud = PointCloud::from_data_frame(
212            point_data,
213            self.info.clone(),
214            self.reference_frames.clone(),
215        )?;
216        Ok(filtered_point_cloud)
217    }
218
219    pub fn filter_by_row_indices(&self, row_indices: HashSet<usize>) -> Result<PointCloud, Error> {
220        let filtered_point_data = self.point_data.filter_by_row_indices(row_indices)?;
221
222        let filtered_point_cloud = PointCloud::from_data_frame(
223            filtered_point_data.data_frame,
224            self.info.clone(),
225            self.reference_frames.clone(),
226        )?;
227        Ok(filtered_point_cloud)
228    }
229
230    pub fn filter_by_boolean_mask(&self, mask: &Vec<bool>) -> Result<PointCloud, Error> {
231        let mask_series: Series = mask.iter().collect();
232        let filtered_point_data = self
233            .point_data
234            .filter_by_boolean_mask(mask_series.bool()?)?;
235
236        let filtered_point_cloud = PointCloud::from_data_frame(
237            filtered_point_data.data_frame,
238            self.info.clone(),
239            self.reference_frames.clone(),
240        )?;
241        Ok(filtered_point_cloud)
242    }
243
244    pub fn filter_by_bounds(
245        &self,
246        bound_min: Point3<f64>,
247        bound_max: Point3<f64>,
248    ) -> Result<Option<PointCloud>, Error> {
249        let filtered_point_data = self.point_data.filter_by_bounds(bound_min, bound_max)?;
250
251        let result = if let Some(filtered_point_data) = filtered_point_data {
252            let filtered_point_cloud = PointCloud::from_data_frame(
253                filtered_point_data.data_frame,
254                self.info.clone(),
255                self.reference_frames.clone(),
256            )?;
257            Some(filtered_point_cloud)
258        } else {
259            None
260        };
261
262        Ok(result)
263    }
264
265    pub fn filter_by_beam_length(
266        &self,
267        beam_length_min: f64,
268        beam_length_max: f64,
269    ) -> Result<Option<PointCloud>, Error> {
270        let filtered_point_data = self
271            .point_data
272            .filter_by_beam_length(beam_length_min, beam_length_max)?;
273
274        let result = if let Some(filtered_point_data) = filtered_point_data {
275            let filtered_point_cloud = PointCloud::from_data_frame(
276                filtered_point_data.data_frame,
277                self.info.clone(),
278                self.reference_frames.clone(),
279            )?;
280            Some(filtered_point_cloud)
281        } else {
282            None
283        };
284
285        Ok(result)
286    }
287
288    pub fn filter_by_x_min(&self, x_min: f64) -> Result<Option<PointCloud>, Error> {
289        let filtered_point_data = self.point_data.filter_by_x_min(x_min)?;
290
291        let result = if let Some(filtered_point_data) = filtered_point_data {
292            let filtered_point_cloud = PointCloud::from_data_frame(
293                filtered_point_data.data_frame,
294                self.info.clone(),
295                self.reference_frames.clone(),
296            )?;
297            Some(filtered_point_cloud)
298        } else {
299            None
300        };
301
302        Ok(result)
303    }
304
305    pub fn filter_by_x_max(&self, x_max: f64) -> Result<Option<PointCloud>, Error> {
306        let filtered_point_data = self.point_data.filter_by_x_max(x_max)?;
307
308        let result = if let Some(filtered_point_data) = filtered_point_data {
309            let filtered_point_cloud = PointCloud::from_data_frame(
310                filtered_point_data.data_frame,
311                self.info.clone(),
312                self.reference_frames.clone(),
313            )?;
314            Some(filtered_point_cloud)
315        } else {
316            None
317        };
318
319        Ok(result)
320    }
321
322    pub fn filter_by_y_min(&self, y_min: f64) -> Result<Option<PointCloud>, Error> {
323        let filtered_point_data = self.point_data.filter_by_y_min(y_min)?;
324
325        let result = if let Some(filtered_point_data) = filtered_point_data {
326            let filtered_point_cloud = PointCloud::from_data_frame(
327                filtered_point_data.data_frame,
328                self.info.clone(),
329                self.reference_frames.clone(),
330            )?;
331            Some(filtered_point_cloud)
332        } else {
333            None
334        };
335
336        Ok(result)
337    }
338
339    pub fn filter_by_y_max(&self, y_max: f64) -> Result<Option<PointCloud>, Error> {
340        let filtered_point_data = self.point_data.filter_by_y_max(y_max)?;
341
342        let result = if let Some(filtered_point_data) = filtered_point_data {
343            let filtered_point_cloud = PointCloud::from_data_frame(
344                filtered_point_data.data_frame,
345                self.info.clone(),
346                self.reference_frames.clone(),
347            )?;
348            Some(filtered_point_cloud)
349        } else {
350            None
351        };
352
353        Ok(result)
354    }
355
356    pub fn filter_by_z_min(&self, z_min: f64) -> Result<Option<PointCloud>, Error> {
357        let filtered_point_data = self.point_data.filter_by_z_min(z_min)?;
358
359        let result = if let Some(filtered_point_data) = filtered_point_data {
360            let filtered_point_cloud = PointCloud::from_data_frame(
361                filtered_point_data.data_frame,
362                self.info.clone(),
363                self.reference_frames.clone(),
364            )?;
365            Some(filtered_point_cloud)
366        } else {
367            None
368        };
369
370        Ok(result)
371    }
372
373    pub fn filter_by_z_max(&self, z_max: f64) -> Result<Option<PointCloud>, Error> {
374        let filtered_point_data = self.point_data.filter_by_z_max(z_max)?;
375
376        let result = if let Some(filtered_point_data) = filtered_point_data {
377            let filtered_point_cloud = PointCloud::from_data_frame(
378                filtered_point_data.data_frame,
379                self.info.clone(),
380                self.reference_frames.clone(),
381            )?;
382            Some(filtered_point_cloud)
383        } else {
384            None
385        };
386
387        Ok(result)
388    }
389
390    pub fn filter_by_spherical_range_min(
391        &self,
392        spherical_range_min: f64,
393    ) -> Result<Option<PointCloud>, Error> {
394        let filtered_point_data = self
395            .point_data
396            .filter_by_spherical_range_min(spherical_range_min)?;
397
398        let result = if let Some(filtered_point_data) = filtered_point_data {
399            let filtered_point_cloud = PointCloud::from_data_frame(
400                filtered_point_data.data_frame,
401                self.info.clone(),
402                self.reference_frames.clone(),
403            )?;
404            Some(filtered_point_cloud)
405        } else {
406            None
407        };
408
409        Ok(result)
410    }
411
412    pub fn filter_by_spherical_range_max(
413        &self,
414        spherical_range_max: f64,
415    ) -> Result<Option<PointCloud>, Error> {
416        let filtered_point_data = self
417            .point_data
418            .filter_by_spherical_range_max(spherical_range_max)?;
419
420        let result = if let Some(filtered_point_data) = filtered_point_data {
421            let filtered_point_cloud = PointCloud::from_data_frame(
422                filtered_point_data.data_frame,
423                self.info.clone(),
424                self.reference_frames.clone(),
425            )?;
426            Some(filtered_point_cloud)
427        } else {
428            None
429        };
430
431        Ok(result)
432    }
433}
434
435impl PointCloud {
436    pub fn append_reference_frames(
437        &mut self,
438        reference_frames: ReferenceFrames,
439    ) -> Result<(), ecoord::Error> {
440        let merged_reference_frames =
441            ecoord::merge(&[self.reference_frames.clone(), reference_frames])?;
442        self.set_reference_frames(merged_reference_frames);
443        Ok(())
444    }
445
446    /// Resolves the frame-dependent and time-dependent points of the point cloud to a target frame.
447    ///
448    /// The points are partitioned by frame ids and timestamps and a coordinate transform is
449    /// derived for each partition. It must be
450    ///
451    /// See also: <https://stackoverflow.com/a/65287197>
452    pub fn resolve_to_frame(&mut self, target_frame_id: FrameId) -> Result<(), Error> {
453        if self.info.frame_id.is_none() && !self.point_data.contains_frame_id_column() {
454            return Err(NoFrameIdDefinitions);
455        }
456
457        let mut partition_columns: Vec<&str> = Vec::new();
458        if self.point_data.contains_frame_id_column() {
459            partition_columns.push(PointDataColumnType::FrameId.as_str());
460        }
461        if self.point_data.contains_timestamp_sec_column() {
462            partition_columns.push(PointDataColumnType::TimestampSeconds.as_str());
463        }
464        if self.point_data.contains_timestamp_nanosec_column() {
465            partition_columns.push(PointDataColumnType::TimestampNanoSeconds.as_str());
466        }
467
468        let partitioned_data_frames: Vec<DataFrame> = if partition_columns.is_empty() {
469            vec![self.point_data.data_frame.clone()]
470        } else {
471            self.point_data
472                .data_frame
473                .clone()
474                .partition_by(partition_columns, true)?
475        };
476
477        let partitioned_data_frames: HashMap<(FrameId, Option<DateTime<Utc>>), DataFrame> =
478            partitioned_data_frames
479                .into_iter()
480                .map(|df| {
481                    let point_data = PointData::new_unchecked(df);
482                    // get either the frame id per point or the general frame id in the point cloud info
483                    let frame_ids_series = point_data.get_all_frame_ids();
484                    let frame_id = frame_ids_series.map_or_else(
485                        |_| self.info.frame_id.clone().unwrap(),
486                        |f| f.first().unwrap().clone(),
487                    );
488
489                    let timestamp = point_data
490                        .get_all_timestamps()
491                        .ok()
492                        .map(|t| *t.first().unwrap());
493                    ((frame_id, timestamp), point_data.data_frame)
494                })
495                .collect();
496
497        let partitioned_data_frames: Vec<DataFrame> = partitioned_data_frames
498            .iter()
499            .map(|((current_frame_id, current_timestamp), df)| {
500                let mut point_data = PointData::new_unchecked(df.clone());
501                let r = point_data.resolve_data_frame(
502                    &self.reference_frames,
503                    current_timestamp,
504                    current_frame_id,
505                    &target_frame_id,
506                );
507                if r.is_err() {
508                    print!(
509                        "should work: current_frame_id = {current_frame_id:?}, target_frame_id: {target_frame_id:?}"
510                    );
511                    println!(
512                        "all available frame ids: {:?}",
513                        &self.reference_frames.get_frame_ids()
514                    );
515                    panic!("error");
516                }
517
518                point_data.data_frame
519            })
520            .collect();
521
522        let partitioned_lazy: Vec<LazyFrame> = partitioned_data_frames
523            .iter()
524            .map(|d| d.clone().lazy())
525            .collect();
526
527        let mut merged_again = concat(partitioned_lazy, Default::default())
528            .expect("concatenation should work")
529            .collect()
530            .expect("concatenation should work");
531
532        // sort by timestamp, if available without id
533        if merged_again
534            .column(PointDataColumnType::TimestampSeconds.as_str())
535            .is_ok()
536            && merged_again
537                .column(PointDataColumnType::TimestampNanoSeconds.as_str())
538                .is_ok()
539            && merged_again
540                .column(PointDataColumnType::FrameId.as_str())
541                .is_err()
542        {
543            merged_again = merged_again
544                .sort(
545                    [
546                        PointDataColumnType::TimestampSeconds.as_str(),
547                        PointDataColumnType::TimestampNanoSeconds.as_str(),
548                    ],
549                    SortMultipleOptions::default().with_maintain_order(true),
550                )
551                .expect("");
552        }
553        // sort by id
554        if merged_again
555            .column(PointDataColumnType::Id.as_str())
556            .is_ok()
557        {
558            merged_again = merged_again
559                .sort(
560                    [PointDataColumnType::Id.as_str()],
561                    SortMultipleOptions::default().with_maintain_order(true),
562                )
563                .expect("");
564        }
565
566        self.point_data.data_frame = merged_again;
567        self.info.frame_id = Some(target_frame_id);
568
569        Ok(())
570    }
571}