1use std::{collections::HashMap, 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#[derive(Clone, Eq, PartialEq, Debug)]
43pub enum FileType {
44 Image,
46 LidarPcd,
48 LidarDepth,
50 LidarReflect,
52 RadarPcd,
54 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 TryFrom<&str> for FileType {
73 type Error = crate::Error;
74
75 fn try_from(s: &str) -> Result<Self, Self::Error> {
76 match s {
77 "image" => Ok(FileType::Image),
78 "lidar.pcd" => Ok(FileType::LidarPcd),
79 "lidar.png" => Ok(FileType::LidarDepth),
80 "lidar.jpg" => Ok(FileType::LidarReflect),
81 "radar.pcd" => Ok(FileType::RadarPcd),
82 "radar.png" => Ok(FileType::RadarCube),
83 _ => Err(crate::Error::InvalidFileType(s.to_string())),
84 }
85 }
86}
87
88impl std::str::FromStr for FileType {
89 type Err = crate::Error;
90
91 fn from_str(s: &str) -> Result<Self, Self::Err> {
92 s.try_into()
93 }
94}
95
96#[derive(Clone, Eq, PartialEq, Debug)]
127pub enum AnnotationType {
128 Box2d,
130 Box3d,
132 Mask,
134}
135
136impl TryFrom<&str> for AnnotationType {
137 type Error = crate::Error;
138
139 fn try_from(s: &str) -> Result<Self, Self::Error> {
140 match s {
141 "box2d" => Ok(AnnotationType::Box2d),
142 "box3d" => Ok(AnnotationType::Box3d),
143 "mask" => Ok(AnnotationType::Mask),
144 _ => Err(crate::Error::InvalidAnnotationType(s.to_string())),
145 }
146 }
147}
148
149impl From<String> for AnnotationType {
150 fn from(s: String) -> Self {
151 s.as_str().try_into().unwrap_or(AnnotationType::Box2d)
153 }
154}
155
156impl From<&String> for AnnotationType {
157 fn from(s: &String) -> Self {
158 s.as_str().try_into().unwrap_or(AnnotationType::Box2d)
160 }
161}
162
163impl std::fmt::Display for AnnotationType {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 let value = match self {
166 AnnotationType::Box2d => "box2d",
167 AnnotationType::Box3d => "box3d",
168 AnnotationType::Mask => "mask",
169 };
170 write!(f, "{}", value)
171 }
172}
173
174#[derive(Deserialize, Clone, Debug)]
213pub struct Dataset {
214 id: DatasetID,
215 project_id: ProjectID,
216 name: String,
217 description: String,
218 cloud_key: String,
219 #[serde(rename = "createdAt")]
220 created: DateTime<Utc>,
221}
222
223impl Display for Dataset {
224 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
225 write!(f, "{} {}", self.id, self.name)
226 }
227}
228
229impl Dataset {
230 pub fn id(&self) -> DatasetID {
231 self.id
232 }
233
234 pub fn project_id(&self) -> ProjectID {
235 self.project_id
236 }
237
238 pub fn name(&self) -> &str {
239 &self.name
240 }
241
242 pub fn description(&self) -> &str {
243 &self.description
244 }
245
246 pub fn cloud_key(&self) -> &str {
247 &self.cloud_key
248 }
249
250 pub fn created(&self) -> &DateTime<Utc> {
251 &self.created
252 }
253
254 pub async fn project(&self, client: &Client) -> Result<crate::api::Project, Error> {
255 client.project(self.project_id).await
256 }
257
258 pub async fn annotation_sets(&self, client: &Client) -> Result<Vec<AnnotationSet>, Error> {
259 client.annotation_sets(self.id).await
260 }
261
262 pub async fn labels(&self, client: &Client) -> Result<Vec<Label>, Error> {
263 client.labels(self.id).await
264 }
265
266 pub async fn add_label(&self, client: &Client, name: &str) -> Result<(), Error> {
267 client.add_label(self.id, name).await
268 }
269
270 pub async fn remove_label(&self, client: &Client, name: &str) -> Result<(), Error> {
271 let labels = self.labels(client).await?;
272 let label = labels
273 .iter()
274 .find(|l| l.name() == name)
275 .ok_or_else(|| Error::MissingLabel(name.to_string()))?;
276 client.remove_label(label.id()).await
277 }
278}
279
280#[derive(Deserialize)]
284pub struct AnnotationSet {
285 id: AnnotationSetID,
286 dataset_id: DatasetID,
287 name: String,
288 description: String,
289 #[serde(rename = "date")]
290 created: DateTime<Utc>,
291}
292
293impl Display for AnnotationSet {
294 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
295 write!(f, "{} {}", self.id, self.name)
296 }
297}
298
299impl AnnotationSet {
300 pub fn id(&self) -> AnnotationSetID {
301 self.id
302 }
303
304 pub fn dataset_id(&self) -> DatasetID {
305 self.dataset_id
306 }
307
308 pub fn name(&self) -> &str {
309 &self.name
310 }
311
312 pub fn description(&self) -> &str {
313 &self.description
314 }
315
316 pub fn created(&self) -> DateTime<Utc> {
317 self.created
318 }
319
320 pub async fn dataset(&self, client: &Client) -> Result<Dataset, Error> {
321 client.dataset(self.dataset_id).await
322 }
323}
324
325#[derive(Serialize, Deserialize, Clone, Debug)]
332pub struct Sample {
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub id: Option<SampleID>,
335 #[serde(
340 alias = "group_name",
341 rename(serialize = "group", deserialize = "group_name"),
342 skip_serializing_if = "Option::is_none"
343 )]
344 pub group: Option<String>,
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub sequence_name: Option<String>,
347 #[serde(skip_serializing_if = "Option::is_none")]
348 pub sequence_uuid: Option<String>,
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub sequence_description: Option<String>,
351 #[serde(
352 default,
353 skip_serializing_if = "Option::is_none",
354 deserialize_with = "deserialize_frame_number"
355 )]
356 pub frame_number: Option<u32>,
357 #[serde(skip_serializing_if = "Option::is_none")]
358 pub uuid: Option<String>,
359 #[serde(skip_serializing_if = "Option::is_none")]
360 pub image_name: Option<String>,
361 #[serde(skip_serializing_if = "Option::is_none")]
362 pub image_url: Option<String>,
363 #[serde(skip_serializing_if = "Option::is_none")]
364 pub width: Option<u32>,
365 #[serde(skip_serializing_if = "Option::is_none")]
366 pub height: Option<u32>,
367 #[serde(skip_serializing_if = "Option::is_none")]
368 pub date: Option<DateTime<Utc>>,
369 #[serde(skip_serializing_if = "Option::is_none")]
370 pub source: Option<String>,
371 #[serde(rename = "sensors", skip_serializing_if = "Option::is_none")]
374 pub location: Option<Location>,
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub degradation: Option<String>,
378 #[serde(
383 default,
384 skip_serializing_if = "Vec::is_empty",
385 serialize_with = "serialize_files",
386 deserialize_with = "deserialize_files"
387 )]
388 pub files: Vec<SampleFile>,
389 #[serde(
390 default,
391 skip_serializing_if = "Vec::is_empty",
392 serialize_with = "serialize_annotations",
393 deserialize_with = "deserialize_annotations"
394 )]
395 pub annotations: Vec<Annotation>,
396}
397
398fn deserialize_frame_number<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
401where
402 D: serde::Deserializer<'de>,
403{
404 use serde::Deserialize;
405
406 let value = Option::<i32>::deserialize(deserializer)?;
407 Ok(value.and_then(|v| if v < 0 { None } else { Some(v as u32) }))
408}
409
410fn serialize_files<S>(files: &[SampleFile], serializer: S) -> Result<S::Ok, S::Error>
413where
414 S: serde::Serializer,
415{
416 use serde::Serialize;
417 let map: HashMap<String, String> = files
418 .iter()
419 .filter_map(|f| {
420 f.filename()
421 .map(|filename| (f.file_type().to_string(), filename.to_string()))
422 })
423 .collect();
424 map.serialize(serializer)
425}
426
427fn deserialize_files<'de, D>(deserializer: D) -> Result<Vec<SampleFile>, D::Error>
430where
431 D: serde::Deserializer<'de>,
432{
433 use serde::Deserialize;
434
435 #[derive(Deserialize)]
436 #[serde(untagged)]
437 enum FilesFormat {
438 Vec(Vec<SampleFile>),
439 Map(HashMap<String, String>),
440 }
441
442 let value = Option::<FilesFormat>::deserialize(deserializer)?;
443 Ok(value
444 .map(|v| match v {
445 FilesFormat::Vec(files) => files,
446 FilesFormat::Map(map) => convert_files_map_to_vec(map),
447 })
448 .unwrap_or_default())
449}
450
451fn serialize_annotations<S>(annotations: &Vec<Annotation>, serializer: S) -> Result<S::Ok, S::Error>
455where
456 S: serde::Serializer,
457{
458 serde::Serialize::serialize(annotations, serializer)
459}
460
461fn deserialize_annotations<'de, D>(deserializer: D) -> Result<Vec<Annotation>, D::Error>
464where
465 D: serde::Deserializer<'de>,
466{
467 use serde::Deserialize;
468
469 #[derive(Deserialize)]
470 #[serde(untagged)]
471 enum AnnotationsFormat {
472 Vec(Vec<Annotation>),
473 Map(HashMap<String, Vec<Annotation>>),
474 }
475
476 let value = Option::<AnnotationsFormat>::deserialize(deserializer)?;
477 Ok(value
478 .map(|v| match v {
479 AnnotationsFormat::Vec(annotations) => annotations,
480 AnnotationsFormat::Map(map) => convert_annotations_map_to_vec(map),
481 })
482 .unwrap_or_default())
483}
484
485impl Display for Sample {
486 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
487 write!(
488 f,
489 "{} {}",
490 self.id
491 .map(|id| id.to_string())
492 .unwrap_or_else(|| "unknown".to_string()),
493 self.image_name().unwrap_or("unknown")
494 )
495 }
496}
497
498impl Default for Sample {
499 fn default() -> Self {
500 Self::new()
501 }
502}
503
504impl Sample {
505 pub fn new() -> Self {
507 Self {
508 id: None,
509 group: None,
510 sequence_name: None,
511 sequence_uuid: None,
512 sequence_description: None,
513 frame_number: None,
514 uuid: None,
515 image_name: None,
516 image_url: None,
517 width: None,
518 height: None,
519 date: None,
520 source: None,
521 location: None,
522 degradation: None,
523 files: vec![],
524 annotations: vec![],
525 }
526 }
527
528 pub fn id(&self) -> Option<SampleID> {
529 self.id
530 }
531
532 pub fn name(&self) -> Option<String> {
533 self.image_name.as_ref().map(|n| extract_sample_name(n))
534 }
535
536 pub fn group(&self) -> Option<&String> {
537 self.group.as_ref()
538 }
539
540 pub fn sequence_name(&self) -> Option<&String> {
541 self.sequence_name.as_ref()
542 }
543
544 pub fn sequence_uuid(&self) -> Option<&String> {
545 self.sequence_uuid.as_ref()
546 }
547
548 pub fn sequence_description(&self) -> Option<&String> {
549 self.sequence_description.as_ref()
550 }
551
552 pub fn frame_number(&self) -> Option<u32> {
553 self.frame_number
554 }
555
556 pub fn uuid(&self) -> Option<&String> {
557 self.uuid.as_ref()
558 }
559
560 pub fn image_name(&self) -> Option<&str> {
561 self.image_name.as_deref()
562 }
563
564 pub fn image_url(&self) -> Option<&str> {
565 self.image_url.as_deref()
566 }
567
568 pub fn width(&self) -> Option<u32> {
569 self.width
570 }
571
572 pub fn height(&self) -> Option<u32> {
573 self.height
574 }
575
576 pub fn date(&self) -> Option<DateTime<Utc>> {
577 self.date
578 }
579
580 pub fn source(&self) -> Option<&String> {
581 self.source.as_ref()
582 }
583
584 pub fn location(&self) -> Option<&Location> {
585 self.location.as_ref()
586 }
587
588 pub fn files(&self) -> &[SampleFile] {
589 &self.files
590 }
591
592 pub fn annotations(&self) -> &[Annotation] {
593 &self.annotations
594 }
595
596 pub fn with_annotations(mut self, annotations: Vec<Annotation>) -> Self {
597 self.annotations = annotations;
598 self
599 }
600
601 pub fn with_frame_number(mut self, frame_number: Option<u32>) -> Self {
602 self.frame_number = frame_number;
603 self
604 }
605
606 pub async fn download(
607 &self,
608 client: &Client,
609 file_type: FileType,
610 ) -> Result<Option<Vec<u8>>, Error> {
611 let url = resolve_file_url(&file_type, self.image_url.as_deref(), &self.files);
612
613 Ok(match url {
614 Some(url) => Some(client.download(url).await?),
615 None => None,
616 })
617 }
618}
619
620#[derive(Serialize, Deserialize, Clone, Debug)]
625pub struct SampleFile {
626 r#type: String,
627 #[serde(skip_serializing_if = "Option::is_none")]
628 url: Option<String>,
629 #[serde(skip_serializing_if = "Option::is_none")]
630 filename: Option<String>,
631}
632
633impl SampleFile {
634 pub fn with_url(file_type: String, url: String) -> Self {
636 Self {
637 r#type: file_type,
638 url: Some(url),
639 filename: None,
640 }
641 }
642
643 pub fn with_filename(file_type: String, filename: String) -> Self {
645 Self {
646 r#type: file_type,
647 url: None,
648 filename: Some(filename),
649 }
650 }
651
652 pub fn file_type(&self) -> &str {
653 &self.r#type
654 }
655
656 pub fn url(&self) -> Option<&str> {
657 self.url.as_deref()
658 }
659
660 pub fn filename(&self) -> Option<&str> {
661 self.filename.as_deref()
662 }
663}
664
665#[derive(Serialize, Deserialize, Clone, Debug)]
670pub struct Location {
671 #[serde(skip_serializing_if = "Option::is_none")]
672 pub gps: Option<GpsData>,
673 #[serde(skip_serializing_if = "Option::is_none")]
674 pub imu: Option<ImuData>,
675}
676
677#[derive(Serialize, Deserialize, Clone, Debug)]
679pub struct GpsData {
680 pub lat: f64,
681 pub lon: f64,
682}
683
684impl GpsData {
685 pub fn validate(&self) -> Result<(), String> {
715 validate_gps_coordinates(self.lat, self.lon)
716 }
717}
718
719#[derive(Serialize, Deserialize, Clone, Debug)]
721pub struct ImuData {
722 pub roll: f64,
723 pub pitch: f64,
724 pub yaw: f64,
725}
726
727impl ImuData {
728 pub fn validate(&self) -> Result<(), String> {
761 validate_imu_orientation(self.roll, self.pitch, self.yaw)
762 }
763}
764
765#[allow(dead_code)]
766pub trait TypeName {
767 fn type_name() -> String;
768}
769
770#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
771pub struct Box3d {
772 x: f32,
773 y: f32,
774 z: f32,
775 w: f32,
776 h: f32,
777 l: f32,
778}
779
780impl TypeName for Box3d {
781 fn type_name() -> String {
782 "box3d".to_owned()
783 }
784}
785
786impl Box3d {
787 pub fn new(cx: f32, cy: f32, cz: f32, width: f32, height: f32, length: f32) -> Self {
788 Self {
789 x: cx,
790 y: cy,
791 z: cz,
792 w: width,
793 h: height,
794 l: length,
795 }
796 }
797
798 pub fn width(&self) -> f32 {
799 self.w
800 }
801
802 pub fn height(&self) -> f32 {
803 self.h
804 }
805
806 pub fn length(&self) -> f32 {
807 self.l
808 }
809
810 pub fn cx(&self) -> f32 {
811 self.x
812 }
813
814 pub fn cy(&self) -> f32 {
815 self.y
816 }
817
818 pub fn cz(&self) -> f32 {
819 self.z
820 }
821
822 pub fn left(&self) -> f32 {
823 self.x - self.w / 2.0
824 }
825
826 pub fn top(&self) -> f32 {
827 self.y - self.h / 2.0
828 }
829
830 pub fn front(&self) -> f32 {
831 self.z - self.l / 2.0
832 }
833}
834
835#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
836pub struct Box2d {
837 h: f32,
838 w: f32,
839 x: f32,
840 y: f32,
841}
842
843impl TypeName for Box2d {
844 fn type_name() -> String {
845 "box2d".to_owned()
846 }
847}
848
849impl Box2d {
850 pub fn new(left: f32, top: f32, width: f32, height: f32) -> Self {
851 Self {
852 x: left,
853 y: top,
854 w: width,
855 h: height,
856 }
857 }
858
859 pub fn width(&self) -> f32 {
860 self.w
861 }
862
863 pub fn height(&self) -> f32 {
864 self.h
865 }
866
867 pub fn left(&self) -> f32 {
868 self.x
869 }
870
871 pub fn top(&self) -> f32 {
872 self.y
873 }
874
875 pub fn cx(&self) -> f32 {
876 self.x + self.w / 2.0
877 }
878
879 pub fn cy(&self) -> f32 {
880 self.y + self.h / 2.0
881 }
882}
883
884#[derive(Clone, Debug, PartialEq)]
885pub struct Mask {
886 pub polygon: Vec<Vec<(f32, f32)>>,
887}
888
889impl TypeName for Mask {
890 fn type_name() -> String {
891 "mask".to_owned()
892 }
893}
894
895impl Mask {
896 pub fn new(polygon: Vec<Vec<(f32, f32)>>) -> Self {
897 Self { polygon }
898 }
899}
900
901impl serde::Serialize for Mask {
902 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
903 where
904 S: serde::Serializer,
905 {
906 serde::Serialize::serialize(&self.polygon, serializer)
907 }
908}
909
910impl<'de> serde::Deserialize<'de> for Mask {
911 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
912 where
913 D: serde::Deserializer<'de>,
914 {
915 use serde::Deserialize;
916
917 #[derive(Deserialize)]
918 #[serde(untagged)]
919 enum MaskFormat {
920 Polygon { polygon: Vec<Vec<(f32, f32)>> },
921 Direct(Vec<Vec<(f32, f32)>>),
922 }
923
924 match MaskFormat::deserialize(deserializer)? {
925 MaskFormat::Polygon { polygon } => Ok(Self { polygon }),
926 MaskFormat::Direct(polygon) => Ok(Self { polygon }),
927 }
928 }
929}
930
931#[derive(Serialize, Deserialize, Clone, Debug)]
932pub struct Annotation {
933 #[serde(skip_serializing_if = "Option::is_none")]
934 sample_id: Option<SampleID>,
935 #[serde(skip_serializing_if = "Option::is_none")]
936 name: Option<String>,
937 #[serde(skip_serializing_if = "Option::is_none")]
938 sequence_name: Option<String>,
939 #[serde(skip_serializing_if = "Option::is_none")]
940 frame_number: Option<u32>,
941 #[serde(rename = "group_name", skip_serializing_if = "Option::is_none")]
945 group: Option<String>,
946 #[serde(
950 rename = "object_reference",
951 alias = "object_id",
952 skip_serializing_if = "Option::is_none"
953 )]
954 object_id: Option<String>,
955 #[serde(skip_serializing_if = "Option::is_none")]
956 label_name: Option<String>,
957 #[serde(skip_serializing_if = "Option::is_none")]
958 label_index: Option<u64>,
959 #[serde(skip_serializing_if = "Option::is_none")]
960 box2d: Option<Box2d>,
961 #[serde(skip_serializing_if = "Option::is_none")]
962 box3d: Option<Box3d>,
963 #[serde(skip_serializing_if = "Option::is_none")]
964 mask: Option<Mask>,
965}
966
967impl Default for Annotation {
968 fn default() -> Self {
969 Self::new()
970 }
971}
972
973impl Annotation {
974 pub fn new() -> Self {
975 Self {
976 sample_id: None,
977 name: None,
978 sequence_name: None,
979 frame_number: None,
980 group: None,
981 object_id: None,
982 label_name: None,
983 label_index: None,
984 box2d: None,
985 box3d: None,
986 mask: None,
987 }
988 }
989
990 pub fn set_sample_id(&mut self, sample_id: Option<SampleID>) {
991 self.sample_id = sample_id;
992 }
993
994 pub fn sample_id(&self) -> Option<SampleID> {
995 self.sample_id
996 }
997
998 pub fn set_name(&mut self, name: Option<String>) {
999 self.name = name;
1000 }
1001
1002 pub fn name(&self) -> Option<&String> {
1003 self.name.as_ref()
1004 }
1005
1006 pub fn set_sequence_name(&mut self, sequence_name: Option<String>) {
1007 self.sequence_name = sequence_name;
1008 }
1009
1010 pub fn sequence_name(&self) -> Option<&String> {
1011 self.sequence_name.as_ref()
1012 }
1013
1014 pub fn set_frame_number(&mut self, frame_number: Option<u32>) {
1015 self.frame_number = frame_number;
1016 }
1017
1018 pub fn frame_number(&self) -> Option<u32> {
1019 self.frame_number
1020 }
1021
1022 pub fn set_group(&mut self, group: Option<String>) {
1023 self.group = group;
1024 }
1025
1026 pub fn group(&self) -> Option<&String> {
1027 self.group.as_ref()
1028 }
1029
1030 pub fn object_id(&self) -> Option<&String> {
1031 self.object_id.as_ref()
1032 }
1033
1034 pub fn set_object_id(&mut self, object_id: Option<String>) {
1035 self.object_id = object_id;
1036 }
1037
1038 #[deprecated(note = "renamed to object_id")]
1039 pub fn object_reference(&self) -> Option<&String> {
1040 self.object_id()
1041 }
1042
1043 #[deprecated(note = "renamed to set_object_id")]
1044 pub fn set_object_reference(&mut self, object_reference: Option<String>) {
1045 self.set_object_id(object_reference);
1046 }
1047
1048 pub fn label(&self) -> Option<&String> {
1049 self.label_name.as_ref()
1050 }
1051
1052 pub fn set_label(&mut self, label_name: Option<String>) {
1053 self.label_name = label_name;
1054 }
1055
1056 pub fn label_index(&self) -> Option<u64> {
1057 self.label_index
1058 }
1059
1060 pub fn set_label_index(&mut self, label_index: Option<u64>) {
1061 self.label_index = label_index;
1062 }
1063
1064 pub fn box2d(&self) -> Option<&Box2d> {
1065 self.box2d.as_ref()
1066 }
1067
1068 pub fn set_box2d(&mut self, box2d: Option<Box2d>) {
1069 self.box2d = box2d;
1070 }
1071
1072 pub fn box3d(&self) -> Option<&Box3d> {
1073 self.box3d.as_ref()
1074 }
1075
1076 pub fn set_box3d(&mut self, box3d: Option<Box3d>) {
1077 self.box3d = box3d;
1078 }
1079
1080 pub fn mask(&self) -> Option<&Mask> {
1081 self.mask.as_ref()
1082 }
1083
1084 pub fn set_mask(&mut self, mask: Option<Mask>) {
1085 self.mask = mask;
1086 }
1087}
1088
1089#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
1090pub struct Label {
1091 id: u64,
1092 dataset_id: DatasetID,
1093 index: u64,
1094 name: String,
1095}
1096
1097impl Label {
1098 pub fn id(&self) -> u64 {
1099 self.id
1100 }
1101
1102 pub fn dataset_id(&self) -> DatasetID {
1103 self.dataset_id
1104 }
1105
1106 pub fn index(&self) -> u64 {
1107 self.index
1108 }
1109
1110 pub fn name(&self) -> &str {
1111 &self.name
1112 }
1113
1114 pub async fn remove(&self, client: &Client) -> Result<(), Error> {
1115 client.remove_label(self.id()).await
1116 }
1117
1118 pub async fn set_name(&mut self, client: &Client, name: &str) -> Result<(), Error> {
1119 self.name = name.to_string();
1120 client.update_label(self).await
1121 }
1122
1123 pub async fn set_index(&mut self, client: &Client, index: u64) -> Result<(), Error> {
1124 self.index = index;
1125 client.update_label(self).await
1126 }
1127}
1128
1129impl Display for Label {
1130 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1131 write!(f, "{}", self.name())
1132 }
1133}
1134
1135#[derive(Serialize, Clone, Debug)]
1136pub struct NewLabelObject {
1137 pub name: String,
1138}
1139
1140#[derive(Serialize, Clone, Debug)]
1141pub struct NewLabel {
1142 pub dataset_id: DatasetID,
1143 pub labels: Vec<NewLabelObject>,
1144}
1145
1146#[derive(Deserialize, Clone, Debug)]
1147#[allow(dead_code)]
1148pub struct Group {
1149 pub id: u64, pub name: String,
1151}
1152
1153#[cfg(feature = "polars")]
1154fn extract_annotation_name(ann: &Annotation) -> Option<(String, Option<u32>)> {
1155 use std::path::Path;
1156
1157 let name = ann.name.as_ref()?;
1158 let name = Path::new(name).file_stem()?.to_str()?;
1159
1160 match &ann.sequence_name {
1163 Some(sequence) => Some((sequence.clone(), ann.frame_number)),
1164 None => Some((name.to_string(), None)),
1165 }
1166}
1167
1168#[cfg(feature = "polars")]
1169fn convert_mask_to_series(mask: &Mask) -> Series {
1170 use polars::series::Series;
1171
1172 let list = flatten_polygon_coordinates(&mask.polygon);
1173 Series::new("mask".into(), list)
1174}
1175
1176#[deprecated(
1231 since = "0.8.0",
1232 note = "Use `samples_dataframe()` for complete 2025.10 schema support"
1233)]
1234#[cfg(feature = "polars")]
1235pub fn annotations_dataframe(annotations: &[Annotation]) -> Result<DataFrame, Error> {
1236 use itertools::Itertools;
1237
1238 let (names, frames, objects, labels, label_indices, groups, masks, boxes2d, boxes3d) =
1239 annotations
1240 .iter()
1241 .filter_map(|ann| {
1242 let (name, frame) = extract_annotation_name(ann)?;
1243
1244 let masks = ann.mask.as_ref().map(convert_mask_to_series);
1245
1246 let box2d = ann.box2d.as_ref().map(|box2d| {
1247 Series::new(
1248 "box2d".into(),
1249 [box2d.cx(), box2d.cy(), box2d.width(), box2d.height()],
1250 )
1251 });
1252
1253 let box3d = ann.box3d.as_ref().map(|box3d| {
1254 Series::new(
1255 "box3d".into(),
1256 [box3d.x, box3d.y, box3d.z, box3d.w, box3d.h, box3d.l],
1257 )
1258 });
1259
1260 Some((
1261 name,
1262 frame,
1263 ann.object_id().cloned(),
1264 ann.label_name.clone(),
1265 ann.label_index,
1266 ann.group.clone(),
1267 masks,
1268 box2d,
1269 box3d,
1270 ))
1271 })
1272 .multiunzip::<(
1273 Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, )>();
1283 let names = Series::new("name".into(), names).into();
1284 let frames = Series::new("frame".into(), frames).into();
1285 let objects = Series::new("object_id".into(), objects).into();
1286 let labels = Series::new("label".into(), labels)
1287 .cast(&DataType::Categorical(
1288 Categories::new("labels".into(), "labels".into(), CategoricalPhysical::U8),
1289 Arc::new(CategoricalMapping::new(u8::MAX as usize)),
1290 ))?
1291 .into();
1292 let label_indices = Series::new("label_index".into(), label_indices).into();
1293 let groups = Series::new("group".into(), groups)
1294 .cast(&DataType::Categorical(
1295 Categories::new("groups".into(), "groups".into(), CategoricalPhysical::U8),
1296 Arc::new(CategoricalMapping::new(u8::MAX as usize)),
1297 ))?
1298 .into();
1299 let masks = Series::new("mask".into(), masks)
1300 .cast(&DataType::List(Box::new(DataType::Float32)))?
1301 .into();
1302 let boxes2d = Series::new("box2d".into(), boxes2d)
1303 .cast(&DataType::Array(Box::new(DataType::Float32), 4))?
1304 .into();
1305 let boxes3d = Series::new("box3d".into(), boxes3d)
1306 .cast(&DataType::Array(Box::new(DataType::Float32), 6))?
1307 .into();
1308
1309 Ok(DataFrame::new(vec![
1310 names,
1311 frames,
1312 objects,
1313 labels,
1314 label_indices,
1315 groups,
1316 masks,
1317 boxes2d,
1318 boxes3d,
1319 ])?)
1320}
1321
1322#[cfg(feature = "polars")]
1362pub fn samples_dataframe(samples: &[Sample]) -> Result<DataFrame, Error> {
1363 let rows: Vec<_> = samples
1365 .iter()
1366 .flat_map(|sample| {
1367 let size = match (sample.width, sample.height) {
1369 (Some(w), Some(h)) => Some(vec![w, h]),
1370 _ => None,
1371 };
1372
1373 let location = sample.location.as_ref().and_then(|loc| {
1374 loc.gps
1375 .as_ref()
1376 .map(|gps| vec![gps.lat as f32, gps.lon as f32])
1377 });
1378
1379 let pose = sample.location.as_ref().and_then(|loc| {
1380 loc.imu
1381 .as_ref()
1382 .map(|imu| vec![imu.yaw as f32, imu.pitch as f32, imu.roll as f32])
1383 });
1384
1385 let degradation = sample.degradation.clone();
1386
1387 if sample.annotations.is_empty() {
1389 let (name, frame) = match extract_annotation_name_from_sample(sample) {
1390 Some(nf) => nf,
1391 None => return vec![],
1392 };
1393
1394 return vec![(
1395 name,
1396 frame,
1397 None, None, None, sample.group.clone(), None, None, None, size.clone(),
1405 location.clone(),
1406 pose.clone(),
1407 degradation.clone(),
1408 )];
1409 }
1410
1411 sample
1413 .annotations
1414 .iter()
1415 .filter_map(|ann| {
1416 let (name, frame) = extract_annotation_name(ann)?;
1417
1418 let mask = ann.mask.as_ref().map(convert_mask_to_series);
1419
1420 let box2d = ann.box2d.as_ref().map(|box2d| {
1421 Series::new(
1422 "box2d".into(),
1423 [box2d.cx(), box2d.cy(), box2d.width(), box2d.height()],
1424 )
1425 });
1426
1427 let box3d = ann.box3d.as_ref().map(|box3d| {
1428 Series::new(
1429 "box3d".into(),
1430 [box3d.x, box3d.y, box3d.z, box3d.w, box3d.h, box3d.l],
1431 )
1432 });
1433
1434 Some((
1435 name,
1436 frame,
1437 ann.object_id().cloned(),
1438 ann.label_name.clone(),
1439 ann.label_index,
1440 sample.group.clone(), mask,
1442 box2d,
1443 box3d,
1444 size.clone(),
1445 location.clone(),
1446 pose.clone(),
1447 degradation.clone(),
1448 ))
1449 })
1450 .collect::<Vec<_>>()
1451 })
1452 .collect();
1453
1454 let mut names = Vec::new();
1456 let mut frames = Vec::new();
1457 let mut objects = Vec::new();
1458 let mut labels = Vec::new();
1459 let mut label_indices = Vec::new();
1460 let mut groups = Vec::new();
1461 let mut masks = Vec::new();
1462 let mut boxes2d = Vec::new();
1463 let mut boxes3d = Vec::new();
1464 let mut sizes = Vec::new();
1465 let mut locations = Vec::new();
1466 let mut poses = Vec::new();
1467 let mut degradations = Vec::new();
1468
1469 for (
1470 name,
1471 frame,
1472 object,
1473 label,
1474 label_index,
1475 group,
1476 mask,
1477 box2d,
1478 box3d,
1479 size,
1480 location,
1481 pose,
1482 degradation,
1483 ) in rows
1484 {
1485 names.push(name);
1486 frames.push(frame);
1487 objects.push(object);
1488 labels.push(label);
1489 label_indices.push(label_index);
1490 groups.push(group);
1491 masks.push(mask);
1492 boxes2d.push(box2d);
1493 boxes3d.push(box3d);
1494 sizes.push(size);
1495 locations.push(location);
1496 poses.push(pose);
1497 degradations.push(degradation);
1498 }
1499
1500 let names = Series::new("name".into(), names).into();
1502 let frames = Series::new("frame".into(), frames).into();
1503 let objects = Series::new("object_id".into(), objects).into();
1504
1505 let labels = Series::new("label".into(), labels)
1507 .cast(&DataType::Categorical(
1508 Categories::new("labels".into(), "labels".into(), CategoricalPhysical::U8),
1509 Arc::new(CategoricalMapping::new(u8::MAX as usize)),
1510 ))?
1511 .into();
1512
1513 let label_indices = Series::new("label_index".into(), label_indices).into();
1514
1515 let groups = Series::new("group".into(), groups)
1517 .cast(&DataType::Categorical(
1518 Categories::new("groups".into(), "groups".into(), CategoricalPhysical::U8),
1519 Arc::new(CategoricalMapping::new(u8::MAX as usize)),
1520 ))?
1521 .into();
1522
1523 let masks = Series::new("mask".into(), masks)
1524 .cast(&DataType::List(Box::new(DataType::Float32)))?
1525 .into();
1526 let boxes2d = Series::new("box2d".into(), boxes2d)
1527 .cast(&DataType::Array(Box::new(DataType::Float32), 4))?
1528 .into();
1529 let boxes3d = Series::new("box3d".into(), boxes3d)
1530 .cast(&DataType::Array(Box::new(DataType::Float32), 6))?
1531 .into();
1532
1533 let size_series: Vec<Option<Series>> = sizes
1536 .into_iter()
1537 .map(|opt_vec| opt_vec.map(|vec| Series::new("size".into(), vec)))
1538 .collect();
1539 let sizes = Series::new("size".into(), size_series)
1540 .cast(&DataType::Array(Box::new(DataType::UInt32), 2))?
1541 .into();
1542
1543 let location_series: Vec<Option<Series>> = locations
1544 .into_iter()
1545 .map(|opt_vec| opt_vec.map(|vec| Series::new("location".into(), vec)))
1546 .collect();
1547 let locations = Series::new("location".into(), location_series)
1548 .cast(&DataType::Array(Box::new(DataType::Float32), 2))?
1549 .into();
1550
1551 let pose_series: Vec<Option<Series>> = poses
1552 .into_iter()
1553 .map(|opt_vec| opt_vec.map(|vec| Series::new("pose".into(), vec)))
1554 .collect();
1555 let poses = Series::new("pose".into(), pose_series)
1556 .cast(&DataType::Array(Box::new(DataType::Float32), 3))?
1557 .into();
1558
1559 let degradations = Series::new("degradation".into(), degradations).into();
1560
1561 Ok(DataFrame::new(vec![
1562 names,
1563 frames,
1564 objects,
1565 labels,
1566 label_indices,
1567 groups,
1568 masks,
1569 boxes2d,
1570 boxes3d,
1571 sizes,
1572 locations,
1573 poses,
1574 degradations,
1575 ])?)
1576}
1577
1578#[cfg(feature = "polars")]
1580fn extract_annotation_name_from_sample(sample: &Sample) -> Option<(String, Option<u32>)> {
1581 use std::path::Path;
1582
1583 let name = sample.image_name.as_ref()?;
1584 let name = Path::new(name).file_stem()?.to_str()?;
1585
1586 match &sample.sequence_name {
1589 Some(sequence) => Some((sequence.clone(), sample.frame_number)),
1590 None => Some((name.to_string(), None)),
1591 }
1592}
1593
1594fn extract_sample_name(image_name: &str) -> String {
1607 let name = image_name
1609 .rsplit_once('.')
1610 .and_then(|(name, _)| {
1611 if name.is_empty() {
1613 None
1614 } else {
1615 Some(name.to_string())
1616 }
1617 })
1618 .unwrap_or_else(|| image_name.to_string());
1619
1620 name.rsplit_once(".camera")
1622 .and_then(|(name, _)| {
1623 if name.is_empty() {
1625 None
1626 } else {
1627 Some(name.to_string())
1628 }
1629 })
1630 .unwrap_or_else(|| name.clone())
1631}
1632
1633fn resolve_file_url<'a>(
1648 file_type: &FileType,
1649 image_url: Option<&'a str>,
1650 files: &'a [SampleFile],
1651) -> Option<&'a str> {
1652 match file_type {
1653 FileType::Image => image_url,
1654 file => files
1655 .iter()
1656 .find(|f| f.r#type == file.to_string())
1657 .and_then(|f| f.url.as_deref()),
1658 }
1659}
1660
1661fn convert_files_map_to_vec(map: HashMap<String, String>) -> Vec<SampleFile> {
1675 map.into_iter()
1676 .map(|(file_type, filename)| SampleFile::with_filename(file_type, filename))
1677 .collect()
1678}
1679
1680fn convert_annotations_map_to_vec(map: HashMap<String, Vec<Annotation>>) -> Vec<Annotation> {
1689 let mut all_annotations = Vec::new();
1690 if let Some(bbox_anns) = map.get("bbox") {
1691 all_annotations.extend(bbox_anns.clone());
1692 }
1693 if let Some(box3d_anns) = map.get("box3d") {
1694 all_annotations.extend(box3d_anns.clone());
1695 }
1696 if let Some(mask_anns) = map.get("mask") {
1697 all_annotations.extend(mask_anns.clone());
1698 }
1699 all_annotations
1700}
1701
1702fn validate_gps_coordinates(lat: f64, lon: f64) -> Result<(), String> {
1722 if !lat.is_finite() {
1723 return Err(format!("GPS latitude is not finite: {}", lat));
1724 }
1725 if !lon.is_finite() {
1726 return Err(format!("GPS longitude is not finite: {}", lon));
1727 }
1728 if !(-90.0..=90.0).contains(&lat) {
1729 return Err(format!("GPS latitude out of range [-90, 90]: {}", lat));
1730 }
1731 if !(-180.0..=180.0).contains(&lon) {
1732 return Err(format!("GPS longitude out of range [-180, 180]: {}", lon));
1733 }
1734 Ok(())
1735}
1736
1737fn validate_imu_orientation(roll: f64, pitch: f64, yaw: f64) -> Result<(), String> {
1756 if !roll.is_finite() {
1757 return Err(format!("IMU roll is not finite: {}", roll));
1758 }
1759 if !pitch.is_finite() {
1760 return Err(format!("IMU pitch is not finite: {}", pitch));
1761 }
1762 if !yaw.is_finite() {
1763 return Err(format!("IMU yaw is not finite: {}", yaw));
1764 }
1765 if !(-180.0..=180.0).contains(&roll) {
1766 return Err(format!("IMU roll out of range [-180, 180]: {}", roll));
1767 }
1768 if !(-90.0..=90.0).contains(&pitch) {
1769 return Err(format!("IMU pitch out of range [-90, 90]: {}", pitch));
1770 }
1771 if !(-180.0..=180.0).contains(&yaw) {
1772 return Err(format!("IMU yaw out of range [-180, 180]: {}", yaw));
1773 }
1774 Ok(())
1775}
1776
1777#[cfg(feature = "polars")]
1789fn flatten_polygon_coordinates(polygons: &[Vec<(f32, f32)>]) -> Vec<f32> {
1790 let mut list = Vec::new();
1791
1792 for polygon in polygons {
1793 for &(x, y) in polygon {
1794 list.push(x);
1795 list.push(y);
1796 }
1797 if !polygons.is_empty() {
1799 list.push(f32::NAN);
1800 }
1801 }
1802
1803 if !list.is_empty() && list[list.len() - 1].is_nan() {
1805 list.pop();
1806 }
1807
1808 list
1809}
1810
1811#[cfg(feature = "polars")]
1835pub fn unflatten_polygon_coordinates(coords: &[f32]) -> Vec<Vec<(f32, f32)>> {
1836 let mut polygons = Vec::new();
1837 let mut current_polygon = Vec::new();
1838 let mut i = 0;
1839
1840 while i < coords.len() {
1841 if coords[i].is_nan() {
1842 if !current_polygon.is_empty() {
1844 polygons.push(current_polygon.clone());
1845 current_polygon.clear();
1846 }
1847 i += 1;
1848 } else if i + 1 < coords.len() {
1849 current_polygon.push((coords[i], coords[i + 1]));
1851 i += 2;
1852 } else {
1853 i += 1;
1855 }
1856 }
1857
1858 if !current_polygon.is_empty() {
1860 polygons.push(current_polygon);
1861 }
1862
1863 polygons
1864}
1865
1866#[cfg(test)]
1867mod tests {
1868 use super::*;
1869 use std::str::FromStr;
1870
1871 fn flatten_annotation_map(
1880 map: std::collections::HashMap<String, Vec<Annotation>>,
1881 ) -> Vec<Annotation> {
1882 let mut all_annotations = Vec::new();
1883
1884 for key in ["bbox", "box3d", "mask"] {
1886 if let Some(mut anns) = map.get(key).cloned() {
1887 all_annotations.append(&mut anns);
1888 }
1889 }
1890
1891 all_annotations
1892 }
1893
1894 fn annotation_group_field_name() -> &'static str {
1896 "group_name"
1897 }
1898
1899 fn annotation_object_id_field_name() -> &'static str {
1901 "object_reference"
1902 }
1903
1904 fn annotation_object_id_alias() -> &'static str {
1906 "object_id"
1907 }
1908
1909 fn validate_annotation_field_names(
1912 json_str: &str,
1913 expected_group: bool,
1914 expected_object_ref: bool,
1915 ) -> Result<(), String> {
1916 if expected_group && !json_str.contains("\"group_name\"") {
1917 return Err("Missing expected field: group_name".to_string());
1918 }
1919 if expected_object_ref && !json_str.contains("\"object_reference\"") {
1920 return Err("Missing expected field: object_reference".to_string());
1921 }
1922 Ok(())
1923 }
1924
1925 #[test]
1927 fn test_file_type_conversions() {
1928 let cases = vec![
1929 (FileType::Image, "image"),
1930 (FileType::LidarPcd, "lidar.pcd"),
1931 (FileType::LidarDepth, "lidar.png"),
1932 (FileType::LidarReflect, "lidar.jpg"),
1933 (FileType::RadarPcd, "radar.pcd"),
1934 (FileType::RadarCube, "radar.png"),
1935 ];
1936
1937 for (file_type, expected_str) in &cases {
1939 assert_eq!(file_type.to_string(), *expected_str);
1940 }
1941
1942 for (file_type, type_str) in &cases {
1944 assert_eq!(FileType::try_from(*type_str).unwrap(), *file_type);
1945 }
1946
1947 for (file_type, type_str) in &cases {
1949 assert_eq!(FileType::from_str(type_str).unwrap(), *file_type);
1950 }
1951
1952 assert!(FileType::try_from("invalid").is_err());
1954
1955 for (file_type, _) in &cases {
1957 let s = file_type.to_string();
1958 let parsed = FileType::try_from(s.as_str()).unwrap();
1959 assert_eq!(parsed, *file_type);
1960 }
1961 }
1962
1963 #[test]
1965 fn test_annotation_type_conversions() {
1966 let cases = vec![
1967 (AnnotationType::Box2d, "box2d"),
1968 (AnnotationType::Box3d, "box3d"),
1969 (AnnotationType::Mask, "mask"),
1970 ];
1971
1972 for (ann_type, expected_str) in &cases {
1974 assert_eq!(ann_type.to_string(), *expected_str);
1975 }
1976
1977 for (ann_type, type_str) in &cases {
1979 assert_eq!(AnnotationType::try_from(*type_str).unwrap(), *ann_type);
1980 }
1981
1982 assert_eq!(
1984 AnnotationType::from("box2d".to_string()),
1985 AnnotationType::Box2d
1986 );
1987 assert_eq!(
1988 AnnotationType::from("box3d".to_string()),
1989 AnnotationType::Box3d
1990 );
1991 assert_eq!(
1992 AnnotationType::from("mask".to_string()),
1993 AnnotationType::Mask
1994 );
1995
1996 assert_eq!(
1998 AnnotationType::from("invalid".to_string()),
1999 AnnotationType::Box2d
2000 );
2001
2002 assert!(AnnotationType::try_from("invalid").is_err());
2004
2005 for (ann_type, _) in &cases {
2007 let s = ann_type.to_string();
2008 let parsed = AnnotationType::try_from(s.as_str()).unwrap();
2009 assert_eq!(parsed, *ann_type);
2010 }
2011 }
2012
2013 #[test]
2015 fn test_extract_sample_name_with_extension_and_camera() {
2016 assert_eq!(extract_sample_name("scene_001.camera.jpg"), "scene_001");
2017 }
2018
2019 #[test]
2020 fn test_extract_sample_name_multiple_dots() {
2021 assert_eq!(extract_sample_name("image.v2.camera.png"), "image.v2");
2022 }
2023
2024 #[test]
2025 fn test_extract_sample_name_extension_only() {
2026 assert_eq!(extract_sample_name("test.jpg"), "test");
2027 }
2028
2029 #[test]
2030 fn test_extract_sample_name_no_extension() {
2031 assert_eq!(extract_sample_name("test"), "test");
2032 }
2033
2034 #[test]
2035 fn test_extract_sample_name_edge_case_dot_prefix() {
2036 assert_eq!(extract_sample_name(".jpg"), ".jpg");
2037 }
2038
2039 #[test]
2041 fn test_resolve_file_url_image_type() {
2042 let image_url = Some("https://example.com/image.jpg");
2043 let files = vec![];
2044 let result = resolve_file_url(&FileType::Image, image_url, &files);
2045 assert_eq!(result, Some("https://example.com/image.jpg"));
2046 }
2047
2048 #[test]
2049 fn test_resolve_file_url_lidar_pcd() {
2050 let image_url = Some("https://example.com/image.jpg");
2051 let files = vec![
2052 SampleFile::with_url(
2053 "lidar.pcd".to_string(),
2054 "https://example.com/file.pcd".to_string(),
2055 ),
2056 SampleFile::with_url(
2057 "radar.pcd".to_string(),
2058 "https://example.com/radar.pcd".to_string(),
2059 ),
2060 ];
2061 let result = resolve_file_url(&FileType::LidarPcd, image_url, &files);
2062 assert_eq!(result, Some("https://example.com/file.pcd"));
2063 }
2064
2065 #[test]
2066 fn test_resolve_file_url_not_found() {
2067 let image_url = Some("https://example.com/image.jpg");
2068 let files = vec![SampleFile::with_url(
2069 "lidar.pcd".to_string(),
2070 "https://example.com/file.pcd".to_string(),
2071 )];
2072 let result = resolve_file_url(&FileType::RadarPcd, image_url, &files);
2074 assert_eq!(result, None);
2075 }
2076
2077 #[test]
2078 fn test_resolve_file_url_no_image_url() {
2079 let image_url = None;
2080 let files = vec![];
2081 let result = resolve_file_url(&FileType::Image, image_url, &files);
2082 assert_eq!(result, None);
2083 }
2084
2085 #[test]
2087 fn test_convert_files_map_to_vec_single_file() {
2088 let mut map = HashMap::new();
2089 map.insert("lidar.pcd".to_string(), "scan001.pcd".to_string());
2090
2091 let files = convert_files_map_to_vec(map);
2092 assert_eq!(files.len(), 1);
2093 assert_eq!(files[0].file_type(), "lidar.pcd");
2094 assert_eq!(files[0].filename(), Some("scan001.pcd"));
2095 }
2096
2097 #[test]
2098 fn test_convert_files_map_to_vec_multiple_files() {
2099 let mut map = HashMap::new();
2100 map.insert("lidar.pcd".to_string(), "scan.pcd".to_string());
2101 map.insert("radar.pcd".to_string(), "radar.pcd".to_string());
2102
2103 let files = convert_files_map_to_vec(map);
2104 assert_eq!(files.len(), 2);
2105 }
2106
2107 #[test]
2108 fn test_convert_files_map_to_vec_empty() {
2109 let map = HashMap::new();
2110 let files = convert_files_map_to_vec(map);
2111 assert_eq!(files.len(), 0);
2112 }
2113
2114 #[test]
2115 fn test_convert_annotations_map_to_vec_with_bbox() {
2116 let mut map = HashMap::new();
2117 let bbox_ann = Annotation::new();
2118 map.insert("bbox".to_string(), vec![bbox_ann.clone()]);
2119
2120 let annotations = convert_annotations_map_to_vec(map);
2121 assert_eq!(annotations.len(), 1);
2122 }
2123
2124 #[test]
2125 fn test_convert_annotations_map_to_vec_all_types() {
2126 let mut map = HashMap::new();
2127 map.insert("bbox".to_string(), vec![Annotation::new()]);
2128 map.insert("box3d".to_string(), vec![Annotation::new()]);
2129 map.insert("mask".to_string(), vec![Annotation::new()]);
2130
2131 let annotations = convert_annotations_map_to_vec(map);
2132 assert_eq!(annotations.len(), 3);
2133 }
2134
2135 #[test]
2136 fn test_convert_annotations_map_to_vec_empty() {
2137 let map = HashMap::new();
2138 let annotations = convert_annotations_map_to_vec(map);
2139 assert_eq!(annotations.len(), 0);
2140 }
2141
2142 #[test]
2143 fn test_convert_annotations_map_to_vec_unknown_type_ignored() {
2144 let mut map = HashMap::new();
2145 map.insert("unknown".to_string(), vec![Annotation::new()]);
2146
2147 let annotations = convert_annotations_map_to_vec(map);
2148 assert_eq!(annotations.len(), 0);
2150 }
2151
2152 #[test]
2154 fn test_annotation_group_field_name() {
2155 assert_eq!(annotation_group_field_name(), "group_name");
2156 }
2157
2158 #[test]
2159 fn test_annotation_object_id_field_name() {
2160 assert_eq!(annotation_object_id_field_name(), "object_reference");
2161 }
2162
2163 #[test]
2164 fn test_annotation_object_id_alias() {
2165 assert_eq!(annotation_object_id_alias(), "object_id");
2166 }
2167
2168 #[test]
2169 fn test_validate_annotation_field_names_success() {
2170 let json = r#"{"group_name":"train","object_reference":"obj1"}"#;
2171 assert!(validate_annotation_field_names(json, true, true).is_ok());
2172 }
2173
2174 #[test]
2175 fn test_validate_annotation_field_names_missing_group() {
2176 let json = r#"{"object_reference":"obj1"}"#;
2177 let result = validate_annotation_field_names(json, true, false);
2178 assert!(result.is_err());
2179 assert!(result.unwrap_err().contains("group_name"));
2180 }
2181
2182 #[test]
2183 fn test_validate_annotation_field_names_missing_object_ref() {
2184 let json = r#"{"group_name":"train"}"#;
2185 let result = validate_annotation_field_names(json, false, true);
2186 assert!(result.is_err());
2187 assert!(result.unwrap_err().contains("object_reference"));
2188 }
2189
2190 #[test]
2191 fn test_annotation_serialization_field_names() {
2192 let mut ann = Annotation::new();
2194 ann.set_group(Some("train".to_string()));
2195 ann.set_object_id(Some("obj1".to_string()));
2196
2197 let json = serde_json::to_string(&ann).unwrap();
2198 assert!(validate_annotation_field_names(&json, true, true).is_ok());
2200 }
2201
2202 #[test]
2204 fn test_validate_gps_coordinates_valid() {
2205 assert!(validate_gps_coordinates(37.7749, -122.4194).is_ok()); assert!(validate_gps_coordinates(0.0, 0.0).is_ok()); assert!(validate_gps_coordinates(90.0, 180.0).is_ok()); assert!(validate_gps_coordinates(-90.0, -180.0).is_ok()); }
2210
2211 #[test]
2212 fn test_validate_gps_coordinates_invalid_latitude() {
2213 let result = validate_gps_coordinates(91.0, 0.0);
2214 assert!(result.is_err());
2215 assert!(result.unwrap_err().contains("latitude out of range"));
2216
2217 let result = validate_gps_coordinates(-91.0, 0.0);
2218 assert!(result.is_err());
2219 assert!(result.unwrap_err().contains("latitude out of range"));
2220 }
2221
2222 #[test]
2223 fn test_validate_gps_coordinates_invalid_longitude() {
2224 let result = validate_gps_coordinates(0.0, 181.0);
2225 assert!(result.is_err());
2226 assert!(result.unwrap_err().contains("longitude out of range"));
2227
2228 let result = validate_gps_coordinates(0.0, -181.0);
2229 assert!(result.is_err());
2230 assert!(result.unwrap_err().contains("longitude out of range"));
2231 }
2232
2233 #[test]
2234 fn test_validate_gps_coordinates_non_finite() {
2235 let result = validate_gps_coordinates(f64::NAN, 0.0);
2236 assert!(result.is_err());
2237 assert!(result.unwrap_err().contains("not finite"));
2238
2239 let result = validate_gps_coordinates(0.0, f64::INFINITY);
2240 assert!(result.is_err());
2241 assert!(result.unwrap_err().contains("not finite"));
2242 }
2243
2244 #[test]
2245 fn test_validate_imu_orientation_valid() {
2246 assert!(validate_imu_orientation(0.0, 0.0, 0.0).is_ok());
2247 assert!(validate_imu_orientation(45.0, 30.0, 90.0).is_ok());
2248 assert!(validate_imu_orientation(180.0, 90.0, -180.0).is_ok()); assert!(validate_imu_orientation(-180.0, -90.0, 180.0).is_ok()); }
2251
2252 #[test]
2253 fn test_validate_imu_orientation_invalid_roll() {
2254 let result = validate_imu_orientation(181.0, 0.0, 0.0);
2255 assert!(result.is_err());
2256 assert!(result.unwrap_err().contains("roll out of range"));
2257
2258 let result = validate_imu_orientation(-181.0, 0.0, 0.0);
2259 assert!(result.is_err());
2260 }
2261
2262 #[test]
2263 fn test_validate_imu_orientation_invalid_pitch() {
2264 let result = validate_imu_orientation(0.0, 91.0, 0.0);
2265 assert!(result.is_err());
2266 assert!(result.unwrap_err().contains("pitch out of range"));
2267
2268 let result = validate_imu_orientation(0.0, -91.0, 0.0);
2269 assert!(result.is_err());
2270 }
2271
2272 #[test]
2273 fn test_validate_imu_orientation_non_finite() {
2274 let result = validate_imu_orientation(f64::NAN, 0.0, 0.0);
2275 assert!(result.is_err());
2276 assert!(result.unwrap_err().contains("not finite"));
2277
2278 let result = validate_imu_orientation(0.0, f64::INFINITY, 0.0);
2279 assert!(result.is_err());
2280
2281 let result = validate_imu_orientation(0.0, 0.0, f64::NEG_INFINITY);
2282 assert!(result.is_err());
2283 }
2284
2285 #[test]
2287 #[cfg(feature = "polars")]
2288 fn test_flatten_polygon_coordinates_single_polygon() {
2289 let polygons = vec![vec![(1.0, 2.0), (3.0, 4.0)]];
2290 let result = flatten_polygon_coordinates(&polygons);
2291
2292 assert_eq!(result.len(), 4);
2294 assert_eq!(&result[..4], &[1.0, 2.0, 3.0, 4.0]);
2295 }
2296
2297 #[test]
2298 #[cfg(feature = "polars")]
2299 fn test_flatten_polygon_coordinates_multiple_polygons() {
2300 let polygons = vec![vec![(1.0, 2.0), (3.0, 4.0)], vec![(5.0, 6.0), (7.0, 8.0)]];
2301 let result = flatten_polygon_coordinates(&polygons);
2302
2303 assert_eq!(result.len(), 9);
2305 assert_eq!(&result[..4], &[1.0, 2.0, 3.0, 4.0]);
2306 assert!(result[4].is_nan()); assert_eq!(&result[5..9], &[5.0, 6.0, 7.0, 8.0]);
2308 }
2309
2310 #[test]
2311 #[cfg(feature = "polars")]
2312 fn test_flatten_polygon_coordinates_empty() {
2313 let polygons: Vec<Vec<(f32, f32)>> = vec![];
2314 let result = flatten_polygon_coordinates(&polygons);
2315
2316 assert_eq!(result.len(), 0);
2317 }
2318
2319 #[test]
2321 #[cfg(feature = "polars")]
2322 fn test_unflatten_polygon_coordinates_single_polygon() {
2323 let coords = vec![1.0, 2.0, 3.0, 4.0];
2324 let result = unflatten_polygon_coordinates(&coords);
2325
2326 assert_eq!(result.len(), 1);
2327 assert_eq!(result[0].len(), 2);
2328 assert_eq!(result[0][0], (1.0, 2.0));
2329 assert_eq!(result[0][1], (3.0, 4.0));
2330 }
2331
2332 #[test]
2333 #[cfg(feature = "polars")]
2334 fn test_unflatten_polygon_coordinates_multiple_polygons() {
2335 let coords = vec![1.0, 2.0, 3.0, 4.0, f32::NAN, 5.0, 6.0, 7.0, 8.0];
2336 let result = unflatten_polygon_coordinates(&coords);
2337
2338 assert_eq!(result.len(), 2);
2339 assert_eq!(result[0].len(), 2);
2340 assert_eq!(result[0][0], (1.0, 2.0));
2341 assert_eq!(result[0][1], (3.0, 4.0));
2342 assert_eq!(result[1].len(), 2);
2343 assert_eq!(result[1][0], (5.0, 6.0));
2344 assert_eq!(result[1][1], (7.0, 8.0));
2345 }
2346
2347 #[test]
2348 #[cfg(feature = "polars")]
2349 fn test_unflatten_polygon_coordinates_roundtrip() {
2350 let original = vec![vec![(1.0, 2.0), (3.0, 4.0)], vec![(5.0, 6.0), (7.0, 8.0)]];
2352 let flattened = flatten_polygon_coordinates(&original);
2353 let result = unflatten_polygon_coordinates(&flattened);
2354
2355 assert_eq!(result, original);
2356 }
2357
2358 #[test]
2360 fn test_flatten_annotation_map_all_types() {
2361 use std::collections::HashMap;
2362
2363 let mut map = HashMap::new();
2364
2365 let mut bbox_ann = Annotation::new();
2367 bbox_ann.set_label(Some("bbox_label".to_string()));
2368
2369 let mut box3d_ann = Annotation::new();
2370 box3d_ann.set_label(Some("box3d_label".to_string()));
2371
2372 let mut mask_ann = Annotation::new();
2373 mask_ann.set_label(Some("mask_label".to_string()));
2374
2375 map.insert("bbox".to_string(), vec![bbox_ann.clone()]);
2376 map.insert("box3d".to_string(), vec![box3d_ann.clone()]);
2377 map.insert("mask".to_string(), vec![mask_ann.clone()]);
2378
2379 let result = flatten_annotation_map(map);
2380
2381 assert_eq!(result.len(), 3);
2382 assert_eq!(result[0].label(), Some(&"bbox_label".to_string()));
2384 assert_eq!(result[1].label(), Some(&"box3d_label".to_string()));
2385 assert_eq!(result[2].label(), Some(&"mask_label".to_string()));
2386 }
2387
2388 #[test]
2389 fn test_flatten_annotation_map_single_type() {
2390 use std::collections::HashMap;
2391
2392 let mut map = HashMap::new();
2393 let mut bbox_ann = Annotation::new();
2394 bbox_ann.set_label(Some("test".to_string()));
2395 map.insert("bbox".to_string(), vec![bbox_ann]);
2396
2397 let result = flatten_annotation_map(map);
2398
2399 assert_eq!(result.len(), 1);
2400 assert_eq!(result[0].label(), Some(&"test".to_string()));
2401 }
2402
2403 #[test]
2404 fn test_flatten_annotation_map_empty() {
2405 use std::collections::HashMap;
2406
2407 let map = HashMap::new();
2408 let result = flatten_annotation_map(map);
2409
2410 assert_eq!(result.len(), 0);
2411 }
2412
2413 #[test]
2414 fn test_flatten_annotation_map_deterministic_order() {
2415 use std::collections::HashMap;
2416
2417 let mut map = HashMap::new();
2418
2419 let mut bbox_ann = Annotation::new();
2420 bbox_ann.set_label(Some("bbox".to_string()));
2421
2422 let mut box3d_ann = Annotation::new();
2423 box3d_ann.set_label(Some("box3d".to_string()));
2424
2425 let mut mask_ann = Annotation::new();
2426 mask_ann.set_label(Some("mask".to_string()));
2427
2428 map.insert("mask".to_string(), vec![mask_ann]);
2430 map.insert("box3d".to_string(), vec![box3d_ann]);
2431 map.insert("bbox".to_string(), vec![bbox_ann]);
2432
2433 let result = flatten_annotation_map(map);
2434
2435 assert_eq!(result.len(), 3);
2437 assert_eq!(result[0].label(), Some(&"bbox".to_string()));
2438 assert_eq!(result[1].label(), Some(&"box3d".to_string()));
2439 assert_eq!(result[2].label(), Some(&"mask".to_string()));
2440 }
2441
2442 #[test]
2444 fn test_box2d_construction_and_accessors() {
2445 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
2447 assert_eq!(
2448 (bbox.left(), bbox.top(), bbox.width(), bbox.height()),
2449 (10.0, 20.0, 100.0, 50.0)
2450 );
2451
2452 assert_eq!((bbox.cx(), bbox.cy()), (60.0, 45.0)); let bbox = Box2d::new(0.0, 0.0, 640.0, 480.0);
2457 assert_eq!(
2458 (bbox.left(), bbox.top(), bbox.width(), bbox.height()),
2459 (0.0, 0.0, 640.0, 480.0)
2460 );
2461 assert_eq!((bbox.cx(), bbox.cy()), (320.0, 240.0));
2462 }
2463
2464 #[test]
2465 fn test_box2d_center_calculation() {
2466 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
2467
2468 assert_eq!(bbox.cx(), 60.0); assert_eq!(bbox.cy(), 45.0); }
2472
2473 #[test]
2474 fn test_box2d_zero_dimensions() {
2475 let bbox = Box2d::new(10.0, 20.0, 0.0, 0.0);
2476
2477 assert_eq!(bbox.cx(), 10.0);
2479 assert_eq!(bbox.cy(), 20.0);
2480 }
2481
2482 #[test]
2483 fn test_box2d_negative_dimensions() {
2484 let bbox = Box2d::new(100.0, 100.0, -50.0, -50.0);
2485
2486 assert_eq!(bbox.width(), -50.0);
2488 assert_eq!(bbox.height(), -50.0);
2489 assert_eq!(bbox.cx(), 75.0); assert_eq!(bbox.cy(), 75.0); }
2492
2493 #[test]
2495 fn test_box3d_construction_and_accessors() {
2496 let bbox = Box3d::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
2498 assert_eq!((bbox.cx(), bbox.cy(), bbox.cz()), (1.0, 2.0, 3.0));
2499 assert_eq!(
2500 (bbox.width(), bbox.height(), bbox.length()),
2501 (4.0, 5.0, 6.0)
2502 );
2503
2504 let bbox = Box3d::new(10.0, 20.0, 30.0, 4.0, 6.0, 8.0);
2506 assert_eq!((bbox.left(), bbox.top(), bbox.front()), (8.0, 17.0, 26.0)); let bbox = Box3d::new(0.0, 0.0, 0.0, 2.0, 3.0, 4.0);
2510 assert_eq!((bbox.cx(), bbox.cy(), bbox.cz()), (0.0, 0.0, 0.0));
2511 assert_eq!(
2512 (bbox.width(), bbox.height(), bbox.length()),
2513 (2.0, 3.0, 4.0)
2514 );
2515 assert_eq!((bbox.left(), bbox.top(), bbox.front()), (-1.0, -1.5, -2.0));
2516 }
2517
2518 #[test]
2519 fn test_box3d_center_calculation() {
2520 let bbox = Box3d::new(10.0, 20.0, 30.0, 100.0, 50.0, 40.0);
2521
2522 assert_eq!(bbox.cx(), 10.0);
2524 assert_eq!(bbox.cy(), 20.0);
2525 assert_eq!(bbox.cz(), 30.0);
2526 }
2527
2528 #[test]
2529 fn test_box3d_zero_dimensions() {
2530 let bbox = Box3d::new(5.0, 10.0, 15.0, 0.0, 0.0, 0.0);
2531
2532 assert_eq!(bbox.cx(), 5.0);
2534 assert_eq!(bbox.cy(), 10.0);
2535 assert_eq!(bbox.cz(), 15.0);
2536 assert_eq!((bbox.left(), bbox.top(), bbox.front()), (5.0, 10.0, 15.0));
2537 }
2538
2539 #[test]
2540 fn test_box3d_negative_dimensions() {
2541 let bbox = Box3d::new(100.0, 100.0, 100.0, -50.0, -50.0, -50.0);
2542
2543 assert_eq!(bbox.width(), -50.0);
2545 assert_eq!(bbox.height(), -50.0);
2546 assert_eq!(bbox.length(), -50.0);
2547 assert_eq!(
2548 (bbox.left(), bbox.top(), bbox.front()),
2549 (125.0, 125.0, 125.0)
2550 );
2551 }
2552
2553 #[test]
2555 fn test_mask_creation_and_deserialization() {
2556 let polygon = vec![vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]];
2558 let mask = Mask::new(polygon.clone());
2559 assert_eq!(mask.polygon, polygon);
2560
2561 let legacy = serde_json::json!({
2563 "mask": {
2564 "polygon": [[
2565 [0.0_f32, 0.0_f32],
2566 [1.0_f32, 0.0_f32],
2567 [1.0_f32, 1.0_f32]
2568 ]]
2569 }
2570 });
2571
2572 #[derive(serde::Deserialize)]
2573 struct Wrapper {
2574 mask: Mask,
2575 }
2576
2577 let parsed: Wrapper = serde_json::from_value(legacy).unwrap();
2578 assert_eq!(parsed.mask.polygon.len(), 1);
2579 assert_eq!(parsed.mask.polygon[0].len(), 3);
2580 }
2581
2582 #[test]
2584 fn test_sample_construction_and_accessors() {
2585 let sample = Sample::new();
2587 assert_eq!(sample.id(), None);
2588 assert_eq!(sample.image_name(), None);
2589 assert_eq!(sample.width(), None);
2590 assert_eq!(sample.height(), None);
2591
2592 let mut sample = Sample::new();
2594 sample.image_name = Some("test.jpg".to_string());
2595 sample.width = Some(1920);
2596 sample.height = Some(1080);
2597 sample.group = Some("group1".to_string());
2598
2599 assert_eq!(sample.image_name(), Some("test.jpg"));
2600 assert_eq!(sample.width(), Some(1920));
2601 assert_eq!(sample.height(), Some(1080));
2602 assert_eq!(sample.group(), Some(&"group1".to_string()));
2603 }
2604
2605 #[test]
2606 fn test_sample_name_extraction_from_image_name() {
2607 let mut sample = Sample::new();
2608
2609 sample.image_name = Some("test_image.jpg".to_string());
2611 assert_eq!(sample.name(), Some("test_image".to_string()));
2612
2613 sample.image_name = Some("test_image.camera.jpg".to_string());
2615 assert_eq!(sample.name(), Some("test_image".to_string()));
2616
2617 sample.image_name = Some("test_image".to_string());
2619 assert_eq!(sample.name(), Some("test_image".to_string()));
2620 }
2621
2622 #[test]
2624 fn test_annotation_construction_and_setters() {
2625 let ann = Annotation::new();
2627 assert_eq!(ann.sample_id(), None);
2628 assert_eq!(ann.label(), None);
2629 assert_eq!(ann.box2d(), None);
2630 assert_eq!(ann.box3d(), None);
2631 assert_eq!(ann.mask(), None);
2632
2633 let mut ann = Annotation::new();
2635 ann.set_label(Some("car".to_string()));
2636 assert_eq!(ann.label(), Some(&"car".to_string()));
2637
2638 ann.set_label_index(Some(42));
2639 assert_eq!(ann.label_index(), Some(42));
2640
2641 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
2643 ann.set_box2d(Some(bbox.clone()));
2644 assert!(ann.box2d().is_some());
2645 assert_eq!(ann.box2d().unwrap().left(), 10.0);
2646 }
2647
2648 #[test]
2650 fn test_sample_file_with_url_and_filename() {
2651 let file = SampleFile::with_url(
2653 "lidar.pcd".to_string(),
2654 "https://example.com/file.pcd".to_string(),
2655 );
2656 assert_eq!(file.file_type(), "lidar.pcd");
2657 assert_eq!(file.url(), Some("https://example.com/file.pcd"));
2658 assert_eq!(file.filename(), None);
2659
2660 let file = SampleFile::with_filename("image".to_string(), "test.jpg".to_string());
2662 assert_eq!(file.file_type(), "image");
2663 assert_eq!(file.filename(), Some("test.jpg"));
2664 assert_eq!(file.url(), None);
2665 }
2666
2667 #[test]
2669 fn test_label_deserialization_and_accessors() {
2670 use serde_json::json;
2671
2672 let label_json = json!({
2674 "id": 123,
2675 "dataset_id": 456,
2676 "index": 5,
2677 "name": "car"
2678 });
2679
2680 let label: Label = serde_json::from_value(label_json).unwrap();
2681 assert_eq!(label.id(), 123);
2682 assert_eq!(label.index(), 5);
2683 assert_eq!(label.name(), "car");
2684 assert_eq!(label.to_string(), "car");
2685 assert_eq!(format!("{}", label), "car");
2686
2687 let label_json = json!({
2689 "id": 1,
2690 "dataset_id": 100,
2691 "index": 0,
2692 "name": "person"
2693 });
2694
2695 let label: Label = serde_json::from_value(label_json).unwrap();
2696 assert_eq!(format!("{}", label), "person");
2697 }
2698
2699 #[test]
2701 fn test_annotation_serialization_with_mask_and_box() {
2702 let polygon = vec![vec![
2703 (0.0_f32, 0.0_f32),
2704 (1.0_f32, 0.0_f32),
2705 (1.0_f32, 1.0_f32),
2706 ]];
2707
2708 let mut annotation = Annotation::new();
2709 annotation.set_label(Some("test".to_string()));
2710 annotation.set_box2d(Some(Box2d::new(10.0, 20.0, 30.0, 40.0)));
2711 annotation.set_mask(Some(Mask::new(polygon)));
2712
2713 let mut sample = Sample::new();
2714 sample.annotations.push(annotation);
2715
2716 let json = serde_json::to_value(&sample).unwrap();
2717 let annotations = json
2718 .get("annotations")
2719 .and_then(|value| value.as_array())
2720 .expect("annotations serialized as array");
2721 assert_eq!(annotations.len(), 1);
2722
2723 let annotation_json = annotations[0].as_object().expect("annotation object");
2724 assert!(annotation_json.contains_key("box2d"));
2725 assert!(annotation_json.contains_key("mask"));
2726 assert!(!annotation_json.contains_key("x"));
2727 assert!(
2728 annotation_json
2729 .get("mask")
2730 .and_then(|value| value.as_array())
2731 .is_some()
2732 );
2733 }
2734
2735 #[test]
2736 fn test_frame_number_negative_one_deserializes_as_none() {
2737 let json = r#"{
2740 "uuid": "test-uuid",
2741 "frame_number": -1
2742 }"#;
2743
2744 let sample: Sample = serde_json::from_str(json).unwrap();
2745 assert_eq!(sample.frame_number, None);
2746 }
2747
2748 #[test]
2749 fn test_frame_number_positive_value_deserializes_correctly() {
2750 let json = r#"{
2752 "uuid": "test-uuid",
2753 "frame_number": 5
2754 }"#;
2755
2756 let sample: Sample = serde_json::from_str(json).unwrap();
2757 assert_eq!(sample.frame_number, Some(5));
2758 }
2759
2760 #[test]
2761 fn test_frame_number_null_deserializes_as_none() {
2762 let json = r#"{
2764 "uuid": "test-uuid",
2765 "frame_number": null
2766 }"#;
2767
2768 let sample: Sample = serde_json::from_str(json).unwrap();
2769 assert_eq!(sample.frame_number, None);
2770 }
2771
2772 #[test]
2773 fn test_frame_number_missing_deserializes_as_none() {
2774 let json = r#"{
2776 "uuid": "test-uuid"
2777 }"#;
2778
2779 let sample: Sample = serde_json::from_str(json).unwrap();
2780 assert_eq!(sample.frame_number, None);
2781 }
2782}