1use std::{collections::HashMap, fmt::Display};
5
6use crate::{
7 Client, Error,
8 api::{AnnotationSetID, DatasetID, ProjectID, SampleID},
9 mask::MaskData,
10};
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13
14#[cfg(feature = "polars")]
15use polars::prelude::*;
16
17#[derive(Clone, Eq, PartialEq, Debug)]
52pub enum FileType {
53 Image,
55 LidarPcd,
57 LidarDepth,
59 LidarReflect,
61 RadarPcd,
63 RadarCube,
65 All,
67}
68
69impl std::fmt::Display for FileType {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 let value = match self {
74 FileType::Image => "image",
75 FileType::LidarPcd => "lidar.pcd",
76 FileType::LidarDepth => "lidar.depth",
77 FileType::LidarReflect => "lidar.reflect",
78 FileType::RadarPcd => "radar.pcd",
79 FileType::RadarCube => "radar.png",
80 FileType::All => "all",
81 };
82 write!(f, "{}", value)
83 }
84}
85
86impl FileType {
87 pub fn file_extension(&self) -> &'static str {
90 match self {
91 FileType::Image => "jpg", FileType::LidarPcd => "lidar.pcd",
93 FileType::LidarDepth => "lidar.png",
94 FileType::LidarReflect => "lidar.jpg",
95 FileType::RadarPcd => "radar.pcd",
96 FileType::RadarCube => "radar.png",
97 FileType::All => "",
98 }
99 }
100}
101
102impl TryFrom<&str> for FileType {
103 type Error = crate::Error;
104
105 fn try_from(s: &str) -> Result<Self, Self::Error> {
106 match s {
110 "image" => Ok(FileType::Image),
111 "lidar.pcd" => Ok(FileType::LidarPcd),
112 "lidar.png" | "lidar.depth" | "depth.png" | "depthmap" => Ok(FileType::LidarDepth),
114 "lidar.jpg" | "lidar.jpeg" | "lidar.reflect" => Ok(FileType::LidarReflect),
115 "radar.pcd" | "pcd" => Ok(FileType::RadarPcd),
116 "radar.png" | "cube" => Ok(FileType::RadarCube),
117 "all" => Ok(FileType::All),
118 _ => Err(crate::Error::InvalidFileType(s.to_string())),
119 }
120 }
121}
122
123impl std::str::FromStr for FileType {
124 type Err = crate::Error;
125
126 fn from_str(s: &str) -> Result<Self, Self::Err> {
127 s.try_into()
128 }
129}
130
131impl FileType {
132 pub fn all_sensor_types() -> Vec<FileType> {
147 vec![
148 FileType::Image,
149 FileType::LidarPcd,
150 FileType::LidarDepth,
151 FileType::LidarReflect,
152 FileType::RadarPcd,
153 FileType::RadarCube,
154 ]
155 }
156
157 pub fn type_names() -> Vec<&'static str> {
169 vec![
170 "image",
171 "lidar.pcd",
172 "lidar.png",
173 "lidar.jpg",
174 "radar.pcd",
175 "radar.png",
176 "all",
177 ]
178 }
179
180 pub fn expand_types(types: &[FileType]) -> Vec<FileType> {
200 if types.contains(&FileType::All) {
201 FileType::all_sensor_types()
202 } else {
203 types.to_vec()
204 }
205 }
206}
207
208#[derive(Clone, Eq, PartialEq, Debug)]
240pub enum AnnotationType {
241 Box2d,
243 Box3d,
245 Polygon,
247 Mask,
249}
250
251impl TryFrom<&str> for AnnotationType {
252 type Error = crate::Error;
253
254 fn try_from(s: &str) -> Result<Self, Self::Error> {
255 match s {
256 "box2d" => Ok(AnnotationType::Box2d),
257 "box3d" => Ok(AnnotationType::Box3d),
258 "polygon" => Ok(AnnotationType::Polygon),
259 "seg" => Ok(AnnotationType::Polygon),
260 "mask" => Ok(AnnotationType::Polygon), "raster" => Ok(AnnotationType::Mask),
262 _ => Err(crate::Error::InvalidAnnotationType(s.to_string())),
263 }
264 }
265}
266
267impl From<String> for AnnotationType {
268 fn from(s: String) -> Self {
269 s.as_str().try_into().unwrap_or(AnnotationType::Box2d)
271 }
272}
273
274impl From<&String> for AnnotationType {
275 fn from(s: &String) -> Self {
276 s.as_str().try_into().unwrap_or(AnnotationType::Box2d)
278 }
279}
280
281impl AnnotationType {
282 pub fn as_server_type(&self) -> &'static str {
293 match self {
294 AnnotationType::Box2d => "box2d",
295 AnnotationType::Box3d => "box3d",
296 AnnotationType::Polygon => "mask",
297 AnnotationType::Mask => "mask",
298 }
299 }
300}
301
302impl std::fmt::Display for AnnotationType {
303 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304 let value = match self {
305 AnnotationType::Box2d => "box2d",
306 AnnotationType::Box3d => "box3d",
307 AnnotationType::Polygon => "polygon",
308 AnnotationType::Mask => "mask",
309 };
310 write!(f, "{}", value)
311 }
312}
313
314#[derive(Deserialize, Clone, Debug)]
353pub struct Dataset {
354 id: DatasetID,
355 project_id: ProjectID,
356 name: String,
357 description: String,
358 cloud_key: String,
359 #[serde(rename = "createdAt")]
360 created: DateTime<Utc>,
361}
362
363impl Display for Dataset {
364 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
365 write!(f, "{} {}", self.id, self.name)
366 }
367}
368
369impl Dataset {
370 pub fn id(&self) -> DatasetID {
371 self.id
372 }
373
374 pub fn project_id(&self) -> ProjectID {
375 self.project_id
376 }
377
378 pub fn name(&self) -> &str {
379 &self.name
380 }
381
382 pub fn description(&self) -> &str {
383 &self.description
384 }
385
386 pub fn cloud_key(&self) -> &str {
387 &self.cloud_key
388 }
389
390 pub fn created(&self) -> &DateTime<Utc> {
391 &self.created
392 }
393
394 pub async fn project(&self, client: &Client) -> Result<crate::api::Project, Error> {
395 client.project(self.project_id).await
396 }
397
398 pub async fn annotation_sets(&self, client: &Client) -> Result<Vec<AnnotationSet>, Error> {
399 client.annotation_sets(self.id).await
400 }
401
402 pub async fn labels(&self, client: &Client) -> Result<Vec<Label>, Error> {
403 client.labels(self.id).await
404 }
405
406 pub async fn add_label(&self, client: &Client, name: &str) -> Result<(), Error> {
407 client.add_label(self.id, name).await
408 }
409
410 pub async fn add_label_with_index(
411 &self,
412 client: &Client,
413 name: &str,
414 index: u64,
415 ) -> Result<(), Error> {
416 client.add_label_with_index(self.id, name, index).await
417 }
418
419 pub async fn remove_label(&self, client: &Client, name: &str) -> Result<(), Error> {
420 let labels = self.labels(client).await?;
421 let label = labels
422 .iter()
423 .find(|l| l.name() == name)
424 .ok_or_else(|| Error::MissingLabel(name.to_string()))?;
425 client.remove_label(label.id()).await
426 }
427}
428
429#[derive(Deserialize)]
433pub struct AnnotationSet {
434 id: AnnotationSetID,
435 dataset_id: DatasetID,
436 name: String,
437 description: String,
438 #[serde(rename = "date")]
439 created: DateTime<Utc>,
440}
441
442impl Display for AnnotationSet {
443 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
444 write!(f, "{} {}", self.id, self.name)
445 }
446}
447
448impl AnnotationSet {
449 pub fn id(&self) -> AnnotationSetID {
450 self.id
451 }
452
453 pub fn dataset_id(&self) -> DatasetID {
454 self.dataset_id
455 }
456
457 pub fn name(&self) -> &str {
458 &self.name
459 }
460
461 pub fn description(&self) -> &str {
462 &self.description
463 }
464
465 pub fn created(&self) -> DateTime<Utc> {
466 self.created
467 }
468
469 pub async fn dataset(&self, client: &Client) -> Result<Dataset, Error> {
470 client.dataset(self.dataset_id).await
471 }
472}
473
474#[derive(Clone, Debug, Default, PartialEq)]
479pub struct Timing {
480 pub load: Option<i64>,
482 pub preprocess: Option<i64>,
484 pub inference: Option<i64>,
486 pub decode: Option<i64>,
488}
489
490#[derive(Serialize, Clone, Debug)]
497pub struct Sample {
498 #[serde(skip_serializing_if = "Option::is_none")]
499 pub id: Option<SampleID>,
500 #[serde(
505 alias = "group_name",
506 rename(serialize = "group", deserialize = "group_name"),
507 skip_serializing_if = "Option::is_none"
508 )]
509 pub group: Option<String>,
510 #[serde(skip_serializing_if = "Option::is_none")]
511 pub sequence_name: Option<String>,
512 #[serde(skip_serializing_if = "Option::is_none")]
513 pub sequence_uuid: Option<String>,
514 #[serde(skip_serializing_if = "Option::is_none")]
515 pub sequence_description: Option<String>,
516 #[serde(
517 default,
518 skip_serializing_if = "Option::is_none",
519 deserialize_with = "deserialize_frame_number"
520 )]
521 pub frame_number: Option<u32>,
522 #[serde(skip_serializing_if = "Option::is_none")]
523 pub uuid: Option<String>,
524 #[serde(skip_serializing_if = "Option::is_none")]
525 pub image_name: Option<String>,
526 #[serde(skip_serializing_if = "Option::is_none")]
527 pub image_url: Option<String>,
528 #[serde(skip_serializing_if = "Option::is_none")]
529 pub width: Option<u32>,
530 #[serde(skip_serializing_if = "Option::is_none")]
531 pub height: Option<u32>,
532 #[serde(skip_serializing_if = "Option::is_none")]
533 pub date: Option<DateTime<Utc>>,
534 #[serde(skip_serializing_if = "Option::is_none")]
535 pub source: Option<String>,
536 #[serde(skip_serializing_if = "Option::is_none", rename(serialize = "sensors"))]
541 pub location: Option<Location>,
542 #[serde(skip_serializing_if = "Option::is_none")]
544 pub degradation: Option<String>,
545 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub neg_label_indices: Option<Vec<u32>>,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
550 pub not_exhaustive_label_indices: Option<Vec<u32>>,
551 #[serde(
556 default,
557 skip_serializing_if = "Vec::is_empty",
558 serialize_with = "serialize_files"
559 )]
560 pub files: Vec<SampleFile>,
561 #[serde(
564 default,
565 skip_serializing_if = "Vec::is_empty",
566 serialize_with = "serialize_annotations"
567 )]
568 pub annotations: Vec<Annotation>,
569 #[serde(skip)]
572 pub timing: Option<Timing>,
573}
574
575fn deserialize_frame_number<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
578where
579 D: serde::Deserializer<'de>,
580{
581 use serde::Deserialize;
582
583 let value = Option::<i32>::deserialize(deserializer)?;
584 Ok(value.and_then(|v| if v < 0 { None } else { Some(v as u32) }))
585}
586
587fn is_valid_url(s: &str) -> bool {
590 s.starts_with("http://") || s.starts_with("https://")
591}
592
593fn serialize_files<S>(files: &[SampleFile], serializer: S) -> Result<S::Ok, S::Error>
596where
597 S: serde::Serializer,
598{
599 use serde::Serialize;
600 let map: HashMap<String, String> = files
601 .iter()
602 .filter_map(|f| {
603 f.filename()
604 .map(|filename| (f.file_type().to_string(), filename.to_string()))
605 })
606 .collect();
607 map.serialize(serializer)
608}
609
610fn serialize_annotations<S>(annotations: &Vec<Annotation>, serializer: S) -> Result<S::Ok, S::Error>
614where
615 S: serde::Serializer,
616{
617 serde::Serialize::serialize(annotations, serializer)
618}
619
620fn deserialize_annotations<'de, D>(deserializer: D) -> Result<Vec<Annotation>, D::Error>
623where
624 D: serde::Deserializer<'de>,
625{
626 use serde::Deserialize;
627
628 #[derive(Deserialize)]
629 #[serde(untagged)]
630 enum AnnotationsFormat {
631 Vec(Vec<Annotation>),
632 Map(HashMap<String, Vec<Annotation>>),
633 }
634
635 let value = Option::<AnnotationsFormat>::deserialize(deserializer)?;
636 Ok(value
637 .map(|v| match v {
638 AnnotationsFormat::Vec(annotations) => annotations,
639 AnnotationsFormat::Map(map) => convert_annotations_map_to_vec(map),
640 })
641 .unwrap_or_default())
642}
643
644#[derive(Debug, Default)]
647struct SensorsData {
648 files: Vec<SampleFile>,
649 location: Option<Location>,
650}
651
652fn deserialize_sensors_data(value: Option<serde_json::Value>) -> SensorsData {
654 use serde_json::Value;
655
656 fn create_sample_file(file_type: String, value: String) -> SampleFile {
659 if is_valid_url(&value) {
660 SampleFile::with_url(file_type, value)
661 } else {
662 SampleFile::with_data(file_type, value)
663 }
664 }
665
666 fn create_sample_file_from_value(file_type: String, value: Value) -> Option<SampleFile> {
668 match value {
669 Value::String(s) => Some(create_sample_file(file_type, s)),
670 Value::Object(_) | Value::Array(_) => {
671 serde_json::to_string(&value)
673 .ok()
674 .map(|data| SampleFile::with_data(file_type, data))
675 }
676 _ => None,
677 }
678 }
679
680 fn extract_location(map: &serde_json::Map<String, Value>) -> Option<Location> {
682 let gps = map
683 .get("gps")
684 .and_then(|v| serde_json::from_value::<GpsData>(v.clone()).ok());
685 let imu = map
686 .get("imu")
687 .and_then(|v| serde_json::from_value::<ImuData>(v.clone()).ok());
688
689 if gps.is_some() || imu.is_some() {
690 Some(Location { gps, imu })
691 } else {
692 None
693 }
694 }
695
696 let mut result = SensorsData::default();
697
698 match value {
699 None => result,
700 Some(Value::Array(arr)) => {
701 for item in arr {
703 if let Value::Object(map) = item {
704 if map.contains_key("type") {
706 if let Ok(file) =
708 serde_json::from_value::<SampleFile>(Value::Object(map.clone()))
709 {
710 result.files.push(file);
711 }
712 } else {
713 if let Some(loc) = extract_location(&map) {
715 if let Some(ref mut existing) = result.location {
717 if loc.gps.is_some() {
718 existing.gps = loc.gps;
719 }
720 if loc.imu.is_some() {
721 existing.imu = loc.imu;
722 }
723 } else {
724 result.location = Some(loc);
725 }
726 } else {
727 for (file_type, value) in map {
729 if let Some(file) = create_sample_file_from_value(file_type, value)
730 {
731 result.files.push(file);
732 }
733 }
734 }
735 }
736 }
737 }
738 result
739 }
740 Some(Value::Object(map)) => {
741 if let Some(loc) = extract_location(&map) {
743 result.location = Some(loc);
744 }
745
746 for (key, value) in map {
748 if key != "gps"
749 && key != "imu"
750 && let Some(file) = create_sample_file_from_value(key, value)
751 {
752 result.files.push(file);
753 }
754 }
755 result
756 }
757 Some(_) => result,
758 }
759}
760
761#[derive(Deserialize)]
765struct SampleRaw {
766 #[serde(default)]
767 id: Option<SampleID>,
768 #[serde(alias = "group_name")]
769 group: Option<String>,
770 sequence_name: Option<String>,
771 sequence_uuid: Option<String>,
772 sequence_description: Option<String>,
773 #[serde(default, deserialize_with = "deserialize_frame_number")]
774 frame_number: Option<u32>,
775 uuid: Option<String>,
776 image_name: Option<String>,
777 image_url: Option<String>,
778 width: Option<u32>,
779 height: Option<u32>,
780 date: Option<DateTime<Utc>>,
781 source: Option<String>,
782 degradation: Option<String>,
783 #[serde(default)]
784 neg_label_indices: Option<Vec<u32>>,
785 #[serde(default)]
786 not_exhaustive_label_indices: Option<Vec<u32>>,
787 #[serde(default, alias = "sensors")]
789 sensors: Option<serde_json::Value>,
790 #[serde(default, deserialize_with = "deserialize_annotations")]
791 annotations: Vec<Annotation>,
792}
793
794impl From<SampleRaw> for Sample {
795 fn from(raw: SampleRaw) -> Self {
796 let sensors_data = deserialize_sensors_data(raw.sensors);
797
798 Sample {
799 id: raw.id,
800 group: raw.group,
801 sequence_name: raw.sequence_name,
802 sequence_uuid: raw.sequence_uuid,
803 sequence_description: raw.sequence_description,
804 frame_number: raw.frame_number,
805 uuid: raw.uuid,
806 image_name: raw.image_name,
807 image_url: raw.image_url,
808 width: raw.width,
809 height: raw.height,
810 date: raw.date,
811 source: raw.source,
812 location: sensors_data.location,
813 degradation: raw.degradation,
814 neg_label_indices: raw.neg_label_indices,
815 not_exhaustive_label_indices: raw.not_exhaustive_label_indices,
816 files: sensors_data.files,
817 annotations: raw.annotations,
818 timing: None,
819 }
820 }
821}
822
823impl<'de> serde::Deserialize<'de> for Sample {
824 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
825 where
826 D: serde::Deserializer<'de>,
827 {
828 let raw = SampleRaw::deserialize(deserializer)?;
829 Ok(Sample::from(raw))
830 }
831}
832
833impl Display for Sample {
834 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
835 write!(
836 f,
837 "{} {}",
838 self.id
839 .map(|id| id.to_string())
840 .unwrap_or_else(|| "unknown".to_string()),
841 self.image_name().unwrap_or("unknown")
842 )
843 }
844}
845
846impl Default for Sample {
847 fn default() -> Self {
848 Self::new()
849 }
850}
851
852impl Sample {
853 pub fn new() -> Self {
855 Self {
856 id: None,
857 group: None,
858 sequence_name: None,
859 sequence_uuid: None,
860 sequence_description: None,
861 frame_number: None,
862 uuid: None,
863 image_name: None,
864 image_url: None,
865 width: None,
866 height: None,
867 date: None,
868 source: None,
869 location: None,
870 degradation: None,
871 neg_label_indices: None,
872 not_exhaustive_label_indices: None,
873 files: vec![],
874 annotations: vec![],
875 timing: None,
876 }
877 }
878
879 pub fn id(&self) -> Option<SampleID> {
880 self.id
881 }
882
883 pub fn name(&self) -> Option<String> {
884 self.image_name.as_ref().map(|n| extract_sample_name(n))
885 }
886
887 pub fn group(&self) -> Option<&String> {
888 self.group.as_ref()
889 }
890
891 pub fn sequence_name(&self) -> Option<&String> {
892 self.sequence_name.as_ref()
893 }
894
895 pub fn sequence_uuid(&self) -> Option<&String> {
896 self.sequence_uuid.as_ref()
897 }
898
899 pub fn sequence_description(&self) -> Option<&String> {
900 self.sequence_description.as_ref()
901 }
902
903 pub fn frame_number(&self) -> Option<u32> {
904 self.frame_number
905 }
906
907 pub fn uuid(&self) -> Option<&String> {
908 self.uuid.as_ref()
909 }
910
911 pub fn image_name(&self) -> Option<&str> {
912 self.image_name.as_deref()
913 }
914
915 pub fn image_url(&self) -> Option<&str> {
916 self.image_url.as_deref()
917 }
918
919 pub fn width(&self) -> Option<u32> {
920 self.width
921 }
922
923 pub fn height(&self) -> Option<u32> {
924 self.height
925 }
926
927 pub fn date(&self) -> Option<DateTime<Utc>> {
928 self.date
929 }
930
931 pub fn source(&self) -> Option<&String> {
932 self.source.as_ref()
933 }
934
935 pub fn location(&self) -> Option<&Location> {
936 self.location.as_ref()
937 }
938
939 pub fn files(&self) -> &[SampleFile] {
940 &self.files
941 }
942
943 pub fn annotations(&self) -> &[Annotation] {
944 &self.annotations
945 }
946
947 pub fn with_annotations(mut self, annotations: Vec<Annotation>) -> Self {
948 self.annotations = annotations;
949 self
950 }
951
952 pub fn with_frame_number(mut self, frame_number: Option<u32>) -> Self {
953 self.frame_number = frame_number;
954 self
955 }
956
957 pub async fn download(
964 &self,
965 client: &Client,
966 file_type: FileType,
967 ) -> Result<Option<Vec<u8>>, Error> {
968 use base64::{Engine, engine::general_purpose::STANDARD};
969
970 if file_type == FileType::Image {
972 if let Some(url) = self.image_url.as_deref()
973 && is_valid_url(url)
974 {
975 return Ok(Some(client.download(url).await?));
976 }
977 return Ok(None);
978 }
979
980 let file = resolve_file(&file_type, &self.files);
982
983 match file {
984 Some(f) => {
985 if let Some(url) = f.url() {
987 return Ok(Some(client.download(url).await?));
988 }
989
990 if let Some(data) = f.data() {
992 let decoded = if let Ok(bytes) = STANDARD.decode(data) {
999 if let Ok(text) = String::from_utf8(bytes.clone()) {
1001 if text.starts_with('{') {
1002 text
1004 } else {
1005 return Ok(Some(bytes));
1007 }
1008 } else {
1009 return Ok(Some(bytes));
1011 }
1012 } else {
1013 data.to_string()
1015 };
1016
1017 let content = if decoded.starts_with('{') {
1019 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&decoded) {
1020 if let Some(obj) = json.as_object() {
1021 obj.values()
1022 .next()
1023 .and_then(|v| v.as_str())
1024 .map(|s| s.to_string())
1025 .unwrap_or(decoded)
1026 } else {
1027 decoded
1028 }
1029 } else {
1030 decoded
1031 }
1032 } else {
1033 decoded
1034 };
1035
1036 return Ok(Some(content.as_bytes().to_vec()));
1037 }
1038
1039 Ok(None)
1040 }
1041 None => Ok(None),
1042 }
1043 }
1044}
1045
1046#[derive(Serialize, Deserialize, Clone, Debug)]
1054pub struct SampleFile {
1055 r#type: String,
1056 #[serde(skip_serializing_if = "Option::is_none")]
1057 url: Option<String>,
1058 #[serde(skip_serializing_if = "Option::is_none")]
1059 filename: Option<String>,
1060 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1062 data: Option<String>,
1063 #[serde(skip)]
1066 bytes: Option<Vec<u8>>,
1067}
1068
1069impl SampleFile {
1070 pub fn with_url(file_type: String, url: String) -> Self {
1072 Self {
1073 r#type: file_type,
1074 url: Some(url),
1075 filename: None,
1076 data: None,
1077 bytes: None,
1078 }
1079 }
1080
1081 pub fn with_filename(file_type: String, filename: String) -> Self {
1083 Self {
1084 r#type: file_type,
1085 url: None,
1086 filename: Some(filename),
1087 data: None,
1088 bytes: None,
1089 }
1090 }
1091
1092 pub fn with_data(file_type: String, data: String) -> Self {
1094 Self {
1095 r#type: file_type,
1096 url: None,
1097 filename: None,
1098 data: Some(data),
1099 bytes: None,
1100 }
1101 }
1102
1103 pub fn with_bytes(file_type: String, filename: String, bytes: Vec<u8>) -> Self {
1113 Self {
1114 r#type: file_type,
1115 url: None,
1116 filename: Some(filename),
1117 data: None,
1118 bytes: Some(bytes),
1119 }
1120 }
1121
1122 pub fn file_type(&self) -> &str {
1123 &self.r#type
1124 }
1125
1126 pub fn url(&self) -> Option<&str> {
1127 self.url.as_deref()
1128 }
1129
1130 pub fn filename(&self) -> Option<&str> {
1131 self.filename.as_deref()
1132 }
1133
1134 pub fn data(&self) -> Option<&str> {
1136 self.data.as_deref()
1137 }
1138
1139 pub fn bytes(&self) -> Option<&[u8]> {
1141 self.bytes.as_deref()
1142 }
1143}
1144
1145#[derive(Serialize, Deserialize, Clone, Debug)]
1150pub struct Location {
1151 #[serde(skip_serializing_if = "Option::is_none")]
1152 pub gps: Option<GpsData>,
1153 #[serde(skip_serializing_if = "Option::is_none")]
1154 pub imu: Option<ImuData>,
1155}
1156
1157#[derive(Serialize, Deserialize, Clone, Debug)]
1159pub struct GpsData {
1160 pub lat: f64,
1161 pub lon: f64,
1162}
1163
1164impl GpsData {
1165 pub fn validate(&self) -> Result<(), String> {
1195 validate_gps_coordinates(self.lat, self.lon)
1196 }
1197}
1198
1199#[derive(Serialize, Deserialize, Clone, Debug)]
1201pub struct ImuData {
1202 pub roll: f64,
1203 pub pitch: f64,
1204 pub yaw: f64,
1205}
1206
1207impl ImuData {
1208 pub fn validate(&self) -> Result<(), String> {
1241 validate_imu_orientation(self.roll, self.pitch, self.yaw)
1242 }
1243}
1244
1245#[allow(dead_code)]
1246pub trait TypeName {
1247 fn type_name() -> String;
1248}
1249
1250#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
1251pub struct Box3d {
1252 x: f32,
1253 y: f32,
1254 z: f32,
1255 w: f32,
1256 h: f32,
1257 l: f32,
1258}
1259
1260impl TypeName for Box3d {
1261 fn type_name() -> String {
1262 "box3d".to_owned()
1263 }
1264}
1265
1266impl Box3d {
1267 pub fn new(cx: f32, cy: f32, cz: f32, width: f32, height: f32, length: f32) -> Self {
1268 Self {
1269 x: cx,
1270 y: cy,
1271 z: cz,
1272 w: width,
1273 h: height,
1274 l: length,
1275 }
1276 }
1277
1278 pub fn width(&self) -> f32 {
1279 self.w
1280 }
1281
1282 pub fn height(&self) -> f32 {
1283 self.h
1284 }
1285
1286 pub fn length(&self) -> f32 {
1287 self.l
1288 }
1289
1290 pub fn cx(&self) -> f32 {
1291 self.x
1292 }
1293
1294 pub fn cy(&self) -> f32 {
1295 self.y
1296 }
1297
1298 pub fn cz(&self) -> f32 {
1299 self.z
1300 }
1301
1302 pub fn left(&self) -> f32 {
1303 self.x - self.w / 2.0
1304 }
1305
1306 pub fn top(&self) -> f32 {
1307 self.y - self.h / 2.0
1308 }
1309
1310 pub fn front(&self) -> f32 {
1311 self.z - self.l / 2.0
1312 }
1313}
1314
1315#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
1316pub struct Box2d {
1317 h: f32,
1318 w: f32,
1319 x: f32,
1320 y: f32,
1321}
1322
1323impl TypeName for Box2d {
1324 fn type_name() -> String {
1325 "box2d".to_owned()
1326 }
1327}
1328
1329impl Box2d {
1330 pub fn new(left: f32, top: f32, width: f32, height: f32) -> Self {
1331 Self {
1332 x: left,
1333 y: top,
1334 w: width,
1335 h: height,
1336 }
1337 }
1338
1339 pub fn width(&self) -> f32 {
1340 self.w
1341 }
1342
1343 pub fn height(&self) -> f32 {
1344 self.h
1345 }
1346
1347 pub fn left(&self) -> f32 {
1348 self.x
1349 }
1350
1351 pub fn top(&self) -> f32 {
1352 self.y
1353 }
1354
1355 pub fn cx(&self) -> f32 {
1356 self.x + self.w / 2.0
1357 }
1358
1359 pub fn cy(&self) -> f32 {
1360 self.y + self.h / 2.0
1361 }
1362}
1363
1364#[derive(Clone, Debug, PartialEq)]
1365pub struct Polygon {
1366 pub rings: Vec<Vec<(f32, f32)>>,
1367}
1368
1369impl TypeName for Polygon {
1370 fn type_name() -> String {
1371 "polygon".to_owned()
1372 }
1373}
1374
1375impl Polygon {
1376 pub fn new(rings: Vec<Vec<(f32, f32)>>) -> Self {
1377 Self { rings }
1378 }
1379}
1380
1381impl serde::Serialize for Polygon {
1382 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1383 where
1384 S: serde::Serializer,
1385 {
1386 serde::Serialize::serialize(&self.rings, serializer)
1387 }
1388}
1389
1390impl<'de> serde::Deserialize<'de> for Polygon {
1391 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1392 where
1393 D: serde::Deserializer<'de>,
1394 {
1395 let value = serde_json::Value::deserialize(deserializer)?;
1397
1398 let polygon_value = if let Some(obj) = value.as_object() {
1400 obj.get("rings")
1402 .or_else(|| obj.get("polygon"))
1403 .cloned()
1404 .unwrap_or(serde_json::Value::Null)
1405 } else {
1406 value
1408 };
1409
1410 let rings = parse_polygon_value(&polygon_value);
1412
1413 Ok(Self { rings })
1414 }
1415}
1416
1417fn parse_polygon_value(value: &serde_json::Value) -> Vec<Vec<(f32, f32)>> {
1425 let Some(outer_array) = value.as_array() else {
1426 return vec![];
1427 };
1428
1429 let mut result = Vec::new();
1430
1431 for ring in outer_array {
1432 let Some(ring_array) = ring.as_array() else {
1433 continue;
1434 };
1435
1436 let is_3d = ring_array
1438 .first()
1439 .map(|first| first.is_array())
1440 .unwrap_or(false);
1441
1442 let points: Vec<(f32, f32)> = if is_3d {
1443 ring_array
1445 .iter()
1446 .filter_map(|point| {
1447 let arr = point.as_array()?;
1448 if arr.len() >= 2 {
1449 let x = arr[0].as_f64()? as f32;
1450 let y = arr[1].as_f64()? as f32;
1451 if x.is_finite() && y.is_finite() {
1452 Some((x, y))
1453 } else {
1454 None
1455 }
1456 } else {
1457 None
1458 }
1459 })
1460 .collect()
1461 } else {
1462 ring_array
1464 .chunks(2)
1465 .filter_map(|chunk| {
1466 if chunk.len() >= 2 {
1467 let x = chunk[0].as_f64()? as f32;
1468 let y = chunk[1].as_f64()? as f32;
1469 if x.is_finite() && y.is_finite() {
1470 Some((x, y))
1471 } else {
1472 None
1473 }
1474 } else {
1475 None
1476 }
1477 })
1478 .collect()
1479 };
1480
1481 if points.len() >= 3 {
1483 result.push(points);
1484 }
1485 }
1486
1487 result
1488}
1489
1490#[derive(Deserialize)]
1495struct AnnotationRaw {
1496 #[serde(default)]
1497 sample_id: Option<SampleID>,
1498 #[serde(default)]
1499 name: Option<String>,
1500 #[serde(default)]
1501 sequence_name: Option<String>,
1502 #[serde(default)]
1503 frame_number: Option<u32>,
1504 #[serde(rename = "group_name", default)]
1505 group: Option<String>,
1506 #[serde(rename = "object_reference", alias = "object_id", default)]
1507 object_id: Option<String>,
1508 #[serde(default)]
1509 label_name: Option<String>,
1510 #[serde(default)]
1511 label_index: Option<u64>,
1512 #[serde(default)]
1513 iscrowd: Option<bool>,
1514 #[serde(default)]
1515 category_frequency: Option<String>,
1516 #[serde(default)]
1518 box2d: Option<Box2d>,
1519 #[serde(default)]
1520 box3d: Option<Box3d>,
1521 #[serde(default, alias = "mask")]
1522 polygon: Option<Polygon>,
1523 #[serde(default)]
1525 x: Option<f64>,
1526 #[serde(default)]
1527 y: Option<f64>,
1528 #[serde(default)]
1529 w: Option<f64>,
1530 #[serde(default)]
1531 h: Option<f64>,
1532}
1533
1534#[derive(Serialize, Clone, Debug)]
1535pub struct Annotation {
1536 #[serde(skip_serializing_if = "Option::is_none")]
1537 sample_id: Option<SampleID>,
1538 #[serde(skip_serializing_if = "Option::is_none")]
1539 name: Option<String>,
1540 #[serde(skip_serializing_if = "Option::is_none")]
1541 sequence_name: Option<String>,
1542 #[serde(skip_serializing_if = "Option::is_none")]
1543 frame_number: Option<u32>,
1544 #[serde(rename = "group_name", skip_serializing_if = "Option::is_none")]
1548 group: Option<String>,
1549 #[serde(
1553 rename = "object_reference",
1554 alias = "object_id",
1555 skip_serializing_if = "Option::is_none"
1556 )]
1557 object_id: Option<String>,
1558 #[serde(skip_serializing_if = "Option::is_none")]
1559 label_name: Option<String>,
1560 #[serde(skip_serializing_if = "Option::is_none")]
1561 label_index: Option<u64>,
1562 #[serde(default, skip_serializing_if = "Option::is_none")]
1564 iscrowd: Option<bool>,
1565 #[serde(default, skip_serializing_if = "Option::is_none")]
1567 category_frequency: Option<String>,
1568 #[serde(skip_serializing_if = "Option::is_none")]
1569 box2d: Option<Box2d>,
1570 #[serde(skip_serializing_if = "Option::is_none")]
1571 box3d: Option<Box3d>,
1572 #[serde(rename(serialize = "mask"), skip_serializing_if = "Option::is_none")]
1581 polygon: Option<Polygon>,
1582 #[serde(skip)]
1584 mask: Option<MaskData>,
1585 #[serde(skip_serializing_if = "Option::is_none")]
1587 box2d_score: Option<f32>,
1588 #[serde(skip_serializing_if = "Option::is_none")]
1590 box3d_score: Option<f32>,
1591 #[serde(skip_serializing_if = "Option::is_none")]
1593 polygon_score: Option<f32>,
1594 #[serde(skip_serializing_if = "Option::is_none")]
1596 mask_score: Option<f32>,
1597}
1598
1599impl<'de> serde::Deserialize<'de> for Annotation {
1600 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1601 where
1602 D: serde::Deserializer<'de>,
1603 {
1604 let raw: AnnotationRaw = serde::Deserialize::deserialize(deserializer)?;
1606
1607 let box2d = raw.box2d.or_else(|| match (raw.x, raw.y, raw.w, raw.h) {
1609 (Some(x), Some(y), Some(w), Some(h)) if w > 0.0 && h > 0.0 => {
1610 Some(Box2d::new(x as f32, y as f32, w as f32, h as f32))
1611 }
1612 _ => None,
1613 });
1614
1615 Ok(Annotation {
1616 sample_id: raw.sample_id,
1617 name: raw.name,
1618 sequence_name: raw.sequence_name,
1619 frame_number: raw.frame_number,
1620 group: raw.group,
1621 object_id: raw.object_id,
1622 label_name: raw.label_name,
1623 label_index: raw.label_index,
1624 iscrowd: raw.iscrowd,
1625 category_frequency: raw.category_frequency,
1626 box2d,
1627 box3d: raw.box3d,
1628 polygon: raw.polygon,
1629 mask: None,
1630 box2d_score: None,
1631 box3d_score: None,
1632 polygon_score: None,
1633 mask_score: None,
1634 })
1635 }
1636}
1637
1638impl Default for Annotation {
1639 fn default() -> Self {
1640 Self::new()
1641 }
1642}
1643
1644impl Annotation {
1645 pub fn new() -> Self {
1646 Self {
1647 sample_id: None,
1648 name: None,
1649 sequence_name: None,
1650 frame_number: None,
1651 group: None,
1652 object_id: None,
1653 label_name: None,
1654 label_index: None,
1655 iscrowd: None,
1656 category_frequency: None,
1657 box2d: None,
1658 box3d: None,
1659 polygon: None,
1660 mask: None,
1661 box2d_score: None,
1662 box3d_score: None,
1663 polygon_score: None,
1664 mask_score: None,
1665 }
1666 }
1667
1668 pub fn set_sample_id(&mut self, sample_id: Option<SampleID>) {
1669 self.sample_id = sample_id;
1670 }
1671
1672 pub fn sample_id(&self) -> Option<SampleID> {
1673 self.sample_id
1674 }
1675
1676 pub fn set_name(&mut self, name: Option<String>) {
1677 self.name = name;
1678 }
1679
1680 pub fn name(&self) -> Option<&String> {
1681 self.name.as_ref()
1682 }
1683
1684 pub fn set_sequence_name(&mut self, sequence_name: Option<String>) {
1685 self.sequence_name = sequence_name;
1686 }
1687
1688 pub fn sequence_name(&self) -> Option<&String> {
1689 self.sequence_name.as_ref()
1690 }
1691
1692 pub fn set_frame_number(&mut self, frame_number: Option<u32>) {
1693 self.frame_number = frame_number;
1694 }
1695
1696 pub fn frame_number(&self) -> Option<u32> {
1697 self.frame_number
1698 }
1699
1700 pub fn set_group(&mut self, group: Option<String>) {
1701 self.group = group;
1702 }
1703
1704 pub fn group(&self) -> Option<&String> {
1705 self.group.as_ref()
1706 }
1707
1708 pub fn object_id(&self) -> Option<&String> {
1709 self.object_id.as_ref()
1710 }
1711
1712 pub fn set_object_id(&mut self, object_id: Option<String>) {
1713 self.object_id = object_id;
1714 }
1715
1716 pub fn label(&self) -> Option<&String> {
1717 self.label_name.as_ref()
1718 }
1719
1720 pub fn set_label(&mut self, label_name: Option<String>) {
1721 self.label_name = label_name;
1722 }
1723
1724 pub fn label_index(&self) -> Option<u64> {
1725 self.label_index
1726 }
1727
1728 pub fn set_label_index(&mut self, label_index: Option<u64>) {
1729 self.label_index = label_index;
1730 }
1731
1732 pub fn iscrowd(&self) -> Option<bool> {
1733 self.iscrowd
1734 }
1735
1736 pub fn set_iscrowd(&mut self, iscrowd: Option<bool>) {
1737 self.iscrowd = iscrowd;
1738 }
1739
1740 pub fn category_frequency(&self) -> Option<&String> {
1741 self.category_frequency.as_ref()
1742 }
1743
1744 pub fn set_category_frequency(&mut self, category_frequency: Option<String>) {
1745 self.category_frequency = category_frequency;
1746 }
1747
1748 pub fn box2d(&self) -> Option<&Box2d> {
1749 self.box2d.as_ref()
1750 }
1751
1752 pub fn set_box2d(&mut self, box2d: Option<Box2d>) {
1753 self.box2d = box2d;
1754 }
1755
1756 pub fn box3d(&self) -> Option<&Box3d> {
1757 self.box3d.as_ref()
1758 }
1759
1760 pub fn set_box3d(&mut self, box3d: Option<Box3d>) {
1761 self.box3d = box3d;
1762 }
1763
1764 pub fn polygon(&self) -> Option<&Polygon> {
1765 self.polygon.as_ref()
1766 }
1767
1768 pub fn set_polygon(&mut self, polygon: Option<Polygon>) {
1769 self.polygon = polygon;
1770 }
1771
1772 pub fn mask(&self) -> Option<&MaskData> {
1773 self.mask.as_ref()
1774 }
1775
1776 pub fn set_mask(&mut self, mask: Option<MaskData>) {
1777 self.mask = mask;
1778 }
1779
1780 pub fn box2d_score(&self) -> Option<f32> {
1781 self.box2d_score
1782 }
1783
1784 pub fn set_box2d_score(&mut self, score: Option<f32>) {
1785 self.box2d_score = score;
1786 }
1787
1788 pub fn box3d_score(&self) -> Option<f32> {
1789 self.box3d_score
1790 }
1791
1792 pub fn set_box3d_score(&mut self, score: Option<f32>) {
1793 self.box3d_score = score;
1794 }
1795
1796 pub fn polygon_score(&self) -> Option<f32> {
1797 self.polygon_score
1798 }
1799
1800 pub fn set_polygon_score(&mut self, score: Option<f32>) {
1801 self.polygon_score = score;
1802 }
1803
1804 pub fn mask_score(&self) -> Option<f32> {
1805 self.mask_score
1806 }
1807
1808 pub fn set_mask_score(&mut self, score: Option<f32>) {
1809 self.mask_score = score;
1810 }
1811}
1812
1813#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
1814pub struct Label {
1815 id: u64,
1816 dataset_id: DatasetID,
1817 index: u64,
1818 name: String,
1819}
1820
1821impl Label {
1822 pub fn id(&self) -> u64 {
1823 self.id
1824 }
1825
1826 pub fn dataset_id(&self) -> DatasetID {
1827 self.dataset_id
1828 }
1829
1830 pub fn index(&self) -> u64 {
1831 self.index
1832 }
1833
1834 pub fn name(&self) -> &str {
1835 &self.name
1836 }
1837
1838 pub async fn remove(&self, client: &Client) -> Result<(), Error> {
1839 client.remove_label(self.id()).await
1840 }
1841
1842 pub async fn set_name(&mut self, client: &Client, name: &str) -> Result<(), Error> {
1843 self.name = name.to_string();
1844 client.update_label(self).await
1845 }
1846
1847 pub async fn set_index(&mut self, client: &Client, index: u64) -> Result<(), Error> {
1848 self.index = index;
1849 client.update_label(self).await
1850 }
1851}
1852
1853impl Display for Label {
1854 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1855 write!(f, "{}", self.name())
1856 }
1857}
1858
1859#[derive(Serialize, Clone, Debug)]
1860pub struct NewLabelObject {
1861 pub name: String,
1862}
1863
1864#[derive(Serialize, Clone, Debug)]
1865pub struct NewLabel {
1866 pub dataset_id: DatasetID,
1867 pub labels: Vec<NewLabelObject>,
1868}
1869
1870#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
1900pub struct Group {
1901 pub id: u64,
1906
1907 pub name: String,
1911}
1912
1913#[cfg(feature = "polars")]
1914fn extract_annotation_name(ann: &Annotation) -> Option<(String, Option<u32>)> {
1915 use std::path::Path;
1916
1917 let name = ann.name.as_ref()?;
1918 let name = Path::new(name).file_stem()?.to_str()?;
1919
1920 match &ann.sequence_name {
1923 Some(sequence) => Some((sequence.clone(), ann.frame_number)),
1924 None => Some((name.to_string(), None)),
1925 }
1926}
1927
1928#[cfg(feature = "polars")]
1932fn convert_polygon_to_nested_series(polygon: &Polygon) -> Series {
1933 let ring_series: Vec<Option<Series>> = polygon
1934 .rings
1935 .iter()
1936 .map(|ring| {
1937 let coords: Vec<f32> = ring.iter().flat_map(|&(x, y)| [x, y]).collect();
1938 Some(Series::new("".into(), coords))
1939 })
1940 .collect();
1941 Series::new("".into(), ring_series)
1942}
1943
1944#[cfg(feature = "polars")]
1994pub fn samples_dataframe(samples: &[Sample]) -> Result<DataFrame, Error> {
1995 let mut names: Vec<String> = Vec::new();
1997 let mut frames: Vec<Option<u32>> = Vec::new();
1998 let mut objects: Vec<Option<String>> = Vec::new();
1999 let mut labels: Vec<Option<String>> = Vec::new();
2000 let mut label_indices: Vec<Option<u64>> = Vec::new();
2001 let mut groups: Vec<Option<String>> = Vec::new();
2002 let mut polygons: Vec<Option<Series>> = Vec::new();
2003 let mut boxes2d: Vec<Option<Series>> = Vec::new();
2004 let mut boxes3d: Vec<Option<Series>> = Vec::new();
2005 let mut mask_bytes: Vec<Option<Vec<u8>>> = Vec::new();
2006 let mut box2d_scores: Vec<Option<f32>> = Vec::new();
2007 let mut box3d_scores: Vec<Option<f32>> = Vec::new();
2008 let mut polygon_scores: Vec<Option<f32>> = Vec::new();
2009 let mut mask_scores: Vec<Option<f32>> = Vec::new();
2010 let mut sizes: Vec<Option<Vec<u32>>> = Vec::new();
2011 let mut locations: Vec<Option<Vec<f32>>> = Vec::new();
2012 let mut poses: Vec<Option<Vec<f32>>> = Vec::new();
2013 let mut degradations: Vec<Option<String>> = Vec::new();
2014 let mut iscrowds: Vec<Option<bool>> = Vec::new();
2015 let mut category_frequencies: Vec<Option<String>> = Vec::new();
2016 let mut neg_label_indices_vec: Vec<Option<Vec<u32>>> = Vec::new();
2017 let mut not_exhaustive_label_indices_vec: Vec<Option<Vec<u32>>> = Vec::new();
2018 let mut timing_load: Vec<Option<i64>> = Vec::new();
2019 let mut timing_preprocess: Vec<Option<i64>> = Vec::new();
2020 let mut timing_inference: Vec<Option<i64>> = Vec::new();
2021 let mut timing_decode: Vec<Option<i64>> = Vec::new();
2022
2023 for sample in samples {
2024 let size = match (sample.width, sample.height) {
2026 (Some(w), Some(h)) => Some(vec![w, h]),
2027 _ => None,
2028 };
2029
2030 let location = sample.location.as_ref().and_then(|loc| {
2031 loc.gps
2032 .as_ref()
2033 .map(|gps| vec![gps.lat as f32, gps.lon as f32])
2034 });
2035
2036 let pose = sample.location.as_ref().and_then(|loc| {
2037 loc.imu
2038 .as_ref()
2039 .map(|imu| vec![imu.yaw as f32, imu.pitch as f32, imu.roll as f32])
2040 });
2041
2042 let degradation = sample.degradation.clone();
2043
2044 let t_load = sample.timing.as_ref().and_then(|t| t.load);
2046 let t_preprocess = sample.timing.as_ref().and_then(|t| t.preprocess);
2047 let t_inference = sample.timing.as_ref().and_then(|t| t.inference);
2048 let t_decode = sample.timing.as_ref().and_then(|t| t.decode);
2049
2050 macro_rules! push_sample_fields {
2052 () => {
2053 sizes.push(size.clone());
2054 locations.push(location.clone());
2055 poses.push(pose.clone());
2056 degradations.push(degradation.clone());
2057 neg_label_indices_vec.push(sample.neg_label_indices.clone());
2058 not_exhaustive_label_indices_vec.push(sample.not_exhaustive_label_indices.clone());
2059 timing_load.push(t_load);
2060 timing_preprocess.push(t_preprocess);
2061 timing_inference.push(t_inference);
2062 timing_decode.push(t_decode);
2063 };
2064 }
2065
2066 if sample.annotations.is_empty() {
2067 let (name, frame) = match extract_annotation_name_from_sample(sample) {
2069 Some(nf) => nf,
2070 None => continue,
2071 };
2072
2073 names.push(name);
2074 frames.push(frame);
2075 objects.push(None);
2076 labels.push(None);
2077 label_indices.push(None);
2078 groups.push(sample.group.clone());
2079 polygons.push(None);
2080 boxes2d.push(None);
2081 boxes3d.push(None);
2082 mask_bytes.push(None);
2083 box2d_scores.push(None);
2084 box3d_scores.push(None);
2085 polygon_scores.push(None);
2086 mask_scores.push(None);
2087 iscrowds.push(None);
2088 category_frequencies.push(None);
2089 push_sample_fields!();
2090 } else {
2091 for ann in &sample.annotations {
2093 let (name, frame) = match extract_annotation_name(ann) {
2094 Some(nf) => nf,
2095 None => continue,
2096 };
2097
2098 let polygon = ann.polygon.as_ref().map(convert_polygon_to_nested_series);
2099
2100 let box2d = ann
2101 .box2d
2102 .as_ref()
2103 .map(|b| Series::new("box2d".into(), [b.cx(), b.cy(), b.width(), b.height()]));
2104
2105 let box3d = ann
2106 .box3d
2107 .as_ref()
2108 .map(|b| Series::new("box3d".into(), [b.x, b.y, b.z, b.w, b.h, b.l]));
2109
2110 names.push(name);
2111 frames.push(frame);
2112 objects.push(ann.object_id().cloned());
2113 labels.push(ann.label_name.clone());
2114 label_indices.push(ann.label_index);
2115 groups.push(sample.group.clone());
2116 polygons.push(polygon);
2117 boxes2d.push(box2d);
2118 boxes3d.push(box3d);
2119 mask_bytes.push(ann.mask.as_ref().map(|m| m.as_bytes().to_vec()));
2120 box2d_scores.push(ann.box2d_score());
2121 box3d_scores.push(ann.box3d_score());
2122 polygon_scores.push(ann.polygon_score());
2123 mask_scores.push(ann.mask_score());
2124 iscrowds.push(ann.iscrowd);
2125 category_frequencies.push(ann.category_frequency.clone());
2126 push_sample_fields!();
2127 }
2128 }
2129 }
2130
2131 let names_col: Column = Series::new("name".into(), names).into();
2133 let frames_col: Column = Series::new("frame".into(), frames).into();
2134 let objects_col: Column = Series::new("object_id".into(), objects).into();
2135
2136 let labels_col: Column = Series::new("label".into(), labels)
2142 .cast(&DataType::Categorical(
2143 Categories::new("labels".into(), "labels".into(), CategoricalPhysical::U16),
2144 Arc::new(CategoricalMapping::with_hasher(
2145 u16::MAX as usize,
2146 Default::default(),
2147 )),
2148 ))?
2149 .into();
2150
2151 let label_indices_col: Column = Series::new("label_index".into(), label_indices).into();
2152
2153 let groups_col: Column = Series::new("group".into(), groups)
2155 .cast(&DataType::Categorical(
2156 Categories::new("groups".into(), "groups".into(), CategoricalPhysical::U8),
2157 Arc::new(CategoricalMapping::with_hasher(
2158 u8::MAX as usize,
2159 Default::default(),
2160 )),
2161 ))?
2162 .into();
2163
2164 let polygons_col: Column = if polygons.iter().all(|p| p.is_none()) {
2169 Series::new_null("polygon".into(), polygons.len()).into()
2171 } else {
2172 let typed_polygons: Vec<Option<Series>> = polygons
2175 .into_iter()
2176 .map(|opt| {
2177 opt.map(|s| {
2178 s.cast(&DataType::List(Box::new(DataType::Float32)))
2179 .unwrap_or(s)
2180 })
2181 })
2182 .collect();
2183 Series::new("polygon".into(), &typed_polygons)
2184 .cast(&DataType::List(Box::new(DataType::List(Box::new(
2185 DataType::Float32,
2186 )))))?
2187 .into()
2188 };
2189
2190 let boxes2d_col: Column = Series::new("box2d".into(), boxes2d)
2191 .cast(&DataType::Array(Box::new(DataType::Float32), 4))?
2192 .into();
2193 let boxes3d_col: Column = Series::new("box3d".into(), boxes3d)
2194 .cast(&DataType::Array(Box::new(DataType::Float32), 6))?
2195 .into();
2196
2197 let mask_col: Column = Series::new("mask".into(), mask_bytes).into();
2199
2200 let box2d_score_col: Column = Series::new("box2d_score".into(), box2d_scores).into();
2202 let box3d_score_col: Column = Series::new("box3d_score".into(), box3d_scores).into();
2203 let polygon_score_col: Column = Series::new("polygon_score".into(), polygon_scores).into();
2204 let mask_score_col: Column = Series::new("mask_score".into(), mask_scores).into();
2205
2206 let size_series: Vec<Option<Series>> = sizes
2208 .into_iter()
2209 .map(|opt_vec| opt_vec.map(|vec| Series::new("size".into(), vec)))
2210 .collect();
2211 let sizes_col: Column = Series::new("size".into(), size_series)
2212 .cast(&DataType::Array(Box::new(DataType::UInt32), 2))?
2213 .into();
2214
2215 let location_series: Vec<Option<Series>> = locations
2216 .into_iter()
2217 .map(|opt_vec| opt_vec.map(|vec| Series::new("location".into(), vec)))
2218 .collect();
2219 let locations_col: Column = Series::new("location".into(), location_series)
2220 .cast(&DataType::Array(Box::new(DataType::Float32), 2))?
2221 .into();
2222
2223 let pose_series: Vec<Option<Series>> = poses
2224 .into_iter()
2225 .map(|opt_vec| opt_vec.map(|vec| Series::new("pose".into(), vec)))
2226 .collect();
2227 let poses_col: Column = Series::new("pose".into(), pose_series)
2228 .cast(&DataType::Array(Box::new(DataType::Float32), 3))?
2229 .into();
2230
2231 let degradations_col: Column = Series::new("degradation".into(), degradations).into();
2232
2233 let iscrowds_col: Column = Series::new("iscrowd".into(), iscrowds).into();
2235
2236 let category_frequencies_col: Column =
2237 Series::new("category_frequency".into(), category_frequencies)
2238 .cast(&DataType::Categorical(
2239 Categories::new(
2240 "cat_freq".into(),
2241 "cat_freq".into(),
2242 CategoricalPhysical::U8,
2243 ),
2244 Arc::new(CategoricalMapping::with_hasher(
2245 u8::MAX as usize,
2246 Default::default(),
2247 )),
2248 ))?
2249 .into();
2250
2251 let neg_label_indices_series: Vec<Option<Series>> = neg_label_indices_vec
2252 .into_iter()
2253 .map(|opt_vec| opt_vec.map(|vec| Series::new("neg_label_indices".into(), vec)))
2254 .collect();
2255 let neg_label_indices_col: Column =
2256 Series::new("neg_label_indices".into(), neg_label_indices_series)
2257 .cast(&DataType::List(Box::new(DataType::UInt32)))?
2258 .into();
2259
2260 let not_exhaustive_label_indices_series: Vec<Option<Series>> = not_exhaustive_label_indices_vec
2261 .into_iter()
2262 .map(|opt_vec| opt_vec.map(|vec| Series::new("not_exhaustive_label_indices".into(), vec)))
2263 .collect();
2264 let not_exhaustive_label_indices_col: Column = Series::new(
2265 "not_exhaustive_label_indices".into(),
2266 not_exhaustive_label_indices_series,
2267 )
2268 .cast(&DataType::List(Box::new(DataType::UInt32)))?
2269 .into();
2270
2271 let timing_col: Column = StructChunked::from_series(
2273 "timing".into(),
2274 frames_col.len(),
2275 [
2276 Series::new("load".into(), &timing_load),
2277 Series::new("preprocess".into(), &timing_preprocess),
2278 Series::new("inference".into(), &timing_inference),
2279 Series::new("decode".into(), &timing_decode),
2280 ]
2281 .iter(),
2282 )?
2283 .into_series()
2284 .into();
2285
2286 let all_columns: Vec<Column> = vec![
2288 names_col,
2289 frames_col,
2290 objects_col,
2291 labels_col,
2292 label_indices_col,
2293 groups_col,
2294 polygons_col,
2295 boxes2d_col,
2296 boxes3d_col,
2297 mask_col,
2298 box2d_score_col,
2299 box3d_score_col,
2300 polygon_score_col,
2301 mask_score_col,
2302 sizes_col,
2303 locations_col,
2304 poses_col,
2305 degradations_col,
2306 iscrowds_col,
2307 category_frequencies_col,
2308 neg_label_indices_col,
2309 not_exhaustive_label_indices_col,
2310 timing_col,
2311 ];
2312
2313 let height = all_columns.first().map(|c| c.len()).unwrap_or(0);
2314
2315 let non_empty_columns: Vec<Column> = all_columns
2316 .into_iter()
2317 .filter(|col| col.name() == "name" || !is_all_null_column(col))
2318 .collect();
2319
2320 Ok(DataFrame::new(height, non_empty_columns)?)
2321}
2322
2323#[cfg(feature = "polars")]
2327fn is_all_null_column(col: &Column) -> bool {
2328 if col.is_empty() {
2329 return true;
2330 }
2331 if col.null_count() == col.len() {
2332 return true;
2333 }
2334 if let DataType::Struct(..) = col.dtype()
2336 && let Ok(s) = col.as_materialized_series().struct_()
2337 {
2338 return s
2339 .fields_as_series()
2340 .iter()
2341 .all(|field| field.null_count() == field.len());
2342 }
2343 false
2344}
2345
2346#[cfg(feature = "polars")]
2348fn extract_annotation_name_from_sample(sample: &Sample) -> Option<(String, Option<u32>)> {
2349 use std::path::Path;
2350
2351 let name = sample.image_name.as_ref()?;
2352 let name = Path::new(name).file_stem()?.to_str()?;
2353
2354 match &sample.sequence_name {
2357 Some(sequence) => Some((sequence.clone(), sample.frame_number)),
2358 None => Some((name.to_string(), None)),
2359 }
2360}
2361
2362fn extract_sample_name(image_name: &str) -> String {
2375 let name = image_name
2377 .rsplit_once('.')
2378 .and_then(|(name, _)| {
2379 if name.is_empty() {
2381 None
2382 } else {
2383 Some(name.to_string())
2384 }
2385 })
2386 .unwrap_or_else(|| image_name.to_string());
2387
2388 name.rsplit_once(".camera")
2390 .and_then(|(name, _)| {
2391 if name.is_empty() {
2393 None
2394 } else {
2395 Some(name.to_string())
2396 }
2397 })
2398 .unwrap_or_else(|| name.clone())
2399}
2400
2401fn resolve_file<'a>(file_type: &FileType, files: &'a [SampleFile]) -> Option<&'a SampleFile> {
2410 match file_type {
2411 FileType::Image => None, FileType::All => None, file => {
2414 let type_names = file_type_names(file);
2416 files
2417 .iter()
2418 .find(|f| type_names.contains(&f.r#type.as_str()))
2419 }
2420 }
2421}
2422
2423fn file_type_names(file_type: &FileType) -> Vec<&'static str> {
2426 match file_type {
2427 FileType::Image => vec!["image"],
2428 FileType::LidarPcd => vec!["lidar.pcd"],
2429 FileType::LidarDepth => vec!["lidar.depth", "depth.png", "depthmap"],
2430 FileType::LidarReflect => vec!["lidar.reflect"],
2431 FileType::RadarPcd => vec!["radar.pcd", "pcd"],
2432 FileType::RadarCube => vec!["radar.png", "cube"],
2433 FileType::All => vec![],
2434 }
2435}
2436
2437fn convert_annotations_map_to_vec(map: HashMap<String, Vec<Annotation>>) -> Vec<Annotation> {
2450 let mut all_annotations = Vec::new();
2451 if let Some(bbox_anns) = map.get("bbox") {
2452 all_annotations.extend(bbox_anns.clone());
2453 }
2454 if let Some(box3d_anns) = map.get("box3d") {
2455 all_annotations.extend(box3d_anns.clone());
2456 }
2457 if let Some(mask_anns) = map.get("mask") {
2458 all_annotations.extend(mask_anns.clone());
2459 }
2460 all_annotations
2461}
2462
2463fn validate_gps_coordinates(lat: f64, lon: f64) -> Result<(), String> {
2483 if !lat.is_finite() {
2484 return Err(format!("GPS latitude is not finite: {}", lat));
2485 }
2486 if !lon.is_finite() {
2487 return Err(format!("GPS longitude is not finite: {}", lon));
2488 }
2489 if !(-90.0..=90.0).contains(&lat) {
2490 return Err(format!("GPS latitude out of range [-90, 90]: {}", lat));
2491 }
2492 if !(-180.0..=180.0).contains(&lon) {
2493 return Err(format!("GPS longitude out of range [-180, 180]: {}", lon));
2494 }
2495 Ok(())
2496}
2497
2498fn validate_imu_orientation(roll: f64, pitch: f64, yaw: f64) -> Result<(), String> {
2517 if !roll.is_finite() {
2518 return Err(format!("IMU roll is not finite: {}", roll));
2519 }
2520 if !pitch.is_finite() {
2521 return Err(format!("IMU pitch is not finite: {}", pitch));
2522 }
2523 if !yaw.is_finite() {
2524 return Err(format!("IMU yaw is not finite: {}", yaw));
2525 }
2526 if !(-180.0..=180.0).contains(&roll) {
2527 return Err(format!("IMU roll out of range [-180, 180]: {}", roll));
2528 }
2529 if !(-90.0..=90.0).contains(&pitch) {
2530 return Err(format!("IMU pitch out of range [-90, 90]: {}", pitch));
2531 }
2532 if !(-180.0..=180.0).contains(&yaw) {
2533 return Err(format!("IMU yaw out of range [-180, 180]: {}", yaw));
2534 }
2535 Ok(())
2536}
2537
2538#[cfg(feature = "polars")]
2566pub fn unflatten_polygon_coordinates(coords: &[f32]) -> Vec<Vec<(f32, f32)>> {
2567 let mut polygons = Vec::new();
2568 let mut current_polygon = Vec::new();
2569 let mut i = 0;
2570
2571 while i < coords.len() {
2572 if coords[i].is_nan() {
2573 if !current_polygon.is_empty() {
2575 polygons.push(std::mem::take(&mut current_polygon));
2576 }
2577 i += 1;
2578 } else if i + 1 < coords.len() && !coords[i + 1].is_nan() {
2579 current_polygon.push((coords[i], coords[i + 1]));
2581 i += 2;
2582 } else if i + 1 < coords.len() && coords[i + 1].is_nan() {
2583 i += 1;
2586 } else {
2587 i += 1;
2589 }
2590 }
2591
2592 if !current_polygon.is_empty() {
2594 polygons.push(current_polygon);
2595 }
2596
2597 polygons
2598}
2599
2600#[cfg(test)]
2601mod tests {
2602 use super::*;
2603
2604 fn flatten_annotation_map(
2613 map: std::collections::HashMap<String, Vec<Annotation>>,
2614 ) -> Vec<Annotation> {
2615 let mut all_annotations = Vec::new();
2616
2617 for key in ["bbox", "box3d", "mask"] {
2619 if let Some(mut anns) = map.get(key).cloned() {
2620 all_annotations.append(&mut anns);
2621 }
2622 }
2623
2624 all_annotations
2625 }
2626
2627 fn annotation_group_field_name() -> &'static str {
2629 "group_name"
2630 }
2631
2632 fn annotation_object_id_field_name() -> &'static str {
2634 "object_reference"
2635 }
2636
2637 fn annotation_object_id_alias() -> &'static str {
2639 "object_id"
2640 }
2641
2642 fn validate_annotation_field_names(
2645 json_str: &str,
2646 expected_group: bool,
2647 expected_object_ref: bool,
2648 ) -> Result<(), String> {
2649 if expected_group && !json_str.contains("\"group_name\"") {
2650 return Err("Missing expected field: group_name".to_string());
2651 }
2652 if expected_object_ref && !json_str.contains("\"object_reference\"") {
2653 return Err("Missing expected field: object_reference".to_string());
2654 }
2655 Ok(())
2656 }
2657
2658 #[test]
2660 fn test_file_type_conversions() {
2661 let api_cases = vec![
2663 (FileType::Image, "image"),
2664 (FileType::LidarPcd, "lidar.pcd"),
2665 (FileType::LidarDepth, "lidar.depth"),
2666 (FileType::LidarReflect, "lidar.reflect"),
2667 (FileType::RadarPcd, "radar.pcd"),
2668 (FileType::RadarCube, "radar.png"),
2669 ];
2670
2671 let ext_cases = vec![
2673 (FileType::Image, "jpg"),
2674 (FileType::LidarPcd, "lidar.pcd"),
2675 (FileType::LidarDepth, "lidar.png"),
2676 (FileType::LidarReflect, "lidar.jpg"),
2677 (FileType::RadarPcd, "radar.pcd"),
2678 (FileType::RadarCube, "radar.png"),
2679 ];
2680
2681 for (file_type, expected_str) in &api_cases {
2683 assert_eq!(file_type.to_string(), *expected_str);
2684 }
2685
2686 for (file_type, expected_ext) in &ext_cases {
2688 assert_eq!(file_type.file_extension(), *expected_ext);
2689 }
2690
2691 assert_eq!(
2693 FileType::try_from("lidar.depth").unwrap(),
2694 FileType::LidarDepth
2695 );
2696 assert_eq!(
2697 FileType::try_from("lidar.png").unwrap(),
2698 FileType::LidarDepth
2699 );
2700 assert_eq!(
2701 FileType::try_from("depth.png").unwrap(),
2702 FileType::LidarDepth
2703 );
2704 assert_eq!(
2705 FileType::try_from("lidar.reflect").unwrap(),
2706 FileType::LidarReflect
2707 );
2708 assert_eq!(
2709 FileType::try_from("lidar.jpg").unwrap(),
2710 FileType::LidarReflect
2711 );
2712 assert_eq!(
2713 FileType::try_from("lidar.jpeg").unwrap(),
2714 FileType::LidarReflect
2715 );
2716
2717 assert!(FileType::try_from("invalid").is_err());
2719
2720 for (file_type, _) in &api_cases {
2722 let s = file_type.to_string();
2723 let parsed = FileType::try_from(s.as_str()).unwrap();
2724 assert_eq!(parsed, *file_type);
2725 }
2726 }
2727
2728 #[test]
2730 fn test_annotation_type_conversions() {
2731 let cases = vec![
2732 (AnnotationType::Box2d, "box2d"),
2733 (AnnotationType::Box3d, "box3d"),
2734 (AnnotationType::Polygon, "polygon"),
2735 (AnnotationType::Mask, "mask"),
2736 ];
2737
2738 for (ann_type, expected_str) in &cases {
2740 assert_eq!(ann_type.to_string(), *expected_str);
2741 }
2742
2743 assert_eq!(
2745 AnnotationType::try_from("box2d").unwrap(),
2746 AnnotationType::Box2d
2747 );
2748 assert_eq!(
2749 AnnotationType::try_from("box3d").unwrap(),
2750 AnnotationType::Box3d
2751 );
2752 assert_eq!(
2753 AnnotationType::try_from("polygon").unwrap(),
2754 AnnotationType::Polygon
2755 );
2756 assert_eq!(
2758 AnnotationType::try_from("mask").unwrap(),
2759 AnnotationType::Polygon
2760 );
2761 assert_eq!(
2763 AnnotationType::try_from("raster").unwrap(),
2764 AnnotationType::Mask
2765 );
2766
2767 assert_eq!(
2769 AnnotationType::from("box2d".to_string()),
2770 AnnotationType::Box2d
2771 );
2772 assert_eq!(
2773 AnnotationType::from("box3d".to_string()),
2774 AnnotationType::Box3d
2775 );
2776 assert_eq!(
2777 AnnotationType::from("polygon".to_string()),
2778 AnnotationType::Polygon
2779 );
2780 assert_eq!(
2782 AnnotationType::from("mask".to_string()),
2783 AnnotationType::Polygon
2784 );
2785
2786 assert_eq!(
2788 AnnotationType::from("invalid".to_string()),
2789 AnnotationType::Box2d
2790 );
2791
2792 assert!(AnnotationType::try_from("invalid").is_err());
2794
2795 assert_eq!(
2800 AnnotationType::try_from(AnnotationType::Box2d.to_string().as_str()).unwrap(),
2801 AnnotationType::Box2d
2802 );
2803 assert_eq!(
2804 AnnotationType::try_from(AnnotationType::Box3d.to_string().as_str()).unwrap(),
2805 AnnotationType::Box3d
2806 );
2807 assert_eq!(
2808 AnnotationType::try_from(AnnotationType::Polygon.to_string().as_str()).unwrap(),
2809 AnnotationType::Polygon
2810 );
2811 }
2812
2813 #[test]
2814 fn test_annotation_type_as_server_type() {
2815 assert_eq!(AnnotationType::Box2d.as_server_type(), "box2d");
2820 assert_eq!(AnnotationType::Box3d.as_server_type(), "box3d");
2821 assert_eq!(AnnotationType::Polygon.as_server_type(), "mask");
2822 assert_eq!(AnnotationType::Mask.as_server_type(), "mask");
2823
2824 assert_ne!(
2827 AnnotationType::Polygon.as_server_type(),
2828 AnnotationType::Polygon.to_string().as_str()
2829 );
2830 assert_eq!(
2831 AnnotationType::Box2d.as_server_type(),
2832 AnnotationType::Box2d.to_string().as_str()
2833 );
2834 }
2835
2836 #[test]
2838 fn test_extract_sample_name_with_extension_and_camera() {
2839 assert_eq!(extract_sample_name("scene_001.camera.jpg"), "scene_001");
2840 }
2841
2842 #[test]
2843 fn test_extract_sample_name_multiple_dots() {
2844 assert_eq!(extract_sample_name("image.v2.camera.png"), "image.v2");
2845 }
2846
2847 #[test]
2848 fn test_extract_sample_name_extension_only() {
2849 assert_eq!(extract_sample_name("test.jpg"), "test");
2850 }
2851
2852 #[test]
2853 fn test_extract_sample_name_no_extension() {
2854 assert_eq!(extract_sample_name("test"), "test");
2855 }
2856
2857 #[test]
2858 fn test_extract_sample_name_edge_case_dot_prefix() {
2859 assert_eq!(extract_sample_name(".jpg"), ".jpg");
2860 }
2861
2862 #[test]
2864 fn test_resolve_file_image_type_returns_none() {
2865 let files = vec![];
2867 let result = resolve_file(&FileType::Image, &files);
2868 assert!(result.is_none());
2869 }
2870
2871 #[test]
2872 fn test_resolve_file_lidar_pcd() {
2873 let files = vec![
2874 SampleFile::with_url(
2875 "lidar.pcd".to_string(),
2876 "https://example.com/file.pcd".to_string(),
2877 ),
2878 SampleFile::with_url(
2879 "radar.pcd".to_string(),
2880 "https://example.com/radar.pcd".to_string(),
2881 ),
2882 ];
2883 let result = resolve_file(&FileType::LidarPcd, &files);
2884 assert!(result.is_some());
2885 assert_eq!(result.unwrap().url(), Some("https://example.com/file.pcd"));
2886 }
2887
2888 #[test]
2889 fn test_resolve_file_not_found() {
2890 let files = vec![SampleFile::with_url(
2891 "lidar.pcd".to_string(),
2892 "https://example.com/file.pcd".to_string(),
2893 )];
2894 let result = resolve_file(&FileType::RadarPcd, &files);
2896 assert!(result.is_none());
2897 }
2898
2899 #[test]
2900 fn test_resolve_file_lidar_depth() {
2901 let files = vec![SampleFile::with_url(
2903 "lidar.depth".to_string(),
2904 "https://example.com/depth.png".to_string(),
2905 )];
2906 let result = resolve_file(&FileType::LidarDepth, &files);
2907 assert!(result.is_some());
2908 assert_eq!(result.unwrap().url(), Some("https://example.com/depth.png"));
2909 }
2910
2911 #[test]
2912 fn test_resolve_file_lidar_reflect() {
2913 let files = vec![SampleFile::with_url(
2915 "lidar.reflect".to_string(),
2916 "https://example.com/reflect.png".to_string(),
2917 )];
2918 let result = resolve_file(&FileType::LidarReflect, &files);
2919 assert!(result.is_some());
2920 assert_eq!(
2921 result.unwrap().url(),
2922 Some("https://example.com/reflect.png")
2923 );
2924 }
2925
2926 #[test]
2927 fn test_resolve_file_radar_cube() {
2928 let files = vec![SampleFile::with_url(
2930 "radar.png".to_string(),
2931 "https://example.com/radar.png".to_string(),
2932 )];
2933 let result = resolve_file(&FileType::RadarCube, &files);
2934 assert!(result.is_some());
2935 assert_eq!(result.unwrap().url(), Some("https://example.com/radar.png"));
2936 }
2937
2938 #[test]
2939 fn test_resolve_file_with_inline_data() {
2940 let files = vec![SampleFile::with_data(
2942 "radar.pcd".to_string(),
2943 "SGVsbG8gV29ybGQ=".to_string(), )];
2945 let result = resolve_file(&FileType::RadarPcd, &files);
2946 assert!(result.is_some());
2947 let file = result.unwrap();
2948 assert!(file.url().is_none());
2949 assert_eq!(file.data(), Some("SGVsbG8gV29ybGQ="));
2950 }
2951
2952 #[test]
2953 fn test_convert_annotations_map_to_vec_with_bbox() {
2954 let mut map = HashMap::new();
2955 let bbox_ann = Annotation::new();
2956 map.insert("bbox".to_string(), vec![bbox_ann.clone()]);
2957
2958 let annotations = convert_annotations_map_to_vec(map);
2959 assert_eq!(annotations.len(), 1);
2960 }
2961
2962 #[test]
2963 fn test_convert_annotations_map_to_vec_all_types() {
2964 let mut map = HashMap::new();
2965 map.insert("bbox".to_string(), vec![Annotation::new()]);
2966 map.insert("box3d".to_string(), vec![Annotation::new()]);
2967 map.insert("mask".to_string(), vec![Annotation::new()]);
2968
2969 let annotations = convert_annotations_map_to_vec(map);
2970 assert_eq!(annotations.len(), 3);
2971 }
2972
2973 #[test]
2974 fn test_convert_annotations_map_to_vec_empty() {
2975 let map = HashMap::new();
2976 let annotations = convert_annotations_map_to_vec(map);
2977 assert_eq!(annotations.len(), 0);
2978 }
2979
2980 #[test]
2981 fn test_convert_annotations_map_to_vec_unknown_type_ignored() {
2982 let mut map = HashMap::new();
2983 map.insert("unknown".to_string(), vec![Annotation::new()]);
2984
2985 let annotations = convert_annotations_map_to_vec(map);
2986 assert_eq!(annotations.len(), 0);
2988 }
2989
2990 #[test]
2992 fn test_annotation_group_field_name() {
2993 assert_eq!(annotation_group_field_name(), "group_name");
2994 }
2995
2996 #[test]
2997 fn test_annotation_object_id_field_name() {
2998 assert_eq!(annotation_object_id_field_name(), "object_reference");
2999 }
3000
3001 #[test]
3002 fn test_annotation_object_id_alias() {
3003 assert_eq!(annotation_object_id_alias(), "object_id");
3004 }
3005
3006 #[test]
3007 fn test_validate_annotation_field_names_success() {
3008 let json = r#"{"group_name":"train","object_reference":"obj1"}"#;
3009 assert!(validate_annotation_field_names(json, true, true).is_ok());
3010 }
3011
3012 #[test]
3013 fn test_validate_annotation_field_names_missing_group() {
3014 let json = r#"{"object_reference":"obj1"}"#;
3015 let result = validate_annotation_field_names(json, true, false);
3016 assert!(result.is_err());
3017 assert!(result.unwrap_err().contains("group_name"));
3018 }
3019
3020 #[test]
3021 fn test_validate_annotation_field_names_missing_object_ref() {
3022 let json = r#"{"group_name":"train"}"#;
3023 let result = validate_annotation_field_names(json, false, true);
3024 assert!(result.is_err());
3025 assert!(result.unwrap_err().contains("object_reference"));
3026 }
3027
3028 #[test]
3029 fn test_annotation_serialization_field_names() {
3030 let mut ann = Annotation::new();
3032 ann.set_group(Some("train".to_string()));
3033 ann.set_object_id(Some("obj1".to_string()));
3034
3035 let json = serde_json::to_string(&ann).unwrap();
3036 assert!(validate_annotation_field_names(&json, true, true).is_ok());
3038 }
3039
3040 #[test]
3042 fn test_validate_gps_coordinates_valid() {
3043 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()); }
3048
3049 #[test]
3050 fn test_validate_gps_coordinates_invalid_latitude() {
3051 let result = validate_gps_coordinates(91.0, 0.0);
3052 assert!(result.is_err());
3053 assert!(result.unwrap_err().contains("latitude out of range"));
3054
3055 let result = validate_gps_coordinates(-91.0, 0.0);
3056 assert!(result.is_err());
3057 assert!(result.unwrap_err().contains("latitude out of range"));
3058 }
3059
3060 #[test]
3061 fn test_validate_gps_coordinates_invalid_longitude() {
3062 let result = validate_gps_coordinates(0.0, 181.0);
3063 assert!(result.is_err());
3064 assert!(result.unwrap_err().contains("longitude out of range"));
3065
3066 let result = validate_gps_coordinates(0.0, -181.0);
3067 assert!(result.is_err());
3068 assert!(result.unwrap_err().contains("longitude out of range"));
3069 }
3070
3071 #[test]
3072 fn test_validate_gps_coordinates_non_finite() {
3073 let result = validate_gps_coordinates(f64::NAN, 0.0);
3074 assert!(result.is_err());
3075 assert!(result.unwrap_err().contains("not finite"));
3076
3077 let result = validate_gps_coordinates(0.0, f64::INFINITY);
3078 assert!(result.is_err());
3079 assert!(result.unwrap_err().contains("not finite"));
3080 }
3081
3082 #[test]
3083 fn test_validate_imu_orientation_valid() {
3084 assert!(validate_imu_orientation(0.0, 0.0, 0.0).is_ok());
3085 assert!(validate_imu_orientation(45.0, 30.0, 90.0).is_ok());
3086 assert!(validate_imu_orientation(180.0, 90.0, -180.0).is_ok()); assert!(validate_imu_orientation(-180.0, -90.0, 180.0).is_ok()); }
3089
3090 #[test]
3091 fn test_validate_imu_orientation_invalid_roll() {
3092 let result = validate_imu_orientation(181.0, 0.0, 0.0);
3093 assert!(result.is_err());
3094 assert!(result.unwrap_err().contains("roll out of range"));
3095
3096 let result = validate_imu_orientation(-181.0, 0.0, 0.0);
3097 assert!(result.is_err());
3098 }
3099
3100 #[test]
3101 fn test_validate_imu_orientation_invalid_pitch() {
3102 let result = validate_imu_orientation(0.0, 91.0, 0.0);
3103 assert!(result.is_err());
3104 assert!(result.unwrap_err().contains("pitch out of range"));
3105
3106 let result = validate_imu_orientation(0.0, -91.0, 0.0);
3107 assert!(result.is_err());
3108 }
3109
3110 #[test]
3111 fn test_validate_imu_orientation_non_finite() {
3112 let result = validate_imu_orientation(f64::NAN, 0.0, 0.0);
3113 assert!(result.is_err());
3114 assert!(result.unwrap_err().contains("not finite"));
3115
3116 let result = validate_imu_orientation(0.0, f64::INFINITY, 0.0);
3117 assert!(result.is_err());
3118
3119 let result = validate_imu_orientation(0.0, 0.0, f64::NEG_INFINITY);
3120 assert!(result.is_err());
3121 }
3122
3123 #[test]
3125 #[cfg(feature = "polars")]
3126 fn test_unflatten_polygon_coordinates_single_polygon() {
3127 let coords = vec![1.0, 2.0, 3.0, 4.0];
3128 let result = unflatten_polygon_coordinates(&coords);
3129
3130 assert_eq!(result.len(), 1);
3131 assert_eq!(result[0].len(), 2);
3132 assert_eq!(result[0][0], (1.0, 2.0));
3133 assert_eq!(result[0][1], (3.0, 4.0));
3134 }
3135
3136 #[test]
3137 #[cfg(feature = "polars")]
3138 fn test_unflatten_polygon_coordinates_multiple_polygons() {
3139 let coords = vec![1.0, 2.0, 3.0, 4.0, f32::NAN, 5.0, 6.0, 7.0, 8.0];
3140 let result = unflatten_polygon_coordinates(&coords);
3141
3142 assert_eq!(result.len(), 2);
3143 assert_eq!(result[0].len(), 2);
3144 assert_eq!(result[0][0], (1.0, 2.0));
3145 assert_eq!(result[0][1], (3.0, 4.0));
3146 assert_eq!(result[1].len(), 2);
3147 assert_eq!(result[1][0], (5.0, 6.0));
3148 assert_eq!(result[1][1], (7.0, 8.0));
3149 }
3150
3151 #[test]
3152 #[cfg(feature = "polars")]
3153 fn test_unflatten_polygon_coordinates_roundtrip() {
3154 let flat = vec![1.0, 2.0, 3.0, 4.0, f32::NAN, 5.0, 6.0, 7.0, 8.0];
3156 let result = unflatten_polygon_coordinates(&flat);
3157
3158 let expected = vec![vec![(1.0, 2.0), (3.0, 4.0)], vec![(5.0, 6.0), (7.0, 8.0)]];
3159 assert_eq!(result, expected);
3160 }
3161
3162 #[test]
3164 fn test_flatten_annotation_map_all_types() {
3165 use std::collections::HashMap;
3166
3167 let mut map = HashMap::new();
3168
3169 let mut bbox_ann = Annotation::new();
3171 bbox_ann.set_label(Some("bbox_label".to_string()));
3172
3173 let mut box3d_ann = Annotation::new();
3174 box3d_ann.set_label(Some("box3d_label".to_string()));
3175
3176 let mut mask_ann = Annotation::new();
3177 mask_ann.set_label(Some("mask_label".to_string()));
3178
3179 map.insert("bbox".to_string(), vec![bbox_ann.clone()]);
3180 map.insert("box3d".to_string(), vec![box3d_ann.clone()]);
3181 map.insert("mask".to_string(), vec![mask_ann.clone()]);
3182
3183 let result = flatten_annotation_map(map);
3184
3185 assert_eq!(result.len(), 3);
3186 assert_eq!(result[0].label(), Some(&"bbox_label".to_string()));
3188 assert_eq!(result[1].label(), Some(&"box3d_label".to_string()));
3189 assert_eq!(result[2].label(), Some(&"mask_label".to_string()));
3190 }
3191
3192 #[test]
3193 fn test_flatten_annotation_map_single_type() {
3194 use std::collections::HashMap;
3195
3196 let mut map = HashMap::new();
3197 let mut bbox_ann = Annotation::new();
3198 bbox_ann.set_label(Some("test".to_string()));
3199 map.insert("bbox".to_string(), vec![bbox_ann]);
3200
3201 let result = flatten_annotation_map(map);
3202
3203 assert_eq!(result.len(), 1);
3204 assert_eq!(result[0].label(), Some(&"test".to_string()));
3205 }
3206
3207 #[test]
3208 fn test_flatten_annotation_map_empty() {
3209 use std::collections::HashMap;
3210
3211 let map = HashMap::new();
3212 let result = flatten_annotation_map(map);
3213
3214 assert_eq!(result.len(), 0);
3215 }
3216
3217 #[test]
3218 fn test_flatten_annotation_map_deterministic_order() {
3219 use std::collections::HashMap;
3220
3221 let mut map = HashMap::new();
3222
3223 let mut bbox_ann = Annotation::new();
3224 bbox_ann.set_label(Some("bbox".to_string()));
3225
3226 let mut box3d_ann = Annotation::new();
3227 box3d_ann.set_label(Some("box3d".to_string()));
3228
3229 let mut mask_ann = Annotation::new();
3230 mask_ann.set_label(Some("mask".to_string()));
3231
3232 map.insert("mask".to_string(), vec![mask_ann]);
3234 map.insert("box3d".to_string(), vec![box3d_ann]);
3235 map.insert("bbox".to_string(), vec![bbox_ann]);
3236
3237 let result = flatten_annotation_map(map);
3238
3239 assert_eq!(result.len(), 3);
3241 assert_eq!(result[0].label(), Some(&"bbox".to_string()));
3242 assert_eq!(result[1].label(), Some(&"box3d".to_string()));
3243 assert_eq!(result[2].label(), Some(&"mask".to_string()));
3244 }
3245
3246 #[test]
3248 fn test_box2d_construction_and_accessors() {
3249 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
3251 assert_eq!(
3252 (bbox.left(), bbox.top(), bbox.width(), bbox.height()),
3253 (10.0, 20.0, 100.0, 50.0)
3254 );
3255
3256 assert_eq!((bbox.cx(), bbox.cy()), (60.0, 45.0)); let bbox = Box2d::new(0.0, 0.0, 640.0, 480.0);
3261 assert_eq!(
3262 (bbox.left(), bbox.top(), bbox.width(), bbox.height()),
3263 (0.0, 0.0, 640.0, 480.0)
3264 );
3265 assert_eq!((bbox.cx(), bbox.cy()), (320.0, 240.0));
3266 }
3267
3268 #[test]
3269 fn test_box2d_center_calculation() {
3270 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
3271
3272 assert_eq!(bbox.cx(), 60.0); assert_eq!(bbox.cy(), 45.0); }
3276
3277 #[test]
3278 fn test_box2d_zero_dimensions() {
3279 let bbox = Box2d::new(10.0, 20.0, 0.0, 0.0);
3280
3281 assert_eq!(bbox.cx(), 10.0);
3283 assert_eq!(bbox.cy(), 20.0);
3284 }
3285
3286 #[test]
3287 fn test_box2d_negative_dimensions() {
3288 let bbox = Box2d::new(100.0, 100.0, -50.0, -50.0);
3289
3290 assert_eq!(bbox.width(), -50.0);
3292 assert_eq!(bbox.height(), -50.0);
3293 assert_eq!(bbox.cx(), 75.0); assert_eq!(bbox.cy(), 75.0); }
3296
3297 #[test]
3299 fn test_box3d_construction_and_accessors() {
3300 let bbox = Box3d::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
3302 assert_eq!((bbox.cx(), bbox.cy(), bbox.cz()), (1.0, 2.0, 3.0));
3303 assert_eq!(
3304 (bbox.width(), bbox.height(), bbox.length()),
3305 (4.0, 5.0, 6.0)
3306 );
3307
3308 let bbox = Box3d::new(10.0, 20.0, 30.0, 4.0, 6.0, 8.0);
3310 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);
3314 assert_eq!((bbox.cx(), bbox.cy(), bbox.cz()), (0.0, 0.0, 0.0));
3315 assert_eq!(
3316 (bbox.width(), bbox.height(), bbox.length()),
3317 (2.0, 3.0, 4.0)
3318 );
3319 assert_eq!((bbox.left(), bbox.top(), bbox.front()), (-1.0, -1.5, -2.0));
3320 }
3321
3322 #[test]
3323 fn test_box3d_center_calculation() {
3324 let bbox = Box3d::new(10.0, 20.0, 30.0, 100.0, 50.0, 40.0);
3325
3326 assert_eq!(bbox.cx(), 10.0);
3328 assert_eq!(bbox.cy(), 20.0);
3329 assert_eq!(bbox.cz(), 30.0);
3330 }
3331
3332 #[test]
3333 fn test_box3d_zero_dimensions() {
3334 let bbox = Box3d::new(5.0, 10.0, 15.0, 0.0, 0.0, 0.0);
3335
3336 assert_eq!(bbox.cx(), 5.0);
3338 assert_eq!(bbox.cy(), 10.0);
3339 assert_eq!(bbox.cz(), 15.0);
3340 assert_eq!((bbox.left(), bbox.top(), bbox.front()), (5.0, 10.0, 15.0));
3341 }
3342
3343 #[test]
3344 fn test_box3d_negative_dimensions() {
3345 let bbox = Box3d::new(100.0, 100.0, 100.0, -50.0, -50.0, -50.0);
3346
3347 assert_eq!(bbox.width(), -50.0);
3349 assert_eq!(bbox.height(), -50.0);
3350 assert_eq!(bbox.length(), -50.0);
3351 assert_eq!(
3352 (bbox.left(), bbox.top(), bbox.front()),
3353 (125.0, 125.0, 125.0)
3354 );
3355 }
3356
3357 #[test]
3359 fn test_polygon_creation_and_deserialization() {
3360 let rings = vec![vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]];
3362 let polygon = Polygon::new(rings.clone());
3363 assert_eq!(polygon.rings, rings);
3364
3365 let legacy = serde_json::json!({
3367 "polygon": {
3368 "polygon": [[
3369 [0.0_f32, 0.0_f32],
3370 [1.0_f32, 0.0_f32],
3371 [1.0_f32, 1.0_f32]
3372 ]]
3373 }
3374 });
3375
3376 #[derive(serde::Deserialize)]
3377 struct Wrapper {
3378 polygon: Polygon,
3379 }
3380
3381 let parsed: Wrapper = serde_json::from_value(legacy).unwrap();
3382 assert_eq!(parsed.polygon.rings.len(), 1);
3383 assert_eq!(parsed.polygon.rings[0].len(), 3);
3384 }
3385
3386 #[test]
3388 fn test_sample_construction_and_accessors() {
3389 let sample = Sample::new();
3391 assert_eq!(sample.id(), None);
3392 assert_eq!(sample.image_name(), None);
3393 assert_eq!(sample.width(), None);
3394 assert_eq!(sample.height(), None);
3395
3396 let mut sample = Sample::new();
3398 sample.image_name = Some("test.jpg".to_string());
3399 sample.width = Some(1920);
3400 sample.height = Some(1080);
3401 sample.group = Some("group1".to_string());
3402
3403 assert_eq!(sample.image_name(), Some("test.jpg"));
3404 assert_eq!(sample.width(), Some(1920));
3405 assert_eq!(sample.height(), Some(1080));
3406 assert_eq!(sample.group(), Some(&"group1".to_string()));
3407 }
3408
3409 #[test]
3410 fn test_sample_name_extraction_from_image_name() {
3411 let mut sample = Sample::new();
3412
3413 sample.image_name = Some("test_image.jpg".to_string());
3415 assert_eq!(sample.name(), Some("test_image".to_string()));
3416
3417 sample.image_name = Some("test_image.camera.jpg".to_string());
3419 assert_eq!(sample.name(), Some("test_image".to_string()));
3420
3421 sample.image_name = Some("test_image".to_string());
3423 assert_eq!(sample.name(), Some("test_image".to_string()));
3424 }
3425
3426 #[test]
3428 fn test_annotation_construction_and_setters() {
3429 let ann = Annotation::new();
3431 assert_eq!(ann.sample_id(), None);
3432 assert_eq!(ann.label(), None);
3433 assert_eq!(ann.box2d(), None);
3434 assert_eq!(ann.box3d(), None);
3435 assert_eq!(ann.polygon(), None);
3436
3437 let mut ann = Annotation::new();
3439 ann.set_label(Some("car".to_string()));
3440 assert_eq!(ann.label(), Some(&"car".to_string()));
3441
3442 ann.set_label_index(Some(42));
3443 assert_eq!(ann.label_index(), Some(42));
3444
3445 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
3447 ann.set_box2d(Some(bbox.clone()));
3448 assert!(ann.box2d().is_some());
3449 assert_eq!(ann.box2d().unwrap().left(), 10.0);
3450 }
3451
3452 #[test]
3454 fn test_sample_file_with_url_and_filename() {
3455 let file = SampleFile::with_url(
3457 "lidar.pcd".to_string(),
3458 "https://example.com/file.pcd".to_string(),
3459 );
3460 assert_eq!(file.file_type(), "lidar.pcd");
3461 assert_eq!(file.url(), Some("https://example.com/file.pcd"));
3462 assert_eq!(file.filename(), None);
3463
3464 let file = SampleFile::with_filename("image".to_string(), "test.jpg".to_string());
3466 assert_eq!(file.file_type(), "image");
3467 assert_eq!(file.filename(), Some("test.jpg"));
3468 assert_eq!(file.url(), None);
3469 }
3470
3471 #[test]
3473 fn test_sample_deserializes_gps_imu_from_sensors() {
3474 use serde_json::json;
3475
3476 let sample_json = json!({
3478 "id": 123,
3479 "image_name": "test.jpg",
3480 "sensors": [
3481 {"gps": {"lat": 37.7749, "lon": -122.4194}},
3482 {"imu": {"roll": 1.5, "pitch": 2.5, "yaw": 3.5}},
3483 {"radar.pcd": "https://example.com/radar.pcd"}
3484 ]
3485 });
3486
3487 let sample: Sample = serde_json::from_value(sample_json).unwrap();
3488
3489 assert!(sample.location.is_some());
3491 let location = sample.location.as_ref().unwrap();
3492
3493 assert!(location.gps.is_some());
3495 let gps = location.gps.as_ref().unwrap();
3496 assert!((gps.lat - 37.7749).abs() < 0.0001);
3497 assert!((gps.lon - (-122.4194)).abs() < 0.0001);
3498
3499 assert!(location.imu.is_some());
3501 let imu = location.imu.as_ref().unwrap();
3502 assert!((imu.roll - 1.5).abs() < 0.0001);
3503 assert!((imu.pitch - 2.5).abs() < 0.0001);
3504 assert!((imu.yaw - 3.5).abs() < 0.0001);
3505
3506 assert_eq!(sample.files.len(), 1);
3508 assert_eq!(sample.files[0].file_type(), "radar.pcd");
3509 assert_eq!(sample.files[0].url(), Some("https://example.com/radar.pcd"));
3510 }
3511
3512 #[test]
3513 fn test_sample_deserializes_gps_only() {
3514 use serde_json::json;
3515
3516 let sample_json = json!({
3518 "id": 456,
3519 "sensors": [
3520 {"gps": {"lat": 40.7128, "lon": -74.0060}}
3521 ]
3522 });
3523
3524 let sample: Sample = serde_json::from_value(sample_json).unwrap();
3525
3526 assert!(sample.location.is_some());
3527 let location = sample.location.as_ref().unwrap();
3528
3529 assert!(location.gps.is_some());
3530 assert!(location.imu.is_none());
3531
3532 let gps = location.gps.as_ref().unwrap();
3533 assert!((gps.lat - 40.7128).abs() < 0.0001);
3534 assert!((gps.lon - (-74.0060)).abs() < 0.0001);
3535 }
3536
3537 #[test]
3538 fn test_sample_deserializes_without_location() {
3539 use serde_json::json;
3540
3541 let sample_json = json!({
3543 "id": 789,
3544 "sensors": [
3545 {"radar.pcd": "https://example.com/radar.pcd"},
3546 {"lidar.pcd": "https://example.com/lidar.pcd"}
3547 ]
3548 });
3549
3550 let sample: Sample = serde_json::from_value(sample_json).unwrap();
3551
3552 assert!(sample.location.is_none());
3554
3555 assert_eq!(sample.files.len(), 2);
3557 }
3558
3559 #[test]
3561 fn test_label_deserialization_and_accessors() {
3562 use serde_json::json;
3563
3564 let label_json = json!({
3566 "id": 123,
3567 "dataset_id": 456,
3568 "index": 5,
3569 "name": "car"
3570 });
3571
3572 let label: Label = serde_json::from_value(label_json).unwrap();
3573 assert_eq!(label.id(), 123);
3574 assert_eq!(label.index(), 5);
3575 assert_eq!(label.name(), "car");
3576 assert_eq!(label.to_string(), "car");
3577 assert_eq!(format!("{}", label), "car");
3578
3579 let label_json = json!({
3581 "id": 1,
3582 "dataset_id": 100,
3583 "index": 0,
3584 "name": "person"
3585 });
3586
3587 let label: Label = serde_json::from_value(label_json).unwrap();
3588 assert_eq!(format!("{}", label), "person");
3589 }
3590
3591 #[test]
3593 fn test_annotation_serialization_with_mask_and_box() {
3594 let polygon = vec![vec![
3595 (0.0_f32, 0.0_f32),
3596 (1.0_f32, 0.0_f32),
3597 (1.0_f32, 1.0_f32),
3598 ]];
3599
3600 let mut annotation = Annotation::new();
3601 annotation.set_label(Some("test".to_string()));
3602 annotation.set_box2d(Some(Box2d::new(10.0, 20.0, 30.0, 40.0)));
3603 annotation.set_polygon(Some(Polygon::new(polygon)));
3604
3605 let mut sample = Sample::new();
3606 sample.annotations.push(annotation);
3607
3608 let json = serde_json::to_value(&sample).unwrap();
3609 let annotations = json
3610 .get("annotations")
3611 .and_then(|value| value.as_array())
3612 .expect("annotations serialized as array");
3613 assert_eq!(annotations.len(), 1);
3614
3615 let annotation_json = annotations[0].as_object().expect("annotation object");
3616 assert!(annotation_json.contains_key("box2d"));
3617 assert!(
3622 annotation_json.contains_key("mask"),
3623 "Annotation must serialise polygon under 'mask' key for samples.populate2; got keys: {:?}",
3624 annotation_json.keys().collect::<Vec<_>>()
3625 );
3626 assert!(!annotation_json.contains_key("polygon"));
3627 assert!(!annotation_json.contains_key("x"));
3628 assert!(
3629 annotation_json
3630 .get("mask")
3631 .and_then(|value| value.as_array())
3632 .is_some()
3633 );
3634 }
3635
3636 #[test]
3637 fn test_frame_number_negative_one_deserializes_as_none() {
3638 let json = r#"{
3641 "uuid": "test-uuid",
3642 "frame_number": -1
3643 }"#;
3644
3645 let sample: Sample = serde_json::from_str(json).unwrap();
3646 assert_eq!(sample.frame_number, None);
3647 }
3648
3649 #[test]
3650 fn test_frame_number_positive_value_deserializes_correctly() {
3651 let json = r#"{
3653 "uuid": "test-uuid",
3654 "frame_number": 5
3655 }"#;
3656
3657 let sample: Sample = serde_json::from_str(json).unwrap();
3658 assert_eq!(sample.frame_number, Some(5));
3659 }
3660
3661 #[test]
3662 fn test_frame_number_null_deserializes_as_none() {
3663 let json = r#"{
3665 "uuid": "test-uuid",
3666 "frame_number": null
3667 }"#;
3668
3669 let sample: Sample = serde_json::from_str(json).unwrap();
3670 assert_eq!(sample.frame_number, None);
3671 }
3672
3673 #[test]
3674 fn test_frame_number_missing_deserializes_as_none() {
3675 let json = r#"{
3677 "uuid": "test-uuid"
3678 }"#;
3679
3680 let sample: Sample = serde_json::from_str(json).unwrap();
3681 assert_eq!(sample.frame_number, None);
3682 }
3683
3684 #[cfg(feature = "polars")]
3689 #[test]
3690 fn test_samples_dataframe_preserves_group_for_samples_without_annotations() {
3691 use polars::prelude::*;
3692
3693 let mut sample_with_ann = Sample::new();
3695 sample_with_ann.image_name = Some("annotated.jpg".to_string());
3696 sample_with_ann.group = Some("train".to_string());
3697 let mut annotation = Annotation::new();
3698 annotation.set_label(Some("car".to_string()));
3699 annotation.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
3700 annotation.set_name(Some("annotated".to_string()));
3701 sample_with_ann.annotations = vec![annotation];
3702
3703 let mut sample_no_ann = Sample::new();
3705 sample_no_ann.image_name = Some("unannotated.jpg".to_string());
3706 sample_no_ann.group = Some("val".to_string()); sample_no_ann.annotations = vec![]; let samples = vec![sample_with_ann, sample_no_ann];
3710
3711 let df = samples_dataframe(&samples).expect("Failed to create DataFrame");
3713
3714 assert_eq!(df.height(), 2, "Expected 2 rows (one per sample)");
3716
3717 let groups_col = df.column("group").expect("group column should exist");
3719 let groups_cast = groups_col.cast(&DataType::String).expect("cast to string");
3720 let groups = groups_cast.str().expect("as str");
3721
3722 let names_col = df.column("name").expect("name column should exist");
3724 let names_cast = names_col.cast(&DataType::String).expect("cast to string");
3725 let names = names_cast.str().expect("as str");
3726
3727 let mut found_unannotated = false;
3728 for idx in 0..df.height() {
3729 if let Some(name) = names.get(idx)
3730 && name == "unannotated"
3731 {
3732 found_unannotated = true;
3733 let group = groups.get(idx);
3734 assert_eq!(
3735 group,
3736 Some("val"),
3737 "CRITICAL: Sample 'unannotated' without annotations must have group 'val'"
3738 );
3739 }
3740 }
3741
3742 assert!(
3743 found_unannotated,
3744 "Did not find 'unannotated' sample in DataFrame - \
3745 this means samples without annotations are not being included"
3746 );
3747 }
3748
3749 #[cfg(feature = "polars")]
3750 #[test]
3751 fn test_samples_dataframe_includes_all_samples_even_without_annotations() {
3752 let mut sample1 = Sample::new();
3756 sample1.image_name = Some("with_ann.jpg".to_string());
3757 sample1.group = Some("train".to_string());
3758 let mut ann = Annotation::new();
3759 ann.set_label(Some("person".to_string()));
3760 ann.set_box2d(Some(Box2d::new(0.0, 0.0, 0.5, 0.5)));
3761 ann.set_name(Some("with_ann".to_string()));
3762 sample1.annotations = vec![ann];
3763
3764 let mut sample2 = Sample::new();
3765 sample2.image_name = Some("no_ann_train.jpg".to_string());
3766 sample2.group = Some("train".to_string());
3767 sample2.annotations = vec![];
3768
3769 let mut sample3 = Sample::new();
3770 sample3.image_name = Some("no_ann_val.jpg".to_string());
3771 sample3.group = Some("val".to_string());
3772 sample3.annotations = vec![];
3773
3774 let samples = vec![sample1, sample2, sample3];
3775
3776 let df = samples_dataframe(&samples).expect("Failed to create DataFrame");
3777
3778 assert_eq!(
3780 df.height(),
3781 3,
3782 "Expected 3 rows (samples without annotations should create one row each)"
3783 );
3784
3785 let groups_col = df.column("group").expect("group column");
3787 let groups_cast = groups_col.cast(&polars::prelude::DataType::String).unwrap();
3788 let groups = groups_cast.str().unwrap();
3789
3790 let mut train_count = 0;
3791 let mut val_count = 0;
3792
3793 for idx in 0..df.height() {
3794 match groups.get(idx) {
3795 Some("train") => train_count += 1,
3796 Some("val") => val_count += 1,
3797 other => panic!(
3798 "Unexpected group value at row {}: {:?}. \
3799 All samples should have their group preserved.",
3800 idx, other
3801 ),
3802 }
3803 }
3804
3805 assert_eq!(train_count, 2, "Expected 2 samples in 'train' group");
3806 assert_eq!(val_count, 1, "Expected 1 sample in 'val' group");
3807 }
3808
3809 #[cfg(feature = "polars")]
3810 #[test]
3811 fn test_samples_dataframe_group_is_not_null_for_samples_with_group() {
3812 let mut sample = Sample::new();
3816 sample.image_name = Some("test.jpg".to_string());
3817 sample.group = Some("test_group".to_string());
3818 sample.annotations = vec![];
3819
3820 let df = samples_dataframe(&[sample]).expect("Failed to create DataFrame");
3821
3822 let groups_col = df.column("group").expect("group column");
3823
3824 assert_eq!(
3826 groups_col.null_count(),
3827 0,
3828 "Sample with group='test_group' but no annotations has NULL group in DataFrame. \
3829 This is a bug in samples_dataframe - group must be preserved!"
3830 );
3831 }
3832
3833 #[cfg(feature = "polars")]
3834 #[test]
3835 fn test_samples_dataframe_group_consistent_across_all_rows_for_same_image() {
3836 use polars::prelude::*;
3837
3838 let mut sample = Sample::new();
3842 sample.image_name = Some("multi_ann.jpg".to_string());
3843 sample.group = Some("train".to_string());
3844
3845 let mut ann1 = Annotation::new();
3847 ann1.set_label(Some("car".to_string()));
3848 ann1.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
3849 ann1.set_name(Some("multi_ann".to_string()));
3850
3851 let mut ann2 = Annotation::new();
3852 ann2.set_label(Some("truck".to_string()));
3853 ann2.set_box2d(Some(Box2d::new(0.5, 0.6, 0.2, 0.2)));
3854 ann2.set_name(Some("multi_ann".to_string()));
3855
3856 let mut ann3 = Annotation::new();
3857 ann3.set_label(Some("bus".to_string()));
3858 ann3.set_box2d(Some(Box2d::new(0.7, 0.8, 0.1, 0.1)));
3859 ann3.set_name(Some("multi_ann".to_string()));
3860
3861 sample.annotations = vec![ann1, ann2, ann3];
3862
3863 let df = samples_dataframe(&[sample]).expect("Failed to create DataFrame");
3864
3865 assert_eq!(df.height(), 3, "Expected 3 rows (one per annotation)");
3867
3868 let groups_col = df.column("group").expect("group column");
3870 let groups_cast = groups_col.cast(&DataType::String).expect("cast to string");
3871 let groups = groups_cast.str().expect("as str");
3872
3873 assert_eq!(groups_col.null_count(), 0, "No rows should have null group");
3875
3876 for idx in 0..df.height() {
3878 let group = groups.get(idx);
3879 assert_eq!(
3880 group,
3881 Some("train"),
3882 "Row {} should have group 'train', got {:?}. \
3883 All rows for the same image must have identical group values.",
3884 idx,
3885 group
3886 );
3887 }
3888 }
3889
3890 #[cfg(feature = "polars")]
3891 #[test]
3892 fn test_samples_dataframe_lvis_columns() {
3893 let mut ann = Annotation::new();
3894 ann.set_name(Some("test".to_string()));
3895 ann.set_label(Some("person".to_string()));
3896 ann.set_label_index(Some(1));
3897 ann.set_iscrowd(Some(false));
3898 ann.set_category_frequency(Some("f".to_string()));
3899
3900 let sample = Sample {
3901 image_name: Some("test.jpg".to_string()),
3902 width: Some(640),
3903 height: Some(480),
3904 annotations: vec![ann],
3905 neg_label_indices: Some(vec![5, 12]),
3906 not_exhaustive_label_indices: Some(vec![3]),
3907 ..Default::default()
3908 };
3909
3910 let df = samples_dataframe(&[sample]).unwrap();
3911
3912 assert!(df.column("iscrowd").is_ok(), "iscrowd column missing");
3914 assert!(
3915 df.column("category_frequency").is_ok(),
3916 "category_frequency column missing"
3917 );
3918 assert!(
3919 df.column("neg_label_indices").is_ok(),
3920 "neg_label_indices column missing"
3921 );
3922 assert!(
3923 df.column("not_exhaustive_label_indices").is_ok(),
3924 "not_exhaustive_label_indices column missing"
3925 );
3926
3927 assert!(
3929 df.column("polygon").is_err(),
3930 "polygon column should be dropped (all null)"
3931 );
3932 assert!(
3933 df.column("box2d").is_err(),
3934 "box2d column should be dropped (all null)"
3935 );
3936 }
3937
3938 #[test]
3939 fn test_annotation_serialization_skips_lvis_fields() {
3940 let ann = Annotation::new();
3941 let json = serde_json::to_string(&ann).unwrap();
3942 assert!(
3943 !json.contains("iscrowd"),
3944 "iscrowd should be omitted when None"
3945 );
3946 assert!(
3947 !json.contains("category_frequency"),
3948 "category_frequency should be omitted when None"
3949 );
3950 }
3951
3952 #[test]
3953 fn test_sample_serialization_skips_lvis_fields() {
3954 let sample = Sample::new();
3955 let json = serde_json::to_string(&sample).unwrap();
3956 assert!(
3957 !json.contains("neg_label_indices"),
3958 "neg_label_indices should be omitted when None"
3959 );
3960 assert!(
3961 !json.contains("not_exhaustive_label_indices"),
3962 "not_exhaustive_label_indices should be omitted when None"
3963 );
3964 }
3965
3966 #[test]
3967 fn test_annotation_score_fields() {
3968 let mut ann = Annotation::default();
3969 assert!(ann.box2d_score.is_none());
3970 assert!(ann.polygon_score.is_none());
3971 assert!(ann.mask_score.is_none());
3972 ann.box2d_score = Some(0.95);
3973 ann.polygon_score = Some(0.87);
3974 ann.mask_score = Some(0.42);
3975 assert_eq!(ann.box2d_score, Some(0.95));
3976 assert_eq!(ann.polygon_score, Some(0.87));
3977 assert_eq!(ann.mask_score, Some(0.42));
3978 }
3979
3980 #[test]
3981 fn test_timing_struct() {
3982 let timing = Timing {
3983 load: Some(1_000_000),
3984 preprocess: Some(2_000_000),
3985 inference: Some(50_000_000),
3986 decode: Some(3_000_000),
3987 };
3988 assert_eq!(timing.inference, Some(50_000_000));
3989
3990 let default = Timing::default();
3991 assert!(default.load.is_none());
3992 }
3993
3994 #[test]
3995 fn test_sample_timing() {
3996 let mut sample = Sample::default();
3997 assert!(sample.timing.is_none());
3998 sample.timing = Some(Timing {
3999 load: Some(1_000_000),
4000 ..Default::default()
4001 });
4002 assert!(sample.timing.is_some());
4003 }
4004
4005 #[cfg(feature = "polars")]
4010 #[test]
4011 fn test_samples_dataframe_polygon_column() {
4012 let mut ann = Annotation::new();
4013 ann.set_name(Some("test".to_string()));
4014 ann.set_polygon(Some(Polygon::new(vec![vec![
4015 (0.1, 0.2),
4016 (0.3, 0.4),
4017 (0.5, 0.6),
4018 ]])));
4019
4020 let sample = Sample {
4021 image_name: Some("test.jpg".to_string()),
4022 annotations: vec![ann],
4023 ..Default::default()
4024 };
4025
4026 let df = samples_dataframe(&[sample]).unwrap();
4027
4028 assert!(df.column("polygon").is_ok(), "Should have polygon column");
4030
4031 if let Ok(mask_col) = df.column("mask") {
4034 assert_eq!(
4036 mask_col.dtype(),
4037 &polars::prelude::DataType::Binary,
4038 "mask column must be Binary type (PNG bytes), not float list"
4039 );
4040 }
4041 }
4042
4043 #[cfg(feature = "polars")]
4044 #[test]
4045 fn test_samples_dataframe_column_presence_drops_all_null() {
4046 let sample = Sample {
4048 image_name: Some("test.jpg".to_string()),
4049 ..Default::default()
4050 };
4051
4052 let df = samples_dataframe(&[sample]).unwrap();
4053
4054 assert!(df.column("name").is_ok(), "name column must always exist");
4056
4057 assert!(
4059 df.column("polygon").is_err(),
4060 "All-null polygon should be dropped"
4061 );
4062 assert!(
4063 df.column("box2d").is_err(),
4064 "All-null box2d should be dropped"
4065 );
4066 assert!(
4067 df.column("box3d").is_err(),
4068 "All-null box3d should be dropped"
4069 );
4070 assert!(
4071 df.column("mask").is_err(),
4072 "All-null mask should be dropped"
4073 );
4074 assert!(
4075 df.column("box2d_score").is_err(),
4076 "All-null score columns should be dropped"
4077 );
4078 assert!(
4079 df.column("timing").is_err(),
4080 "All-null timing should be dropped"
4081 );
4082 }
4083
4084 #[cfg(feature = "polars")]
4085 #[test]
4086 fn test_samples_dataframe_size_column() {
4087 let sample1 = Sample {
4089 image_name: Some("img1.jpg".to_string()),
4090 width: Some(1920),
4091 height: Some(1080),
4092 ..Default::default()
4093 };
4094 let sample2 = Sample {
4095 image_name: Some("img2.jpg".to_string()),
4096 width: Some(640),
4097 height: Some(480),
4098 ..Default::default()
4099 };
4100
4101 let df = samples_dataframe(&[sample1, sample2]).unwrap();
4102
4103 let size_col = df
4105 .column("size")
4106 .expect("size column should be present when width/height are set");
4107 assert_eq!(size_col.len(), 2);
4108
4109 let arr = size_col.array().expect("size column should be Array dtype");
4111 let row0 = arr.get_as_series(0).unwrap();
4112 let row0_vals: Vec<u32> = row0.u32().unwrap().into_no_null_iter().collect();
4113 assert_eq!(row0_vals, vec![1920, 1080]);
4114
4115 let row1 = arr.get_as_series(1).unwrap();
4116 let row1_vals: Vec<u32> = row1.u32().unwrap().into_no_null_iter().collect();
4117 assert_eq!(row1_vals, vec![640, 480]);
4118 }
4119
4120 #[cfg(feature = "polars")]
4121 #[test]
4122 fn test_samples_dataframe_size_column_partial() {
4123 let sample1 = Sample {
4125 image_name: Some("img1.jpg".to_string()),
4126 width: Some(1920),
4127 height: Some(1080),
4128 ..Default::default()
4129 };
4130 let sample2 = Sample {
4131 image_name: Some("img2.jpg".to_string()),
4132 ..Default::default()
4134 };
4135
4136 let df = samples_dataframe(&[sample1, sample2]).unwrap();
4137
4138 let size_col = df
4140 .column("size")
4141 .expect("size column should be present when at least one sample has dimensions");
4142 assert_eq!(size_col.len(), 2);
4143 assert_eq!(size_col.null_count(), 1, "one row should be null");
4144 }
4145
4146 #[cfg(feature = "polars")]
4147 #[test]
4148 fn test_samples_dataframe_score_columns() {
4149 let mut ann = Annotation::new();
4150 ann.set_name(Some("test".to_string()));
4151 ann.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
4152 ann.set_box2d_score(Some(0.95));
4153 ann.set_polygon(Some(Polygon::new(vec![vec![
4154 (0.0, 0.0),
4155 (1.0, 0.0),
4156 (1.0, 1.0),
4157 ]])));
4158 ann.set_polygon_score(Some(0.87));
4159
4160 let sample = Sample {
4161 image_name: Some("test.jpg".to_string()),
4162 annotations: vec![ann],
4163 ..Default::default()
4164 };
4165
4166 let df = samples_dataframe(&[sample]).unwrap();
4167
4168 assert!(
4170 df.column("box2d_score").is_ok(),
4171 "box2d_score column missing"
4172 );
4173 assert!(
4174 df.column("polygon_score").is_ok(),
4175 "polygon_score column missing"
4176 );
4177
4178 assert!(
4180 df.column("box3d_score").is_err(),
4181 "box3d_score should be dropped (all null)"
4182 );
4183 assert!(
4184 df.column("mask_score").is_err(),
4185 "mask_score should be dropped (all null)"
4186 );
4187
4188 let box2d_scores = df.column("box2d_score").unwrap();
4190 let val = box2d_scores.f32().unwrap().get(0);
4191 assert_eq!(val, Some(0.95));
4192 }
4193
4194 #[cfg(feature = "polars")]
4195 #[test]
4196 fn test_samples_dataframe_timing_column() {
4197 let mut ann = Annotation::new();
4198 ann.set_name(Some("test".to_string()));
4199 ann.set_label(Some("person".to_string()));
4200
4201 let sample = Sample {
4202 image_name: Some("test.jpg".to_string()),
4203 annotations: vec![ann],
4204 timing: Some(Timing {
4205 load: Some(1_000_000),
4206 preprocess: Some(2_000_000),
4207 inference: Some(50_000_000),
4208 decode: Some(3_000_000),
4209 }),
4210 ..Default::default()
4211 };
4212
4213 let df = samples_dataframe(&[sample]).unwrap();
4214
4215 assert!(df.column("timing").is_ok(), "timing column missing");
4217
4218 let timing_col = df.column("timing").unwrap();
4220 assert!(
4221 matches!(timing_col.dtype(), polars::prelude::DataType::Struct(..)),
4222 "timing column should be Struct type, got {:?}",
4223 timing_col.dtype()
4224 );
4225 }
4226
4227 #[cfg(feature = "polars")]
4228 #[test]
4229 fn test_samples_dataframe_mask_binary_column() {
4230 let mut ann = Annotation::new();
4231 ann.set_name(Some("test".to_string()));
4232 let pixels = vec![0u8, 255, 128, 64];
4234 let mask_data = MaskData::encode(&pixels, 2, 2, 8).unwrap();
4235 ann.set_mask(Some(mask_data));
4236
4237 let sample = Sample {
4238 image_name: Some("test.jpg".to_string()),
4239 annotations: vec![ann],
4240 ..Default::default()
4241 };
4242
4243 let df = samples_dataframe(&[sample]).unwrap();
4244
4245 let mask_col = df.column("mask").unwrap();
4247 assert_eq!(
4248 mask_col.dtype(),
4249 &polars::prelude::DataType::Binary,
4250 "mask column should be Binary"
4251 );
4252 assert_eq!(mask_col.null_count(), 0, "mask value should not be null");
4253 }
4254
4255 #[test]
4260 fn test_annotation_type_seg_alias() {
4261 assert_eq!(
4262 AnnotationType::try_from("seg").unwrap(),
4263 AnnotationType::Polygon,
4264 "\"seg\" should map to Polygon for server round-trip"
4265 );
4266 }
4267
4268 #[cfg(feature = "polars")]
4273 #[test]
4274 fn test_samples_dataframe_timing_partial() {
4275 let mut ann = Annotation::new();
4277 ann.set_name(Some("test".to_string()));
4278 ann.set_label(Some("person".to_string()));
4279
4280 let sample = Sample {
4281 image_name: Some("test.jpg".to_string()),
4282 annotations: vec![ann],
4283 timing: Some(Timing {
4284 load: Some(1000),
4285 ..Default::default()
4286 }),
4287 ..Default::default()
4288 };
4289
4290 let df = samples_dataframe(&[sample]).unwrap();
4291
4292 assert!(
4294 df.column("timing").is_ok(),
4295 "timing column should be present when partial data exists"
4296 );
4297 }
4298
4299 #[cfg(feature = "polars")]
4300 #[test]
4301 fn test_samples_dataframe_timing_all_none_omitted() {
4302 let mut ann = Annotation::new();
4304 ann.set_name(Some("test".to_string()));
4305 ann.set_label(Some("person".to_string()));
4306
4307 let sample = Sample {
4308 image_name: Some("test.jpg".to_string()),
4309 annotations: vec![ann],
4310 timing: None,
4311 ..Default::default()
4312 };
4313
4314 let df = samples_dataframe(&[sample]).unwrap();
4315
4316 assert!(
4317 df.column("timing").is_err(),
4318 "timing column should be omitted when all samples have timing: None"
4319 );
4320 }
4321
4322 #[cfg(feature = "polars")]
4327 #[test]
4328 fn test_samples_dataframe_score_zero_survives() {
4329 let mut ann = Annotation::new();
4331 ann.set_name(Some("test".to_string()));
4332 ann.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
4333 ann.set_box2d_score(Some(0.0));
4334
4335 let sample = Sample {
4336 image_name: Some("test.jpg".to_string()),
4337 annotations: vec![ann],
4338 ..Default::default()
4339 };
4340
4341 let df = samples_dataframe(&[sample]).unwrap();
4342
4343 let scores = df.column("box2d_score").unwrap();
4344 let val = scores.f32().unwrap().get(0);
4345 assert_eq!(val, Some(0.0), "score of 0.0 should survive as non-null");
4346 }
4347
4348 #[cfg(feature = "polars")]
4349 #[test]
4350 fn test_samples_dataframe_score_one_survives() {
4351 let mut ann = Annotation::new();
4352 ann.set_name(Some("test".to_string()));
4353 ann.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
4354 ann.set_box2d_score(Some(1.0));
4355
4356 let sample = Sample {
4357 image_name: Some("test.jpg".to_string()),
4358 annotations: vec![ann],
4359 ..Default::default()
4360 };
4361
4362 let df = samples_dataframe(&[sample]).unwrap();
4363
4364 let scores = df.column("box2d_score").unwrap();
4365 let val = scores.f32().unwrap().get(0);
4366 assert_eq!(val, Some(1.0), "score of 1.0 should survive as non-null");
4367 }
4368}