edgefirst_client/
dataset.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright © 2025 Au-Zone Technologies. All Rights Reserved.
3
4use std::fmt::Display;
5
6use crate::{
7    Client, Error,
8    api::{AnnotationSetID, DatasetID, ProjectID, SampleID},
9};
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13#[cfg(feature = "polars")]
14use polars::prelude::*;
15
16/// File types supported in EdgeFirst Studio datasets.
17///
18/// Represents the different types of sensor data files that can be stored
19/// and processed in a dataset. EdgeFirst Studio supports various modalities
20/// including visual images and different forms of LiDAR and radar data.
21///
22/// # Examples
23///
24/// ```rust
25/// use edgefirst_client::FileType;
26///
27/// // Create file types from strings
28/// let image_type = FileType::from("image");
29/// let lidar_type = FileType::from("lidar.pcd");
30///
31/// // Display file types
32/// println!("Processing {} files", image_type); // "Processing image files"
33///
34/// // Use in dataset operations - example usage
35/// let file_type = FileType::Image;
36/// match file_type {
37///     FileType::Image => println!("Processing image files"),
38///     FileType::LidarPcd => println!("Processing LiDAR point cloud files"),
39///     _ => println!("Processing other sensor data"),
40/// }
41/// ```
42#[derive(Clone, Eq, PartialEq, Debug)]
43pub enum FileType {
44    /// Standard image files (JPEG, PNG, etc.)
45    Image,
46    /// LiDAR point cloud data files (.pcd format)
47    LidarPcd,
48    /// LiDAR depth images (.png format)
49    LidarDepth,
50    /// LiDAR reflectance images (.jpg format)
51    LidarReflect,
52    /// Radar point cloud data files (.pcd format)
53    RadarPcd,
54    /// Radar cube data files (.png format)
55    RadarCube,
56}
57
58impl std::fmt::Display for FileType {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        let value = match self {
61            FileType::Image => "image",
62            FileType::LidarPcd => "lidar.pcd",
63            FileType::LidarDepth => "lidar.png",
64            FileType::LidarReflect => "lidar.jpg",
65            FileType::RadarPcd => "radar.pcd",
66            FileType::RadarCube => "radar.png",
67        };
68        write!(f, "{}", value)
69    }
70}
71
72impl From<&str> for FileType {
73    fn from(s: &str) -> Self {
74        match s {
75            "image" => FileType::Image,
76            "lidar.pcd" => FileType::LidarPcd,
77            "lidar.png" => FileType::LidarDepth,
78            "lidar.jpg" => FileType::LidarReflect,
79            "radar.pcd" => FileType::RadarPcd,
80            "radar.png" => FileType::RadarCube,
81            _ => panic!("Invalid file type"),
82        }
83    }
84}
85
86/// Annotation types supported for labeling data in EdgeFirst Studio.
87///
88/// Represents the different types of annotations that can be applied to
89/// sensor data for machine learning tasks. Each type corresponds to a
90/// different annotation geometry and use case.
91///
92/// # Examples
93///
94/// ```rust
95/// use edgefirst_client::AnnotationType;
96///
97/// // Create annotation types from strings
98/// let box_2d = AnnotationType::from("box2d");
99/// let segmentation = AnnotationType::from("mask");
100///
101/// // Display annotation types
102/// println!("Annotation type: {}", box_2d); // "Annotation type: box2d"
103///
104/// // Use in matching and processing
105/// let annotation_type = AnnotationType::Box2d;
106/// match annotation_type {
107///     AnnotationType::Box2d => println!("Processing 2D bounding boxes"),
108///     AnnotationType::Box3d => println!("Processing 3D bounding boxes"),
109///     AnnotationType::Mask => println!("Processing segmentation masks"),
110/// }
111/// ```
112#[derive(Clone, Eq, PartialEq, Debug)]
113pub enum AnnotationType {
114    /// 2D bounding boxes for object detection in images
115    Box2d,
116    /// 3D bounding boxes for object detection in 3D space (LiDAR, etc.)
117    Box3d,
118    /// Pixel-level segmentation masks for semantic/instance segmentation
119    Mask,
120}
121
122impl From<&str> for AnnotationType {
123    fn from(s: &str) -> Self {
124        match s {
125            "box2d" => AnnotationType::Box2d,
126            "box3d" => AnnotationType::Box3d,
127            "mask" => AnnotationType::Mask,
128            _ => panic!("Invalid annotation type"),
129        }
130    }
131}
132
133impl From<String> for AnnotationType {
134    fn from(s: String) -> Self {
135        s.as_str().into()
136    }
137}
138
139impl From<&String> for AnnotationType {
140    fn from(s: &String) -> Self {
141        s.as_str().into()
142    }
143}
144
145impl std::fmt::Display for AnnotationType {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        let value = match self {
148            AnnotationType::Box2d => "box2d",
149            AnnotationType::Box3d => "box3d",
150            AnnotationType::Mask => "mask",
151        };
152        write!(f, "{}", value)
153    }
154}
155
156/// A dataset in EdgeFirst Studio containing sensor data and annotations.
157///
158/// Datasets are collections of multi-modal sensor data (images, LiDAR, radar)
159/// along with their corresponding annotations (bounding boxes, segmentation
160/// masks, 3D annotations). Datasets belong to projects and can be used for
161/// training and validation of machine learning models.
162///
163/// # Features
164///
165/// - **Multi-modal Data**: Support for images, LiDAR point clouds, radar data
166/// - **Rich Annotations**: 2D/3D bounding boxes, segmentation masks
167/// - **Metadata**: Timestamps, sensor configurations, calibration data
168/// - **Version Control**: Track changes and maintain data lineage
169/// - **Format Conversion**: Export to popular ML frameworks
170///
171/// # Examples
172///
173/// ```no_run
174/// use edgefirst_client::{Client, Dataset, DatasetID};
175/// use std::str::FromStr;
176///
177/// # async fn example() -> Result<(), edgefirst_client::Error> {
178/// # let client = Client::new()?;
179/// // Get dataset information
180/// let dataset_id = DatasetID::from_str("ds-abc123")?;
181/// let dataset = client.dataset(dataset_id).await?;
182/// println!("Dataset: {}", dataset.name());
183///
184/// // Access dataset metadata
185/// println!("Dataset ID: {}", dataset.id());
186/// println!("Description: {}", dataset.description());
187/// println!("Created: {}", dataset.created());
188///
189/// // Work with dataset data would require additional methods
190/// // that are implemented in the full API
191/// # Ok(())
192/// # }
193/// ```
194#[derive(Deserialize, Clone, Debug)]
195pub struct Dataset {
196    id: DatasetID,
197    project_id: ProjectID,
198    name: String,
199    description: String,
200    cloud_key: String,
201    #[serde(rename = "createdAt")]
202    created: DateTime<Utc>,
203}
204
205impl Display for Dataset {
206    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
207        write!(f, "{} {}", self.uid(), self.name)
208    }
209}
210
211impl Dataset {
212    pub fn id(&self) -> DatasetID {
213        self.id
214    }
215
216    pub fn uid(&self) -> String {
217        self.id.to_string()
218    }
219
220    pub fn project_id(&self) -> ProjectID {
221        self.project_id
222    }
223
224    pub fn name(&self) -> &str {
225        &self.name
226    }
227
228    pub fn description(&self) -> &str {
229        &self.description
230    }
231
232    pub fn cloud_key(&self) -> &str {
233        &self.cloud_key
234    }
235
236    pub fn created(&self) -> &DateTime<Utc> {
237        &self.created
238    }
239
240    pub async fn project(&self, client: &Client) -> Result<crate::api::Project, Error> {
241        client.project(self.project_id).await
242    }
243
244    pub async fn annotation_sets(&self, client: &Client) -> Result<Vec<AnnotationSet>, Error> {
245        client.annotation_sets(self.id).await
246    }
247
248    pub async fn labels(&self, client: &Client) -> Result<Vec<Label>, Error> {
249        client.labels(self.id).await
250    }
251
252    pub async fn add_label(&self, client: &Client, name: &str) -> Result<(), Error> {
253        client.add_label(self.id, name).await
254    }
255
256    pub async fn remove_label(&self, client: &Client, name: &str) -> Result<(), Error> {
257        let labels = self.labels(client).await?;
258        let label = labels
259            .iter()
260            .find(|l| l.name() == name)
261            .ok_or_else(|| Error::MissingLabel(name.to_string()))?;
262        client.remove_label(label.id()).await
263    }
264}
265
266/// The AnnotationSet class represents a collection of annotations in a dataset.
267/// A dataset can have multiple annotation sets, each containing annotations for
268/// different tasks or purposes.
269#[derive(Deserialize)]
270pub struct AnnotationSet {
271    id: AnnotationSetID,
272    dataset_id: DatasetID,
273    name: String,
274    description: String,
275    #[serde(rename = "date")]
276    created: DateTime<Utc>,
277}
278
279impl Display for AnnotationSet {
280    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
281        write!(f, "{} {}", self.uid(), self.name)
282    }
283}
284
285impl AnnotationSet {
286    pub fn id(&self) -> AnnotationSetID {
287        self.id
288    }
289
290    pub fn uid(&self) -> String {
291        self.id.to_string()
292    }
293
294    pub fn dataset_id(&self) -> DatasetID {
295        self.dataset_id
296    }
297
298    pub fn name(&self) -> &str {
299        &self.name
300    }
301
302    pub fn description(&self) -> &str {
303        &self.description
304    }
305
306    pub fn created(&self) -> DateTime<Utc> {
307        self.created
308    }
309
310    pub async fn dataset(&self, client: &Client) -> Result<Dataset, Error> {
311        client.dataset(self.dataset_id).await
312    }
313}
314
315#[derive(Serialize, Deserialize, Clone, Debug)]
316pub struct Sample {
317    id: SampleID,
318    #[serde(alias = "group_name", skip_serializing_if = "Option::is_none")]
319    group: Option<String>,
320    sequence_name: Option<String>,
321    image_name: String,
322    image_url: String,
323    #[serde(rename = "sensors")]
324    files: Option<Vec<SampleFile>>,
325    annotations: Option<Vec<Annotation>>,
326}
327
328impl Display for Sample {
329    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
330        write!(f, "{} {}", self.uid(), self.image_name())
331    }
332}
333
334impl Sample {
335    pub fn id(&self) -> SampleID {
336        self.id
337    }
338
339    pub fn uid(&self) -> String {
340        self.id.to_string()
341    }
342
343    pub fn name(&self) -> String {
344        let name = self
345            .image_name
346            .rsplit_once('.')
347            .map_or_else(|| self.image_name.clone(), |(name, _)| name.to_string());
348        name.rsplit_once(".camera")
349            .map_or_else(|| name.clone(), |(name, _)| name.to_string())
350    }
351
352    pub fn group(&self) -> Option<&String> {
353        self.group.as_ref()
354    }
355
356    pub fn sequence_name(&self) -> Option<&String> {
357        self.sequence_name.as_ref()
358    }
359
360    pub fn image_name(&self) -> &str {
361        &self.image_name
362    }
363
364    pub fn image_url(&self) -> &str {
365        &self.image_url
366    }
367
368    pub fn files(&self) -> &[SampleFile] {
369        match &self.files {
370            Some(files) => files,
371            None => &[],
372        }
373    }
374
375    pub fn annotations(&self) -> &[Annotation] {
376        match &self.annotations {
377            Some(annotations) => annotations,
378            None => &[],
379        }
380    }
381
382    pub fn with_annotations(mut self, annotations: Vec<Annotation>) -> Self {
383        self.annotations = Some(annotations);
384        self
385    }
386
387    pub async fn download(
388        &self,
389        client: &Client,
390        file_type: FileType,
391    ) -> Result<Option<Vec<u8>>, Error> {
392        let url = match file_type {
393            FileType::Image => Some(&self.image_url),
394            file => self
395                .files
396                .as_ref()
397                .and_then(|files| files.iter().find(|f| f.r#type == file.to_string()))
398                .map(|f| &f.url),
399        };
400
401        Ok(match url {
402            Some(url) => Some(client.download(url).await?),
403            None => None,
404        })
405    }
406}
407
408#[derive(Serialize, Deserialize, Clone, Debug)]
409pub struct SampleFile {
410    r#type: String,
411    url: String,
412}
413
414pub trait TypeName {
415    fn type_name() -> String;
416}
417
418#[derive(Serialize, Deserialize, Clone, Debug)]
419pub struct Box3d {
420    x: f32,
421    y: f32,
422    z: f32,
423    w: f32,
424    h: f32,
425    l: f32,
426}
427
428impl TypeName for Box3d {
429    fn type_name() -> String {
430        "box3d".to_owned()
431    }
432}
433
434impl Box3d {
435    pub fn new(cx: f32, cy: f32, cz: f32, width: f32, height: f32, length: f32) -> Self {
436        Self {
437            x: cx,
438            y: cy,
439            z: cz,
440            w: width,
441            h: height,
442            l: length,
443        }
444    }
445
446    pub fn width(&self) -> f32 {
447        self.w
448    }
449
450    pub fn height(&self) -> f32 {
451        self.h
452    }
453
454    pub fn length(&self) -> f32 {
455        self.l
456    }
457
458    pub fn cx(&self) -> f32 {
459        self.x
460    }
461
462    pub fn cy(&self) -> f32 {
463        self.y
464    }
465
466    pub fn cz(&self) -> f32 {
467        self.z
468    }
469
470    pub fn left(&self) -> f32 {
471        self.x - self.w / 2.0
472    }
473
474    pub fn top(&self) -> f32 {
475        self.y - self.h / 2.0
476    }
477
478    pub fn front(&self) -> f32 {
479        self.z - self.l / 2.0
480    }
481}
482
483#[derive(Serialize, Deserialize, Clone, Debug)]
484pub struct Box2d {
485    h: f32,
486    w: f32,
487    x: f32,
488    y: f32,
489}
490
491impl TypeName for Box2d {
492    fn type_name() -> String {
493        "box2d".to_owned()
494    }
495}
496
497impl Box2d {
498    pub fn new(left: f32, top: f32, width: f32, height: f32) -> Self {
499        Self {
500            x: left,
501            y: top,
502            w: width,
503            h: height,
504        }
505    }
506
507    pub fn width(&self) -> f32 {
508        self.w
509    }
510
511    pub fn height(&self) -> f32 {
512        self.h
513    }
514
515    pub fn left(&self) -> f32 {
516        self.x
517    }
518
519    pub fn top(&self) -> f32 {
520        self.y
521    }
522
523    pub fn cx(&self) -> f32 {
524        self.x + self.w / 2.0
525    }
526
527    pub fn cy(&self) -> f32 {
528        self.y + self.h / 2.0
529    }
530}
531
532#[derive(Serialize, Deserialize, Clone, Debug)]
533pub struct Mask {
534    pub polygon: Vec<Vec<(f32, f32)>>,
535}
536
537impl TypeName for Mask {
538    fn type_name() -> String {
539        "mask".to_owned()
540    }
541}
542
543impl Mask {
544    pub fn new(polygon: Vec<Vec<(f32, f32)>>) -> Self {
545        Self { polygon }
546    }
547}
548
549#[derive(Serialize, Deserialize, Clone, Debug)]
550pub struct Annotation {
551    #[serde(skip_serializing_if = "Option::is_none")]
552    sample_id: Option<SampleID>,
553    #[serde(skip_serializing_if = "Option::is_none")]
554    name: Option<String>,
555    #[serde(skip_serializing_if = "Option::is_none")]
556    sequence_name: Option<String>,
557    #[serde(skip_serializing_if = "Option::is_none")]
558    group: Option<String>,
559    #[serde(skip_serializing_if = "Option::is_none")]
560    object_id: Option<String>,
561    #[serde(alias = "label_name", skip_serializing_if = "Option::is_none")]
562    label: Option<String>,
563    #[serde(skip_serializing_if = "Option::is_none")]
564    label_index: Option<u64>,
565    #[serde(skip_serializing_if = "Option::is_none")]
566    box2d: Option<Box2d>,
567    #[serde(skip_serializing_if = "Option::is_none")]
568    box3d: Option<Box3d>,
569    #[serde(skip_serializing_if = "Option::is_none")]
570    mask: Option<Mask>,
571}
572
573impl Default for Annotation {
574    fn default() -> Self {
575        Self::new()
576    }
577}
578
579impl Annotation {
580    pub fn new() -> Self {
581        Self {
582            sample_id: None,
583            name: None,
584            sequence_name: None,
585            group: None,
586            object_id: None,
587            label: None,
588            label_index: None,
589            box2d: None,
590            box3d: None,
591            mask: None,
592        }
593    }
594
595    pub fn set_sample_id(&mut self, sample_id: Option<SampleID>) {
596        self.sample_id = sample_id;
597    }
598
599    pub fn sample_id(&self) -> Option<SampleID> {
600        self.sample_id
601    }
602
603    pub fn set_name(&mut self, name: Option<String>) {
604        self.name = name;
605    }
606
607    pub fn name(&self) -> Option<&String> {
608        self.name.as_ref()
609    }
610
611    pub fn set_sequence_name(&mut self, sequence_name: Option<String>) {
612        self.sequence_name = sequence_name;
613    }
614
615    pub fn sequence_name(&self) -> Option<&String> {
616        self.sequence_name.as_ref()
617    }
618
619    pub fn set_group(&mut self, group: Option<String>) {
620        self.group = group;
621    }
622
623    pub fn group(&self) -> Option<&String> {
624        self.group.as_ref()
625    }
626
627    pub fn object_id(&self) -> Option<&String> {
628        self.object_id.as_ref()
629    }
630
631    pub fn label(&self) -> Option<&String> {
632        self.label.as_ref()
633    }
634
635    pub fn label_index(&self) -> Option<u64> {
636        self.label_index
637    }
638
639    pub fn set_label_index(&mut self, label_index: Option<u64>) {
640        self.label_index = label_index;
641    }
642
643    pub fn box2d(&self) -> Option<&Box2d> {
644        self.box2d.as_ref()
645    }
646
647    pub fn box3d(&self) -> Option<&Box3d> {
648        self.box3d.as_ref()
649    }
650
651    pub fn mask(&self) -> Option<&Mask> {
652        self.mask.as_ref()
653    }
654}
655
656#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
657pub struct Label {
658    id: u64,
659    dataset_id: DatasetID,
660    index: u64,
661    name: String,
662}
663
664impl Label {
665    pub fn id(&self) -> u64 {
666        self.id
667    }
668
669    pub fn dataset_id(&self) -> DatasetID {
670        self.dataset_id
671    }
672
673    pub fn index(&self) -> u64 {
674        self.index
675    }
676
677    pub fn name(&self) -> &str {
678        &self.name
679    }
680
681    pub async fn remove(&self, client: &Client) -> Result<(), Error> {
682        client.remove_label(self.id()).await
683    }
684
685    pub async fn set_name(&mut self, client: &Client, name: &str) -> Result<(), Error> {
686        self.name = name.to_string();
687        client.update_label(self).await
688    }
689
690    pub async fn set_index(&mut self, client: &Client, index: u64) -> Result<(), Error> {
691        self.index = index;
692        client.update_label(self).await
693    }
694}
695
696impl Display for Label {
697    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
698        write!(f, "{}", self.name())
699    }
700}
701
702#[derive(Serialize, Clone, Debug)]
703pub struct NewLabelObject {
704    pub name: String,
705}
706
707#[derive(Serialize, Clone, Debug)]
708pub struct NewLabel {
709    pub dataset_id: DatasetID,
710    pub labels: Vec<NewLabelObject>,
711}
712
713#[derive(Deserialize, Clone, Debug)]
714pub struct Group {
715    pub id: u64, // Groups seem to use raw u64, not a specific ID type
716    pub name: String,
717}
718
719#[cfg(feature = "polars")]
720pub fn annotations_dataframe(annotations: &[Annotation]) -> DataFrame {
721    use itertools::Itertools;
722    use log::warn;
723    use std::path::Path;
724
725    let (names, frames, objects, labels, label_indices, groups, masks, boxes2d, boxes3d) =
726        annotations
727            .iter()
728            .map(|ann| {
729                let name = match &ann.name {
730                    Some(name) => name,
731                    None => {
732                        warn!("annotation missing image name, skipping");
733                        return (
734                            String::new(),
735                            None,
736                            None,
737                            None,
738                            None,
739                            None,
740                            None,
741                            None,
742                            None,
743                        );
744                    }
745                };
746
747                let name = Path::new(name).file_stem().unwrap().to_str().unwrap();
748
749                let (name, frame) = match &ann.sequence_name {
750                    Some(sequence) => match name.strip_prefix(sequence) {
751                        Some(frame) => (
752                            sequence.to_string(),
753                            Some(frame.trim_start_matches('_').to_string()),
754                        ),
755                        None => {
756                            warn!(
757                                "image_name {} does not match sequence_name {}",
758                                name, sequence
759                            );
760                            return (
761                                String::new(),
762                                None,
763                                None,
764                                None,
765                                None,
766                                None,
767                                None,
768                                None,
769                                None,
770                            );
771                        }
772                    },
773                    None => (name.to_string(), None),
774                };
775
776                let masks = match &ann.mask {
777                    Some(seg) => {
778                        use polars::series::Series;
779
780                        let mut list = Vec::new();
781                        for polygon in &seg.polygon {
782                            for &(x, y) in polygon {
783                                list.push(x);
784                                list.push(y);
785                            }
786                            // Separate polygons with NaN
787                            list.push(f32::NAN);
788                        }
789
790                        // Remove the last NaN if it exists
791                        let list = if !list.is_empty() {
792                            list[..list.len() - 1].to_vec()
793                        } else {
794                            vec![]
795                        };
796
797                        Some(Series::new("mask".into(), list))
798                    }
799                    None => Option::<Series>::None,
800                };
801
802                let box2d = ann.box2d.as_ref().map(|box2d| {
803                    Series::new(
804                        "box2d".into(),
805                        [box2d.cx(), box2d.cy(), box2d.width(), box2d.height()],
806                    )
807                });
808
809                let box3d = ann.box3d.as_ref().map(|box3d| {
810                    Series::new(
811                        "box3d".into(),
812                        [box3d.x, box3d.y, box3d.z, box3d.w, box3d.h, box3d.l],
813                    )
814                });
815
816                (
817                    name,
818                    frame,
819                    ann.object_id.clone(),
820                    ann.label.clone(),
821                    ann.label_index,
822                    ann.group.clone(),
823                    masks,
824                    box2d,
825                    box3d,
826                )
827            })
828            .multiunzip::<(
829                Vec<_>, // names
830                Vec<_>, // frames
831                Vec<_>, // objects
832                Vec<_>, // labels
833                Vec<_>, // label_indices
834                Vec<_>, // groups
835                Vec<_>, // masks
836                Vec<_>, // boxes2d
837                Vec<_>, // boxes3d
838            )>();
839    let names = Series::new("name".into(), names).into();
840    let frames = Series::new("frame".into(), frames).into();
841    let objects = Series::new("object_id".into(), objects).into();
842    let labels = Series::new("label".into(), labels)
843        .cast(&DataType::Categorical(
844            Categories::new("labels".into(), "labels".into(), CategoricalPhysical::U8),
845            Arc::new(CategoricalMapping::new(u8::MAX as usize)),
846        ))
847        .unwrap()
848        .into();
849    let label_indices = Series::new("label_index".into(), label_indices).into();
850    let groups = Series::new("group".into(), groups)
851        .cast(&DataType::Categorical(
852            Categories::new("groups".into(), "groups".into(), CategoricalPhysical::U8),
853            Arc::new(CategoricalMapping::new(u8::MAX as usize)),
854        ))
855        .unwrap()
856        .into();
857    let masks = Series::new("mask".into(), masks)
858        .cast(&DataType::List(Box::new(DataType::Float32)))
859        .unwrap()
860        .into();
861    let boxes2d = Series::new("box2d".into(), boxes2d)
862        .cast(&DataType::Array(Box::new(DataType::Float32), 4))
863        .unwrap()
864        .into();
865    let boxes3d = Series::new("box3d".into(), boxes3d)
866        .cast(&DataType::Array(Box::new(DataType::Float32), 6))
867        .unwrap()
868        .into();
869
870    DataFrame::new(vec![
871        names,
872        frames,
873        objects,
874        labels,
875        label_indices,
876        groups,
877        masks,
878        boxes2d,
879        boxes3d,
880    ])
881    .unwrap()
882}