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 #[serde(default)]
362 tag_id: Option<u64>,
363 #[serde(default)]
364 tag: String,
365 #[serde(default)]
366 tag_description: String,
367}
368
369impl Display for Dataset {
370 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
371 write!(f, "{} {}", self.id, self.name)
372 }
373}
374
375impl Dataset {
376 pub fn id(&self) -> DatasetID {
377 self.id
378 }
379
380 pub fn project_id(&self) -> ProjectID {
381 self.project_id
382 }
383
384 pub fn name(&self) -> &str {
385 &self.name
386 }
387
388 pub fn description(&self) -> &str {
389 &self.description
390 }
391
392 pub fn cloud_key(&self) -> &str {
393 &self.cloud_key
394 }
395
396 pub fn created(&self) -> &DateTime<Utc> {
397 &self.created
398 }
399
400 pub fn tag_id(&self) -> Option<u64> {
403 self.tag_id
404 }
405
406 pub fn tag(&self) -> &str {
409 &self.tag
410 }
411
412 pub fn tag_description(&self) -> &str {
415 &self.tag_description
416 }
417
418 pub async fn project(&self, client: &Client) -> Result<crate::api::Project, Error> {
419 client.project(self.project_id).await
420 }
421
422 pub async fn annotation_sets(
423 &self,
424 client: &Client,
425 version: Option<&str>,
426 ) -> Result<Vec<AnnotationSet>, Error> {
427 client.annotation_sets(self.id, version).await
428 }
429
430 pub async fn labels(
431 &self,
432 client: &Client,
433 version: Option<&str>,
434 ) -> Result<Vec<Label>, Error> {
435 client.labels(self.id, version).await
436 }
437
438 pub async fn add_label(&self, client: &Client, name: &str) -> Result<(), Error> {
439 client.add_label(self.id, name).await
440 }
441
442 pub async fn add_label_with_index(
443 &self,
444 client: &Client,
445 name: &str,
446 index: u64,
447 ) -> Result<(), Error> {
448 client.add_label_with_index(self.id, name, index).await
449 }
450
451 pub async fn remove_label(&self, client: &Client, name: &str) -> Result<(), Error> {
452 let labels = self.labels(client, None).await?;
453 let label = labels
454 .iter()
455 .find(|l| l.name() == name)
456 .ok_or_else(|| Error::MissingLabel(name.to_string()))?;
457 client.remove_label(label.id()).await
458 }
459
460 pub async fn delete_samples(
461 &self,
462 client: &Client,
463 sample_ids: &[SampleID],
464 ) -> Result<(), Error> {
465 client.delete_samples(self.id, sample_ids).await
466 }
467}
468
469#[derive(Deserialize, Debug)]
478pub struct AnnotationSet {
479 id: AnnotationSetID,
480 #[serde(default)]
481 dataset_id: Option<DatasetID>,
482 name: String,
483 description: String,
484 #[serde(rename = "date", default)]
485 created: Option<DateTime<Utc>>,
486}
487
488impl Display for AnnotationSet {
489 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
490 write!(f, "{} {}", self.id, self.name)
491 }
492}
493
494impl AnnotationSet {
495 pub fn id(&self) -> AnnotationSetID {
496 self.id
497 }
498
499 pub fn dataset_id(&self) -> Option<DatasetID> {
504 self.dataset_id
505 }
506
507 pub(crate) fn backfill_dataset_id(&mut self, dataset_id: DatasetID) {
511 if self.dataset_id.is_none() {
512 self.dataset_id = Some(dataset_id);
513 }
514 }
515
516 pub fn name(&self) -> &str {
517 &self.name
518 }
519
520 pub fn description(&self) -> &str {
521 &self.description
522 }
523
524 pub fn created(&self) -> Option<DateTime<Utc>> {
528 self.created
529 }
530
531 pub async fn dataset(&self, client: &Client) -> Result<Dataset, Error> {
532 client
533 .dataset(self.dataset_id.ok_or_else(|| {
534 Error::InvalidParameters(
535 "annotation set has no dataset_id (tag-scoped query result)".to_string(),
536 )
537 })?)
538 .await
539 }
540}
541
542#[derive(Clone, Debug, Default, PartialEq)]
547pub struct Timing {
548 pub load: Option<i64>,
550 pub preprocess: Option<i64>,
552 pub inference: Option<i64>,
554 pub decode: Option<i64>,
556}
557
558#[derive(Serialize, Clone, Debug)]
565pub struct Sample {
566 #[serde(skip_serializing_if = "Option::is_none")]
567 pub id: Option<SampleID>,
568 #[serde(
573 alias = "group_name",
574 rename(serialize = "group", deserialize = "group_name"),
575 skip_serializing_if = "Option::is_none"
576 )]
577 pub group: Option<String>,
578 #[serde(skip_serializing_if = "Option::is_none")]
579 pub sequence_name: Option<String>,
580 #[serde(skip_serializing_if = "Option::is_none")]
581 pub sequence_uuid: Option<String>,
582 #[serde(skip_serializing_if = "Option::is_none")]
583 pub sequence_description: Option<String>,
584 #[serde(
585 default,
586 skip_serializing_if = "Option::is_none",
587 deserialize_with = "deserialize_frame_number"
588 )]
589 pub frame_number: Option<u32>,
590 #[serde(skip_serializing_if = "Option::is_none")]
591 pub uuid: Option<String>,
592 #[serde(skip_serializing_if = "Option::is_none")]
593 pub image_name: Option<String>,
594 #[serde(skip_serializing_if = "Option::is_none")]
595 pub image_url: Option<String>,
596 #[serde(skip_serializing_if = "Option::is_none")]
597 pub width: Option<u32>,
598 #[serde(skip_serializing_if = "Option::is_none")]
599 pub height: Option<u32>,
600 #[serde(skip_serializing_if = "Option::is_none")]
601 pub date: Option<DateTime<Utc>>,
602 #[serde(skip_serializing_if = "Option::is_none")]
603 pub source: Option<String>,
604 #[serde(skip_serializing_if = "Option::is_none", rename(serialize = "sensors"))]
609 pub location: Option<Location>,
610 #[serde(skip_serializing_if = "Option::is_none")]
612 pub degradation: Option<String>,
613 #[serde(default, skip_serializing_if = "Option::is_none")]
615 pub neg_label_indices: Option<Vec<u32>>,
616 #[serde(default, skip_serializing_if = "Option::is_none")]
618 pub not_exhaustive_label_indices: Option<Vec<u32>>,
619 #[serde(
624 default,
625 skip_serializing_if = "Vec::is_empty",
626 serialize_with = "serialize_files"
627 )]
628 pub files: Vec<SampleFile>,
629 #[serde(
632 default,
633 skip_serializing_if = "Vec::is_empty",
634 serialize_with = "serialize_annotations"
635 )]
636 pub annotations: Vec<Annotation>,
637 #[serde(skip)]
640 pub timing: Option<Timing>,
641}
642
643fn deserialize_frame_number<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
646where
647 D: serde::Deserializer<'de>,
648{
649 use serde::Deserialize;
650
651 let value = Option::<i32>::deserialize(deserializer)?;
652 Ok(value.and_then(|v| if v < 0 { None } else { Some(v as u32) }))
653}
654
655fn is_valid_url(s: &str) -> bool {
658 s.starts_with("http://") || s.starts_with("https://")
659}
660
661fn serialize_files<S>(files: &[SampleFile], serializer: S) -> Result<S::Ok, S::Error>
664where
665 S: serde::Serializer,
666{
667 use serde::Serialize;
668 let map: HashMap<String, String> = files
669 .iter()
670 .filter_map(|f| {
671 f.filename()
672 .map(|filename| (f.file_type().to_string(), filename.to_string()))
673 })
674 .collect();
675 map.serialize(serializer)
676}
677
678fn serialize_annotations<S>(annotations: &Vec<Annotation>, serializer: S) -> Result<S::Ok, S::Error>
682where
683 S: serde::Serializer,
684{
685 serde::Serialize::serialize(annotations, serializer)
686}
687
688fn deserialize_annotations<'de, D>(deserializer: D) -> Result<Vec<Annotation>, D::Error>
691where
692 D: serde::Deserializer<'de>,
693{
694 use serde::Deserialize;
695
696 #[derive(Deserialize)]
697 #[serde(untagged)]
698 enum AnnotationsFormat {
699 Vec(Vec<Annotation>),
700 Map(HashMap<String, Vec<Annotation>>),
701 }
702
703 let value = Option::<AnnotationsFormat>::deserialize(deserializer)?;
704 Ok(value
705 .map(|v| match v {
706 AnnotationsFormat::Vec(annotations) => annotations,
707 AnnotationsFormat::Map(map) => convert_annotations_map_to_vec(map),
708 })
709 .unwrap_or_default())
710}
711
712#[derive(Debug, Default)]
715struct SensorsData {
716 files: Vec<SampleFile>,
717 location: Option<Location>,
718}
719
720fn deserialize_sensors_data(value: Option<serde_json::Value>) -> SensorsData {
722 use serde_json::Value;
723
724 fn create_sample_file(file_type: String, value: String) -> SampleFile {
727 if is_valid_url(&value) {
728 SampleFile::with_url(file_type, value)
729 } else {
730 SampleFile::with_data(file_type, value)
731 }
732 }
733
734 fn create_sample_file_from_value(file_type: String, value: Value) -> Option<SampleFile> {
736 match value {
737 Value::String(s) => Some(create_sample_file(file_type, s)),
738 Value::Object(_) | Value::Array(_) => {
739 serde_json::to_string(&value)
741 .ok()
742 .map(|data| SampleFile::with_data(file_type, data))
743 }
744 _ => None,
745 }
746 }
747
748 fn extract_location(map: &serde_json::Map<String, Value>) -> Option<Location> {
750 let gps = map
751 .get("gps")
752 .and_then(|v| serde_json::from_value::<GpsData>(v.clone()).ok());
753 let imu = map
754 .get("imu")
755 .and_then(|v| serde_json::from_value::<ImuData>(v.clone()).ok());
756
757 if gps.is_some() || imu.is_some() {
758 Some(Location { gps, imu })
759 } else {
760 None
761 }
762 }
763
764 let mut result = SensorsData::default();
765
766 match value {
767 None => result,
768 Some(Value::Array(arr)) => {
769 for item in arr {
771 if let Value::Object(map) = item {
772 if map.contains_key("type") {
774 if let Ok(file) =
776 serde_json::from_value::<SampleFile>(Value::Object(map.clone()))
777 {
778 result.files.push(file);
779 }
780 } else {
781 if let Some(loc) = extract_location(&map) {
783 if let Some(ref mut existing) = result.location {
785 if loc.gps.is_some() {
786 existing.gps = loc.gps;
787 }
788 if loc.imu.is_some() {
789 existing.imu = loc.imu;
790 }
791 } else {
792 result.location = Some(loc);
793 }
794 } else {
795 for (file_type, value) in map {
797 if let Some(file) = create_sample_file_from_value(file_type, value)
798 {
799 result.files.push(file);
800 }
801 }
802 }
803 }
804 }
805 }
806 result
807 }
808 Some(Value::Object(map)) => {
809 if let Some(loc) = extract_location(&map) {
811 result.location = Some(loc);
812 }
813
814 for (key, value) in map {
816 if key != "gps"
817 && key != "imu"
818 && let Some(file) = create_sample_file_from_value(key, value)
819 {
820 result.files.push(file);
821 }
822 }
823 result
824 }
825 Some(_) => result,
826 }
827}
828
829#[derive(Deserialize)]
833struct SampleRaw {
834 #[serde(default)]
835 id: Option<SampleID>,
836 #[serde(alias = "group_name")]
837 group: Option<String>,
838 sequence_name: Option<String>,
839 sequence_uuid: Option<String>,
840 sequence_description: Option<String>,
841 #[serde(default, deserialize_with = "deserialize_frame_number")]
842 frame_number: Option<u32>,
843 uuid: Option<String>,
844 image_name: Option<String>,
845 image_url: Option<String>,
846 width: Option<u32>,
847 height: Option<u32>,
848 date: Option<DateTime<Utc>>,
849 source: Option<String>,
850 degradation: Option<String>,
851 #[serde(default)]
852 neg_label_indices: Option<Vec<u32>>,
853 #[serde(default)]
854 not_exhaustive_label_indices: Option<Vec<u32>>,
855 #[serde(default, alias = "sensors")]
857 sensors: Option<serde_json::Value>,
858 #[serde(default, deserialize_with = "deserialize_annotations")]
859 annotations: Vec<Annotation>,
860}
861
862impl From<SampleRaw> for Sample {
863 fn from(raw: SampleRaw) -> Self {
864 let sensors_data = deserialize_sensors_data(raw.sensors);
865
866 Sample {
867 id: raw.id,
868 group: raw.group,
869 sequence_name: raw.sequence_name,
870 sequence_uuid: raw.sequence_uuid,
871 sequence_description: raw.sequence_description,
872 frame_number: raw.frame_number,
873 uuid: raw.uuid,
874 image_name: raw.image_name,
875 image_url: raw.image_url,
876 width: raw.width,
877 height: raw.height,
878 date: raw.date,
879 source: raw.source,
880 location: sensors_data.location,
881 degradation: raw.degradation,
882 neg_label_indices: raw.neg_label_indices,
883 not_exhaustive_label_indices: raw.not_exhaustive_label_indices,
884 files: sensors_data.files,
885 annotations: raw.annotations,
886 timing: None,
887 }
888 }
889}
890
891impl<'de> serde::Deserialize<'de> for Sample {
892 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
893 where
894 D: serde::Deserializer<'de>,
895 {
896 let raw = SampleRaw::deserialize(deserializer)?;
897 Ok(Sample::from(raw))
898 }
899}
900
901impl Display for Sample {
902 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
903 write!(
904 f,
905 "{} {}",
906 self.id
907 .map(|id| id.to_string())
908 .unwrap_or_else(|| "unknown".to_string()),
909 self.image_name().unwrap_or("unknown")
910 )
911 }
912}
913
914impl Default for Sample {
915 fn default() -> Self {
916 Self::new()
917 }
918}
919
920impl Sample {
921 pub fn new() -> Self {
923 Self {
924 id: None,
925 group: None,
926 sequence_name: None,
927 sequence_uuid: None,
928 sequence_description: None,
929 frame_number: None,
930 uuid: None,
931 image_name: None,
932 image_url: None,
933 width: None,
934 height: None,
935 date: None,
936 source: None,
937 location: None,
938 degradation: None,
939 neg_label_indices: None,
940 not_exhaustive_label_indices: None,
941 files: vec![],
942 annotations: vec![],
943 timing: None,
944 }
945 }
946
947 pub fn id(&self) -> Option<SampleID> {
948 self.id
949 }
950
951 pub fn name(&self) -> Option<String> {
952 self.image_name.as_ref().map(|n| extract_sample_name(n))
953 }
954
955 pub fn group(&self) -> Option<&String> {
956 self.group.as_ref()
957 }
958
959 pub fn sequence_name(&self) -> Option<&String> {
960 self.sequence_name.as_ref()
961 }
962
963 pub fn sequence_uuid(&self) -> Option<&String> {
964 self.sequence_uuid.as_ref()
965 }
966
967 pub fn sequence_description(&self) -> Option<&String> {
968 self.sequence_description.as_ref()
969 }
970
971 pub fn frame_number(&self) -> Option<u32> {
972 self.frame_number
973 }
974
975 pub fn uuid(&self) -> Option<&String> {
976 self.uuid.as_ref()
977 }
978
979 pub fn image_name(&self) -> Option<&str> {
980 self.image_name.as_deref()
981 }
982
983 pub fn image_url(&self) -> Option<&str> {
984 self.image_url.as_deref()
985 }
986
987 pub fn width(&self) -> Option<u32> {
988 self.width
989 }
990
991 pub fn height(&self) -> Option<u32> {
992 self.height
993 }
994
995 pub fn date(&self) -> Option<DateTime<Utc>> {
996 self.date
997 }
998
999 pub fn source(&self) -> Option<&String> {
1000 self.source.as_ref()
1001 }
1002
1003 pub fn location(&self) -> Option<&Location> {
1004 self.location.as_ref()
1005 }
1006
1007 pub fn files(&self) -> &[SampleFile] {
1008 &self.files
1009 }
1010
1011 pub fn annotations(&self) -> &[Annotation] {
1012 &self.annotations
1013 }
1014
1015 pub fn with_annotations(mut self, annotations: Vec<Annotation>) -> Self {
1016 self.annotations = annotations;
1017 self
1018 }
1019
1020 pub fn with_frame_number(mut self, frame_number: Option<u32>) -> Self {
1021 self.frame_number = frame_number;
1022 self
1023 }
1024
1025 pub async fn download(
1032 &self,
1033 client: &Client,
1034 file_type: FileType,
1035 ) -> Result<Option<Vec<u8>>, Error> {
1036 use base64::{Engine, engine::general_purpose::STANDARD};
1037
1038 if file_type == FileType::Image {
1040 if let Some(url) = self.image_url.as_deref()
1041 && is_valid_url(url)
1042 {
1043 return Ok(Some(client.download(url).await?));
1044 }
1045 if self.image_name.is_none() {
1058 return Ok(None);
1059 }
1060 let identity = self
1061 .id()
1062 .map(|id| id.to_string())
1063 .or_else(|| self.name())
1064 .unwrap_or_else(|| "<unknown sample>".to_string());
1065 let reason = match self.image_url.as_deref() {
1071 None => "missing image_url".to_string(),
1072 Some("") => "empty image_url".to_string(),
1073 Some(u) => {
1074 let scheme = u.split_once("://").map(|(scheme, _)| scheme);
1075 match scheme {
1076 Some(scheme) => format!(
1077 "image_url has unsupported scheme {scheme:?} ({} bytes)",
1078 u.len()
1079 ),
1080 None => format!("image_url has no scheme ({} bytes)", u.len()),
1081 }
1082 }
1083 };
1084 return Err(Error::MissingResource(format!(
1085 "Sample {identity} has image_name {:?} but no fetchable image ({reason})",
1086 self.image_name
1087 )));
1088 }
1089
1090 let file = resolve_file(&file_type, &self.files);
1092
1093 match file {
1094 Some(f) => {
1095 if let Some(url) = f.url() {
1097 return Ok(Some(client.download(url).await?));
1098 }
1099
1100 if let Some(data) = f.data() {
1102 let decoded = if let Ok(bytes) = STANDARD.decode(data) {
1109 if let Ok(text) = String::from_utf8(bytes.clone()) {
1111 if text.starts_with('{') {
1112 text
1114 } else {
1115 return Ok(Some(bytes));
1117 }
1118 } else {
1119 return Ok(Some(bytes));
1121 }
1122 } else {
1123 data.to_string()
1125 };
1126
1127 let content = if decoded.starts_with('{') {
1129 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&decoded) {
1130 if let Some(obj) = json.as_object() {
1131 obj.values()
1132 .next()
1133 .and_then(|v| v.as_str())
1134 .map(|s| s.to_string())
1135 .unwrap_or(decoded)
1136 } else {
1137 decoded
1138 }
1139 } else {
1140 decoded
1141 }
1142 } else {
1143 decoded
1144 };
1145
1146 return Ok(Some(content.as_bytes().to_vec()));
1147 }
1148
1149 Ok(None)
1150 }
1151 None => Ok(None),
1152 }
1153 }
1154}
1155
1156#[derive(Serialize, Deserialize, Clone, Debug)]
1164pub struct SampleFile {
1165 r#type: String,
1166 #[serde(skip_serializing_if = "Option::is_none")]
1167 url: Option<String>,
1168 #[serde(skip_serializing_if = "Option::is_none")]
1169 filename: Option<String>,
1170 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1172 data: Option<String>,
1173 #[serde(skip)]
1176 bytes: Option<Vec<u8>>,
1177}
1178
1179impl SampleFile {
1180 pub fn with_url(file_type: String, url: String) -> Self {
1182 Self {
1183 r#type: file_type,
1184 url: Some(url),
1185 filename: None,
1186 data: None,
1187 bytes: None,
1188 }
1189 }
1190
1191 pub fn with_filename(file_type: String, filename: String) -> Self {
1193 Self {
1194 r#type: file_type,
1195 url: None,
1196 filename: Some(filename),
1197 data: None,
1198 bytes: None,
1199 }
1200 }
1201
1202 pub fn with_data(file_type: String, data: String) -> Self {
1204 Self {
1205 r#type: file_type,
1206 url: None,
1207 filename: None,
1208 data: Some(data),
1209 bytes: None,
1210 }
1211 }
1212
1213 pub fn with_bytes(file_type: String, filename: String, bytes: Vec<u8>) -> Self {
1223 Self {
1224 r#type: file_type,
1225 url: None,
1226 filename: Some(filename),
1227 data: None,
1228 bytes: Some(bytes),
1229 }
1230 }
1231
1232 pub fn file_type(&self) -> &str {
1233 &self.r#type
1234 }
1235
1236 pub fn url(&self) -> Option<&str> {
1237 self.url.as_deref()
1238 }
1239
1240 pub fn filename(&self) -> Option<&str> {
1241 self.filename.as_deref()
1242 }
1243
1244 pub fn data(&self) -> Option<&str> {
1246 self.data.as_deref()
1247 }
1248
1249 pub fn bytes(&self) -> Option<&[u8]> {
1251 self.bytes.as_deref()
1252 }
1253}
1254
1255#[derive(Serialize, Deserialize, Clone, Debug)]
1260pub struct Location {
1261 #[serde(skip_serializing_if = "Option::is_none")]
1262 pub gps: Option<GpsData>,
1263 #[serde(skip_serializing_if = "Option::is_none")]
1264 pub imu: Option<ImuData>,
1265}
1266
1267#[derive(Serialize, Deserialize, Clone, Debug)]
1269pub struct GpsData {
1270 pub lat: f64,
1271 pub lon: f64,
1272}
1273
1274impl GpsData {
1275 pub fn validate(&self) -> Result<(), String> {
1305 validate_gps_coordinates(self.lat, self.lon)
1306 }
1307}
1308
1309#[derive(Serialize, Deserialize, Clone, Debug)]
1311pub struct ImuData {
1312 pub roll: f64,
1313 pub pitch: f64,
1314 pub yaw: f64,
1315}
1316
1317impl ImuData {
1318 pub fn validate(&self) -> Result<(), String> {
1351 validate_imu_orientation(self.roll, self.pitch, self.yaw)
1352 }
1353}
1354
1355#[allow(dead_code)]
1356pub trait TypeName {
1357 fn type_name() -> String;
1358}
1359
1360#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
1361pub struct Box3d {
1362 x: f32,
1363 y: f32,
1364 z: f32,
1365 w: f32,
1366 h: f32,
1367 l: f32,
1368}
1369
1370impl TypeName for Box3d {
1371 fn type_name() -> String {
1372 "box3d".to_owned()
1373 }
1374}
1375
1376impl Box3d {
1377 pub fn new(cx: f32, cy: f32, cz: f32, width: f32, height: f32, length: f32) -> Self {
1378 Self {
1379 x: cx,
1380 y: cy,
1381 z: cz,
1382 w: width,
1383 h: height,
1384 l: length,
1385 }
1386 }
1387
1388 pub fn width(&self) -> f32 {
1389 self.w
1390 }
1391
1392 pub fn height(&self) -> f32 {
1393 self.h
1394 }
1395
1396 pub fn length(&self) -> f32 {
1397 self.l
1398 }
1399
1400 pub fn cx(&self) -> f32 {
1401 self.x
1402 }
1403
1404 pub fn cy(&self) -> f32 {
1405 self.y
1406 }
1407
1408 pub fn cz(&self) -> f32 {
1409 self.z
1410 }
1411
1412 pub fn left(&self) -> f32 {
1413 self.x - self.w / 2.0
1414 }
1415
1416 pub fn top(&self) -> f32 {
1417 self.y - self.h / 2.0
1418 }
1419
1420 pub fn front(&self) -> f32 {
1421 self.z - self.l / 2.0
1422 }
1423}
1424
1425#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
1426pub struct Box2d {
1427 h: f32,
1428 w: f32,
1429 x: f32,
1430 y: f32,
1431}
1432
1433impl TypeName for Box2d {
1434 fn type_name() -> String {
1435 "box2d".to_owned()
1436 }
1437}
1438
1439impl Box2d {
1440 pub fn new(left: f32, top: f32, width: f32, height: f32) -> Self {
1441 Self {
1442 x: left,
1443 y: top,
1444 w: width,
1445 h: height,
1446 }
1447 }
1448
1449 pub fn width(&self) -> f32 {
1450 self.w
1451 }
1452
1453 pub fn height(&self) -> f32 {
1454 self.h
1455 }
1456
1457 pub fn left(&self) -> f32 {
1458 self.x
1459 }
1460
1461 pub fn top(&self) -> f32 {
1462 self.y
1463 }
1464
1465 pub fn cx(&self) -> f32 {
1466 self.x + self.w / 2.0
1467 }
1468
1469 pub fn cy(&self) -> f32 {
1470 self.y + self.h / 2.0
1471 }
1472}
1473
1474#[derive(Clone, Debug, PartialEq)]
1475pub struct Polygon {
1476 pub rings: Vec<Vec<(f32, f32)>>,
1477}
1478
1479impl TypeName for Polygon {
1480 fn type_name() -> String {
1481 "polygon".to_owned()
1482 }
1483}
1484
1485impl Polygon {
1486 pub fn new(rings: Vec<Vec<(f32, f32)>>) -> Self {
1487 Self { rings }
1488 }
1489}
1490
1491impl serde::Serialize for Polygon {
1492 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1493 where
1494 S: serde::Serializer,
1495 {
1496 serde::Serialize::serialize(&self.rings, serializer)
1497 }
1498}
1499
1500impl<'de> serde::Deserialize<'de> for Polygon {
1501 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1502 where
1503 D: serde::Deserializer<'de>,
1504 {
1505 let value = serde_json::Value::deserialize(deserializer)?;
1507
1508 let polygon_value = if let Some(obj) = value.as_object() {
1510 obj.get("rings")
1512 .or_else(|| obj.get("polygon"))
1513 .cloned()
1514 .unwrap_or(serde_json::Value::Null)
1515 } else {
1516 value
1518 };
1519
1520 let rings = parse_polygon_value(&polygon_value);
1522
1523 Ok(Self { rings })
1524 }
1525}
1526
1527fn parse_polygon_value(value: &serde_json::Value) -> Vec<Vec<(f32, f32)>> {
1535 let Some(outer_array) = value.as_array() else {
1536 return vec![];
1537 };
1538
1539 let mut result = Vec::new();
1540
1541 for ring in outer_array {
1542 let Some(ring_array) = ring.as_array() else {
1543 continue;
1544 };
1545
1546 let is_3d = ring_array
1548 .first()
1549 .map(|first| first.is_array())
1550 .unwrap_or(false);
1551
1552 let points: Vec<(f32, f32)> = if is_3d {
1553 ring_array
1555 .iter()
1556 .filter_map(|point| {
1557 let arr = point.as_array()?;
1558 if arr.len() >= 2 {
1559 let x = arr[0].as_f64()? as f32;
1560 let y = arr[1].as_f64()? as f32;
1561 if x.is_finite() && y.is_finite() {
1562 Some((x, y))
1563 } else {
1564 None
1565 }
1566 } else {
1567 None
1568 }
1569 })
1570 .collect()
1571 } else {
1572 ring_array
1574 .chunks(2)
1575 .filter_map(|chunk| {
1576 if chunk.len() >= 2 {
1577 let x = chunk[0].as_f64()? as f32;
1578 let y = chunk[1].as_f64()? as f32;
1579 if x.is_finite() && y.is_finite() {
1580 Some((x, y))
1581 } else {
1582 None
1583 }
1584 } else {
1585 None
1586 }
1587 })
1588 .collect()
1589 };
1590
1591 if points.len() >= 3 {
1593 result.push(points);
1594 }
1595 }
1596
1597 result
1598}
1599
1600#[derive(Deserialize)]
1605struct AnnotationRaw {
1606 #[serde(default)]
1607 sample_id: Option<SampleID>,
1608 #[serde(default)]
1609 name: Option<String>,
1610 #[serde(default)]
1611 sequence_name: Option<String>,
1612 #[serde(default)]
1613 frame_number: Option<u32>,
1614 #[serde(rename = "group_name", default)]
1615 group: Option<String>,
1616 #[serde(rename = "object_reference", alias = "object_id", default)]
1617 object_id: Option<String>,
1618 #[serde(default)]
1619 label_name: Option<String>,
1620 #[serde(default)]
1621 label_index: Option<u64>,
1622 #[serde(default)]
1623 iscrowd: Option<bool>,
1624 #[serde(default)]
1625 category_frequency: Option<String>,
1626 #[serde(default)]
1628 box2d: Option<Box2d>,
1629 #[serde(default)]
1630 box3d: Option<Box3d>,
1631 #[serde(default, alias = "mask")]
1632 polygon: Option<Polygon>,
1633 #[serde(default)]
1635 x: Option<f64>,
1636 #[serde(default)]
1637 y: Option<f64>,
1638 #[serde(default)]
1639 w: Option<f64>,
1640 #[serde(default)]
1641 h: Option<f64>,
1642}
1643
1644#[derive(Serialize, Clone, Debug)]
1645pub struct Annotation {
1646 #[serde(skip_serializing_if = "Option::is_none")]
1647 sample_id: Option<SampleID>,
1648 #[serde(skip_serializing_if = "Option::is_none")]
1649 name: Option<String>,
1650 #[serde(skip_serializing_if = "Option::is_none")]
1651 sequence_name: Option<String>,
1652 #[serde(skip_serializing_if = "Option::is_none")]
1653 frame_number: Option<u32>,
1654 #[serde(rename = "group_name", skip_serializing_if = "Option::is_none")]
1658 group: Option<String>,
1659 #[serde(
1663 rename = "object_reference",
1664 alias = "object_id",
1665 skip_serializing_if = "Option::is_none"
1666 )]
1667 object_id: Option<String>,
1668 #[serde(skip_serializing_if = "Option::is_none")]
1669 label_name: Option<String>,
1670 #[serde(skip_serializing_if = "Option::is_none")]
1671 label_index: Option<u64>,
1672 #[serde(default, skip_serializing_if = "Option::is_none")]
1674 iscrowd: Option<bool>,
1675 #[serde(default, skip_serializing_if = "Option::is_none")]
1677 category_frequency: Option<String>,
1678 #[serde(skip_serializing_if = "Option::is_none")]
1679 box2d: Option<Box2d>,
1680 #[serde(skip_serializing_if = "Option::is_none")]
1681 box3d: Option<Box3d>,
1682 #[serde(rename(serialize = "mask"), skip_serializing_if = "Option::is_none")]
1691 polygon: Option<Polygon>,
1692 #[serde(skip)]
1694 mask: Option<MaskData>,
1695 #[serde(skip_serializing_if = "Option::is_none")]
1697 box2d_score: Option<f32>,
1698 #[serde(skip_serializing_if = "Option::is_none")]
1700 box3d_score: Option<f32>,
1701 #[serde(skip_serializing_if = "Option::is_none")]
1703 polygon_score: Option<f32>,
1704 #[serde(skip_serializing_if = "Option::is_none")]
1706 mask_score: Option<f32>,
1707}
1708
1709impl<'de> serde::Deserialize<'de> for Annotation {
1710 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1711 where
1712 D: serde::Deserializer<'de>,
1713 {
1714 let raw: AnnotationRaw = serde::Deserialize::deserialize(deserializer)?;
1716
1717 let box2d = raw.box2d.or_else(|| match (raw.x, raw.y, raw.w, raw.h) {
1719 (Some(x), Some(y), Some(w), Some(h)) if w > 0.0 && h > 0.0 => {
1720 Some(Box2d::new(x as f32, y as f32, w as f32, h as f32))
1721 }
1722 _ => None,
1723 });
1724
1725 Ok(Annotation {
1726 sample_id: raw.sample_id,
1727 name: raw.name,
1728 sequence_name: raw.sequence_name,
1729 frame_number: raw.frame_number,
1730 group: raw.group,
1731 object_id: raw.object_id,
1732 label_name: raw.label_name,
1733 label_index: raw.label_index,
1734 iscrowd: raw.iscrowd,
1735 category_frequency: raw.category_frequency,
1736 box2d,
1737 box3d: raw.box3d,
1738 polygon: raw.polygon,
1739 mask: None,
1740 box2d_score: None,
1741 box3d_score: None,
1742 polygon_score: None,
1743 mask_score: None,
1744 })
1745 }
1746}
1747
1748impl Default for Annotation {
1749 fn default() -> Self {
1750 Self::new()
1751 }
1752}
1753
1754impl Annotation {
1755 pub fn new() -> Self {
1756 Self {
1757 sample_id: None,
1758 name: None,
1759 sequence_name: None,
1760 frame_number: None,
1761 group: None,
1762 object_id: None,
1763 label_name: None,
1764 label_index: None,
1765 iscrowd: None,
1766 category_frequency: None,
1767 box2d: None,
1768 box3d: None,
1769 polygon: None,
1770 mask: None,
1771 box2d_score: None,
1772 box3d_score: None,
1773 polygon_score: None,
1774 mask_score: None,
1775 }
1776 }
1777
1778 pub fn set_sample_id(&mut self, sample_id: Option<SampleID>) {
1779 self.sample_id = sample_id;
1780 }
1781
1782 pub fn sample_id(&self) -> Option<SampleID> {
1783 self.sample_id
1784 }
1785
1786 pub fn set_name(&mut self, name: Option<String>) {
1787 self.name = name;
1788 }
1789
1790 pub fn name(&self) -> Option<&String> {
1791 self.name.as_ref()
1792 }
1793
1794 pub fn set_sequence_name(&mut self, sequence_name: Option<String>) {
1795 self.sequence_name = sequence_name;
1796 }
1797
1798 pub fn sequence_name(&self) -> Option<&String> {
1799 self.sequence_name.as_ref()
1800 }
1801
1802 pub fn set_frame_number(&mut self, frame_number: Option<u32>) {
1803 self.frame_number = frame_number;
1804 }
1805
1806 pub fn frame_number(&self) -> Option<u32> {
1807 self.frame_number
1808 }
1809
1810 pub fn set_group(&mut self, group: Option<String>) {
1811 self.group = group;
1812 }
1813
1814 pub fn group(&self) -> Option<&String> {
1815 self.group.as_ref()
1816 }
1817
1818 pub fn object_id(&self) -> Option<&String> {
1819 self.object_id.as_ref()
1820 }
1821
1822 pub fn set_object_id(&mut self, object_id: Option<String>) {
1823 self.object_id = object_id;
1824 }
1825
1826 pub fn label(&self) -> Option<&String> {
1827 self.label_name.as_ref()
1828 }
1829
1830 pub fn set_label(&mut self, label_name: Option<String>) {
1831 self.label_name = label_name;
1832 }
1833
1834 pub fn label_index(&self) -> Option<u64> {
1835 self.label_index
1836 }
1837
1838 pub fn set_label_index(&mut self, label_index: Option<u64>) {
1839 self.label_index = label_index;
1840 }
1841
1842 pub fn iscrowd(&self) -> Option<bool> {
1843 self.iscrowd
1844 }
1845
1846 pub fn set_iscrowd(&mut self, iscrowd: Option<bool>) {
1847 self.iscrowd = iscrowd;
1848 }
1849
1850 pub fn category_frequency(&self) -> Option<&String> {
1851 self.category_frequency.as_ref()
1852 }
1853
1854 pub fn set_category_frequency(&mut self, category_frequency: Option<String>) {
1855 self.category_frequency = category_frequency;
1856 }
1857
1858 pub fn box2d(&self) -> Option<&Box2d> {
1859 self.box2d.as_ref()
1860 }
1861
1862 pub fn set_box2d(&mut self, box2d: Option<Box2d>) {
1863 self.box2d = box2d;
1864 }
1865
1866 pub fn box3d(&self) -> Option<&Box3d> {
1867 self.box3d.as_ref()
1868 }
1869
1870 pub fn set_box3d(&mut self, box3d: Option<Box3d>) {
1871 self.box3d = box3d;
1872 }
1873
1874 pub fn polygon(&self) -> Option<&Polygon> {
1875 self.polygon.as_ref()
1876 }
1877
1878 pub fn set_polygon(&mut self, polygon: Option<Polygon>) {
1879 self.polygon = polygon;
1880 }
1881
1882 pub fn mask(&self) -> Option<&MaskData> {
1883 self.mask.as_ref()
1884 }
1885
1886 pub fn set_mask(&mut self, mask: Option<MaskData>) {
1887 self.mask = mask;
1888 }
1889
1890 pub fn box2d_score(&self) -> Option<f32> {
1891 self.box2d_score
1892 }
1893
1894 pub fn set_box2d_score(&mut self, score: Option<f32>) {
1895 self.box2d_score = score;
1896 }
1897
1898 pub fn box3d_score(&self) -> Option<f32> {
1899 self.box3d_score
1900 }
1901
1902 pub fn set_box3d_score(&mut self, score: Option<f32>) {
1903 self.box3d_score = score;
1904 }
1905
1906 pub fn polygon_score(&self) -> Option<f32> {
1907 self.polygon_score
1908 }
1909
1910 pub fn set_polygon_score(&mut self, score: Option<f32>) {
1911 self.polygon_score = score;
1912 }
1913
1914 pub fn mask_score(&self) -> Option<f32> {
1915 self.mask_score
1916 }
1917
1918 pub fn set_mask_score(&mut self, score: Option<f32>) {
1919 self.mask_score = score;
1920 }
1921}
1922
1923#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
1932pub struct Label {
1933 id: u64,
1934 #[serde(default)]
1935 dataset_id: Option<DatasetID>,
1936 index: u64,
1937 name: String,
1938 #[serde(default)]
1939 color: Option<u64>,
1940}
1941
1942impl Label {
1943 pub fn id(&self) -> u64 {
1944 self.id
1945 }
1946
1947 pub fn dataset_id(&self) -> Option<DatasetID> {
1952 self.dataset_id
1953 }
1954
1955 pub(crate) fn backfill_dataset_id(&mut self, dataset_id: DatasetID) {
1959 if self.dataset_id.is_none() {
1960 self.dataset_id = Some(dataset_id);
1961 }
1962 }
1963
1964 pub fn index(&self) -> u64 {
1965 self.index
1966 }
1967
1968 pub fn name(&self) -> &str {
1969 &self.name
1970 }
1971
1972 pub fn color(&self) -> Option<u64> {
1975 self.color
1976 }
1977
1978 pub async fn remove(&self, client: &Client) -> Result<(), Error> {
1979 client.remove_label(self.id()).await
1980 }
1981
1982 pub async fn set_name(&mut self, client: &Client, name: &str) -> Result<(), Error> {
1983 self.name = name.to_string();
1984 client.update_label(self).await
1985 }
1986
1987 pub async fn set_index(&mut self, client: &Client, index: u64) -> Result<(), Error> {
1988 self.index = index;
1989 client.update_label(self).await
1990 }
1991}
1992
1993impl Display for Label {
1994 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1995 write!(f, "{}", self.name())
1996 }
1997}
1998
1999#[derive(Serialize, Clone, Debug)]
2000pub struct NewLabelObject {
2001 pub name: String,
2002 #[serde(skip_serializing_if = "Option::is_none")]
2009 pub index: Option<u64>,
2010}
2011
2012#[derive(Serialize, Clone, Debug)]
2013pub struct NewLabel {
2014 pub dataset_id: DatasetID,
2015 pub labels: Vec<NewLabelObject>,
2016}
2017
2018#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
2048pub struct Group {
2049 pub id: u64,
2054
2055 pub name: String,
2059}
2060
2061#[cfg(feature = "polars")]
2062fn extract_annotation_name(ann: &Annotation) -> Option<(String, Option<u32>)> {
2063 use std::path::Path;
2064
2065 let name = ann.name.as_ref()?;
2066 let name = Path::new(name).file_stem()?.to_str()?;
2067
2068 match &ann.sequence_name {
2071 Some(sequence) => Some((sequence.clone(), ann.frame_number)),
2072 None => Some((name.to_string(), None)),
2073 }
2074}
2075
2076#[cfg(feature = "polars")]
2080fn convert_polygon_to_nested_series(polygon: &Polygon) -> Series {
2081 let ring_series: Vec<Option<Series>> = polygon
2082 .rings
2083 .iter()
2084 .map(|ring| {
2085 let coords: Vec<f32> = ring.iter().flat_map(|&(x, y)| [x, y]).collect();
2086 Some(Series::new("".into(), coords))
2087 })
2088 .collect();
2089 Series::new("".into(), ring_series)
2090}
2091
2092#[cfg(feature = "polars")]
2142pub fn samples_dataframe(samples: &[Sample]) -> Result<DataFrame, Error> {
2143 let mut names: Vec<String> = Vec::new();
2145 let mut frames: Vec<Option<u32>> = Vec::new();
2146 let mut objects: Vec<Option<String>> = Vec::new();
2147 let mut labels: Vec<Option<String>> = Vec::new();
2148 let mut label_indices: Vec<Option<u64>> = Vec::new();
2149 let mut groups: Vec<Option<String>> = Vec::new();
2150 let mut polygons: Vec<Option<Series>> = Vec::new();
2151 let mut boxes2d: Vec<Option<Series>> = Vec::new();
2152 let mut boxes3d: Vec<Option<Series>> = Vec::new();
2153 let mut mask_bytes: Vec<Option<Vec<u8>>> = Vec::new();
2154 let mut box2d_scores: Vec<Option<f32>> = Vec::new();
2155 let mut box3d_scores: Vec<Option<f32>> = Vec::new();
2156 let mut polygon_scores: Vec<Option<f32>> = Vec::new();
2157 let mut mask_scores: Vec<Option<f32>> = Vec::new();
2158 let mut sizes: Vec<Option<Vec<u32>>> = Vec::new();
2159 let mut locations: Vec<Option<Vec<f32>>> = Vec::new();
2160 let mut poses: Vec<Option<Vec<f32>>> = Vec::new();
2161 let mut degradations: Vec<Option<String>> = Vec::new();
2162 let mut iscrowds: Vec<Option<bool>> = Vec::new();
2163 let mut category_frequencies: Vec<Option<String>> = Vec::new();
2164 let mut neg_label_indices_vec: Vec<Option<Vec<u32>>> = Vec::new();
2165 let mut not_exhaustive_label_indices_vec: Vec<Option<Vec<u32>>> = Vec::new();
2166 let mut timing_load: Vec<Option<i64>> = Vec::new();
2167 let mut timing_preprocess: Vec<Option<i64>> = Vec::new();
2168 let mut timing_inference: Vec<Option<i64>> = Vec::new();
2169 let mut timing_decode: Vec<Option<i64>> = Vec::new();
2170
2171 for sample in samples {
2172 let size = match (sample.width, sample.height) {
2174 (Some(w), Some(h)) => Some(vec![w, h]),
2175 _ => None,
2176 };
2177
2178 let location = sample.location.as_ref().and_then(|loc| {
2179 loc.gps
2180 .as_ref()
2181 .map(|gps| vec![gps.lat as f32, gps.lon as f32])
2182 });
2183
2184 let pose = sample.location.as_ref().and_then(|loc| {
2185 loc.imu
2186 .as_ref()
2187 .map(|imu| vec![imu.yaw as f32, imu.pitch as f32, imu.roll as f32])
2188 });
2189
2190 let degradation = sample.degradation.clone();
2191
2192 let t_load = sample.timing.as_ref().and_then(|t| t.load);
2194 let t_preprocess = sample.timing.as_ref().and_then(|t| t.preprocess);
2195 let t_inference = sample.timing.as_ref().and_then(|t| t.inference);
2196 let t_decode = sample.timing.as_ref().and_then(|t| t.decode);
2197
2198 macro_rules! push_sample_fields {
2200 () => {
2201 sizes.push(size.clone());
2202 locations.push(location.clone());
2203 poses.push(pose.clone());
2204 degradations.push(degradation.clone());
2205 neg_label_indices_vec.push(sample.neg_label_indices.clone());
2206 not_exhaustive_label_indices_vec.push(sample.not_exhaustive_label_indices.clone());
2207 timing_load.push(t_load);
2208 timing_preprocess.push(t_preprocess);
2209 timing_inference.push(t_inference);
2210 timing_decode.push(t_decode);
2211 };
2212 }
2213
2214 if sample.annotations.is_empty() {
2215 let (name, frame) = match extract_annotation_name_from_sample(sample) {
2217 Some(nf) => nf,
2218 None => continue,
2219 };
2220
2221 names.push(name);
2222 frames.push(frame);
2223 objects.push(None);
2224 labels.push(None);
2225 label_indices.push(None);
2226 groups.push(sample.group.clone());
2227 polygons.push(None);
2228 boxes2d.push(None);
2229 boxes3d.push(None);
2230 mask_bytes.push(None);
2231 box2d_scores.push(None);
2232 box3d_scores.push(None);
2233 polygon_scores.push(None);
2234 mask_scores.push(None);
2235 iscrowds.push(None);
2236 category_frequencies.push(None);
2237 push_sample_fields!();
2238 } else {
2239 for ann in &sample.annotations {
2241 let (name, frame) = match extract_annotation_name(ann) {
2242 Some(nf) => nf,
2243 None => continue,
2244 };
2245
2246 let polygon = ann.polygon.as_ref().map(convert_polygon_to_nested_series);
2247
2248 let box2d = ann
2249 .box2d
2250 .as_ref()
2251 .map(|b| Series::new("box2d".into(), [b.cx(), b.cy(), b.width(), b.height()]));
2252
2253 let box3d = ann
2254 .box3d
2255 .as_ref()
2256 .map(|b| Series::new("box3d".into(), [b.x, b.y, b.z, b.w, b.h, b.l]));
2257
2258 names.push(name);
2259 frames.push(frame);
2260 objects.push(ann.object_id().cloned());
2261 labels.push(ann.label_name.clone());
2262 label_indices.push(ann.label_index);
2263 groups.push(sample.group.clone());
2264 polygons.push(polygon);
2265 boxes2d.push(box2d);
2266 boxes3d.push(box3d);
2267 mask_bytes.push(ann.mask.as_ref().map(|m| m.as_bytes().to_vec()));
2268 box2d_scores.push(ann.box2d_score());
2269 box3d_scores.push(ann.box3d_score());
2270 polygon_scores.push(ann.polygon_score());
2271 mask_scores.push(ann.mask_score());
2272 iscrowds.push(ann.iscrowd);
2273 category_frequencies.push(ann.category_frequency.clone());
2274 push_sample_fields!();
2275 }
2276 }
2277 }
2278
2279 let names_col: Column = Series::new("name".into(), names).into();
2281 let frames_col: Column = Series::new("frame".into(), frames).into();
2282 let objects_col: Column = Series::new("object_id".into(), objects).into();
2283
2284 let labels_col: Column = Series::new("label".into(), labels)
2290 .cast(&DataType::Categorical(
2291 Categories::new("labels".into(), "labels".into(), CategoricalPhysical::U16),
2292 Arc::new(CategoricalMapping::with_hasher(
2293 u16::MAX as usize,
2294 Default::default(),
2295 )),
2296 ))?
2297 .into();
2298
2299 let label_indices_col: Column = Series::new("label_index".into(), label_indices).into();
2300
2301 let groups_col: Column = Series::new("group".into(), groups)
2303 .cast(&DataType::Categorical(
2304 Categories::new("groups".into(), "groups".into(), CategoricalPhysical::U8),
2305 Arc::new(CategoricalMapping::with_hasher(
2306 u8::MAX as usize,
2307 Default::default(),
2308 )),
2309 ))?
2310 .into();
2311
2312 let polygons_col: Column = if polygons.iter().all(|p| p.is_none()) {
2317 Series::new_null("polygon".into(), polygons.len()).into()
2319 } else {
2320 let typed_polygons: Vec<Option<Series>> = polygons
2323 .into_iter()
2324 .map(|opt| {
2325 opt.map(|s| {
2326 s.cast(&DataType::List(Box::new(DataType::Float32)))
2327 .unwrap_or(s)
2328 })
2329 })
2330 .collect();
2331 Series::new("polygon".into(), &typed_polygons)
2332 .cast(&DataType::List(Box::new(DataType::List(Box::new(
2333 DataType::Float32,
2334 )))))?
2335 .into()
2336 };
2337
2338 let boxes2d_col: Column = Series::new("box2d".into(), boxes2d)
2339 .cast(&DataType::Array(Box::new(DataType::Float32), 4))?
2340 .into();
2341 let boxes3d_col: Column = Series::new("box3d".into(), boxes3d)
2342 .cast(&DataType::Array(Box::new(DataType::Float32), 6))?
2343 .into();
2344
2345 let mask_col: Column = Series::new("mask".into(), mask_bytes).into();
2347
2348 let box2d_score_col: Column = Series::new("box2d_score".into(), box2d_scores).into();
2350 let box3d_score_col: Column = Series::new("box3d_score".into(), box3d_scores).into();
2351 let polygon_score_col: Column = Series::new("polygon_score".into(), polygon_scores).into();
2352 let mask_score_col: Column = Series::new("mask_score".into(), mask_scores).into();
2353
2354 let size_series: Vec<Option<Series>> = sizes
2356 .into_iter()
2357 .map(|opt_vec| opt_vec.map(|vec| Series::new("size".into(), vec)))
2358 .collect();
2359 let sizes_col: Column = Series::new("size".into(), size_series)
2360 .cast(&DataType::Array(Box::new(DataType::UInt32), 2))?
2361 .into();
2362
2363 let location_series: Vec<Option<Series>> = locations
2364 .into_iter()
2365 .map(|opt_vec| opt_vec.map(|vec| Series::new("location".into(), vec)))
2366 .collect();
2367 let locations_col: Column = Series::new("location".into(), location_series)
2368 .cast(&DataType::Array(Box::new(DataType::Float32), 2))?
2369 .into();
2370
2371 let pose_series: Vec<Option<Series>> = poses
2372 .into_iter()
2373 .map(|opt_vec| opt_vec.map(|vec| Series::new("pose".into(), vec)))
2374 .collect();
2375 let poses_col: Column = Series::new("pose".into(), pose_series)
2376 .cast(&DataType::Array(Box::new(DataType::Float32), 3))?
2377 .into();
2378
2379 let degradations_col: Column = Series::new("degradation".into(), degradations).into();
2380
2381 let iscrowds_col: Column = Series::new("iscrowd".into(), iscrowds).into();
2383
2384 let category_frequencies_col: Column =
2385 Series::new("category_frequency".into(), category_frequencies)
2386 .cast(&DataType::Categorical(
2387 Categories::new(
2388 "cat_freq".into(),
2389 "cat_freq".into(),
2390 CategoricalPhysical::U8,
2391 ),
2392 Arc::new(CategoricalMapping::with_hasher(
2393 u8::MAX as usize,
2394 Default::default(),
2395 )),
2396 ))?
2397 .into();
2398
2399 let neg_label_indices_series: Vec<Option<Series>> = neg_label_indices_vec
2400 .into_iter()
2401 .map(|opt_vec| opt_vec.map(|vec| Series::new("neg_label_indices".into(), vec)))
2402 .collect();
2403 let neg_label_indices_col: Column =
2404 Series::new("neg_label_indices".into(), neg_label_indices_series)
2405 .cast(&DataType::List(Box::new(DataType::UInt32)))?
2406 .into();
2407
2408 let not_exhaustive_label_indices_series: Vec<Option<Series>> = not_exhaustive_label_indices_vec
2409 .into_iter()
2410 .map(|opt_vec| opt_vec.map(|vec| Series::new("not_exhaustive_label_indices".into(), vec)))
2411 .collect();
2412 let not_exhaustive_label_indices_col: Column = Series::new(
2413 "not_exhaustive_label_indices".into(),
2414 not_exhaustive_label_indices_series,
2415 )
2416 .cast(&DataType::List(Box::new(DataType::UInt32)))?
2417 .into();
2418
2419 let timing_col: Column = StructChunked::from_series(
2421 "timing".into(),
2422 frames_col.len(),
2423 [
2424 Series::new("load".into(), &timing_load),
2425 Series::new("preprocess".into(), &timing_preprocess),
2426 Series::new("inference".into(), &timing_inference),
2427 Series::new("decode".into(), &timing_decode),
2428 ]
2429 .iter(),
2430 )?
2431 .into_series()
2432 .into();
2433
2434 let all_columns: Vec<Column> = vec![
2436 names_col,
2437 frames_col,
2438 objects_col,
2439 labels_col,
2440 label_indices_col,
2441 groups_col,
2442 polygons_col,
2443 boxes2d_col,
2444 boxes3d_col,
2445 mask_col,
2446 box2d_score_col,
2447 box3d_score_col,
2448 polygon_score_col,
2449 mask_score_col,
2450 sizes_col,
2451 locations_col,
2452 poses_col,
2453 degradations_col,
2454 iscrowds_col,
2455 category_frequencies_col,
2456 neg_label_indices_col,
2457 not_exhaustive_label_indices_col,
2458 timing_col,
2459 ];
2460
2461 let height = all_columns.first().map(|c| c.len()).unwrap_or(0);
2462
2463 let non_empty_columns: Vec<Column> = all_columns
2464 .into_iter()
2465 .filter(|col| col.name() == "name" || !is_all_null_column(col))
2466 .collect();
2467
2468 Ok(DataFrame::new(height, non_empty_columns)?)
2469}
2470
2471#[cfg(feature = "polars")]
2475fn is_all_null_column(col: &Column) -> bool {
2476 if col.is_empty() {
2477 return true;
2478 }
2479 if col.null_count() == col.len() {
2480 return true;
2481 }
2482 if let DataType::Struct(..) = col.dtype()
2484 && let Ok(s) = col.as_materialized_series().struct_()
2485 {
2486 return s
2487 .fields_as_series()
2488 .iter()
2489 .all(|field| field.null_count() == field.len());
2490 }
2491 false
2492}
2493
2494#[cfg(feature = "polars")]
2496fn extract_annotation_name_from_sample(sample: &Sample) -> Option<(String, Option<u32>)> {
2497 use std::path::Path;
2498
2499 let name = sample.image_name.as_ref()?;
2500 let name = Path::new(name).file_stem()?.to_str()?;
2501
2502 match &sample.sequence_name {
2505 Some(sequence) => Some((sequence.clone(), sample.frame_number)),
2506 None => Some((name.to_string(), None)),
2507 }
2508}
2509
2510fn extract_sample_name(image_name: &str) -> String {
2523 let name = image_name
2525 .rsplit_once('.')
2526 .and_then(|(name, _)| {
2527 if name.is_empty() {
2529 None
2530 } else {
2531 Some(name.to_string())
2532 }
2533 })
2534 .unwrap_or_else(|| image_name.to_string());
2535
2536 name.rsplit_once(".camera")
2538 .and_then(|(name, _)| {
2539 if name.is_empty() {
2541 None
2542 } else {
2543 Some(name.to_string())
2544 }
2545 })
2546 .unwrap_or_else(|| name.clone())
2547}
2548
2549fn resolve_file<'a>(file_type: &FileType, files: &'a [SampleFile]) -> Option<&'a SampleFile> {
2558 match file_type {
2559 FileType::Image => None, FileType::All => None, file => {
2562 let type_names = file_type_names(file);
2564 files
2565 .iter()
2566 .find(|f| type_names.contains(&f.r#type.as_str()))
2567 }
2568 }
2569}
2570
2571fn file_type_names(file_type: &FileType) -> Vec<&'static str> {
2574 match file_type {
2575 FileType::Image => vec!["image"],
2576 FileType::LidarPcd => vec!["lidar.pcd"],
2577 FileType::LidarDepth => vec!["lidar.depth", "depth.png", "depthmap"],
2578 FileType::LidarReflect => vec!["lidar.reflect"],
2579 FileType::RadarPcd => vec!["radar.pcd", "pcd"],
2580 FileType::RadarCube => vec!["radar.png", "cube"],
2581 FileType::All => vec![],
2582 }
2583}
2584
2585fn convert_annotations_map_to_vec(map: HashMap<String, Vec<Annotation>>) -> Vec<Annotation> {
2598 let mut all_annotations = Vec::new();
2599 if let Some(bbox_anns) = map.get("bbox") {
2600 all_annotations.extend(bbox_anns.clone());
2601 }
2602 if let Some(box3d_anns) = map.get("box3d") {
2603 all_annotations.extend(box3d_anns.clone());
2604 }
2605 if let Some(mask_anns) = map.get("mask") {
2606 all_annotations.extend(mask_anns.clone());
2607 }
2608 all_annotations
2609}
2610
2611fn validate_gps_coordinates(lat: f64, lon: f64) -> Result<(), String> {
2631 if !lat.is_finite() {
2632 return Err(format!("GPS latitude is not finite: {}", lat));
2633 }
2634 if !lon.is_finite() {
2635 return Err(format!("GPS longitude is not finite: {}", lon));
2636 }
2637 if !(-90.0..=90.0).contains(&lat) {
2638 return Err(format!("GPS latitude out of range [-90, 90]: {}", lat));
2639 }
2640 if !(-180.0..=180.0).contains(&lon) {
2641 return Err(format!("GPS longitude out of range [-180, 180]: {}", lon));
2642 }
2643 Ok(())
2644}
2645
2646fn validate_imu_orientation(roll: f64, pitch: f64, yaw: f64) -> Result<(), String> {
2665 if !roll.is_finite() {
2666 return Err(format!("IMU roll is not finite: {}", roll));
2667 }
2668 if !pitch.is_finite() {
2669 return Err(format!("IMU pitch is not finite: {}", pitch));
2670 }
2671 if !yaw.is_finite() {
2672 return Err(format!("IMU yaw is not finite: {}", yaw));
2673 }
2674 if !(-180.0..=180.0).contains(&roll) {
2675 return Err(format!("IMU roll out of range [-180, 180]: {}", roll));
2676 }
2677 if !(-90.0..=90.0).contains(&pitch) {
2678 return Err(format!("IMU pitch out of range [-90, 90]: {}", pitch));
2679 }
2680 if !(-180.0..=180.0).contains(&yaw) {
2681 return Err(format!("IMU yaw out of range [-180, 180]: {}", yaw));
2682 }
2683 Ok(())
2684}
2685
2686#[cfg(feature = "polars")]
2714pub fn unflatten_polygon_coordinates(coords: &[f32]) -> Vec<Vec<(f32, f32)>> {
2715 let mut polygons = Vec::new();
2716 let mut current_polygon = Vec::new();
2717 let mut i = 0;
2718
2719 while i < coords.len() {
2720 if coords[i].is_nan() {
2721 if !current_polygon.is_empty() {
2723 polygons.push(std::mem::take(&mut current_polygon));
2724 }
2725 i += 1;
2726 } else if i + 1 < coords.len() && !coords[i + 1].is_nan() {
2727 current_polygon.push((coords[i], coords[i + 1]));
2729 i += 2;
2730 } else if i + 1 < coords.len() && coords[i + 1].is_nan() {
2731 i += 1;
2734 } else {
2735 i += 1;
2737 }
2738 }
2739
2740 if !current_polygon.is_empty() {
2742 polygons.push(current_polygon);
2743 }
2744
2745 polygons
2746}
2747
2748#[cfg(test)]
2749mod tests {
2750 use super::*;
2751
2752 fn flatten_annotation_map(
2761 map: std::collections::HashMap<String, Vec<Annotation>>,
2762 ) -> Vec<Annotation> {
2763 let mut all_annotations = Vec::new();
2764
2765 for key in ["bbox", "box3d", "mask"] {
2767 if let Some(mut anns) = map.get(key).cloned() {
2768 all_annotations.append(&mut anns);
2769 }
2770 }
2771
2772 all_annotations
2773 }
2774
2775 fn annotation_group_field_name() -> &'static str {
2777 "group_name"
2778 }
2779
2780 fn annotation_object_id_field_name() -> &'static str {
2782 "object_reference"
2783 }
2784
2785 fn annotation_object_id_alias() -> &'static str {
2787 "object_id"
2788 }
2789
2790 fn validate_annotation_field_names(
2793 json_str: &str,
2794 expected_group: bool,
2795 expected_object_ref: bool,
2796 ) -> Result<(), String> {
2797 if expected_group && !json_str.contains("\"group_name\"") {
2798 return Err("Missing expected field: group_name".to_string());
2799 }
2800 if expected_object_ref && !json_str.contains("\"object_reference\"") {
2801 return Err("Missing expected field: object_reference".to_string());
2802 }
2803 Ok(())
2804 }
2805
2806 #[test]
2808 fn test_file_type_conversions() {
2809 let api_cases = vec![
2811 (FileType::Image, "image"),
2812 (FileType::LidarPcd, "lidar.pcd"),
2813 (FileType::LidarDepth, "lidar.depth"),
2814 (FileType::LidarReflect, "lidar.reflect"),
2815 (FileType::RadarPcd, "radar.pcd"),
2816 (FileType::RadarCube, "radar.png"),
2817 ];
2818
2819 let ext_cases = vec![
2821 (FileType::Image, "jpg"),
2822 (FileType::LidarPcd, "lidar.pcd"),
2823 (FileType::LidarDepth, "lidar.png"),
2824 (FileType::LidarReflect, "lidar.jpg"),
2825 (FileType::RadarPcd, "radar.pcd"),
2826 (FileType::RadarCube, "radar.png"),
2827 ];
2828
2829 for (file_type, expected_str) in &api_cases {
2831 assert_eq!(file_type.to_string(), *expected_str);
2832 }
2833
2834 for (file_type, expected_ext) in &ext_cases {
2836 assert_eq!(file_type.file_extension(), *expected_ext);
2837 }
2838
2839 assert_eq!(
2841 FileType::try_from("lidar.depth").unwrap(),
2842 FileType::LidarDepth
2843 );
2844 assert_eq!(
2845 FileType::try_from("lidar.png").unwrap(),
2846 FileType::LidarDepth
2847 );
2848 assert_eq!(
2849 FileType::try_from("depth.png").unwrap(),
2850 FileType::LidarDepth
2851 );
2852 assert_eq!(
2853 FileType::try_from("lidar.reflect").unwrap(),
2854 FileType::LidarReflect
2855 );
2856 assert_eq!(
2857 FileType::try_from("lidar.jpg").unwrap(),
2858 FileType::LidarReflect
2859 );
2860 assert_eq!(
2861 FileType::try_from("lidar.jpeg").unwrap(),
2862 FileType::LidarReflect
2863 );
2864
2865 assert!(FileType::try_from("invalid").is_err());
2867
2868 for (file_type, _) in &api_cases {
2870 let s = file_type.to_string();
2871 let parsed = FileType::try_from(s.as_str()).unwrap();
2872 assert_eq!(parsed, *file_type);
2873 }
2874 }
2875
2876 #[test]
2878 fn test_annotation_type_conversions() {
2879 let cases = vec![
2880 (AnnotationType::Box2d, "box2d"),
2881 (AnnotationType::Box3d, "box3d"),
2882 (AnnotationType::Polygon, "polygon"),
2883 (AnnotationType::Mask, "mask"),
2884 ];
2885
2886 for (ann_type, expected_str) in &cases {
2888 assert_eq!(ann_type.to_string(), *expected_str);
2889 }
2890
2891 assert_eq!(
2893 AnnotationType::try_from("box2d").unwrap(),
2894 AnnotationType::Box2d
2895 );
2896 assert_eq!(
2897 AnnotationType::try_from("box3d").unwrap(),
2898 AnnotationType::Box3d
2899 );
2900 assert_eq!(
2901 AnnotationType::try_from("polygon").unwrap(),
2902 AnnotationType::Polygon
2903 );
2904 assert_eq!(
2906 AnnotationType::try_from("mask").unwrap(),
2907 AnnotationType::Polygon
2908 );
2909 assert_eq!(
2911 AnnotationType::try_from("raster").unwrap(),
2912 AnnotationType::Mask
2913 );
2914
2915 assert_eq!(
2917 AnnotationType::from("box2d".to_string()),
2918 AnnotationType::Box2d
2919 );
2920 assert_eq!(
2921 AnnotationType::from("box3d".to_string()),
2922 AnnotationType::Box3d
2923 );
2924 assert_eq!(
2925 AnnotationType::from("polygon".to_string()),
2926 AnnotationType::Polygon
2927 );
2928 assert_eq!(
2930 AnnotationType::from("mask".to_string()),
2931 AnnotationType::Polygon
2932 );
2933
2934 assert_eq!(
2936 AnnotationType::from("invalid".to_string()),
2937 AnnotationType::Box2d
2938 );
2939
2940 assert!(AnnotationType::try_from("invalid").is_err());
2942
2943 assert_eq!(
2948 AnnotationType::try_from(AnnotationType::Box2d.to_string().as_str()).unwrap(),
2949 AnnotationType::Box2d
2950 );
2951 assert_eq!(
2952 AnnotationType::try_from(AnnotationType::Box3d.to_string().as_str()).unwrap(),
2953 AnnotationType::Box3d
2954 );
2955 assert_eq!(
2956 AnnotationType::try_from(AnnotationType::Polygon.to_string().as_str()).unwrap(),
2957 AnnotationType::Polygon
2958 );
2959 }
2960
2961 #[test]
2962 fn test_annotation_type_as_server_type() {
2963 assert_eq!(AnnotationType::Box2d.as_server_type(), "box2d");
2968 assert_eq!(AnnotationType::Box3d.as_server_type(), "box3d");
2969 assert_eq!(AnnotationType::Polygon.as_server_type(), "mask");
2970 assert_eq!(AnnotationType::Mask.as_server_type(), "mask");
2971
2972 assert_ne!(
2975 AnnotationType::Polygon.as_server_type(),
2976 AnnotationType::Polygon.to_string().as_str()
2977 );
2978 assert_eq!(
2979 AnnotationType::Box2d.as_server_type(),
2980 AnnotationType::Box2d.to_string().as_str()
2981 );
2982 }
2983
2984 #[test]
2986 fn test_extract_sample_name_with_extension_and_camera() {
2987 assert_eq!(extract_sample_name("scene_001.camera.jpg"), "scene_001");
2988 }
2989
2990 #[test]
2991 fn test_extract_sample_name_multiple_dots() {
2992 assert_eq!(extract_sample_name("image.v2.camera.png"), "image.v2");
2993 }
2994
2995 #[test]
2996 fn test_extract_sample_name_extension_only() {
2997 assert_eq!(extract_sample_name("test.jpg"), "test");
2998 }
2999
3000 #[test]
3001 fn test_extract_sample_name_no_extension() {
3002 assert_eq!(extract_sample_name("test"), "test");
3003 }
3004
3005 #[test]
3006 fn test_extract_sample_name_edge_case_dot_prefix() {
3007 assert_eq!(extract_sample_name(".jpg"), ".jpg");
3008 }
3009
3010 #[test]
3012 fn test_resolve_file_image_type_returns_none() {
3013 let files = vec![];
3015 let result = resolve_file(&FileType::Image, &files);
3016 assert!(result.is_none());
3017 }
3018
3019 #[test]
3020 fn test_resolve_file_lidar_pcd() {
3021 let files = vec![
3022 SampleFile::with_url(
3023 "lidar.pcd".to_string(),
3024 "https://example.com/file.pcd".to_string(),
3025 ),
3026 SampleFile::with_url(
3027 "radar.pcd".to_string(),
3028 "https://example.com/radar.pcd".to_string(),
3029 ),
3030 ];
3031 let result = resolve_file(&FileType::LidarPcd, &files);
3032 assert!(result.is_some());
3033 assert_eq!(result.unwrap().url(), Some("https://example.com/file.pcd"));
3034 }
3035
3036 #[test]
3037 fn test_resolve_file_not_found() {
3038 let files = vec![SampleFile::with_url(
3039 "lidar.pcd".to_string(),
3040 "https://example.com/file.pcd".to_string(),
3041 )];
3042 let result = resolve_file(&FileType::RadarPcd, &files);
3044 assert!(result.is_none());
3045 }
3046
3047 #[test]
3048 fn test_resolve_file_lidar_depth() {
3049 let files = vec![SampleFile::with_url(
3051 "lidar.depth".to_string(),
3052 "https://example.com/depth.png".to_string(),
3053 )];
3054 let result = resolve_file(&FileType::LidarDepth, &files);
3055 assert!(result.is_some());
3056 assert_eq!(result.unwrap().url(), Some("https://example.com/depth.png"));
3057 }
3058
3059 #[test]
3060 fn test_resolve_file_lidar_reflect() {
3061 let files = vec![SampleFile::with_url(
3063 "lidar.reflect".to_string(),
3064 "https://example.com/reflect.png".to_string(),
3065 )];
3066 let result = resolve_file(&FileType::LidarReflect, &files);
3067 assert!(result.is_some());
3068 assert_eq!(
3069 result.unwrap().url(),
3070 Some("https://example.com/reflect.png")
3071 );
3072 }
3073
3074 #[test]
3075 fn test_resolve_file_radar_cube() {
3076 let files = vec![SampleFile::with_url(
3078 "radar.png".to_string(),
3079 "https://example.com/radar.png".to_string(),
3080 )];
3081 let result = resolve_file(&FileType::RadarCube, &files);
3082 assert!(result.is_some());
3083 assert_eq!(result.unwrap().url(), Some("https://example.com/radar.png"));
3084 }
3085
3086 #[test]
3087 fn test_resolve_file_with_inline_data() {
3088 let files = vec![SampleFile::with_data(
3090 "radar.pcd".to_string(),
3091 "SGVsbG8gV29ybGQ=".to_string(), )];
3093 let result = resolve_file(&FileType::RadarPcd, &files);
3094 assert!(result.is_some());
3095 let file = result.unwrap();
3096 assert!(file.url().is_none());
3097 assert_eq!(file.data(), Some("SGVsbG8gV29ybGQ="));
3098 }
3099
3100 #[test]
3101 fn test_convert_annotations_map_to_vec_with_bbox() {
3102 let mut map = HashMap::new();
3103 let bbox_ann = Annotation::new();
3104 map.insert("bbox".to_string(), vec![bbox_ann.clone()]);
3105
3106 let annotations = convert_annotations_map_to_vec(map);
3107 assert_eq!(annotations.len(), 1);
3108 }
3109
3110 #[test]
3111 fn test_convert_annotations_map_to_vec_all_types() {
3112 let mut map = HashMap::new();
3113 map.insert("bbox".to_string(), vec![Annotation::new()]);
3114 map.insert("box3d".to_string(), vec![Annotation::new()]);
3115 map.insert("mask".to_string(), vec![Annotation::new()]);
3116
3117 let annotations = convert_annotations_map_to_vec(map);
3118 assert_eq!(annotations.len(), 3);
3119 }
3120
3121 #[test]
3122 fn test_convert_annotations_map_to_vec_empty() {
3123 let map = HashMap::new();
3124 let annotations = convert_annotations_map_to_vec(map);
3125 assert_eq!(annotations.len(), 0);
3126 }
3127
3128 #[test]
3129 fn test_convert_annotations_map_to_vec_unknown_type_ignored() {
3130 let mut map = HashMap::new();
3131 map.insert("unknown".to_string(), vec![Annotation::new()]);
3132
3133 let annotations = convert_annotations_map_to_vec(map);
3134 assert_eq!(annotations.len(), 0);
3136 }
3137
3138 #[test]
3140 fn test_annotation_group_field_name() {
3141 assert_eq!(annotation_group_field_name(), "group_name");
3142 }
3143
3144 #[test]
3145 fn test_annotation_object_id_field_name() {
3146 assert_eq!(annotation_object_id_field_name(), "object_reference");
3147 }
3148
3149 #[test]
3150 fn test_annotation_object_id_alias() {
3151 assert_eq!(annotation_object_id_alias(), "object_id");
3152 }
3153
3154 #[test]
3155 fn test_validate_annotation_field_names_success() {
3156 let json = r#"{"group_name":"train","object_reference":"obj1"}"#;
3157 assert!(validate_annotation_field_names(json, true, true).is_ok());
3158 }
3159
3160 #[test]
3161 fn test_validate_annotation_field_names_missing_group() {
3162 let json = r#"{"object_reference":"obj1"}"#;
3163 let result = validate_annotation_field_names(json, true, false);
3164 assert!(result.is_err());
3165 assert!(result.unwrap_err().contains("group_name"));
3166 }
3167
3168 #[test]
3169 fn test_validate_annotation_field_names_missing_object_ref() {
3170 let json = r#"{"group_name":"train"}"#;
3171 let result = validate_annotation_field_names(json, false, true);
3172 assert!(result.is_err());
3173 assert!(result.unwrap_err().contains("object_reference"));
3174 }
3175
3176 #[test]
3177 fn test_annotation_serialization_field_names() {
3178 let mut ann = Annotation::new();
3180 ann.set_group(Some("train".to_string()));
3181 ann.set_object_id(Some("obj1".to_string()));
3182
3183 let json = serde_json::to_string(&ann).unwrap();
3184 assert!(validate_annotation_field_names(&json, true, true).is_ok());
3186 }
3187
3188 #[test]
3190 fn test_validate_gps_coordinates_valid() {
3191 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()); }
3196
3197 #[test]
3198 fn test_validate_gps_coordinates_invalid_latitude() {
3199 let result = validate_gps_coordinates(91.0, 0.0);
3200 assert!(result.is_err());
3201 assert!(result.unwrap_err().contains("latitude out of range"));
3202
3203 let result = validate_gps_coordinates(-91.0, 0.0);
3204 assert!(result.is_err());
3205 assert!(result.unwrap_err().contains("latitude out of range"));
3206 }
3207
3208 #[test]
3209 fn test_validate_gps_coordinates_invalid_longitude() {
3210 let result = validate_gps_coordinates(0.0, 181.0);
3211 assert!(result.is_err());
3212 assert!(result.unwrap_err().contains("longitude out of range"));
3213
3214 let result = validate_gps_coordinates(0.0, -181.0);
3215 assert!(result.is_err());
3216 assert!(result.unwrap_err().contains("longitude out of range"));
3217 }
3218
3219 #[test]
3220 fn test_validate_gps_coordinates_non_finite() {
3221 let result = validate_gps_coordinates(f64::NAN, 0.0);
3222 assert!(result.is_err());
3223 assert!(result.unwrap_err().contains("not finite"));
3224
3225 let result = validate_gps_coordinates(0.0, f64::INFINITY);
3226 assert!(result.is_err());
3227 assert!(result.unwrap_err().contains("not finite"));
3228 }
3229
3230 #[test]
3231 fn test_validate_imu_orientation_valid() {
3232 assert!(validate_imu_orientation(0.0, 0.0, 0.0).is_ok());
3233 assert!(validate_imu_orientation(45.0, 30.0, 90.0).is_ok());
3234 assert!(validate_imu_orientation(180.0, 90.0, -180.0).is_ok()); assert!(validate_imu_orientation(-180.0, -90.0, 180.0).is_ok()); }
3237
3238 #[test]
3239 fn test_validate_imu_orientation_invalid_roll() {
3240 let result = validate_imu_orientation(181.0, 0.0, 0.0);
3241 assert!(result.is_err());
3242 assert!(result.unwrap_err().contains("roll out of range"));
3243
3244 let result = validate_imu_orientation(-181.0, 0.0, 0.0);
3245 assert!(result.is_err());
3246 }
3247
3248 #[test]
3249 fn test_validate_imu_orientation_invalid_pitch() {
3250 let result = validate_imu_orientation(0.0, 91.0, 0.0);
3251 assert!(result.is_err());
3252 assert!(result.unwrap_err().contains("pitch out of range"));
3253
3254 let result = validate_imu_orientation(0.0, -91.0, 0.0);
3255 assert!(result.is_err());
3256 }
3257
3258 #[test]
3259 fn test_validate_imu_orientation_non_finite() {
3260 let result = validate_imu_orientation(f64::NAN, 0.0, 0.0);
3261 assert!(result.is_err());
3262 assert!(result.unwrap_err().contains("not finite"));
3263
3264 let result = validate_imu_orientation(0.0, f64::INFINITY, 0.0);
3265 assert!(result.is_err());
3266
3267 let result = validate_imu_orientation(0.0, 0.0, f64::NEG_INFINITY);
3268 assert!(result.is_err());
3269 }
3270
3271 #[test]
3273 #[cfg(feature = "polars")]
3274 fn test_unflatten_polygon_coordinates_single_polygon() {
3275 let coords = vec![1.0, 2.0, 3.0, 4.0];
3276 let result = unflatten_polygon_coordinates(&coords);
3277
3278 assert_eq!(result.len(), 1);
3279 assert_eq!(result[0].len(), 2);
3280 assert_eq!(result[0][0], (1.0, 2.0));
3281 assert_eq!(result[0][1], (3.0, 4.0));
3282 }
3283
3284 #[test]
3285 #[cfg(feature = "polars")]
3286 fn test_unflatten_polygon_coordinates_multiple_polygons() {
3287 let coords = vec![1.0, 2.0, 3.0, 4.0, f32::NAN, 5.0, 6.0, 7.0, 8.0];
3288 let result = unflatten_polygon_coordinates(&coords);
3289
3290 assert_eq!(result.len(), 2);
3291 assert_eq!(result[0].len(), 2);
3292 assert_eq!(result[0][0], (1.0, 2.0));
3293 assert_eq!(result[0][1], (3.0, 4.0));
3294 assert_eq!(result[1].len(), 2);
3295 assert_eq!(result[1][0], (5.0, 6.0));
3296 assert_eq!(result[1][1], (7.0, 8.0));
3297 }
3298
3299 #[test]
3300 #[cfg(feature = "polars")]
3301 fn test_unflatten_polygon_coordinates_roundtrip() {
3302 let flat = vec![1.0, 2.0, 3.0, 4.0, f32::NAN, 5.0, 6.0, 7.0, 8.0];
3304 let result = unflatten_polygon_coordinates(&flat);
3305
3306 let expected = vec![vec![(1.0, 2.0), (3.0, 4.0)], vec![(5.0, 6.0), (7.0, 8.0)]];
3307 assert_eq!(result, expected);
3308 }
3309
3310 #[test]
3312 fn test_flatten_annotation_map_all_types() {
3313 use std::collections::HashMap;
3314
3315 let mut map = HashMap::new();
3316
3317 let mut bbox_ann = Annotation::new();
3319 bbox_ann.set_label(Some("bbox_label".to_string()));
3320
3321 let mut box3d_ann = Annotation::new();
3322 box3d_ann.set_label(Some("box3d_label".to_string()));
3323
3324 let mut mask_ann = Annotation::new();
3325 mask_ann.set_label(Some("mask_label".to_string()));
3326
3327 map.insert("bbox".to_string(), vec![bbox_ann.clone()]);
3328 map.insert("box3d".to_string(), vec![box3d_ann.clone()]);
3329 map.insert("mask".to_string(), vec![mask_ann.clone()]);
3330
3331 let result = flatten_annotation_map(map);
3332
3333 assert_eq!(result.len(), 3);
3334 assert_eq!(result[0].label(), Some(&"bbox_label".to_string()));
3336 assert_eq!(result[1].label(), Some(&"box3d_label".to_string()));
3337 assert_eq!(result[2].label(), Some(&"mask_label".to_string()));
3338 }
3339
3340 #[test]
3341 fn test_flatten_annotation_map_single_type() {
3342 use std::collections::HashMap;
3343
3344 let mut map = HashMap::new();
3345 let mut bbox_ann = Annotation::new();
3346 bbox_ann.set_label(Some("test".to_string()));
3347 map.insert("bbox".to_string(), vec![bbox_ann]);
3348
3349 let result = flatten_annotation_map(map);
3350
3351 assert_eq!(result.len(), 1);
3352 assert_eq!(result[0].label(), Some(&"test".to_string()));
3353 }
3354
3355 #[test]
3356 fn test_flatten_annotation_map_empty() {
3357 use std::collections::HashMap;
3358
3359 let map = HashMap::new();
3360 let result = flatten_annotation_map(map);
3361
3362 assert_eq!(result.len(), 0);
3363 }
3364
3365 #[test]
3366 fn test_flatten_annotation_map_deterministic_order() {
3367 use std::collections::HashMap;
3368
3369 let mut map = HashMap::new();
3370
3371 let mut bbox_ann = Annotation::new();
3372 bbox_ann.set_label(Some("bbox".to_string()));
3373
3374 let mut box3d_ann = Annotation::new();
3375 box3d_ann.set_label(Some("box3d".to_string()));
3376
3377 let mut mask_ann = Annotation::new();
3378 mask_ann.set_label(Some("mask".to_string()));
3379
3380 map.insert("mask".to_string(), vec![mask_ann]);
3382 map.insert("box3d".to_string(), vec![box3d_ann]);
3383 map.insert("bbox".to_string(), vec![bbox_ann]);
3384
3385 let result = flatten_annotation_map(map);
3386
3387 assert_eq!(result.len(), 3);
3389 assert_eq!(result[0].label(), Some(&"bbox".to_string()));
3390 assert_eq!(result[1].label(), Some(&"box3d".to_string()));
3391 assert_eq!(result[2].label(), Some(&"mask".to_string()));
3392 }
3393
3394 #[test]
3396 fn test_box2d_construction_and_accessors() {
3397 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
3399 assert_eq!(
3400 (bbox.left(), bbox.top(), bbox.width(), bbox.height()),
3401 (10.0, 20.0, 100.0, 50.0)
3402 );
3403
3404 assert_eq!((bbox.cx(), bbox.cy()), (60.0, 45.0)); let bbox = Box2d::new(0.0, 0.0, 640.0, 480.0);
3409 assert_eq!(
3410 (bbox.left(), bbox.top(), bbox.width(), bbox.height()),
3411 (0.0, 0.0, 640.0, 480.0)
3412 );
3413 assert_eq!((bbox.cx(), bbox.cy()), (320.0, 240.0));
3414 }
3415
3416 #[test]
3417 fn test_box2d_center_calculation() {
3418 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
3419
3420 assert_eq!(bbox.cx(), 60.0); assert_eq!(bbox.cy(), 45.0); }
3424
3425 #[test]
3426 fn test_box2d_zero_dimensions() {
3427 let bbox = Box2d::new(10.0, 20.0, 0.0, 0.0);
3428
3429 assert_eq!(bbox.cx(), 10.0);
3431 assert_eq!(bbox.cy(), 20.0);
3432 }
3433
3434 #[test]
3435 fn test_box2d_negative_dimensions() {
3436 let bbox = Box2d::new(100.0, 100.0, -50.0, -50.0);
3437
3438 assert_eq!(bbox.width(), -50.0);
3440 assert_eq!(bbox.height(), -50.0);
3441 assert_eq!(bbox.cx(), 75.0); assert_eq!(bbox.cy(), 75.0); }
3444
3445 #[test]
3447 fn test_box3d_construction_and_accessors() {
3448 let bbox = Box3d::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
3450 assert_eq!((bbox.cx(), bbox.cy(), bbox.cz()), (1.0, 2.0, 3.0));
3451 assert_eq!(
3452 (bbox.width(), bbox.height(), bbox.length()),
3453 (4.0, 5.0, 6.0)
3454 );
3455
3456 let bbox = Box3d::new(10.0, 20.0, 30.0, 4.0, 6.0, 8.0);
3458 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);
3462 assert_eq!((bbox.cx(), bbox.cy(), bbox.cz()), (0.0, 0.0, 0.0));
3463 assert_eq!(
3464 (bbox.width(), bbox.height(), bbox.length()),
3465 (2.0, 3.0, 4.0)
3466 );
3467 assert_eq!((bbox.left(), bbox.top(), bbox.front()), (-1.0, -1.5, -2.0));
3468 }
3469
3470 #[test]
3471 fn test_box3d_center_calculation() {
3472 let bbox = Box3d::new(10.0, 20.0, 30.0, 100.0, 50.0, 40.0);
3473
3474 assert_eq!(bbox.cx(), 10.0);
3476 assert_eq!(bbox.cy(), 20.0);
3477 assert_eq!(bbox.cz(), 30.0);
3478 }
3479
3480 #[test]
3481 fn test_box3d_zero_dimensions() {
3482 let bbox = Box3d::new(5.0, 10.0, 15.0, 0.0, 0.0, 0.0);
3483
3484 assert_eq!(bbox.cx(), 5.0);
3486 assert_eq!(bbox.cy(), 10.0);
3487 assert_eq!(bbox.cz(), 15.0);
3488 assert_eq!((bbox.left(), bbox.top(), bbox.front()), (5.0, 10.0, 15.0));
3489 }
3490
3491 #[test]
3492 fn test_box3d_negative_dimensions() {
3493 let bbox = Box3d::new(100.0, 100.0, 100.0, -50.0, -50.0, -50.0);
3494
3495 assert_eq!(bbox.width(), -50.0);
3497 assert_eq!(bbox.height(), -50.0);
3498 assert_eq!(bbox.length(), -50.0);
3499 assert_eq!(
3500 (bbox.left(), bbox.top(), bbox.front()),
3501 (125.0, 125.0, 125.0)
3502 );
3503 }
3504
3505 #[test]
3507 fn test_polygon_creation_and_deserialization() {
3508 let rings = vec![vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]];
3510 let polygon = Polygon::new(rings.clone());
3511 assert_eq!(polygon.rings, rings);
3512
3513 let legacy = serde_json::json!({
3515 "polygon": {
3516 "polygon": [[
3517 [0.0_f32, 0.0_f32],
3518 [1.0_f32, 0.0_f32],
3519 [1.0_f32, 1.0_f32]
3520 ]]
3521 }
3522 });
3523
3524 #[derive(serde::Deserialize)]
3525 struct Wrapper {
3526 polygon: Polygon,
3527 }
3528
3529 let parsed: Wrapper = serde_json::from_value(legacy).unwrap();
3530 assert_eq!(parsed.polygon.rings.len(), 1);
3531 assert_eq!(parsed.polygon.rings[0].len(), 3);
3532 }
3533
3534 #[test]
3536 fn test_sample_construction_and_accessors() {
3537 let sample = Sample::new();
3539 assert_eq!(sample.id(), None);
3540 assert_eq!(sample.image_name(), None);
3541 assert_eq!(sample.width(), None);
3542 assert_eq!(sample.height(), None);
3543
3544 let mut sample = Sample::new();
3546 sample.image_name = Some("test.jpg".to_string());
3547 sample.width = Some(1920);
3548 sample.height = Some(1080);
3549 sample.group = Some("group1".to_string());
3550
3551 assert_eq!(sample.image_name(), Some("test.jpg"));
3552 assert_eq!(sample.width(), Some(1920));
3553 assert_eq!(sample.height(), Some(1080));
3554 assert_eq!(sample.group(), Some(&"group1".to_string()));
3555 }
3556
3557 #[test]
3558 fn test_sample_name_extraction_from_image_name() {
3559 let mut sample = Sample::new();
3560
3561 sample.image_name = Some("test_image.jpg".to_string());
3563 assert_eq!(sample.name(), Some("test_image".to_string()));
3564
3565 sample.image_name = Some("test_image.camera.jpg".to_string());
3567 assert_eq!(sample.name(), Some("test_image".to_string()));
3568
3569 sample.image_name = Some("test_image".to_string());
3571 assert_eq!(sample.name(), Some("test_image".to_string()));
3572 }
3573
3574 #[test]
3576 fn test_annotation_construction_and_setters() {
3577 let ann = Annotation::new();
3579 assert_eq!(ann.sample_id(), None);
3580 assert_eq!(ann.label(), None);
3581 assert_eq!(ann.box2d(), None);
3582 assert_eq!(ann.box3d(), None);
3583 assert_eq!(ann.polygon(), None);
3584
3585 let mut ann = Annotation::new();
3587 ann.set_label(Some("car".to_string()));
3588 assert_eq!(ann.label(), Some(&"car".to_string()));
3589
3590 ann.set_label_index(Some(42));
3591 assert_eq!(ann.label_index(), Some(42));
3592
3593 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
3595 ann.set_box2d(Some(bbox.clone()));
3596 assert!(ann.box2d().is_some());
3597 assert_eq!(ann.box2d().unwrap().left(), 10.0);
3598 }
3599
3600 #[test]
3602 fn test_sample_file_with_url_and_filename() {
3603 let file = SampleFile::with_url(
3605 "lidar.pcd".to_string(),
3606 "https://example.com/file.pcd".to_string(),
3607 );
3608 assert_eq!(file.file_type(), "lidar.pcd");
3609 assert_eq!(file.url(), Some("https://example.com/file.pcd"));
3610 assert_eq!(file.filename(), None);
3611
3612 let file = SampleFile::with_filename("image".to_string(), "test.jpg".to_string());
3614 assert_eq!(file.file_type(), "image");
3615 assert_eq!(file.filename(), Some("test.jpg"));
3616 assert_eq!(file.url(), None);
3617 }
3618
3619 #[test]
3621 fn test_sample_deserializes_gps_imu_from_sensors() {
3622 use serde_json::json;
3623
3624 let sample_json = json!({
3626 "id": 123,
3627 "image_name": "test.jpg",
3628 "sensors": [
3629 {"gps": {"lat": 37.7749, "lon": -122.4194}},
3630 {"imu": {"roll": 1.5, "pitch": 2.5, "yaw": 3.5}},
3631 {"radar.pcd": "https://example.com/radar.pcd"}
3632 ]
3633 });
3634
3635 let sample: Sample = serde_json::from_value(sample_json).unwrap();
3636
3637 assert!(sample.location.is_some());
3639 let location = sample.location.as_ref().unwrap();
3640
3641 assert!(location.gps.is_some());
3643 let gps = location.gps.as_ref().unwrap();
3644 assert!((gps.lat - 37.7749).abs() < 0.0001);
3645 assert!((gps.lon - (-122.4194)).abs() < 0.0001);
3646
3647 assert!(location.imu.is_some());
3649 let imu = location.imu.as_ref().unwrap();
3650 assert!((imu.roll - 1.5).abs() < 0.0001);
3651 assert!((imu.pitch - 2.5).abs() < 0.0001);
3652 assert!((imu.yaw - 3.5).abs() < 0.0001);
3653
3654 assert_eq!(sample.files.len(), 1);
3656 assert_eq!(sample.files[0].file_type(), "radar.pcd");
3657 assert_eq!(sample.files[0].url(), Some("https://example.com/radar.pcd"));
3658 }
3659
3660 #[test]
3661 fn test_sample_deserializes_gps_only() {
3662 use serde_json::json;
3663
3664 let sample_json = json!({
3666 "id": 456,
3667 "sensors": [
3668 {"gps": {"lat": 40.7128, "lon": -74.0060}}
3669 ]
3670 });
3671
3672 let sample: Sample = serde_json::from_value(sample_json).unwrap();
3673
3674 assert!(sample.location.is_some());
3675 let location = sample.location.as_ref().unwrap();
3676
3677 assert!(location.gps.is_some());
3678 assert!(location.imu.is_none());
3679
3680 let gps = location.gps.as_ref().unwrap();
3681 assert!((gps.lat - 40.7128).abs() < 0.0001);
3682 assert!((gps.lon - (-74.0060)).abs() < 0.0001);
3683 }
3684
3685 #[test]
3686 fn test_sample_deserializes_without_location() {
3687 use serde_json::json;
3688
3689 let sample_json = json!({
3691 "id": 789,
3692 "sensors": [
3693 {"radar.pcd": "https://example.com/radar.pcd"},
3694 {"lidar.pcd": "https://example.com/lidar.pcd"}
3695 ]
3696 });
3697
3698 let sample: Sample = serde_json::from_value(sample_json).unwrap();
3699
3700 assert!(sample.location.is_none());
3702
3703 assert_eq!(sample.files.len(), 2);
3705 }
3706
3707 #[test]
3708 fn test_sample_serializes_location_as_sensors_object() {
3709 use serde_json::json;
3710
3711 let mut sample = Sample::new();
3715 sample.files = vec![SampleFile::with_filename(
3716 "image".to_string(),
3717 "pose_location.png".to_string(),
3718 )];
3719 sample.location = Some(Location {
3720 gps: Some(GpsData {
3721 lat: 37.7749,
3722 lon: -122.4194,
3723 }),
3724 imu: Some(ImuData {
3725 roll: 10.0,
3726 pitch: -5.0,
3727 yaw: 90.0,
3728 }),
3729 });
3730
3731 let json = serde_json::to_value(&sample).unwrap();
3732 assert_eq!(
3733 json.get("sensors"),
3734 Some(&json!({
3735 "gps": {"lat": 37.7749, "lon": -122.4194},
3736 "imu": {"roll": 10.0, "pitch": -5.0, "yaw": 90.0}
3737 }))
3738 );
3739 assert_eq!(
3740 json.get("files"),
3741 Some(&json!({ "image": "pose_location.png" }))
3742 );
3743 assert!(json.get("sensors").and_then(|v| v.as_array()).is_none());
3745 }
3746
3747 #[test]
3748 fn test_sample_deserializes_gps_imu_from_sensors_object() {
3749 use serde_json::json;
3750
3751 let sample_json = json!({
3754 "id": 42,
3755 "sensors": {
3756 "gps": {"lat": 40.7128, "lon": -74.0060},
3757 "imu": {"roll": 1.0, "pitch": 2.0, "yaw": 3.0}
3758 }
3759 });
3760
3761 let sample: Sample = serde_json::from_value(sample_json).unwrap();
3762 let location = sample.location.as_ref().expect("location");
3763 let gps = location.gps.as_ref().expect("gps");
3764 let imu = location.imu.as_ref().expect("imu");
3765 assert!((gps.lat - 40.7128).abs() < 0.0001);
3766 assert!((gps.lon - (-74.0060)).abs() < 0.0001);
3767 assert!((imu.roll - 1.0).abs() < 0.0001);
3768 assert!((imu.pitch - 2.0).abs() < 0.0001);
3769 assert!((imu.yaw - 3.0).abs() < 0.0001);
3770 }
3771
3772 #[test]
3774 fn test_label_deserialization_and_accessors() {
3775 use serde_json::json;
3776
3777 let label_json = json!({
3779 "id": 123,
3780 "dataset_id": 456,
3781 "index": 5,
3782 "name": "car"
3783 });
3784
3785 let label: Label = serde_json::from_value(label_json).unwrap();
3786 assert_eq!(label.id(), 123);
3787 assert_eq!(label.index(), 5);
3788 assert_eq!(label.name(), "car");
3789 assert_eq!(label.to_string(), "car");
3790 assert_eq!(format!("{}", label), "car");
3791
3792 let label_json = json!({
3794 "id": 1,
3795 "dataset_id": 100,
3796 "index": 0,
3797 "name": "person"
3798 });
3799
3800 let label: Label = serde_json::from_value(label_json).unwrap();
3801 assert_eq!(format!("{}", label), "person");
3802 }
3803
3804 #[test]
3806 fn test_annotation_serialization_with_mask_and_box() {
3807 let polygon = vec![vec![
3808 (0.0_f32, 0.0_f32),
3809 (1.0_f32, 0.0_f32),
3810 (1.0_f32, 1.0_f32),
3811 ]];
3812
3813 let mut annotation = Annotation::new();
3814 annotation.set_label(Some("test".to_string()));
3815 annotation.set_box2d(Some(Box2d::new(10.0, 20.0, 30.0, 40.0)));
3816 annotation.set_polygon(Some(Polygon::new(polygon)));
3817
3818 let mut sample = Sample::new();
3819 sample.annotations.push(annotation);
3820
3821 let json = serde_json::to_value(&sample).unwrap();
3822 let annotations = json
3823 .get("annotations")
3824 .and_then(|value| value.as_array())
3825 .expect("annotations serialized as array");
3826 assert_eq!(annotations.len(), 1);
3827
3828 let annotation_json = annotations[0].as_object().expect("annotation object");
3829 assert!(annotation_json.contains_key("box2d"));
3830 assert!(
3835 annotation_json.contains_key("mask"),
3836 "Annotation must serialise polygon under 'mask' key for samples.populate2; got keys: {:?}",
3837 annotation_json.keys().collect::<Vec<_>>()
3838 );
3839 assert!(!annotation_json.contains_key("polygon"));
3840 assert!(!annotation_json.contains_key("x"));
3841 assert!(
3842 annotation_json
3843 .get("mask")
3844 .and_then(|value| value.as_array())
3845 .is_some()
3846 );
3847 }
3848
3849 #[test]
3850 fn test_frame_number_negative_one_deserializes_as_none() {
3851 let json = r#"{
3854 "uuid": "test-uuid",
3855 "frame_number": -1
3856 }"#;
3857
3858 let sample: Sample = serde_json::from_str(json).unwrap();
3859 assert_eq!(sample.frame_number, None);
3860 }
3861
3862 #[test]
3863 fn test_frame_number_positive_value_deserializes_correctly() {
3864 let json = r#"{
3866 "uuid": "test-uuid",
3867 "frame_number": 5
3868 }"#;
3869
3870 let sample: Sample = serde_json::from_str(json).unwrap();
3871 assert_eq!(sample.frame_number, Some(5));
3872 }
3873
3874 #[test]
3875 fn test_frame_number_null_deserializes_as_none() {
3876 let json = r#"{
3878 "uuid": "test-uuid",
3879 "frame_number": null
3880 }"#;
3881
3882 let sample: Sample = serde_json::from_str(json).unwrap();
3883 assert_eq!(sample.frame_number, None);
3884 }
3885
3886 #[test]
3887 fn test_frame_number_missing_deserializes_as_none() {
3888 let json = r#"{
3890 "uuid": "test-uuid"
3891 }"#;
3892
3893 let sample: Sample = serde_json::from_str(json).unwrap();
3894 assert_eq!(sample.frame_number, None);
3895 }
3896
3897 #[cfg(feature = "polars")]
3902 #[test]
3903 fn test_samples_dataframe_preserves_group_for_samples_without_annotations() {
3904 use polars::prelude::*;
3905
3906 let mut sample_with_ann = Sample::new();
3908 sample_with_ann.image_name = Some("annotated.jpg".to_string());
3909 sample_with_ann.group = Some("train".to_string());
3910 let mut annotation = Annotation::new();
3911 annotation.set_label(Some("car".to_string()));
3912 annotation.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
3913 annotation.set_name(Some("annotated".to_string()));
3914 sample_with_ann.annotations = vec![annotation];
3915
3916 let mut sample_no_ann = Sample::new();
3918 sample_no_ann.image_name = Some("unannotated.jpg".to_string());
3919 sample_no_ann.group = Some("val".to_string()); sample_no_ann.annotations = vec![]; let samples = vec![sample_with_ann, sample_no_ann];
3923
3924 let df = samples_dataframe(&samples).expect("Failed to create DataFrame");
3926
3927 assert_eq!(df.height(), 2, "Expected 2 rows (one per sample)");
3929
3930 let groups_col = df.column("group").expect("group column should exist");
3932 let groups_cast = groups_col.cast(&DataType::String).expect("cast to string");
3933 let groups = groups_cast.str().expect("as str");
3934
3935 let names_col = df.column("name").expect("name column should exist");
3937 let names_cast = names_col.cast(&DataType::String).expect("cast to string");
3938 let names = names_cast.str().expect("as str");
3939
3940 let mut found_unannotated = false;
3941 for idx in 0..df.height() {
3942 if let Some(name) = names.get(idx)
3943 && name == "unannotated"
3944 {
3945 found_unannotated = true;
3946 let group = groups.get(idx);
3947 assert_eq!(
3948 group,
3949 Some("val"),
3950 "CRITICAL: Sample 'unannotated' without annotations must have group 'val'"
3951 );
3952 }
3953 }
3954
3955 assert!(
3956 found_unannotated,
3957 "Did not find 'unannotated' sample in DataFrame - \
3958 this means samples without annotations are not being included"
3959 );
3960 }
3961
3962 #[cfg(feature = "polars")]
3963 #[test]
3964 fn test_samples_dataframe_includes_all_samples_even_without_annotations() {
3965 let mut sample1 = Sample::new();
3969 sample1.image_name = Some("with_ann.jpg".to_string());
3970 sample1.group = Some("train".to_string());
3971 let mut ann = Annotation::new();
3972 ann.set_label(Some("person".to_string()));
3973 ann.set_box2d(Some(Box2d::new(0.0, 0.0, 0.5, 0.5)));
3974 ann.set_name(Some("with_ann".to_string()));
3975 sample1.annotations = vec![ann];
3976
3977 let mut sample2 = Sample::new();
3978 sample2.image_name = Some("no_ann_train.jpg".to_string());
3979 sample2.group = Some("train".to_string());
3980 sample2.annotations = vec![];
3981
3982 let mut sample3 = Sample::new();
3983 sample3.image_name = Some("no_ann_val.jpg".to_string());
3984 sample3.group = Some("val".to_string());
3985 sample3.annotations = vec![];
3986
3987 let samples = vec![sample1, sample2, sample3];
3988
3989 let df = samples_dataframe(&samples).expect("Failed to create DataFrame");
3990
3991 assert_eq!(
3993 df.height(),
3994 3,
3995 "Expected 3 rows (samples without annotations should create one row each)"
3996 );
3997
3998 let groups_col = df.column("group").expect("group column");
4000 let groups_cast = groups_col.cast(&polars::prelude::DataType::String).unwrap();
4001 let groups = groups_cast.str().unwrap();
4002
4003 let mut train_count = 0;
4004 let mut val_count = 0;
4005
4006 for idx in 0..df.height() {
4007 match groups.get(idx) {
4008 Some("train") => train_count += 1,
4009 Some("val") => val_count += 1,
4010 other => panic!(
4011 "Unexpected group value at row {}: {:?}. \
4012 All samples should have their group preserved.",
4013 idx, other
4014 ),
4015 }
4016 }
4017
4018 assert_eq!(train_count, 2, "Expected 2 samples in 'train' group");
4019 assert_eq!(val_count, 1, "Expected 1 sample in 'val' group");
4020 }
4021
4022 #[cfg(feature = "polars")]
4023 #[test]
4024 fn test_samples_dataframe_group_is_not_null_for_samples_with_group() {
4025 let mut sample = Sample::new();
4029 sample.image_name = Some("test.jpg".to_string());
4030 sample.group = Some("test_group".to_string());
4031 sample.annotations = vec![];
4032
4033 let df = samples_dataframe(&[sample]).expect("Failed to create DataFrame");
4034
4035 let groups_col = df.column("group").expect("group column");
4036
4037 assert_eq!(
4039 groups_col.null_count(),
4040 0,
4041 "Sample with group='test_group' but no annotations has NULL group in DataFrame. \
4042 This is a bug in samples_dataframe - group must be preserved!"
4043 );
4044 }
4045
4046 #[cfg(feature = "polars")]
4047 #[test]
4048 fn test_samples_dataframe_group_consistent_across_all_rows_for_same_image() {
4049 use polars::prelude::*;
4050
4051 let mut sample = Sample::new();
4055 sample.image_name = Some("multi_ann.jpg".to_string());
4056 sample.group = Some("train".to_string());
4057
4058 let mut ann1 = Annotation::new();
4060 ann1.set_label(Some("car".to_string()));
4061 ann1.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
4062 ann1.set_name(Some("multi_ann".to_string()));
4063
4064 let mut ann2 = Annotation::new();
4065 ann2.set_label(Some("truck".to_string()));
4066 ann2.set_box2d(Some(Box2d::new(0.5, 0.6, 0.2, 0.2)));
4067 ann2.set_name(Some("multi_ann".to_string()));
4068
4069 let mut ann3 = Annotation::new();
4070 ann3.set_label(Some("bus".to_string()));
4071 ann3.set_box2d(Some(Box2d::new(0.7, 0.8, 0.1, 0.1)));
4072 ann3.set_name(Some("multi_ann".to_string()));
4073
4074 sample.annotations = vec![ann1, ann2, ann3];
4075
4076 let df = samples_dataframe(&[sample]).expect("Failed to create DataFrame");
4077
4078 assert_eq!(df.height(), 3, "Expected 3 rows (one per annotation)");
4080
4081 let groups_col = df.column("group").expect("group column");
4083 let groups_cast = groups_col.cast(&DataType::String).expect("cast to string");
4084 let groups = groups_cast.str().expect("as str");
4085
4086 assert_eq!(groups_col.null_count(), 0, "No rows should have null group");
4088
4089 for idx in 0..df.height() {
4091 let group = groups.get(idx);
4092 assert_eq!(
4093 group,
4094 Some("train"),
4095 "Row {} should have group 'train', got {:?}. \
4096 All rows for the same image must have identical group values.",
4097 idx,
4098 group
4099 );
4100 }
4101 }
4102
4103 #[cfg(feature = "polars")]
4104 #[test]
4105 fn test_samples_dataframe_lvis_columns() {
4106 let mut ann = Annotation::new();
4107 ann.set_name(Some("test".to_string()));
4108 ann.set_label(Some("person".to_string()));
4109 ann.set_label_index(Some(1));
4110 ann.set_iscrowd(Some(false));
4111 ann.set_category_frequency(Some("f".to_string()));
4112
4113 let sample = Sample {
4114 image_name: Some("test.jpg".to_string()),
4115 width: Some(640),
4116 height: Some(480),
4117 annotations: vec![ann],
4118 neg_label_indices: Some(vec![5, 12]),
4119 not_exhaustive_label_indices: Some(vec![3]),
4120 ..Default::default()
4121 };
4122
4123 let df = samples_dataframe(&[sample]).unwrap();
4124
4125 assert!(df.column("iscrowd").is_ok(), "iscrowd column missing");
4127 assert!(
4128 df.column("category_frequency").is_ok(),
4129 "category_frequency column missing"
4130 );
4131 assert!(
4132 df.column("neg_label_indices").is_ok(),
4133 "neg_label_indices column missing"
4134 );
4135 assert!(
4136 df.column("not_exhaustive_label_indices").is_ok(),
4137 "not_exhaustive_label_indices column missing"
4138 );
4139
4140 assert!(
4142 df.column("polygon").is_err(),
4143 "polygon column should be dropped (all null)"
4144 );
4145 assert!(
4146 df.column("box2d").is_err(),
4147 "box2d column should be dropped (all null)"
4148 );
4149 }
4150
4151 #[test]
4152 fn test_annotation_serialization_skips_lvis_fields() {
4153 let ann = Annotation::new();
4154 let json = serde_json::to_string(&ann).unwrap();
4155 assert!(
4156 !json.contains("iscrowd"),
4157 "iscrowd should be omitted when None"
4158 );
4159 assert!(
4160 !json.contains("category_frequency"),
4161 "category_frequency should be omitted when None"
4162 );
4163 }
4164
4165 #[test]
4166 fn test_sample_serialization_skips_lvis_fields() {
4167 let sample = Sample::new();
4168 let json = serde_json::to_string(&sample).unwrap();
4169 assert!(
4170 !json.contains("neg_label_indices"),
4171 "neg_label_indices should be omitted when None"
4172 );
4173 assert!(
4174 !json.contains("not_exhaustive_label_indices"),
4175 "not_exhaustive_label_indices should be omitted when None"
4176 );
4177 }
4178
4179 #[test]
4180 fn test_annotation_score_fields() {
4181 let mut ann = Annotation::default();
4182 assert!(ann.box2d_score.is_none());
4183 assert!(ann.polygon_score.is_none());
4184 assert!(ann.mask_score.is_none());
4185 ann.box2d_score = Some(0.95);
4186 ann.polygon_score = Some(0.87);
4187 ann.mask_score = Some(0.42);
4188 assert_eq!(ann.box2d_score, Some(0.95));
4189 assert_eq!(ann.polygon_score, Some(0.87));
4190 assert_eq!(ann.mask_score, Some(0.42));
4191 }
4192
4193 #[test]
4194 fn test_timing_struct() {
4195 let timing = Timing {
4196 load: Some(1_000_000),
4197 preprocess: Some(2_000_000),
4198 inference: Some(50_000_000),
4199 decode: Some(3_000_000),
4200 };
4201 assert_eq!(timing.inference, Some(50_000_000));
4202
4203 let default = Timing::default();
4204 assert!(default.load.is_none());
4205 }
4206
4207 #[test]
4208 fn test_sample_timing() {
4209 let mut sample = Sample::default();
4210 assert!(sample.timing.is_none());
4211 sample.timing = Some(Timing {
4212 load: Some(1_000_000),
4213 ..Default::default()
4214 });
4215 assert!(sample.timing.is_some());
4216 }
4217
4218 #[cfg(feature = "polars")]
4223 #[test]
4224 fn test_samples_dataframe_polygon_column() {
4225 let mut ann = Annotation::new();
4226 ann.set_name(Some("test".to_string()));
4227 ann.set_polygon(Some(Polygon::new(vec![vec![
4228 (0.1, 0.2),
4229 (0.3, 0.4),
4230 (0.5, 0.6),
4231 ]])));
4232
4233 let sample = Sample {
4234 image_name: Some("test.jpg".to_string()),
4235 annotations: vec![ann],
4236 ..Default::default()
4237 };
4238
4239 let df = samples_dataframe(&[sample]).unwrap();
4240
4241 assert!(df.column("polygon").is_ok(), "Should have polygon column");
4243
4244 if let Ok(mask_col) = df.column("mask") {
4247 assert_eq!(
4249 mask_col.dtype(),
4250 &polars::prelude::DataType::Binary,
4251 "mask column must be Binary type (PNG bytes), not float list"
4252 );
4253 }
4254 }
4255
4256 #[cfg(feature = "polars")]
4257 #[test]
4258 fn test_samples_dataframe_column_presence_drops_all_null() {
4259 let sample = Sample {
4261 image_name: Some("test.jpg".to_string()),
4262 ..Default::default()
4263 };
4264
4265 let df = samples_dataframe(&[sample]).unwrap();
4266
4267 assert!(df.column("name").is_ok(), "name column must always exist");
4269
4270 assert!(
4272 df.column("polygon").is_err(),
4273 "All-null polygon should be dropped"
4274 );
4275 assert!(
4276 df.column("box2d").is_err(),
4277 "All-null box2d should be dropped"
4278 );
4279 assert!(
4280 df.column("box3d").is_err(),
4281 "All-null box3d should be dropped"
4282 );
4283 assert!(
4284 df.column("mask").is_err(),
4285 "All-null mask should be dropped"
4286 );
4287 assert!(
4288 df.column("box2d_score").is_err(),
4289 "All-null score columns should be dropped"
4290 );
4291 assert!(
4292 df.column("timing").is_err(),
4293 "All-null timing should be dropped"
4294 );
4295 }
4296
4297 #[cfg(feature = "polars")]
4298 #[test]
4299 fn test_samples_dataframe_size_column() {
4300 let sample1 = Sample {
4302 image_name: Some("img1.jpg".to_string()),
4303 width: Some(1920),
4304 height: Some(1080),
4305 ..Default::default()
4306 };
4307 let sample2 = Sample {
4308 image_name: Some("img2.jpg".to_string()),
4309 width: Some(640),
4310 height: Some(480),
4311 ..Default::default()
4312 };
4313
4314 let df = samples_dataframe(&[sample1, sample2]).unwrap();
4315
4316 let size_col = df
4318 .column("size")
4319 .expect("size column should be present when width/height are set");
4320 assert_eq!(size_col.len(), 2);
4321
4322 let arr = size_col.array().expect("size column should be Array dtype");
4324 let row0 = arr.get_as_series(0).unwrap();
4325 let row0_vals: Vec<u32> = row0.u32().unwrap().into_no_null_iter().collect();
4326 assert_eq!(row0_vals, vec![1920, 1080]);
4327
4328 let row1 = arr.get_as_series(1).unwrap();
4329 let row1_vals: Vec<u32> = row1.u32().unwrap().into_no_null_iter().collect();
4330 assert_eq!(row1_vals, vec![640, 480]);
4331 }
4332
4333 #[cfg(feature = "polars")]
4334 #[test]
4335 fn test_samples_dataframe_size_column_partial() {
4336 let sample1 = Sample {
4338 image_name: Some("img1.jpg".to_string()),
4339 width: Some(1920),
4340 height: Some(1080),
4341 ..Default::default()
4342 };
4343 let sample2 = Sample {
4344 image_name: Some("img2.jpg".to_string()),
4345 ..Default::default()
4347 };
4348
4349 let df = samples_dataframe(&[sample1, sample2]).unwrap();
4350
4351 let size_col = df
4353 .column("size")
4354 .expect("size column should be present when at least one sample has dimensions");
4355 assert_eq!(size_col.len(), 2);
4356 assert_eq!(size_col.null_count(), 1, "one row should be null");
4357 }
4358
4359 #[cfg(feature = "polars")]
4360 #[test]
4361 fn test_samples_dataframe_score_columns() {
4362 let mut ann = Annotation::new();
4363 ann.set_name(Some("test".to_string()));
4364 ann.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
4365 ann.set_box2d_score(Some(0.95));
4366 ann.set_polygon(Some(Polygon::new(vec![vec![
4367 (0.0, 0.0),
4368 (1.0, 0.0),
4369 (1.0, 1.0),
4370 ]])));
4371 ann.set_polygon_score(Some(0.87));
4372
4373 let sample = Sample {
4374 image_name: Some("test.jpg".to_string()),
4375 annotations: vec![ann],
4376 ..Default::default()
4377 };
4378
4379 let df = samples_dataframe(&[sample]).unwrap();
4380
4381 assert!(
4383 df.column("box2d_score").is_ok(),
4384 "box2d_score column missing"
4385 );
4386 assert!(
4387 df.column("polygon_score").is_ok(),
4388 "polygon_score column missing"
4389 );
4390
4391 assert!(
4393 df.column("box3d_score").is_err(),
4394 "box3d_score should be dropped (all null)"
4395 );
4396 assert!(
4397 df.column("mask_score").is_err(),
4398 "mask_score should be dropped (all null)"
4399 );
4400
4401 let box2d_scores = df.column("box2d_score").unwrap();
4403 let val = box2d_scores.f32().unwrap().get(0);
4404 assert_eq!(val, Some(0.95));
4405 }
4406
4407 #[cfg(feature = "polars")]
4408 #[test]
4409 fn test_samples_dataframe_timing_column() {
4410 let mut ann = Annotation::new();
4411 ann.set_name(Some("test".to_string()));
4412 ann.set_label(Some("person".to_string()));
4413
4414 let sample = Sample {
4415 image_name: Some("test.jpg".to_string()),
4416 annotations: vec![ann],
4417 timing: Some(Timing {
4418 load: Some(1_000_000),
4419 preprocess: Some(2_000_000),
4420 inference: Some(50_000_000),
4421 decode: Some(3_000_000),
4422 }),
4423 ..Default::default()
4424 };
4425
4426 let df = samples_dataframe(&[sample]).unwrap();
4427
4428 assert!(df.column("timing").is_ok(), "timing column missing");
4430
4431 let timing_col = df.column("timing").unwrap();
4433 assert!(
4434 matches!(timing_col.dtype(), polars::prelude::DataType::Struct(..)),
4435 "timing column should be Struct type, got {:?}",
4436 timing_col.dtype()
4437 );
4438 }
4439
4440 #[cfg(feature = "polars")]
4441 #[test]
4442 fn test_samples_dataframe_mask_binary_column() {
4443 let mut ann = Annotation::new();
4444 ann.set_name(Some("test".to_string()));
4445 let pixels = vec![0u8, 255, 128, 64];
4447 let mask_data = MaskData::encode(&pixels, 2, 2, 8).unwrap();
4448 ann.set_mask(Some(mask_data));
4449
4450 let sample = Sample {
4451 image_name: Some("test.jpg".to_string()),
4452 annotations: vec![ann],
4453 ..Default::default()
4454 };
4455
4456 let df = samples_dataframe(&[sample]).unwrap();
4457
4458 let mask_col = df.column("mask").unwrap();
4460 assert_eq!(
4461 mask_col.dtype(),
4462 &polars::prelude::DataType::Binary,
4463 "mask column should be Binary"
4464 );
4465 assert_eq!(mask_col.null_count(), 0, "mask value should not be null");
4466 }
4467
4468 #[test]
4473 fn test_annotation_type_seg_alias() {
4474 assert_eq!(
4475 AnnotationType::try_from("seg").unwrap(),
4476 AnnotationType::Polygon,
4477 "\"seg\" should map to Polygon for server round-trip"
4478 );
4479 }
4480
4481 #[cfg(feature = "polars")]
4486 #[test]
4487 fn test_samples_dataframe_timing_partial() {
4488 let mut ann = Annotation::new();
4490 ann.set_name(Some("test".to_string()));
4491 ann.set_label(Some("person".to_string()));
4492
4493 let sample = Sample {
4494 image_name: Some("test.jpg".to_string()),
4495 annotations: vec![ann],
4496 timing: Some(Timing {
4497 load: Some(1000),
4498 ..Default::default()
4499 }),
4500 ..Default::default()
4501 };
4502
4503 let df = samples_dataframe(&[sample]).unwrap();
4504
4505 assert!(
4507 df.column("timing").is_ok(),
4508 "timing column should be present when partial data exists"
4509 );
4510 }
4511
4512 #[cfg(feature = "polars")]
4513 #[test]
4514 fn test_samples_dataframe_timing_all_none_omitted() {
4515 let mut ann = Annotation::new();
4517 ann.set_name(Some("test".to_string()));
4518 ann.set_label(Some("person".to_string()));
4519
4520 let sample = Sample {
4521 image_name: Some("test.jpg".to_string()),
4522 annotations: vec![ann],
4523 timing: None,
4524 ..Default::default()
4525 };
4526
4527 let df = samples_dataframe(&[sample]).unwrap();
4528
4529 assert!(
4530 df.column("timing").is_err(),
4531 "timing column should be omitted when all samples have timing: None"
4532 );
4533 }
4534
4535 #[cfg(feature = "polars")]
4540 #[test]
4541 fn test_samples_dataframe_score_zero_survives() {
4542 let mut ann = Annotation::new();
4544 ann.set_name(Some("test".to_string()));
4545 ann.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
4546 ann.set_box2d_score(Some(0.0));
4547
4548 let sample = Sample {
4549 image_name: Some("test.jpg".to_string()),
4550 annotations: vec![ann],
4551 ..Default::default()
4552 };
4553
4554 let df = samples_dataframe(&[sample]).unwrap();
4555
4556 let scores = df.column("box2d_score").unwrap();
4557 let val = scores.f32().unwrap().get(0);
4558 assert_eq!(val, Some(0.0), "score of 0.0 should survive as non-null");
4559 }
4560
4561 #[cfg(feature = "polars")]
4562 #[test]
4563 fn test_samples_dataframe_score_one_survives() {
4564 let mut ann = Annotation::new();
4565 ann.set_name(Some("test".to_string()));
4566 ann.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
4567 ann.set_box2d_score(Some(1.0));
4568
4569 let sample = Sample {
4570 image_name: Some("test.jpg".to_string()),
4571 annotations: vec![ann],
4572 ..Default::default()
4573 };
4574
4575 let df = samples_dataframe(&[sample]).unwrap();
4576
4577 let scores = df.column("box2d_score").unwrap();
4578 let val = scores.f32().unwrap().get(0);
4579 assert_eq!(val, Some(1.0), "score of 1.0 should survive as non-null");
4580 }
4581}
4582
4583#[cfg(test)]
4584mod versioning_deser_tests {
4585 use super::*;
4586
4587 #[test]
4588 fn test_annotation_set_deserializes_from_tag_scoped_response() {
4589 let json = r#"{"id": 42, "name": "Default", "description": "Default set"}"#;
4592 let result: Result<AnnotationSet, _> = serde_json::from_str(json);
4593 assert!(
4594 result.is_ok(),
4595 "tag-scoped annotation set response must deserialize: {:?}",
4596 result.err()
4597 );
4598 let annset = result.unwrap();
4599 assert_eq!(annset.name(), "Default");
4600 assert_eq!(annset.description(), "Default set");
4601 assert_eq!(annset.created(), None);
4602 }
4603
4604 #[test]
4605 fn test_annotation_set_deserializes_from_head_response() {
4606 let json = r#"{"id": 42, "dataset_id": 1, "name": "Default", "description": "Default set", "date": "2026-01-01T00:00:00Z"}"#;
4613 let result: Result<AnnotationSet, _> = serde_json::from_str(json);
4614 assert!(
4615 result.is_ok(),
4616 "HEAD annotation set response must deserialize: {:?}",
4617 result.err()
4618 );
4619 let annset = result.unwrap();
4620 assert!(annset.created().is_some());
4621 }
4622
4623 #[test]
4624 fn test_label_deserializes_from_tag_scoped_response() {
4625 let json = r#"{"id": 7, "name": "circle", "index": 0, "color": 16711680}"#;
4628 let result: Result<Label, _> = serde_json::from_str(json);
4629 assert!(
4630 result.is_ok(),
4631 "tag-scoped label response must deserialize: {:?}",
4632 result.err()
4633 );
4634 let label = result.unwrap();
4635 assert_eq!(label.name(), "circle");
4636 assert_eq!(label.color(), Some(16711680));
4637 assert_eq!(label.dataset_id(), None);
4638 }
4639
4640 #[test]
4641 fn test_label_deserializes_from_head_response() {
4642 let json = r#"{"id": 7, "dataset_id": 1, "name": "circle", "index": 0}"#;
4646 let result: Result<Label, _> = serde_json::from_str(json);
4647 assert!(
4648 result.is_ok(),
4649 "HEAD label response must deserialize: {:?}",
4650 result.err()
4651 );
4652 let label = result.unwrap();
4653 assert!(label.dataset_id().is_some());
4654 assert_eq!(label.color(), None);
4655 }
4656
4657 #[test]
4658 fn test_dataset_deserializes_tag_fields() {
4659 let json = r#"{
4664 "id": 1, "project_id": 1, "name": "My Dataset",
4665 "description": "", "cloud_key": "k", "createdAt": "2026-01-01T00:00:00Z",
4666 "tag_id": 42, "tag": "v1.0", "tag_description": "Release candidate"
4667 }"#;
4668 let dataset: Dataset = serde_json::from_str(json).unwrap();
4669 assert_eq!(dataset.tag_id(), Some(42));
4670 assert_eq!(dataset.tag(), "v1.0");
4671 assert_eq!(dataset.tag_description(), "Release candidate");
4672 }
4673
4674 #[test]
4675 fn test_dataset_deserializes_without_tag_fields() {
4676 let json = r#"{
4679 "id": 1, "project_id": 1, "name": "My Dataset",
4680 "description": "", "cloud_key": "k", "createdAt": "2026-01-01T00:00:00Z"
4681 }"#;
4682 let dataset: Dataset = serde_json::from_str(json).unwrap();
4683 assert_eq!(dataset.tag_id(), None);
4684 assert_eq!(dataset.tag(), "");
4685 assert_eq!(dataset.tag_description(), "");
4686 }
4687}