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 return Ok(None);
1046 }
1047
1048 let file = resolve_file(&file_type, &self.files);
1050
1051 match file {
1052 Some(f) => {
1053 if let Some(url) = f.url() {
1055 return Ok(Some(client.download(url).await?));
1056 }
1057
1058 if let Some(data) = f.data() {
1060 let decoded = if let Ok(bytes) = STANDARD.decode(data) {
1067 if let Ok(text) = String::from_utf8(bytes.clone()) {
1069 if text.starts_with('{') {
1070 text
1072 } else {
1073 return Ok(Some(bytes));
1075 }
1076 } else {
1077 return Ok(Some(bytes));
1079 }
1080 } else {
1081 data.to_string()
1083 };
1084
1085 let content = if decoded.starts_with('{') {
1087 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&decoded) {
1088 if let Some(obj) = json.as_object() {
1089 obj.values()
1090 .next()
1091 .and_then(|v| v.as_str())
1092 .map(|s| s.to_string())
1093 .unwrap_or(decoded)
1094 } else {
1095 decoded
1096 }
1097 } else {
1098 decoded
1099 }
1100 } else {
1101 decoded
1102 };
1103
1104 return Ok(Some(content.as_bytes().to_vec()));
1105 }
1106
1107 Ok(None)
1108 }
1109 None => Ok(None),
1110 }
1111 }
1112}
1113
1114#[derive(Serialize, Deserialize, Clone, Debug)]
1122pub struct SampleFile {
1123 r#type: String,
1124 #[serde(skip_serializing_if = "Option::is_none")]
1125 url: Option<String>,
1126 #[serde(skip_serializing_if = "Option::is_none")]
1127 filename: Option<String>,
1128 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1130 data: Option<String>,
1131 #[serde(skip)]
1134 bytes: Option<Vec<u8>>,
1135}
1136
1137impl SampleFile {
1138 pub fn with_url(file_type: String, url: String) -> Self {
1140 Self {
1141 r#type: file_type,
1142 url: Some(url),
1143 filename: None,
1144 data: None,
1145 bytes: None,
1146 }
1147 }
1148
1149 pub fn with_filename(file_type: String, filename: String) -> Self {
1151 Self {
1152 r#type: file_type,
1153 url: None,
1154 filename: Some(filename),
1155 data: None,
1156 bytes: None,
1157 }
1158 }
1159
1160 pub fn with_data(file_type: String, data: String) -> Self {
1162 Self {
1163 r#type: file_type,
1164 url: None,
1165 filename: None,
1166 data: Some(data),
1167 bytes: None,
1168 }
1169 }
1170
1171 pub fn with_bytes(file_type: String, filename: String, bytes: Vec<u8>) -> Self {
1181 Self {
1182 r#type: file_type,
1183 url: None,
1184 filename: Some(filename),
1185 data: None,
1186 bytes: Some(bytes),
1187 }
1188 }
1189
1190 pub fn file_type(&self) -> &str {
1191 &self.r#type
1192 }
1193
1194 pub fn url(&self) -> Option<&str> {
1195 self.url.as_deref()
1196 }
1197
1198 pub fn filename(&self) -> Option<&str> {
1199 self.filename.as_deref()
1200 }
1201
1202 pub fn data(&self) -> Option<&str> {
1204 self.data.as_deref()
1205 }
1206
1207 pub fn bytes(&self) -> Option<&[u8]> {
1209 self.bytes.as_deref()
1210 }
1211}
1212
1213#[derive(Serialize, Deserialize, Clone, Debug)]
1218pub struct Location {
1219 #[serde(skip_serializing_if = "Option::is_none")]
1220 pub gps: Option<GpsData>,
1221 #[serde(skip_serializing_if = "Option::is_none")]
1222 pub imu: Option<ImuData>,
1223}
1224
1225#[derive(Serialize, Deserialize, Clone, Debug)]
1227pub struct GpsData {
1228 pub lat: f64,
1229 pub lon: f64,
1230}
1231
1232impl GpsData {
1233 pub fn validate(&self) -> Result<(), String> {
1263 validate_gps_coordinates(self.lat, self.lon)
1264 }
1265}
1266
1267#[derive(Serialize, Deserialize, Clone, Debug)]
1269pub struct ImuData {
1270 pub roll: f64,
1271 pub pitch: f64,
1272 pub yaw: f64,
1273}
1274
1275impl ImuData {
1276 pub fn validate(&self) -> Result<(), String> {
1309 validate_imu_orientation(self.roll, self.pitch, self.yaw)
1310 }
1311}
1312
1313#[allow(dead_code)]
1314pub trait TypeName {
1315 fn type_name() -> String;
1316}
1317
1318#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
1319pub struct Box3d {
1320 x: f32,
1321 y: f32,
1322 z: f32,
1323 w: f32,
1324 h: f32,
1325 l: f32,
1326}
1327
1328impl TypeName for Box3d {
1329 fn type_name() -> String {
1330 "box3d".to_owned()
1331 }
1332}
1333
1334impl Box3d {
1335 pub fn new(cx: f32, cy: f32, cz: f32, width: f32, height: f32, length: f32) -> Self {
1336 Self {
1337 x: cx,
1338 y: cy,
1339 z: cz,
1340 w: width,
1341 h: height,
1342 l: length,
1343 }
1344 }
1345
1346 pub fn width(&self) -> f32 {
1347 self.w
1348 }
1349
1350 pub fn height(&self) -> f32 {
1351 self.h
1352 }
1353
1354 pub fn length(&self) -> f32 {
1355 self.l
1356 }
1357
1358 pub fn cx(&self) -> f32 {
1359 self.x
1360 }
1361
1362 pub fn cy(&self) -> f32 {
1363 self.y
1364 }
1365
1366 pub fn cz(&self) -> f32 {
1367 self.z
1368 }
1369
1370 pub fn left(&self) -> f32 {
1371 self.x - self.w / 2.0
1372 }
1373
1374 pub fn top(&self) -> f32 {
1375 self.y - self.h / 2.0
1376 }
1377
1378 pub fn front(&self) -> f32 {
1379 self.z - self.l / 2.0
1380 }
1381}
1382
1383#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
1384pub struct Box2d {
1385 h: f32,
1386 w: f32,
1387 x: f32,
1388 y: f32,
1389}
1390
1391impl TypeName for Box2d {
1392 fn type_name() -> String {
1393 "box2d".to_owned()
1394 }
1395}
1396
1397impl Box2d {
1398 pub fn new(left: f32, top: f32, width: f32, height: f32) -> Self {
1399 Self {
1400 x: left,
1401 y: top,
1402 w: width,
1403 h: height,
1404 }
1405 }
1406
1407 pub fn width(&self) -> f32 {
1408 self.w
1409 }
1410
1411 pub fn height(&self) -> f32 {
1412 self.h
1413 }
1414
1415 pub fn left(&self) -> f32 {
1416 self.x
1417 }
1418
1419 pub fn top(&self) -> f32 {
1420 self.y
1421 }
1422
1423 pub fn cx(&self) -> f32 {
1424 self.x + self.w / 2.0
1425 }
1426
1427 pub fn cy(&self) -> f32 {
1428 self.y + self.h / 2.0
1429 }
1430}
1431
1432#[derive(Clone, Debug, PartialEq)]
1433pub struct Polygon {
1434 pub rings: Vec<Vec<(f32, f32)>>,
1435}
1436
1437impl TypeName for Polygon {
1438 fn type_name() -> String {
1439 "polygon".to_owned()
1440 }
1441}
1442
1443impl Polygon {
1444 pub fn new(rings: Vec<Vec<(f32, f32)>>) -> Self {
1445 Self { rings }
1446 }
1447}
1448
1449impl serde::Serialize for Polygon {
1450 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1451 where
1452 S: serde::Serializer,
1453 {
1454 serde::Serialize::serialize(&self.rings, serializer)
1455 }
1456}
1457
1458impl<'de> serde::Deserialize<'de> for Polygon {
1459 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1460 where
1461 D: serde::Deserializer<'de>,
1462 {
1463 let value = serde_json::Value::deserialize(deserializer)?;
1465
1466 let polygon_value = if let Some(obj) = value.as_object() {
1468 obj.get("rings")
1470 .or_else(|| obj.get("polygon"))
1471 .cloned()
1472 .unwrap_or(serde_json::Value::Null)
1473 } else {
1474 value
1476 };
1477
1478 let rings = parse_polygon_value(&polygon_value);
1480
1481 Ok(Self { rings })
1482 }
1483}
1484
1485fn parse_polygon_value(value: &serde_json::Value) -> Vec<Vec<(f32, f32)>> {
1493 let Some(outer_array) = value.as_array() else {
1494 return vec![];
1495 };
1496
1497 let mut result = Vec::new();
1498
1499 for ring in outer_array {
1500 let Some(ring_array) = ring.as_array() else {
1501 continue;
1502 };
1503
1504 let is_3d = ring_array
1506 .first()
1507 .map(|first| first.is_array())
1508 .unwrap_or(false);
1509
1510 let points: Vec<(f32, f32)> = if is_3d {
1511 ring_array
1513 .iter()
1514 .filter_map(|point| {
1515 let arr = point.as_array()?;
1516 if arr.len() >= 2 {
1517 let x = arr[0].as_f64()? as f32;
1518 let y = arr[1].as_f64()? as f32;
1519 if x.is_finite() && y.is_finite() {
1520 Some((x, y))
1521 } else {
1522 None
1523 }
1524 } else {
1525 None
1526 }
1527 })
1528 .collect()
1529 } else {
1530 ring_array
1532 .chunks(2)
1533 .filter_map(|chunk| {
1534 if chunk.len() >= 2 {
1535 let x = chunk[0].as_f64()? as f32;
1536 let y = chunk[1].as_f64()? as f32;
1537 if x.is_finite() && y.is_finite() {
1538 Some((x, y))
1539 } else {
1540 None
1541 }
1542 } else {
1543 None
1544 }
1545 })
1546 .collect()
1547 };
1548
1549 if points.len() >= 3 {
1551 result.push(points);
1552 }
1553 }
1554
1555 result
1556}
1557
1558#[derive(Deserialize)]
1563struct AnnotationRaw {
1564 #[serde(default)]
1565 sample_id: Option<SampleID>,
1566 #[serde(default)]
1567 name: Option<String>,
1568 #[serde(default)]
1569 sequence_name: Option<String>,
1570 #[serde(default)]
1571 frame_number: Option<u32>,
1572 #[serde(rename = "group_name", default)]
1573 group: Option<String>,
1574 #[serde(rename = "object_reference", alias = "object_id", default)]
1575 object_id: Option<String>,
1576 #[serde(default)]
1577 label_name: Option<String>,
1578 #[serde(default)]
1579 label_index: Option<u64>,
1580 #[serde(default)]
1581 iscrowd: Option<bool>,
1582 #[serde(default)]
1583 category_frequency: Option<String>,
1584 #[serde(default)]
1586 box2d: Option<Box2d>,
1587 #[serde(default)]
1588 box3d: Option<Box3d>,
1589 #[serde(default, alias = "mask")]
1590 polygon: Option<Polygon>,
1591 #[serde(default)]
1593 x: Option<f64>,
1594 #[serde(default)]
1595 y: Option<f64>,
1596 #[serde(default)]
1597 w: Option<f64>,
1598 #[serde(default)]
1599 h: Option<f64>,
1600}
1601
1602#[derive(Serialize, Clone, Debug)]
1603pub struct Annotation {
1604 #[serde(skip_serializing_if = "Option::is_none")]
1605 sample_id: Option<SampleID>,
1606 #[serde(skip_serializing_if = "Option::is_none")]
1607 name: Option<String>,
1608 #[serde(skip_serializing_if = "Option::is_none")]
1609 sequence_name: Option<String>,
1610 #[serde(skip_serializing_if = "Option::is_none")]
1611 frame_number: Option<u32>,
1612 #[serde(rename = "group_name", skip_serializing_if = "Option::is_none")]
1616 group: Option<String>,
1617 #[serde(
1621 rename = "object_reference",
1622 alias = "object_id",
1623 skip_serializing_if = "Option::is_none"
1624 )]
1625 object_id: Option<String>,
1626 #[serde(skip_serializing_if = "Option::is_none")]
1627 label_name: Option<String>,
1628 #[serde(skip_serializing_if = "Option::is_none")]
1629 label_index: Option<u64>,
1630 #[serde(default, skip_serializing_if = "Option::is_none")]
1632 iscrowd: Option<bool>,
1633 #[serde(default, skip_serializing_if = "Option::is_none")]
1635 category_frequency: Option<String>,
1636 #[serde(skip_serializing_if = "Option::is_none")]
1637 box2d: Option<Box2d>,
1638 #[serde(skip_serializing_if = "Option::is_none")]
1639 box3d: Option<Box3d>,
1640 #[serde(rename(serialize = "mask"), skip_serializing_if = "Option::is_none")]
1649 polygon: Option<Polygon>,
1650 #[serde(skip)]
1652 mask: Option<MaskData>,
1653 #[serde(skip_serializing_if = "Option::is_none")]
1655 box2d_score: Option<f32>,
1656 #[serde(skip_serializing_if = "Option::is_none")]
1658 box3d_score: Option<f32>,
1659 #[serde(skip_serializing_if = "Option::is_none")]
1661 polygon_score: Option<f32>,
1662 #[serde(skip_serializing_if = "Option::is_none")]
1664 mask_score: Option<f32>,
1665}
1666
1667impl<'de> serde::Deserialize<'de> for Annotation {
1668 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1669 where
1670 D: serde::Deserializer<'de>,
1671 {
1672 let raw: AnnotationRaw = serde::Deserialize::deserialize(deserializer)?;
1674
1675 let box2d = raw.box2d.or_else(|| match (raw.x, raw.y, raw.w, raw.h) {
1677 (Some(x), Some(y), Some(w), Some(h)) if w > 0.0 && h > 0.0 => {
1678 Some(Box2d::new(x as f32, y as f32, w as f32, h as f32))
1679 }
1680 _ => None,
1681 });
1682
1683 Ok(Annotation {
1684 sample_id: raw.sample_id,
1685 name: raw.name,
1686 sequence_name: raw.sequence_name,
1687 frame_number: raw.frame_number,
1688 group: raw.group,
1689 object_id: raw.object_id,
1690 label_name: raw.label_name,
1691 label_index: raw.label_index,
1692 iscrowd: raw.iscrowd,
1693 category_frequency: raw.category_frequency,
1694 box2d,
1695 box3d: raw.box3d,
1696 polygon: raw.polygon,
1697 mask: None,
1698 box2d_score: None,
1699 box3d_score: None,
1700 polygon_score: None,
1701 mask_score: None,
1702 })
1703 }
1704}
1705
1706impl Default for Annotation {
1707 fn default() -> Self {
1708 Self::new()
1709 }
1710}
1711
1712impl Annotation {
1713 pub fn new() -> Self {
1714 Self {
1715 sample_id: None,
1716 name: None,
1717 sequence_name: None,
1718 frame_number: None,
1719 group: None,
1720 object_id: None,
1721 label_name: None,
1722 label_index: None,
1723 iscrowd: None,
1724 category_frequency: None,
1725 box2d: None,
1726 box3d: None,
1727 polygon: None,
1728 mask: None,
1729 box2d_score: None,
1730 box3d_score: None,
1731 polygon_score: None,
1732 mask_score: None,
1733 }
1734 }
1735
1736 pub fn set_sample_id(&mut self, sample_id: Option<SampleID>) {
1737 self.sample_id = sample_id;
1738 }
1739
1740 pub fn sample_id(&self) -> Option<SampleID> {
1741 self.sample_id
1742 }
1743
1744 pub fn set_name(&mut self, name: Option<String>) {
1745 self.name = name;
1746 }
1747
1748 pub fn name(&self) -> Option<&String> {
1749 self.name.as_ref()
1750 }
1751
1752 pub fn set_sequence_name(&mut self, sequence_name: Option<String>) {
1753 self.sequence_name = sequence_name;
1754 }
1755
1756 pub fn sequence_name(&self) -> Option<&String> {
1757 self.sequence_name.as_ref()
1758 }
1759
1760 pub fn set_frame_number(&mut self, frame_number: Option<u32>) {
1761 self.frame_number = frame_number;
1762 }
1763
1764 pub fn frame_number(&self) -> Option<u32> {
1765 self.frame_number
1766 }
1767
1768 pub fn set_group(&mut self, group: Option<String>) {
1769 self.group = group;
1770 }
1771
1772 pub fn group(&self) -> Option<&String> {
1773 self.group.as_ref()
1774 }
1775
1776 pub fn object_id(&self) -> Option<&String> {
1777 self.object_id.as_ref()
1778 }
1779
1780 pub fn set_object_id(&mut self, object_id: Option<String>) {
1781 self.object_id = object_id;
1782 }
1783
1784 pub fn label(&self) -> Option<&String> {
1785 self.label_name.as_ref()
1786 }
1787
1788 pub fn set_label(&mut self, label_name: Option<String>) {
1789 self.label_name = label_name;
1790 }
1791
1792 pub fn label_index(&self) -> Option<u64> {
1793 self.label_index
1794 }
1795
1796 pub fn set_label_index(&mut self, label_index: Option<u64>) {
1797 self.label_index = label_index;
1798 }
1799
1800 pub fn iscrowd(&self) -> Option<bool> {
1801 self.iscrowd
1802 }
1803
1804 pub fn set_iscrowd(&mut self, iscrowd: Option<bool>) {
1805 self.iscrowd = iscrowd;
1806 }
1807
1808 pub fn category_frequency(&self) -> Option<&String> {
1809 self.category_frequency.as_ref()
1810 }
1811
1812 pub fn set_category_frequency(&mut self, category_frequency: Option<String>) {
1813 self.category_frequency = category_frequency;
1814 }
1815
1816 pub fn box2d(&self) -> Option<&Box2d> {
1817 self.box2d.as_ref()
1818 }
1819
1820 pub fn set_box2d(&mut self, box2d: Option<Box2d>) {
1821 self.box2d = box2d;
1822 }
1823
1824 pub fn box3d(&self) -> Option<&Box3d> {
1825 self.box3d.as_ref()
1826 }
1827
1828 pub fn set_box3d(&mut self, box3d: Option<Box3d>) {
1829 self.box3d = box3d;
1830 }
1831
1832 pub fn polygon(&self) -> Option<&Polygon> {
1833 self.polygon.as_ref()
1834 }
1835
1836 pub fn set_polygon(&mut self, polygon: Option<Polygon>) {
1837 self.polygon = polygon;
1838 }
1839
1840 pub fn mask(&self) -> Option<&MaskData> {
1841 self.mask.as_ref()
1842 }
1843
1844 pub fn set_mask(&mut self, mask: Option<MaskData>) {
1845 self.mask = mask;
1846 }
1847
1848 pub fn box2d_score(&self) -> Option<f32> {
1849 self.box2d_score
1850 }
1851
1852 pub fn set_box2d_score(&mut self, score: Option<f32>) {
1853 self.box2d_score = score;
1854 }
1855
1856 pub fn box3d_score(&self) -> Option<f32> {
1857 self.box3d_score
1858 }
1859
1860 pub fn set_box3d_score(&mut self, score: Option<f32>) {
1861 self.box3d_score = score;
1862 }
1863
1864 pub fn polygon_score(&self) -> Option<f32> {
1865 self.polygon_score
1866 }
1867
1868 pub fn set_polygon_score(&mut self, score: Option<f32>) {
1869 self.polygon_score = score;
1870 }
1871
1872 pub fn mask_score(&self) -> Option<f32> {
1873 self.mask_score
1874 }
1875
1876 pub fn set_mask_score(&mut self, score: Option<f32>) {
1877 self.mask_score = score;
1878 }
1879}
1880
1881#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
1890pub struct Label {
1891 id: u64,
1892 #[serde(default)]
1893 dataset_id: Option<DatasetID>,
1894 index: u64,
1895 name: String,
1896 #[serde(default)]
1897 color: Option<u64>,
1898}
1899
1900impl Label {
1901 pub fn id(&self) -> u64 {
1902 self.id
1903 }
1904
1905 pub fn dataset_id(&self) -> Option<DatasetID> {
1910 self.dataset_id
1911 }
1912
1913 pub(crate) fn backfill_dataset_id(&mut self, dataset_id: DatasetID) {
1917 if self.dataset_id.is_none() {
1918 self.dataset_id = Some(dataset_id);
1919 }
1920 }
1921
1922 pub fn index(&self) -> u64 {
1923 self.index
1924 }
1925
1926 pub fn name(&self) -> &str {
1927 &self.name
1928 }
1929
1930 pub fn color(&self) -> Option<u64> {
1933 self.color
1934 }
1935
1936 pub async fn remove(&self, client: &Client) -> Result<(), Error> {
1937 client.remove_label(self.id()).await
1938 }
1939
1940 pub async fn set_name(&mut self, client: &Client, name: &str) -> Result<(), Error> {
1941 self.name = name.to_string();
1942 client.update_label(self).await
1943 }
1944
1945 pub async fn set_index(&mut self, client: &Client, index: u64) -> Result<(), Error> {
1946 self.index = index;
1947 client.update_label(self).await
1948 }
1949}
1950
1951impl Display for Label {
1952 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1953 write!(f, "{}", self.name())
1954 }
1955}
1956
1957#[derive(Serialize, Clone, Debug)]
1958pub struct NewLabelObject {
1959 pub name: String,
1960}
1961
1962#[derive(Serialize, Clone, Debug)]
1963pub struct NewLabel {
1964 pub dataset_id: DatasetID,
1965 pub labels: Vec<NewLabelObject>,
1966}
1967
1968#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
1998pub struct Group {
1999 pub id: u64,
2004
2005 pub name: String,
2009}
2010
2011#[cfg(feature = "polars")]
2012fn extract_annotation_name(ann: &Annotation) -> Option<(String, Option<u32>)> {
2013 use std::path::Path;
2014
2015 let name = ann.name.as_ref()?;
2016 let name = Path::new(name).file_stem()?.to_str()?;
2017
2018 match &ann.sequence_name {
2021 Some(sequence) => Some((sequence.clone(), ann.frame_number)),
2022 None => Some((name.to_string(), None)),
2023 }
2024}
2025
2026#[cfg(feature = "polars")]
2030fn convert_polygon_to_nested_series(polygon: &Polygon) -> Series {
2031 let ring_series: Vec<Option<Series>> = polygon
2032 .rings
2033 .iter()
2034 .map(|ring| {
2035 let coords: Vec<f32> = ring.iter().flat_map(|&(x, y)| [x, y]).collect();
2036 Some(Series::new("".into(), coords))
2037 })
2038 .collect();
2039 Series::new("".into(), ring_series)
2040}
2041
2042#[cfg(feature = "polars")]
2092pub fn samples_dataframe(samples: &[Sample]) -> Result<DataFrame, Error> {
2093 let mut names: Vec<String> = Vec::new();
2095 let mut frames: Vec<Option<u32>> = Vec::new();
2096 let mut objects: Vec<Option<String>> = Vec::new();
2097 let mut labels: Vec<Option<String>> = Vec::new();
2098 let mut label_indices: Vec<Option<u64>> = Vec::new();
2099 let mut groups: Vec<Option<String>> = Vec::new();
2100 let mut polygons: Vec<Option<Series>> = Vec::new();
2101 let mut boxes2d: Vec<Option<Series>> = Vec::new();
2102 let mut boxes3d: Vec<Option<Series>> = Vec::new();
2103 let mut mask_bytes: Vec<Option<Vec<u8>>> = Vec::new();
2104 let mut box2d_scores: Vec<Option<f32>> = Vec::new();
2105 let mut box3d_scores: Vec<Option<f32>> = Vec::new();
2106 let mut polygon_scores: Vec<Option<f32>> = Vec::new();
2107 let mut mask_scores: Vec<Option<f32>> = Vec::new();
2108 let mut sizes: Vec<Option<Vec<u32>>> = Vec::new();
2109 let mut locations: Vec<Option<Vec<f32>>> = Vec::new();
2110 let mut poses: Vec<Option<Vec<f32>>> = Vec::new();
2111 let mut degradations: Vec<Option<String>> = Vec::new();
2112 let mut iscrowds: Vec<Option<bool>> = Vec::new();
2113 let mut category_frequencies: Vec<Option<String>> = Vec::new();
2114 let mut neg_label_indices_vec: Vec<Option<Vec<u32>>> = Vec::new();
2115 let mut not_exhaustive_label_indices_vec: Vec<Option<Vec<u32>>> = Vec::new();
2116 let mut timing_load: Vec<Option<i64>> = Vec::new();
2117 let mut timing_preprocess: Vec<Option<i64>> = Vec::new();
2118 let mut timing_inference: Vec<Option<i64>> = Vec::new();
2119 let mut timing_decode: Vec<Option<i64>> = Vec::new();
2120
2121 for sample in samples {
2122 let size = match (sample.width, sample.height) {
2124 (Some(w), Some(h)) => Some(vec![w, h]),
2125 _ => None,
2126 };
2127
2128 let location = sample.location.as_ref().and_then(|loc| {
2129 loc.gps
2130 .as_ref()
2131 .map(|gps| vec![gps.lat as f32, gps.lon as f32])
2132 });
2133
2134 let pose = sample.location.as_ref().and_then(|loc| {
2135 loc.imu
2136 .as_ref()
2137 .map(|imu| vec![imu.yaw as f32, imu.pitch as f32, imu.roll as f32])
2138 });
2139
2140 let degradation = sample.degradation.clone();
2141
2142 let t_load = sample.timing.as_ref().and_then(|t| t.load);
2144 let t_preprocess = sample.timing.as_ref().and_then(|t| t.preprocess);
2145 let t_inference = sample.timing.as_ref().and_then(|t| t.inference);
2146 let t_decode = sample.timing.as_ref().and_then(|t| t.decode);
2147
2148 macro_rules! push_sample_fields {
2150 () => {
2151 sizes.push(size.clone());
2152 locations.push(location.clone());
2153 poses.push(pose.clone());
2154 degradations.push(degradation.clone());
2155 neg_label_indices_vec.push(sample.neg_label_indices.clone());
2156 not_exhaustive_label_indices_vec.push(sample.not_exhaustive_label_indices.clone());
2157 timing_load.push(t_load);
2158 timing_preprocess.push(t_preprocess);
2159 timing_inference.push(t_inference);
2160 timing_decode.push(t_decode);
2161 };
2162 }
2163
2164 if sample.annotations.is_empty() {
2165 let (name, frame) = match extract_annotation_name_from_sample(sample) {
2167 Some(nf) => nf,
2168 None => continue,
2169 };
2170
2171 names.push(name);
2172 frames.push(frame);
2173 objects.push(None);
2174 labels.push(None);
2175 label_indices.push(None);
2176 groups.push(sample.group.clone());
2177 polygons.push(None);
2178 boxes2d.push(None);
2179 boxes3d.push(None);
2180 mask_bytes.push(None);
2181 box2d_scores.push(None);
2182 box3d_scores.push(None);
2183 polygon_scores.push(None);
2184 mask_scores.push(None);
2185 iscrowds.push(None);
2186 category_frequencies.push(None);
2187 push_sample_fields!();
2188 } else {
2189 for ann in &sample.annotations {
2191 let (name, frame) = match extract_annotation_name(ann) {
2192 Some(nf) => nf,
2193 None => continue,
2194 };
2195
2196 let polygon = ann.polygon.as_ref().map(convert_polygon_to_nested_series);
2197
2198 let box2d = ann
2199 .box2d
2200 .as_ref()
2201 .map(|b| Series::new("box2d".into(), [b.cx(), b.cy(), b.width(), b.height()]));
2202
2203 let box3d = ann
2204 .box3d
2205 .as_ref()
2206 .map(|b| Series::new("box3d".into(), [b.x, b.y, b.z, b.w, b.h, b.l]));
2207
2208 names.push(name);
2209 frames.push(frame);
2210 objects.push(ann.object_id().cloned());
2211 labels.push(ann.label_name.clone());
2212 label_indices.push(ann.label_index);
2213 groups.push(sample.group.clone());
2214 polygons.push(polygon);
2215 boxes2d.push(box2d);
2216 boxes3d.push(box3d);
2217 mask_bytes.push(ann.mask.as_ref().map(|m| m.as_bytes().to_vec()));
2218 box2d_scores.push(ann.box2d_score());
2219 box3d_scores.push(ann.box3d_score());
2220 polygon_scores.push(ann.polygon_score());
2221 mask_scores.push(ann.mask_score());
2222 iscrowds.push(ann.iscrowd);
2223 category_frequencies.push(ann.category_frequency.clone());
2224 push_sample_fields!();
2225 }
2226 }
2227 }
2228
2229 let names_col: Column = Series::new("name".into(), names).into();
2231 let frames_col: Column = Series::new("frame".into(), frames).into();
2232 let objects_col: Column = Series::new("object_id".into(), objects).into();
2233
2234 let labels_col: Column = Series::new("label".into(), labels)
2240 .cast(&DataType::Categorical(
2241 Categories::new("labels".into(), "labels".into(), CategoricalPhysical::U16),
2242 Arc::new(CategoricalMapping::with_hasher(
2243 u16::MAX as usize,
2244 Default::default(),
2245 )),
2246 ))?
2247 .into();
2248
2249 let label_indices_col: Column = Series::new("label_index".into(), label_indices).into();
2250
2251 let groups_col: Column = Series::new("group".into(), groups)
2253 .cast(&DataType::Categorical(
2254 Categories::new("groups".into(), "groups".into(), CategoricalPhysical::U8),
2255 Arc::new(CategoricalMapping::with_hasher(
2256 u8::MAX as usize,
2257 Default::default(),
2258 )),
2259 ))?
2260 .into();
2261
2262 let polygons_col: Column = if polygons.iter().all(|p| p.is_none()) {
2267 Series::new_null("polygon".into(), polygons.len()).into()
2269 } else {
2270 let typed_polygons: Vec<Option<Series>> = polygons
2273 .into_iter()
2274 .map(|opt| {
2275 opt.map(|s| {
2276 s.cast(&DataType::List(Box::new(DataType::Float32)))
2277 .unwrap_or(s)
2278 })
2279 })
2280 .collect();
2281 Series::new("polygon".into(), &typed_polygons)
2282 .cast(&DataType::List(Box::new(DataType::List(Box::new(
2283 DataType::Float32,
2284 )))))?
2285 .into()
2286 };
2287
2288 let boxes2d_col: Column = Series::new("box2d".into(), boxes2d)
2289 .cast(&DataType::Array(Box::new(DataType::Float32), 4))?
2290 .into();
2291 let boxes3d_col: Column = Series::new("box3d".into(), boxes3d)
2292 .cast(&DataType::Array(Box::new(DataType::Float32), 6))?
2293 .into();
2294
2295 let mask_col: Column = Series::new("mask".into(), mask_bytes).into();
2297
2298 let box2d_score_col: Column = Series::new("box2d_score".into(), box2d_scores).into();
2300 let box3d_score_col: Column = Series::new("box3d_score".into(), box3d_scores).into();
2301 let polygon_score_col: Column = Series::new("polygon_score".into(), polygon_scores).into();
2302 let mask_score_col: Column = Series::new("mask_score".into(), mask_scores).into();
2303
2304 let size_series: Vec<Option<Series>> = sizes
2306 .into_iter()
2307 .map(|opt_vec| opt_vec.map(|vec| Series::new("size".into(), vec)))
2308 .collect();
2309 let sizes_col: Column = Series::new("size".into(), size_series)
2310 .cast(&DataType::Array(Box::new(DataType::UInt32), 2))?
2311 .into();
2312
2313 let location_series: Vec<Option<Series>> = locations
2314 .into_iter()
2315 .map(|opt_vec| opt_vec.map(|vec| Series::new("location".into(), vec)))
2316 .collect();
2317 let locations_col: Column = Series::new("location".into(), location_series)
2318 .cast(&DataType::Array(Box::new(DataType::Float32), 2))?
2319 .into();
2320
2321 let pose_series: Vec<Option<Series>> = poses
2322 .into_iter()
2323 .map(|opt_vec| opt_vec.map(|vec| Series::new("pose".into(), vec)))
2324 .collect();
2325 let poses_col: Column = Series::new("pose".into(), pose_series)
2326 .cast(&DataType::Array(Box::new(DataType::Float32), 3))?
2327 .into();
2328
2329 let degradations_col: Column = Series::new("degradation".into(), degradations).into();
2330
2331 let iscrowds_col: Column = Series::new("iscrowd".into(), iscrowds).into();
2333
2334 let category_frequencies_col: Column =
2335 Series::new("category_frequency".into(), category_frequencies)
2336 .cast(&DataType::Categorical(
2337 Categories::new(
2338 "cat_freq".into(),
2339 "cat_freq".into(),
2340 CategoricalPhysical::U8,
2341 ),
2342 Arc::new(CategoricalMapping::with_hasher(
2343 u8::MAX as usize,
2344 Default::default(),
2345 )),
2346 ))?
2347 .into();
2348
2349 let neg_label_indices_series: Vec<Option<Series>> = neg_label_indices_vec
2350 .into_iter()
2351 .map(|opt_vec| opt_vec.map(|vec| Series::new("neg_label_indices".into(), vec)))
2352 .collect();
2353 let neg_label_indices_col: Column =
2354 Series::new("neg_label_indices".into(), neg_label_indices_series)
2355 .cast(&DataType::List(Box::new(DataType::UInt32)))?
2356 .into();
2357
2358 let not_exhaustive_label_indices_series: Vec<Option<Series>> = not_exhaustive_label_indices_vec
2359 .into_iter()
2360 .map(|opt_vec| opt_vec.map(|vec| Series::new("not_exhaustive_label_indices".into(), vec)))
2361 .collect();
2362 let not_exhaustive_label_indices_col: Column = Series::new(
2363 "not_exhaustive_label_indices".into(),
2364 not_exhaustive_label_indices_series,
2365 )
2366 .cast(&DataType::List(Box::new(DataType::UInt32)))?
2367 .into();
2368
2369 let timing_col: Column = StructChunked::from_series(
2371 "timing".into(),
2372 frames_col.len(),
2373 [
2374 Series::new("load".into(), &timing_load),
2375 Series::new("preprocess".into(), &timing_preprocess),
2376 Series::new("inference".into(), &timing_inference),
2377 Series::new("decode".into(), &timing_decode),
2378 ]
2379 .iter(),
2380 )?
2381 .into_series()
2382 .into();
2383
2384 let all_columns: Vec<Column> = vec![
2386 names_col,
2387 frames_col,
2388 objects_col,
2389 labels_col,
2390 label_indices_col,
2391 groups_col,
2392 polygons_col,
2393 boxes2d_col,
2394 boxes3d_col,
2395 mask_col,
2396 box2d_score_col,
2397 box3d_score_col,
2398 polygon_score_col,
2399 mask_score_col,
2400 sizes_col,
2401 locations_col,
2402 poses_col,
2403 degradations_col,
2404 iscrowds_col,
2405 category_frequencies_col,
2406 neg_label_indices_col,
2407 not_exhaustive_label_indices_col,
2408 timing_col,
2409 ];
2410
2411 let height = all_columns.first().map(|c| c.len()).unwrap_or(0);
2412
2413 let non_empty_columns: Vec<Column> = all_columns
2414 .into_iter()
2415 .filter(|col| col.name() == "name" || !is_all_null_column(col))
2416 .collect();
2417
2418 Ok(DataFrame::new(height, non_empty_columns)?)
2419}
2420
2421#[cfg(feature = "polars")]
2425fn is_all_null_column(col: &Column) -> bool {
2426 if col.is_empty() {
2427 return true;
2428 }
2429 if col.null_count() == col.len() {
2430 return true;
2431 }
2432 if let DataType::Struct(..) = col.dtype()
2434 && let Ok(s) = col.as_materialized_series().struct_()
2435 {
2436 return s
2437 .fields_as_series()
2438 .iter()
2439 .all(|field| field.null_count() == field.len());
2440 }
2441 false
2442}
2443
2444#[cfg(feature = "polars")]
2446fn extract_annotation_name_from_sample(sample: &Sample) -> Option<(String, Option<u32>)> {
2447 use std::path::Path;
2448
2449 let name = sample.image_name.as_ref()?;
2450 let name = Path::new(name).file_stem()?.to_str()?;
2451
2452 match &sample.sequence_name {
2455 Some(sequence) => Some((sequence.clone(), sample.frame_number)),
2456 None => Some((name.to_string(), None)),
2457 }
2458}
2459
2460fn extract_sample_name(image_name: &str) -> String {
2473 let name = image_name
2475 .rsplit_once('.')
2476 .and_then(|(name, _)| {
2477 if name.is_empty() {
2479 None
2480 } else {
2481 Some(name.to_string())
2482 }
2483 })
2484 .unwrap_or_else(|| image_name.to_string());
2485
2486 name.rsplit_once(".camera")
2488 .and_then(|(name, _)| {
2489 if name.is_empty() {
2491 None
2492 } else {
2493 Some(name.to_string())
2494 }
2495 })
2496 .unwrap_or_else(|| name.clone())
2497}
2498
2499fn resolve_file<'a>(file_type: &FileType, files: &'a [SampleFile]) -> Option<&'a SampleFile> {
2508 match file_type {
2509 FileType::Image => None, FileType::All => None, file => {
2512 let type_names = file_type_names(file);
2514 files
2515 .iter()
2516 .find(|f| type_names.contains(&f.r#type.as_str()))
2517 }
2518 }
2519}
2520
2521fn file_type_names(file_type: &FileType) -> Vec<&'static str> {
2524 match file_type {
2525 FileType::Image => vec!["image"],
2526 FileType::LidarPcd => vec!["lidar.pcd"],
2527 FileType::LidarDepth => vec!["lidar.depth", "depth.png", "depthmap"],
2528 FileType::LidarReflect => vec!["lidar.reflect"],
2529 FileType::RadarPcd => vec!["radar.pcd", "pcd"],
2530 FileType::RadarCube => vec!["radar.png", "cube"],
2531 FileType::All => vec![],
2532 }
2533}
2534
2535fn convert_annotations_map_to_vec(map: HashMap<String, Vec<Annotation>>) -> Vec<Annotation> {
2548 let mut all_annotations = Vec::new();
2549 if let Some(bbox_anns) = map.get("bbox") {
2550 all_annotations.extend(bbox_anns.clone());
2551 }
2552 if let Some(box3d_anns) = map.get("box3d") {
2553 all_annotations.extend(box3d_anns.clone());
2554 }
2555 if let Some(mask_anns) = map.get("mask") {
2556 all_annotations.extend(mask_anns.clone());
2557 }
2558 all_annotations
2559}
2560
2561fn validate_gps_coordinates(lat: f64, lon: f64) -> Result<(), String> {
2581 if !lat.is_finite() {
2582 return Err(format!("GPS latitude is not finite: {}", lat));
2583 }
2584 if !lon.is_finite() {
2585 return Err(format!("GPS longitude is not finite: {}", lon));
2586 }
2587 if !(-90.0..=90.0).contains(&lat) {
2588 return Err(format!("GPS latitude out of range [-90, 90]: {}", lat));
2589 }
2590 if !(-180.0..=180.0).contains(&lon) {
2591 return Err(format!("GPS longitude out of range [-180, 180]: {}", lon));
2592 }
2593 Ok(())
2594}
2595
2596fn validate_imu_orientation(roll: f64, pitch: f64, yaw: f64) -> Result<(), String> {
2615 if !roll.is_finite() {
2616 return Err(format!("IMU roll is not finite: {}", roll));
2617 }
2618 if !pitch.is_finite() {
2619 return Err(format!("IMU pitch is not finite: {}", pitch));
2620 }
2621 if !yaw.is_finite() {
2622 return Err(format!("IMU yaw is not finite: {}", yaw));
2623 }
2624 if !(-180.0..=180.0).contains(&roll) {
2625 return Err(format!("IMU roll out of range [-180, 180]: {}", roll));
2626 }
2627 if !(-90.0..=90.0).contains(&pitch) {
2628 return Err(format!("IMU pitch out of range [-90, 90]: {}", pitch));
2629 }
2630 if !(-180.0..=180.0).contains(&yaw) {
2631 return Err(format!("IMU yaw out of range [-180, 180]: {}", yaw));
2632 }
2633 Ok(())
2634}
2635
2636#[cfg(feature = "polars")]
2664pub fn unflatten_polygon_coordinates(coords: &[f32]) -> Vec<Vec<(f32, f32)>> {
2665 let mut polygons = Vec::new();
2666 let mut current_polygon = Vec::new();
2667 let mut i = 0;
2668
2669 while i < coords.len() {
2670 if coords[i].is_nan() {
2671 if !current_polygon.is_empty() {
2673 polygons.push(std::mem::take(&mut current_polygon));
2674 }
2675 i += 1;
2676 } else if i + 1 < coords.len() && !coords[i + 1].is_nan() {
2677 current_polygon.push((coords[i], coords[i + 1]));
2679 i += 2;
2680 } else if i + 1 < coords.len() && coords[i + 1].is_nan() {
2681 i += 1;
2684 } else {
2685 i += 1;
2687 }
2688 }
2689
2690 if !current_polygon.is_empty() {
2692 polygons.push(current_polygon);
2693 }
2694
2695 polygons
2696}
2697
2698#[cfg(test)]
2699mod tests {
2700 use super::*;
2701
2702 fn flatten_annotation_map(
2711 map: std::collections::HashMap<String, Vec<Annotation>>,
2712 ) -> Vec<Annotation> {
2713 let mut all_annotations = Vec::new();
2714
2715 for key in ["bbox", "box3d", "mask"] {
2717 if let Some(mut anns) = map.get(key).cloned() {
2718 all_annotations.append(&mut anns);
2719 }
2720 }
2721
2722 all_annotations
2723 }
2724
2725 fn annotation_group_field_name() -> &'static str {
2727 "group_name"
2728 }
2729
2730 fn annotation_object_id_field_name() -> &'static str {
2732 "object_reference"
2733 }
2734
2735 fn annotation_object_id_alias() -> &'static str {
2737 "object_id"
2738 }
2739
2740 fn validate_annotation_field_names(
2743 json_str: &str,
2744 expected_group: bool,
2745 expected_object_ref: bool,
2746 ) -> Result<(), String> {
2747 if expected_group && !json_str.contains("\"group_name\"") {
2748 return Err("Missing expected field: group_name".to_string());
2749 }
2750 if expected_object_ref && !json_str.contains("\"object_reference\"") {
2751 return Err("Missing expected field: object_reference".to_string());
2752 }
2753 Ok(())
2754 }
2755
2756 #[test]
2758 fn test_file_type_conversions() {
2759 let api_cases = vec![
2761 (FileType::Image, "image"),
2762 (FileType::LidarPcd, "lidar.pcd"),
2763 (FileType::LidarDepth, "lidar.depth"),
2764 (FileType::LidarReflect, "lidar.reflect"),
2765 (FileType::RadarPcd, "radar.pcd"),
2766 (FileType::RadarCube, "radar.png"),
2767 ];
2768
2769 let ext_cases = vec![
2771 (FileType::Image, "jpg"),
2772 (FileType::LidarPcd, "lidar.pcd"),
2773 (FileType::LidarDepth, "lidar.png"),
2774 (FileType::LidarReflect, "lidar.jpg"),
2775 (FileType::RadarPcd, "radar.pcd"),
2776 (FileType::RadarCube, "radar.png"),
2777 ];
2778
2779 for (file_type, expected_str) in &api_cases {
2781 assert_eq!(file_type.to_string(), *expected_str);
2782 }
2783
2784 for (file_type, expected_ext) in &ext_cases {
2786 assert_eq!(file_type.file_extension(), *expected_ext);
2787 }
2788
2789 assert_eq!(
2791 FileType::try_from("lidar.depth").unwrap(),
2792 FileType::LidarDepth
2793 );
2794 assert_eq!(
2795 FileType::try_from("lidar.png").unwrap(),
2796 FileType::LidarDepth
2797 );
2798 assert_eq!(
2799 FileType::try_from("depth.png").unwrap(),
2800 FileType::LidarDepth
2801 );
2802 assert_eq!(
2803 FileType::try_from("lidar.reflect").unwrap(),
2804 FileType::LidarReflect
2805 );
2806 assert_eq!(
2807 FileType::try_from("lidar.jpg").unwrap(),
2808 FileType::LidarReflect
2809 );
2810 assert_eq!(
2811 FileType::try_from("lidar.jpeg").unwrap(),
2812 FileType::LidarReflect
2813 );
2814
2815 assert!(FileType::try_from("invalid").is_err());
2817
2818 for (file_type, _) in &api_cases {
2820 let s = file_type.to_string();
2821 let parsed = FileType::try_from(s.as_str()).unwrap();
2822 assert_eq!(parsed, *file_type);
2823 }
2824 }
2825
2826 #[test]
2828 fn test_annotation_type_conversions() {
2829 let cases = vec![
2830 (AnnotationType::Box2d, "box2d"),
2831 (AnnotationType::Box3d, "box3d"),
2832 (AnnotationType::Polygon, "polygon"),
2833 (AnnotationType::Mask, "mask"),
2834 ];
2835
2836 for (ann_type, expected_str) in &cases {
2838 assert_eq!(ann_type.to_string(), *expected_str);
2839 }
2840
2841 assert_eq!(
2843 AnnotationType::try_from("box2d").unwrap(),
2844 AnnotationType::Box2d
2845 );
2846 assert_eq!(
2847 AnnotationType::try_from("box3d").unwrap(),
2848 AnnotationType::Box3d
2849 );
2850 assert_eq!(
2851 AnnotationType::try_from("polygon").unwrap(),
2852 AnnotationType::Polygon
2853 );
2854 assert_eq!(
2856 AnnotationType::try_from("mask").unwrap(),
2857 AnnotationType::Polygon
2858 );
2859 assert_eq!(
2861 AnnotationType::try_from("raster").unwrap(),
2862 AnnotationType::Mask
2863 );
2864
2865 assert_eq!(
2867 AnnotationType::from("box2d".to_string()),
2868 AnnotationType::Box2d
2869 );
2870 assert_eq!(
2871 AnnotationType::from("box3d".to_string()),
2872 AnnotationType::Box3d
2873 );
2874 assert_eq!(
2875 AnnotationType::from("polygon".to_string()),
2876 AnnotationType::Polygon
2877 );
2878 assert_eq!(
2880 AnnotationType::from("mask".to_string()),
2881 AnnotationType::Polygon
2882 );
2883
2884 assert_eq!(
2886 AnnotationType::from("invalid".to_string()),
2887 AnnotationType::Box2d
2888 );
2889
2890 assert!(AnnotationType::try_from("invalid").is_err());
2892
2893 assert_eq!(
2898 AnnotationType::try_from(AnnotationType::Box2d.to_string().as_str()).unwrap(),
2899 AnnotationType::Box2d
2900 );
2901 assert_eq!(
2902 AnnotationType::try_from(AnnotationType::Box3d.to_string().as_str()).unwrap(),
2903 AnnotationType::Box3d
2904 );
2905 assert_eq!(
2906 AnnotationType::try_from(AnnotationType::Polygon.to_string().as_str()).unwrap(),
2907 AnnotationType::Polygon
2908 );
2909 }
2910
2911 #[test]
2912 fn test_annotation_type_as_server_type() {
2913 assert_eq!(AnnotationType::Box2d.as_server_type(), "box2d");
2918 assert_eq!(AnnotationType::Box3d.as_server_type(), "box3d");
2919 assert_eq!(AnnotationType::Polygon.as_server_type(), "mask");
2920 assert_eq!(AnnotationType::Mask.as_server_type(), "mask");
2921
2922 assert_ne!(
2925 AnnotationType::Polygon.as_server_type(),
2926 AnnotationType::Polygon.to_string().as_str()
2927 );
2928 assert_eq!(
2929 AnnotationType::Box2d.as_server_type(),
2930 AnnotationType::Box2d.to_string().as_str()
2931 );
2932 }
2933
2934 #[test]
2936 fn test_extract_sample_name_with_extension_and_camera() {
2937 assert_eq!(extract_sample_name("scene_001.camera.jpg"), "scene_001");
2938 }
2939
2940 #[test]
2941 fn test_extract_sample_name_multiple_dots() {
2942 assert_eq!(extract_sample_name("image.v2.camera.png"), "image.v2");
2943 }
2944
2945 #[test]
2946 fn test_extract_sample_name_extension_only() {
2947 assert_eq!(extract_sample_name("test.jpg"), "test");
2948 }
2949
2950 #[test]
2951 fn test_extract_sample_name_no_extension() {
2952 assert_eq!(extract_sample_name("test"), "test");
2953 }
2954
2955 #[test]
2956 fn test_extract_sample_name_edge_case_dot_prefix() {
2957 assert_eq!(extract_sample_name(".jpg"), ".jpg");
2958 }
2959
2960 #[test]
2962 fn test_resolve_file_image_type_returns_none() {
2963 let files = vec![];
2965 let result = resolve_file(&FileType::Image, &files);
2966 assert!(result.is_none());
2967 }
2968
2969 #[test]
2970 fn test_resolve_file_lidar_pcd() {
2971 let files = vec![
2972 SampleFile::with_url(
2973 "lidar.pcd".to_string(),
2974 "https://example.com/file.pcd".to_string(),
2975 ),
2976 SampleFile::with_url(
2977 "radar.pcd".to_string(),
2978 "https://example.com/radar.pcd".to_string(),
2979 ),
2980 ];
2981 let result = resolve_file(&FileType::LidarPcd, &files);
2982 assert!(result.is_some());
2983 assert_eq!(result.unwrap().url(), Some("https://example.com/file.pcd"));
2984 }
2985
2986 #[test]
2987 fn test_resolve_file_not_found() {
2988 let files = vec![SampleFile::with_url(
2989 "lidar.pcd".to_string(),
2990 "https://example.com/file.pcd".to_string(),
2991 )];
2992 let result = resolve_file(&FileType::RadarPcd, &files);
2994 assert!(result.is_none());
2995 }
2996
2997 #[test]
2998 fn test_resolve_file_lidar_depth() {
2999 let files = vec![SampleFile::with_url(
3001 "lidar.depth".to_string(),
3002 "https://example.com/depth.png".to_string(),
3003 )];
3004 let result = resolve_file(&FileType::LidarDepth, &files);
3005 assert!(result.is_some());
3006 assert_eq!(result.unwrap().url(), Some("https://example.com/depth.png"));
3007 }
3008
3009 #[test]
3010 fn test_resolve_file_lidar_reflect() {
3011 let files = vec![SampleFile::with_url(
3013 "lidar.reflect".to_string(),
3014 "https://example.com/reflect.png".to_string(),
3015 )];
3016 let result = resolve_file(&FileType::LidarReflect, &files);
3017 assert!(result.is_some());
3018 assert_eq!(
3019 result.unwrap().url(),
3020 Some("https://example.com/reflect.png")
3021 );
3022 }
3023
3024 #[test]
3025 fn test_resolve_file_radar_cube() {
3026 let files = vec![SampleFile::with_url(
3028 "radar.png".to_string(),
3029 "https://example.com/radar.png".to_string(),
3030 )];
3031 let result = resolve_file(&FileType::RadarCube, &files);
3032 assert!(result.is_some());
3033 assert_eq!(result.unwrap().url(), Some("https://example.com/radar.png"));
3034 }
3035
3036 #[test]
3037 fn test_resolve_file_with_inline_data() {
3038 let files = vec![SampleFile::with_data(
3040 "radar.pcd".to_string(),
3041 "SGVsbG8gV29ybGQ=".to_string(), )];
3043 let result = resolve_file(&FileType::RadarPcd, &files);
3044 assert!(result.is_some());
3045 let file = result.unwrap();
3046 assert!(file.url().is_none());
3047 assert_eq!(file.data(), Some("SGVsbG8gV29ybGQ="));
3048 }
3049
3050 #[test]
3051 fn test_convert_annotations_map_to_vec_with_bbox() {
3052 let mut map = HashMap::new();
3053 let bbox_ann = Annotation::new();
3054 map.insert("bbox".to_string(), vec![bbox_ann.clone()]);
3055
3056 let annotations = convert_annotations_map_to_vec(map);
3057 assert_eq!(annotations.len(), 1);
3058 }
3059
3060 #[test]
3061 fn test_convert_annotations_map_to_vec_all_types() {
3062 let mut map = HashMap::new();
3063 map.insert("bbox".to_string(), vec![Annotation::new()]);
3064 map.insert("box3d".to_string(), vec![Annotation::new()]);
3065 map.insert("mask".to_string(), vec![Annotation::new()]);
3066
3067 let annotations = convert_annotations_map_to_vec(map);
3068 assert_eq!(annotations.len(), 3);
3069 }
3070
3071 #[test]
3072 fn test_convert_annotations_map_to_vec_empty() {
3073 let map = HashMap::new();
3074 let annotations = convert_annotations_map_to_vec(map);
3075 assert_eq!(annotations.len(), 0);
3076 }
3077
3078 #[test]
3079 fn test_convert_annotations_map_to_vec_unknown_type_ignored() {
3080 let mut map = HashMap::new();
3081 map.insert("unknown".to_string(), vec![Annotation::new()]);
3082
3083 let annotations = convert_annotations_map_to_vec(map);
3084 assert_eq!(annotations.len(), 0);
3086 }
3087
3088 #[test]
3090 fn test_annotation_group_field_name() {
3091 assert_eq!(annotation_group_field_name(), "group_name");
3092 }
3093
3094 #[test]
3095 fn test_annotation_object_id_field_name() {
3096 assert_eq!(annotation_object_id_field_name(), "object_reference");
3097 }
3098
3099 #[test]
3100 fn test_annotation_object_id_alias() {
3101 assert_eq!(annotation_object_id_alias(), "object_id");
3102 }
3103
3104 #[test]
3105 fn test_validate_annotation_field_names_success() {
3106 let json = r#"{"group_name":"train","object_reference":"obj1"}"#;
3107 assert!(validate_annotation_field_names(json, true, true).is_ok());
3108 }
3109
3110 #[test]
3111 fn test_validate_annotation_field_names_missing_group() {
3112 let json = r#"{"object_reference":"obj1"}"#;
3113 let result = validate_annotation_field_names(json, true, false);
3114 assert!(result.is_err());
3115 assert!(result.unwrap_err().contains("group_name"));
3116 }
3117
3118 #[test]
3119 fn test_validate_annotation_field_names_missing_object_ref() {
3120 let json = r#"{"group_name":"train"}"#;
3121 let result = validate_annotation_field_names(json, false, true);
3122 assert!(result.is_err());
3123 assert!(result.unwrap_err().contains("object_reference"));
3124 }
3125
3126 #[test]
3127 fn test_annotation_serialization_field_names() {
3128 let mut ann = Annotation::new();
3130 ann.set_group(Some("train".to_string()));
3131 ann.set_object_id(Some("obj1".to_string()));
3132
3133 let json = serde_json::to_string(&ann).unwrap();
3134 assert!(validate_annotation_field_names(&json, true, true).is_ok());
3136 }
3137
3138 #[test]
3140 fn test_validate_gps_coordinates_valid() {
3141 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()); }
3146
3147 #[test]
3148 fn test_validate_gps_coordinates_invalid_latitude() {
3149 let result = validate_gps_coordinates(91.0, 0.0);
3150 assert!(result.is_err());
3151 assert!(result.unwrap_err().contains("latitude out of range"));
3152
3153 let result = validate_gps_coordinates(-91.0, 0.0);
3154 assert!(result.is_err());
3155 assert!(result.unwrap_err().contains("latitude out of range"));
3156 }
3157
3158 #[test]
3159 fn test_validate_gps_coordinates_invalid_longitude() {
3160 let result = validate_gps_coordinates(0.0, 181.0);
3161 assert!(result.is_err());
3162 assert!(result.unwrap_err().contains("longitude out of range"));
3163
3164 let result = validate_gps_coordinates(0.0, -181.0);
3165 assert!(result.is_err());
3166 assert!(result.unwrap_err().contains("longitude out of range"));
3167 }
3168
3169 #[test]
3170 fn test_validate_gps_coordinates_non_finite() {
3171 let result = validate_gps_coordinates(f64::NAN, 0.0);
3172 assert!(result.is_err());
3173 assert!(result.unwrap_err().contains("not finite"));
3174
3175 let result = validate_gps_coordinates(0.0, f64::INFINITY);
3176 assert!(result.is_err());
3177 assert!(result.unwrap_err().contains("not finite"));
3178 }
3179
3180 #[test]
3181 fn test_validate_imu_orientation_valid() {
3182 assert!(validate_imu_orientation(0.0, 0.0, 0.0).is_ok());
3183 assert!(validate_imu_orientation(45.0, 30.0, 90.0).is_ok());
3184 assert!(validate_imu_orientation(180.0, 90.0, -180.0).is_ok()); assert!(validate_imu_orientation(-180.0, -90.0, 180.0).is_ok()); }
3187
3188 #[test]
3189 fn test_validate_imu_orientation_invalid_roll() {
3190 let result = validate_imu_orientation(181.0, 0.0, 0.0);
3191 assert!(result.is_err());
3192 assert!(result.unwrap_err().contains("roll out of range"));
3193
3194 let result = validate_imu_orientation(-181.0, 0.0, 0.0);
3195 assert!(result.is_err());
3196 }
3197
3198 #[test]
3199 fn test_validate_imu_orientation_invalid_pitch() {
3200 let result = validate_imu_orientation(0.0, 91.0, 0.0);
3201 assert!(result.is_err());
3202 assert!(result.unwrap_err().contains("pitch out of range"));
3203
3204 let result = validate_imu_orientation(0.0, -91.0, 0.0);
3205 assert!(result.is_err());
3206 }
3207
3208 #[test]
3209 fn test_validate_imu_orientation_non_finite() {
3210 let result = validate_imu_orientation(f64::NAN, 0.0, 0.0);
3211 assert!(result.is_err());
3212 assert!(result.unwrap_err().contains("not finite"));
3213
3214 let result = validate_imu_orientation(0.0, f64::INFINITY, 0.0);
3215 assert!(result.is_err());
3216
3217 let result = validate_imu_orientation(0.0, 0.0, f64::NEG_INFINITY);
3218 assert!(result.is_err());
3219 }
3220
3221 #[test]
3223 #[cfg(feature = "polars")]
3224 fn test_unflatten_polygon_coordinates_single_polygon() {
3225 let coords = vec![1.0, 2.0, 3.0, 4.0];
3226 let result = unflatten_polygon_coordinates(&coords);
3227
3228 assert_eq!(result.len(), 1);
3229 assert_eq!(result[0].len(), 2);
3230 assert_eq!(result[0][0], (1.0, 2.0));
3231 assert_eq!(result[0][1], (3.0, 4.0));
3232 }
3233
3234 #[test]
3235 #[cfg(feature = "polars")]
3236 fn test_unflatten_polygon_coordinates_multiple_polygons() {
3237 let coords = vec![1.0, 2.0, 3.0, 4.0, f32::NAN, 5.0, 6.0, 7.0, 8.0];
3238 let result = unflatten_polygon_coordinates(&coords);
3239
3240 assert_eq!(result.len(), 2);
3241 assert_eq!(result[0].len(), 2);
3242 assert_eq!(result[0][0], (1.0, 2.0));
3243 assert_eq!(result[0][1], (3.0, 4.0));
3244 assert_eq!(result[1].len(), 2);
3245 assert_eq!(result[1][0], (5.0, 6.0));
3246 assert_eq!(result[1][1], (7.0, 8.0));
3247 }
3248
3249 #[test]
3250 #[cfg(feature = "polars")]
3251 fn test_unflatten_polygon_coordinates_roundtrip() {
3252 let flat = vec![1.0, 2.0, 3.0, 4.0, f32::NAN, 5.0, 6.0, 7.0, 8.0];
3254 let result = unflatten_polygon_coordinates(&flat);
3255
3256 let expected = vec![vec![(1.0, 2.0), (3.0, 4.0)], vec![(5.0, 6.0), (7.0, 8.0)]];
3257 assert_eq!(result, expected);
3258 }
3259
3260 #[test]
3262 fn test_flatten_annotation_map_all_types() {
3263 use std::collections::HashMap;
3264
3265 let mut map = HashMap::new();
3266
3267 let mut bbox_ann = Annotation::new();
3269 bbox_ann.set_label(Some("bbox_label".to_string()));
3270
3271 let mut box3d_ann = Annotation::new();
3272 box3d_ann.set_label(Some("box3d_label".to_string()));
3273
3274 let mut mask_ann = Annotation::new();
3275 mask_ann.set_label(Some("mask_label".to_string()));
3276
3277 map.insert("bbox".to_string(), vec![bbox_ann.clone()]);
3278 map.insert("box3d".to_string(), vec![box3d_ann.clone()]);
3279 map.insert("mask".to_string(), vec![mask_ann.clone()]);
3280
3281 let result = flatten_annotation_map(map);
3282
3283 assert_eq!(result.len(), 3);
3284 assert_eq!(result[0].label(), Some(&"bbox_label".to_string()));
3286 assert_eq!(result[1].label(), Some(&"box3d_label".to_string()));
3287 assert_eq!(result[2].label(), Some(&"mask_label".to_string()));
3288 }
3289
3290 #[test]
3291 fn test_flatten_annotation_map_single_type() {
3292 use std::collections::HashMap;
3293
3294 let mut map = HashMap::new();
3295 let mut bbox_ann = Annotation::new();
3296 bbox_ann.set_label(Some("test".to_string()));
3297 map.insert("bbox".to_string(), vec![bbox_ann]);
3298
3299 let result = flatten_annotation_map(map);
3300
3301 assert_eq!(result.len(), 1);
3302 assert_eq!(result[0].label(), Some(&"test".to_string()));
3303 }
3304
3305 #[test]
3306 fn test_flatten_annotation_map_empty() {
3307 use std::collections::HashMap;
3308
3309 let map = HashMap::new();
3310 let result = flatten_annotation_map(map);
3311
3312 assert_eq!(result.len(), 0);
3313 }
3314
3315 #[test]
3316 fn test_flatten_annotation_map_deterministic_order() {
3317 use std::collections::HashMap;
3318
3319 let mut map = HashMap::new();
3320
3321 let mut bbox_ann = Annotation::new();
3322 bbox_ann.set_label(Some("bbox".to_string()));
3323
3324 let mut box3d_ann = Annotation::new();
3325 box3d_ann.set_label(Some("box3d".to_string()));
3326
3327 let mut mask_ann = Annotation::new();
3328 mask_ann.set_label(Some("mask".to_string()));
3329
3330 map.insert("mask".to_string(), vec![mask_ann]);
3332 map.insert("box3d".to_string(), vec![box3d_ann]);
3333 map.insert("bbox".to_string(), vec![bbox_ann]);
3334
3335 let result = flatten_annotation_map(map);
3336
3337 assert_eq!(result.len(), 3);
3339 assert_eq!(result[0].label(), Some(&"bbox".to_string()));
3340 assert_eq!(result[1].label(), Some(&"box3d".to_string()));
3341 assert_eq!(result[2].label(), Some(&"mask".to_string()));
3342 }
3343
3344 #[test]
3346 fn test_box2d_construction_and_accessors() {
3347 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
3349 assert_eq!(
3350 (bbox.left(), bbox.top(), bbox.width(), bbox.height()),
3351 (10.0, 20.0, 100.0, 50.0)
3352 );
3353
3354 assert_eq!((bbox.cx(), bbox.cy()), (60.0, 45.0)); let bbox = Box2d::new(0.0, 0.0, 640.0, 480.0);
3359 assert_eq!(
3360 (bbox.left(), bbox.top(), bbox.width(), bbox.height()),
3361 (0.0, 0.0, 640.0, 480.0)
3362 );
3363 assert_eq!((bbox.cx(), bbox.cy()), (320.0, 240.0));
3364 }
3365
3366 #[test]
3367 fn test_box2d_center_calculation() {
3368 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
3369
3370 assert_eq!(bbox.cx(), 60.0); assert_eq!(bbox.cy(), 45.0); }
3374
3375 #[test]
3376 fn test_box2d_zero_dimensions() {
3377 let bbox = Box2d::new(10.0, 20.0, 0.0, 0.0);
3378
3379 assert_eq!(bbox.cx(), 10.0);
3381 assert_eq!(bbox.cy(), 20.0);
3382 }
3383
3384 #[test]
3385 fn test_box2d_negative_dimensions() {
3386 let bbox = Box2d::new(100.0, 100.0, -50.0, -50.0);
3387
3388 assert_eq!(bbox.width(), -50.0);
3390 assert_eq!(bbox.height(), -50.0);
3391 assert_eq!(bbox.cx(), 75.0); assert_eq!(bbox.cy(), 75.0); }
3394
3395 #[test]
3397 fn test_box3d_construction_and_accessors() {
3398 let bbox = Box3d::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
3400 assert_eq!((bbox.cx(), bbox.cy(), bbox.cz()), (1.0, 2.0, 3.0));
3401 assert_eq!(
3402 (bbox.width(), bbox.height(), bbox.length()),
3403 (4.0, 5.0, 6.0)
3404 );
3405
3406 let bbox = Box3d::new(10.0, 20.0, 30.0, 4.0, 6.0, 8.0);
3408 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);
3412 assert_eq!((bbox.cx(), bbox.cy(), bbox.cz()), (0.0, 0.0, 0.0));
3413 assert_eq!(
3414 (bbox.width(), bbox.height(), bbox.length()),
3415 (2.0, 3.0, 4.0)
3416 );
3417 assert_eq!((bbox.left(), bbox.top(), bbox.front()), (-1.0, -1.5, -2.0));
3418 }
3419
3420 #[test]
3421 fn test_box3d_center_calculation() {
3422 let bbox = Box3d::new(10.0, 20.0, 30.0, 100.0, 50.0, 40.0);
3423
3424 assert_eq!(bbox.cx(), 10.0);
3426 assert_eq!(bbox.cy(), 20.0);
3427 assert_eq!(bbox.cz(), 30.0);
3428 }
3429
3430 #[test]
3431 fn test_box3d_zero_dimensions() {
3432 let bbox = Box3d::new(5.0, 10.0, 15.0, 0.0, 0.0, 0.0);
3433
3434 assert_eq!(bbox.cx(), 5.0);
3436 assert_eq!(bbox.cy(), 10.0);
3437 assert_eq!(bbox.cz(), 15.0);
3438 assert_eq!((bbox.left(), bbox.top(), bbox.front()), (5.0, 10.0, 15.0));
3439 }
3440
3441 #[test]
3442 fn test_box3d_negative_dimensions() {
3443 let bbox = Box3d::new(100.0, 100.0, 100.0, -50.0, -50.0, -50.0);
3444
3445 assert_eq!(bbox.width(), -50.0);
3447 assert_eq!(bbox.height(), -50.0);
3448 assert_eq!(bbox.length(), -50.0);
3449 assert_eq!(
3450 (bbox.left(), bbox.top(), bbox.front()),
3451 (125.0, 125.0, 125.0)
3452 );
3453 }
3454
3455 #[test]
3457 fn test_polygon_creation_and_deserialization() {
3458 let rings = vec![vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]];
3460 let polygon = Polygon::new(rings.clone());
3461 assert_eq!(polygon.rings, rings);
3462
3463 let legacy = serde_json::json!({
3465 "polygon": {
3466 "polygon": [[
3467 [0.0_f32, 0.0_f32],
3468 [1.0_f32, 0.0_f32],
3469 [1.0_f32, 1.0_f32]
3470 ]]
3471 }
3472 });
3473
3474 #[derive(serde::Deserialize)]
3475 struct Wrapper {
3476 polygon: Polygon,
3477 }
3478
3479 let parsed: Wrapper = serde_json::from_value(legacy).unwrap();
3480 assert_eq!(parsed.polygon.rings.len(), 1);
3481 assert_eq!(parsed.polygon.rings[0].len(), 3);
3482 }
3483
3484 #[test]
3486 fn test_sample_construction_and_accessors() {
3487 let sample = Sample::new();
3489 assert_eq!(sample.id(), None);
3490 assert_eq!(sample.image_name(), None);
3491 assert_eq!(sample.width(), None);
3492 assert_eq!(sample.height(), None);
3493
3494 let mut sample = Sample::new();
3496 sample.image_name = Some("test.jpg".to_string());
3497 sample.width = Some(1920);
3498 sample.height = Some(1080);
3499 sample.group = Some("group1".to_string());
3500
3501 assert_eq!(sample.image_name(), Some("test.jpg"));
3502 assert_eq!(sample.width(), Some(1920));
3503 assert_eq!(sample.height(), Some(1080));
3504 assert_eq!(sample.group(), Some(&"group1".to_string()));
3505 }
3506
3507 #[test]
3508 fn test_sample_name_extraction_from_image_name() {
3509 let mut sample = Sample::new();
3510
3511 sample.image_name = Some("test_image.jpg".to_string());
3513 assert_eq!(sample.name(), Some("test_image".to_string()));
3514
3515 sample.image_name = Some("test_image.camera.jpg".to_string());
3517 assert_eq!(sample.name(), Some("test_image".to_string()));
3518
3519 sample.image_name = Some("test_image".to_string());
3521 assert_eq!(sample.name(), Some("test_image".to_string()));
3522 }
3523
3524 #[test]
3526 fn test_annotation_construction_and_setters() {
3527 let ann = Annotation::new();
3529 assert_eq!(ann.sample_id(), None);
3530 assert_eq!(ann.label(), None);
3531 assert_eq!(ann.box2d(), None);
3532 assert_eq!(ann.box3d(), None);
3533 assert_eq!(ann.polygon(), None);
3534
3535 let mut ann = Annotation::new();
3537 ann.set_label(Some("car".to_string()));
3538 assert_eq!(ann.label(), Some(&"car".to_string()));
3539
3540 ann.set_label_index(Some(42));
3541 assert_eq!(ann.label_index(), Some(42));
3542
3543 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
3545 ann.set_box2d(Some(bbox.clone()));
3546 assert!(ann.box2d().is_some());
3547 assert_eq!(ann.box2d().unwrap().left(), 10.0);
3548 }
3549
3550 #[test]
3552 fn test_sample_file_with_url_and_filename() {
3553 let file = SampleFile::with_url(
3555 "lidar.pcd".to_string(),
3556 "https://example.com/file.pcd".to_string(),
3557 );
3558 assert_eq!(file.file_type(), "lidar.pcd");
3559 assert_eq!(file.url(), Some("https://example.com/file.pcd"));
3560 assert_eq!(file.filename(), None);
3561
3562 let file = SampleFile::with_filename("image".to_string(), "test.jpg".to_string());
3564 assert_eq!(file.file_type(), "image");
3565 assert_eq!(file.filename(), Some("test.jpg"));
3566 assert_eq!(file.url(), None);
3567 }
3568
3569 #[test]
3571 fn test_sample_deserializes_gps_imu_from_sensors() {
3572 use serde_json::json;
3573
3574 let sample_json = json!({
3576 "id": 123,
3577 "image_name": "test.jpg",
3578 "sensors": [
3579 {"gps": {"lat": 37.7749, "lon": -122.4194}},
3580 {"imu": {"roll": 1.5, "pitch": 2.5, "yaw": 3.5}},
3581 {"radar.pcd": "https://example.com/radar.pcd"}
3582 ]
3583 });
3584
3585 let sample: Sample = serde_json::from_value(sample_json).unwrap();
3586
3587 assert!(sample.location.is_some());
3589 let location = sample.location.as_ref().unwrap();
3590
3591 assert!(location.gps.is_some());
3593 let gps = location.gps.as_ref().unwrap();
3594 assert!((gps.lat - 37.7749).abs() < 0.0001);
3595 assert!((gps.lon - (-122.4194)).abs() < 0.0001);
3596
3597 assert!(location.imu.is_some());
3599 let imu = location.imu.as_ref().unwrap();
3600 assert!((imu.roll - 1.5).abs() < 0.0001);
3601 assert!((imu.pitch - 2.5).abs() < 0.0001);
3602 assert!((imu.yaw - 3.5).abs() < 0.0001);
3603
3604 assert_eq!(sample.files.len(), 1);
3606 assert_eq!(sample.files[0].file_type(), "radar.pcd");
3607 assert_eq!(sample.files[0].url(), Some("https://example.com/radar.pcd"));
3608 }
3609
3610 #[test]
3611 fn test_sample_deserializes_gps_only() {
3612 use serde_json::json;
3613
3614 let sample_json = json!({
3616 "id": 456,
3617 "sensors": [
3618 {"gps": {"lat": 40.7128, "lon": -74.0060}}
3619 ]
3620 });
3621
3622 let sample: Sample = serde_json::from_value(sample_json).unwrap();
3623
3624 assert!(sample.location.is_some());
3625 let location = sample.location.as_ref().unwrap();
3626
3627 assert!(location.gps.is_some());
3628 assert!(location.imu.is_none());
3629
3630 let gps = location.gps.as_ref().unwrap();
3631 assert!((gps.lat - 40.7128).abs() < 0.0001);
3632 assert!((gps.lon - (-74.0060)).abs() < 0.0001);
3633 }
3634
3635 #[test]
3636 fn test_sample_deserializes_without_location() {
3637 use serde_json::json;
3638
3639 let sample_json = json!({
3641 "id": 789,
3642 "sensors": [
3643 {"radar.pcd": "https://example.com/radar.pcd"},
3644 {"lidar.pcd": "https://example.com/lidar.pcd"}
3645 ]
3646 });
3647
3648 let sample: Sample = serde_json::from_value(sample_json).unwrap();
3649
3650 assert!(sample.location.is_none());
3652
3653 assert_eq!(sample.files.len(), 2);
3655 }
3656
3657 #[test]
3658 fn test_sample_serializes_location_as_sensors_object() {
3659 use serde_json::json;
3660
3661 let mut sample = Sample::new();
3665 sample.files = vec![SampleFile::with_filename(
3666 "image".to_string(),
3667 "pose_location.png".to_string(),
3668 )];
3669 sample.location = Some(Location {
3670 gps: Some(GpsData {
3671 lat: 37.7749,
3672 lon: -122.4194,
3673 }),
3674 imu: Some(ImuData {
3675 roll: 10.0,
3676 pitch: -5.0,
3677 yaw: 90.0,
3678 }),
3679 });
3680
3681 let json = serde_json::to_value(&sample).unwrap();
3682 assert_eq!(
3683 json.get("sensors"),
3684 Some(&json!({
3685 "gps": {"lat": 37.7749, "lon": -122.4194},
3686 "imu": {"roll": 10.0, "pitch": -5.0, "yaw": 90.0}
3687 }))
3688 );
3689 assert_eq!(
3690 json.get("files"),
3691 Some(&json!({ "image": "pose_location.png" }))
3692 );
3693 assert!(json.get("sensors").and_then(|v| v.as_array()).is_none());
3695 }
3696
3697 #[test]
3698 fn test_sample_deserializes_gps_imu_from_sensors_object() {
3699 use serde_json::json;
3700
3701 let sample_json = json!({
3704 "id": 42,
3705 "sensors": {
3706 "gps": {"lat": 40.7128, "lon": -74.0060},
3707 "imu": {"roll": 1.0, "pitch": 2.0, "yaw": 3.0}
3708 }
3709 });
3710
3711 let sample: Sample = serde_json::from_value(sample_json).unwrap();
3712 let location = sample.location.as_ref().expect("location");
3713 let gps = location.gps.as_ref().expect("gps");
3714 let imu = location.imu.as_ref().expect("imu");
3715 assert!((gps.lat - 40.7128).abs() < 0.0001);
3716 assert!((gps.lon - (-74.0060)).abs() < 0.0001);
3717 assert!((imu.roll - 1.0).abs() < 0.0001);
3718 assert!((imu.pitch - 2.0).abs() < 0.0001);
3719 assert!((imu.yaw - 3.0).abs() < 0.0001);
3720 }
3721
3722 #[test]
3724 fn test_label_deserialization_and_accessors() {
3725 use serde_json::json;
3726
3727 let label_json = json!({
3729 "id": 123,
3730 "dataset_id": 456,
3731 "index": 5,
3732 "name": "car"
3733 });
3734
3735 let label: Label = serde_json::from_value(label_json).unwrap();
3736 assert_eq!(label.id(), 123);
3737 assert_eq!(label.index(), 5);
3738 assert_eq!(label.name(), "car");
3739 assert_eq!(label.to_string(), "car");
3740 assert_eq!(format!("{}", label), "car");
3741
3742 let label_json = json!({
3744 "id": 1,
3745 "dataset_id": 100,
3746 "index": 0,
3747 "name": "person"
3748 });
3749
3750 let label: Label = serde_json::from_value(label_json).unwrap();
3751 assert_eq!(format!("{}", label), "person");
3752 }
3753
3754 #[test]
3756 fn test_annotation_serialization_with_mask_and_box() {
3757 let polygon = vec![vec![
3758 (0.0_f32, 0.0_f32),
3759 (1.0_f32, 0.0_f32),
3760 (1.0_f32, 1.0_f32),
3761 ]];
3762
3763 let mut annotation = Annotation::new();
3764 annotation.set_label(Some("test".to_string()));
3765 annotation.set_box2d(Some(Box2d::new(10.0, 20.0, 30.0, 40.0)));
3766 annotation.set_polygon(Some(Polygon::new(polygon)));
3767
3768 let mut sample = Sample::new();
3769 sample.annotations.push(annotation);
3770
3771 let json = serde_json::to_value(&sample).unwrap();
3772 let annotations = json
3773 .get("annotations")
3774 .and_then(|value| value.as_array())
3775 .expect("annotations serialized as array");
3776 assert_eq!(annotations.len(), 1);
3777
3778 let annotation_json = annotations[0].as_object().expect("annotation object");
3779 assert!(annotation_json.contains_key("box2d"));
3780 assert!(
3785 annotation_json.contains_key("mask"),
3786 "Annotation must serialise polygon under 'mask' key for samples.populate2; got keys: {:?}",
3787 annotation_json.keys().collect::<Vec<_>>()
3788 );
3789 assert!(!annotation_json.contains_key("polygon"));
3790 assert!(!annotation_json.contains_key("x"));
3791 assert!(
3792 annotation_json
3793 .get("mask")
3794 .and_then(|value| value.as_array())
3795 .is_some()
3796 );
3797 }
3798
3799 #[test]
3800 fn test_frame_number_negative_one_deserializes_as_none() {
3801 let json = r#"{
3804 "uuid": "test-uuid",
3805 "frame_number": -1
3806 }"#;
3807
3808 let sample: Sample = serde_json::from_str(json).unwrap();
3809 assert_eq!(sample.frame_number, None);
3810 }
3811
3812 #[test]
3813 fn test_frame_number_positive_value_deserializes_correctly() {
3814 let json = r#"{
3816 "uuid": "test-uuid",
3817 "frame_number": 5
3818 }"#;
3819
3820 let sample: Sample = serde_json::from_str(json).unwrap();
3821 assert_eq!(sample.frame_number, Some(5));
3822 }
3823
3824 #[test]
3825 fn test_frame_number_null_deserializes_as_none() {
3826 let json = r#"{
3828 "uuid": "test-uuid",
3829 "frame_number": null
3830 }"#;
3831
3832 let sample: Sample = serde_json::from_str(json).unwrap();
3833 assert_eq!(sample.frame_number, None);
3834 }
3835
3836 #[test]
3837 fn test_frame_number_missing_deserializes_as_none() {
3838 let json = r#"{
3840 "uuid": "test-uuid"
3841 }"#;
3842
3843 let sample: Sample = serde_json::from_str(json).unwrap();
3844 assert_eq!(sample.frame_number, None);
3845 }
3846
3847 #[cfg(feature = "polars")]
3852 #[test]
3853 fn test_samples_dataframe_preserves_group_for_samples_without_annotations() {
3854 use polars::prelude::*;
3855
3856 let mut sample_with_ann = Sample::new();
3858 sample_with_ann.image_name = Some("annotated.jpg".to_string());
3859 sample_with_ann.group = Some("train".to_string());
3860 let mut annotation = Annotation::new();
3861 annotation.set_label(Some("car".to_string()));
3862 annotation.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
3863 annotation.set_name(Some("annotated".to_string()));
3864 sample_with_ann.annotations = vec![annotation];
3865
3866 let mut sample_no_ann = Sample::new();
3868 sample_no_ann.image_name = Some("unannotated.jpg".to_string());
3869 sample_no_ann.group = Some("val".to_string()); sample_no_ann.annotations = vec![]; let samples = vec![sample_with_ann, sample_no_ann];
3873
3874 let df = samples_dataframe(&samples).expect("Failed to create DataFrame");
3876
3877 assert_eq!(df.height(), 2, "Expected 2 rows (one per sample)");
3879
3880 let groups_col = df.column("group").expect("group column should exist");
3882 let groups_cast = groups_col.cast(&DataType::String).expect("cast to string");
3883 let groups = groups_cast.str().expect("as str");
3884
3885 let names_col = df.column("name").expect("name column should exist");
3887 let names_cast = names_col.cast(&DataType::String).expect("cast to string");
3888 let names = names_cast.str().expect("as str");
3889
3890 let mut found_unannotated = false;
3891 for idx in 0..df.height() {
3892 if let Some(name) = names.get(idx)
3893 && name == "unannotated"
3894 {
3895 found_unannotated = true;
3896 let group = groups.get(idx);
3897 assert_eq!(
3898 group,
3899 Some("val"),
3900 "CRITICAL: Sample 'unannotated' without annotations must have group 'val'"
3901 );
3902 }
3903 }
3904
3905 assert!(
3906 found_unannotated,
3907 "Did not find 'unannotated' sample in DataFrame - \
3908 this means samples without annotations are not being included"
3909 );
3910 }
3911
3912 #[cfg(feature = "polars")]
3913 #[test]
3914 fn test_samples_dataframe_includes_all_samples_even_without_annotations() {
3915 let mut sample1 = Sample::new();
3919 sample1.image_name = Some("with_ann.jpg".to_string());
3920 sample1.group = Some("train".to_string());
3921 let mut ann = Annotation::new();
3922 ann.set_label(Some("person".to_string()));
3923 ann.set_box2d(Some(Box2d::new(0.0, 0.0, 0.5, 0.5)));
3924 ann.set_name(Some("with_ann".to_string()));
3925 sample1.annotations = vec![ann];
3926
3927 let mut sample2 = Sample::new();
3928 sample2.image_name = Some("no_ann_train.jpg".to_string());
3929 sample2.group = Some("train".to_string());
3930 sample2.annotations = vec![];
3931
3932 let mut sample3 = Sample::new();
3933 sample3.image_name = Some("no_ann_val.jpg".to_string());
3934 sample3.group = Some("val".to_string());
3935 sample3.annotations = vec![];
3936
3937 let samples = vec![sample1, sample2, sample3];
3938
3939 let df = samples_dataframe(&samples).expect("Failed to create DataFrame");
3940
3941 assert_eq!(
3943 df.height(),
3944 3,
3945 "Expected 3 rows (samples without annotations should create one row each)"
3946 );
3947
3948 let groups_col = df.column("group").expect("group column");
3950 let groups_cast = groups_col.cast(&polars::prelude::DataType::String).unwrap();
3951 let groups = groups_cast.str().unwrap();
3952
3953 let mut train_count = 0;
3954 let mut val_count = 0;
3955
3956 for idx in 0..df.height() {
3957 match groups.get(idx) {
3958 Some("train") => train_count += 1,
3959 Some("val") => val_count += 1,
3960 other => panic!(
3961 "Unexpected group value at row {}: {:?}. \
3962 All samples should have their group preserved.",
3963 idx, other
3964 ),
3965 }
3966 }
3967
3968 assert_eq!(train_count, 2, "Expected 2 samples in 'train' group");
3969 assert_eq!(val_count, 1, "Expected 1 sample in 'val' group");
3970 }
3971
3972 #[cfg(feature = "polars")]
3973 #[test]
3974 fn test_samples_dataframe_group_is_not_null_for_samples_with_group() {
3975 let mut sample = Sample::new();
3979 sample.image_name = Some("test.jpg".to_string());
3980 sample.group = Some("test_group".to_string());
3981 sample.annotations = vec![];
3982
3983 let df = samples_dataframe(&[sample]).expect("Failed to create DataFrame");
3984
3985 let groups_col = df.column("group").expect("group column");
3986
3987 assert_eq!(
3989 groups_col.null_count(),
3990 0,
3991 "Sample with group='test_group' but no annotations has NULL group in DataFrame. \
3992 This is a bug in samples_dataframe - group must be preserved!"
3993 );
3994 }
3995
3996 #[cfg(feature = "polars")]
3997 #[test]
3998 fn test_samples_dataframe_group_consistent_across_all_rows_for_same_image() {
3999 use polars::prelude::*;
4000
4001 let mut sample = Sample::new();
4005 sample.image_name = Some("multi_ann.jpg".to_string());
4006 sample.group = Some("train".to_string());
4007
4008 let mut ann1 = Annotation::new();
4010 ann1.set_label(Some("car".to_string()));
4011 ann1.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
4012 ann1.set_name(Some("multi_ann".to_string()));
4013
4014 let mut ann2 = Annotation::new();
4015 ann2.set_label(Some("truck".to_string()));
4016 ann2.set_box2d(Some(Box2d::new(0.5, 0.6, 0.2, 0.2)));
4017 ann2.set_name(Some("multi_ann".to_string()));
4018
4019 let mut ann3 = Annotation::new();
4020 ann3.set_label(Some("bus".to_string()));
4021 ann3.set_box2d(Some(Box2d::new(0.7, 0.8, 0.1, 0.1)));
4022 ann3.set_name(Some("multi_ann".to_string()));
4023
4024 sample.annotations = vec![ann1, ann2, ann3];
4025
4026 let df = samples_dataframe(&[sample]).expect("Failed to create DataFrame");
4027
4028 assert_eq!(df.height(), 3, "Expected 3 rows (one per annotation)");
4030
4031 let groups_col = df.column("group").expect("group column");
4033 let groups_cast = groups_col.cast(&DataType::String).expect("cast to string");
4034 let groups = groups_cast.str().expect("as str");
4035
4036 assert_eq!(groups_col.null_count(), 0, "No rows should have null group");
4038
4039 for idx in 0..df.height() {
4041 let group = groups.get(idx);
4042 assert_eq!(
4043 group,
4044 Some("train"),
4045 "Row {} should have group 'train', got {:?}. \
4046 All rows for the same image must have identical group values.",
4047 idx,
4048 group
4049 );
4050 }
4051 }
4052
4053 #[cfg(feature = "polars")]
4054 #[test]
4055 fn test_samples_dataframe_lvis_columns() {
4056 let mut ann = Annotation::new();
4057 ann.set_name(Some("test".to_string()));
4058 ann.set_label(Some("person".to_string()));
4059 ann.set_label_index(Some(1));
4060 ann.set_iscrowd(Some(false));
4061 ann.set_category_frequency(Some("f".to_string()));
4062
4063 let sample = Sample {
4064 image_name: Some("test.jpg".to_string()),
4065 width: Some(640),
4066 height: Some(480),
4067 annotations: vec![ann],
4068 neg_label_indices: Some(vec![5, 12]),
4069 not_exhaustive_label_indices: Some(vec![3]),
4070 ..Default::default()
4071 };
4072
4073 let df = samples_dataframe(&[sample]).unwrap();
4074
4075 assert!(df.column("iscrowd").is_ok(), "iscrowd column missing");
4077 assert!(
4078 df.column("category_frequency").is_ok(),
4079 "category_frequency column missing"
4080 );
4081 assert!(
4082 df.column("neg_label_indices").is_ok(),
4083 "neg_label_indices column missing"
4084 );
4085 assert!(
4086 df.column("not_exhaustive_label_indices").is_ok(),
4087 "not_exhaustive_label_indices column missing"
4088 );
4089
4090 assert!(
4092 df.column("polygon").is_err(),
4093 "polygon column should be dropped (all null)"
4094 );
4095 assert!(
4096 df.column("box2d").is_err(),
4097 "box2d column should be dropped (all null)"
4098 );
4099 }
4100
4101 #[test]
4102 fn test_annotation_serialization_skips_lvis_fields() {
4103 let ann = Annotation::new();
4104 let json = serde_json::to_string(&ann).unwrap();
4105 assert!(
4106 !json.contains("iscrowd"),
4107 "iscrowd should be omitted when None"
4108 );
4109 assert!(
4110 !json.contains("category_frequency"),
4111 "category_frequency should be omitted when None"
4112 );
4113 }
4114
4115 #[test]
4116 fn test_sample_serialization_skips_lvis_fields() {
4117 let sample = Sample::new();
4118 let json = serde_json::to_string(&sample).unwrap();
4119 assert!(
4120 !json.contains("neg_label_indices"),
4121 "neg_label_indices should be omitted when None"
4122 );
4123 assert!(
4124 !json.contains("not_exhaustive_label_indices"),
4125 "not_exhaustive_label_indices should be omitted when None"
4126 );
4127 }
4128
4129 #[test]
4130 fn test_annotation_score_fields() {
4131 let mut ann = Annotation::default();
4132 assert!(ann.box2d_score.is_none());
4133 assert!(ann.polygon_score.is_none());
4134 assert!(ann.mask_score.is_none());
4135 ann.box2d_score = Some(0.95);
4136 ann.polygon_score = Some(0.87);
4137 ann.mask_score = Some(0.42);
4138 assert_eq!(ann.box2d_score, Some(0.95));
4139 assert_eq!(ann.polygon_score, Some(0.87));
4140 assert_eq!(ann.mask_score, Some(0.42));
4141 }
4142
4143 #[test]
4144 fn test_timing_struct() {
4145 let timing = Timing {
4146 load: Some(1_000_000),
4147 preprocess: Some(2_000_000),
4148 inference: Some(50_000_000),
4149 decode: Some(3_000_000),
4150 };
4151 assert_eq!(timing.inference, Some(50_000_000));
4152
4153 let default = Timing::default();
4154 assert!(default.load.is_none());
4155 }
4156
4157 #[test]
4158 fn test_sample_timing() {
4159 let mut sample = Sample::default();
4160 assert!(sample.timing.is_none());
4161 sample.timing = Some(Timing {
4162 load: Some(1_000_000),
4163 ..Default::default()
4164 });
4165 assert!(sample.timing.is_some());
4166 }
4167
4168 #[cfg(feature = "polars")]
4173 #[test]
4174 fn test_samples_dataframe_polygon_column() {
4175 let mut ann = Annotation::new();
4176 ann.set_name(Some("test".to_string()));
4177 ann.set_polygon(Some(Polygon::new(vec![vec![
4178 (0.1, 0.2),
4179 (0.3, 0.4),
4180 (0.5, 0.6),
4181 ]])));
4182
4183 let sample = Sample {
4184 image_name: Some("test.jpg".to_string()),
4185 annotations: vec![ann],
4186 ..Default::default()
4187 };
4188
4189 let df = samples_dataframe(&[sample]).unwrap();
4190
4191 assert!(df.column("polygon").is_ok(), "Should have polygon column");
4193
4194 if let Ok(mask_col) = df.column("mask") {
4197 assert_eq!(
4199 mask_col.dtype(),
4200 &polars::prelude::DataType::Binary,
4201 "mask column must be Binary type (PNG bytes), not float list"
4202 );
4203 }
4204 }
4205
4206 #[cfg(feature = "polars")]
4207 #[test]
4208 fn test_samples_dataframe_column_presence_drops_all_null() {
4209 let sample = Sample {
4211 image_name: Some("test.jpg".to_string()),
4212 ..Default::default()
4213 };
4214
4215 let df = samples_dataframe(&[sample]).unwrap();
4216
4217 assert!(df.column("name").is_ok(), "name column must always exist");
4219
4220 assert!(
4222 df.column("polygon").is_err(),
4223 "All-null polygon should be dropped"
4224 );
4225 assert!(
4226 df.column("box2d").is_err(),
4227 "All-null box2d should be dropped"
4228 );
4229 assert!(
4230 df.column("box3d").is_err(),
4231 "All-null box3d should be dropped"
4232 );
4233 assert!(
4234 df.column("mask").is_err(),
4235 "All-null mask should be dropped"
4236 );
4237 assert!(
4238 df.column("box2d_score").is_err(),
4239 "All-null score columns should be dropped"
4240 );
4241 assert!(
4242 df.column("timing").is_err(),
4243 "All-null timing should be dropped"
4244 );
4245 }
4246
4247 #[cfg(feature = "polars")]
4248 #[test]
4249 fn test_samples_dataframe_size_column() {
4250 let sample1 = Sample {
4252 image_name: Some("img1.jpg".to_string()),
4253 width: Some(1920),
4254 height: Some(1080),
4255 ..Default::default()
4256 };
4257 let sample2 = Sample {
4258 image_name: Some("img2.jpg".to_string()),
4259 width: Some(640),
4260 height: Some(480),
4261 ..Default::default()
4262 };
4263
4264 let df = samples_dataframe(&[sample1, sample2]).unwrap();
4265
4266 let size_col = df
4268 .column("size")
4269 .expect("size column should be present when width/height are set");
4270 assert_eq!(size_col.len(), 2);
4271
4272 let arr = size_col.array().expect("size column should be Array dtype");
4274 let row0 = arr.get_as_series(0).unwrap();
4275 let row0_vals: Vec<u32> = row0.u32().unwrap().into_no_null_iter().collect();
4276 assert_eq!(row0_vals, vec![1920, 1080]);
4277
4278 let row1 = arr.get_as_series(1).unwrap();
4279 let row1_vals: Vec<u32> = row1.u32().unwrap().into_no_null_iter().collect();
4280 assert_eq!(row1_vals, vec![640, 480]);
4281 }
4282
4283 #[cfg(feature = "polars")]
4284 #[test]
4285 fn test_samples_dataframe_size_column_partial() {
4286 let sample1 = Sample {
4288 image_name: Some("img1.jpg".to_string()),
4289 width: Some(1920),
4290 height: Some(1080),
4291 ..Default::default()
4292 };
4293 let sample2 = Sample {
4294 image_name: Some("img2.jpg".to_string()),
4295 ..Default::default()
4297 };
4298
4299 let df = samples_dataframe(&[sample1, sample2]).unwrap();
4300
4301 let size_col = df
4303 .column("size")
4304 .expect("size column should be present when at least one sample has dimensions");
4305 assert_eq!(size_col.len(), 2);
4306 assert_eq!(size_col.null_count(), 1, "one row should be null");
4307 }
4308
4309 #[cfg(feature = "polars")]
4310 #[test]
4311 fn test_samples_dataframe_score_columns() {
4312 let mut ann = Annotation::new();
4313 ann.set_name(Some("test".to_string()));
4314 ann.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
4315 ann.set_box2d_score(Some(0.95));
4316 ann.set_polygon(Some(Polygon::new(vec![vec![
4317 (0.0, 0.0),
4318 (1.0, 0.0),
4319 (1.0, 1.0),
4320 ]])));
4321 ann.set_polygon_score(Some(0.87));
4322
4323 let sample = Sample {
4324 image_name: Some("test.jpg".to_string()),
4325 annotations: vec![ann],
4326 ..Default::default()
4327 };
4328
4329 let df = samples_dataframe(&[sample]).unwrap();
4330
4331 assert!(
4333 df.column("box2d_score").is_ok(),
4334 "box2d_score column missing"
4335 );
4336 assert!(
4337 df.column("polygon_score").is_ok(),
4338 "polygon_score column missing"
4339 );
4340
4341 assert!(
4343 df.column("box3d_score").is_err(),
4344 "box3d_score should be dropped (all null)"
4345 );
4346 assert!(
4347 df.column("mask_score").is_err(),
4348 "mask_score should be dropped (all null)"
4349 );
4350
4351 let box2d_scores = df.column("box2d_score").unwrap();
4353 let val = box2d_scores.f32().unwrap().get(0);
4354 assert_eq!(val, Some(0.95));
4355 }
4356
4357 #[cfg(feature = "polars")]
4358 #[test]
4359 fn test_samples_dataframe_timing_column() {
4360 let mut ann = Annotation::new();
4361 ann.set_name(Some("test".to_string()));
4362 ann.set_label(Some("person".to_string()));
4363
4364 let sample = Sample {
4365 image_name: Some("test.jpg".to_string()),
4366 annotations: vec![ann],
4367 timing: Some(Timing {
4368 load: Some(1_000_000),
4369 preprocess: Some(2_000_000),
4370 inference: Some(50_000_000),
4371 decode: Some(3_000_000),
4372 }),
4373 ..Default::default()
4374 };
4375
4376 let df = samples_dataframe(&[sample]).unwrap();
4377
4378 assert!(df.column("timing").is_ok(), "timing column missing");
4380
4381 let timing_col = df.column("timing").unwrap();
4383 assert!(
4384 matches!(timing_col.dtype(), polars::prelude::DataType::Struct(..)),
4385 "timing column should be Struct type, got {:?}",
4386 timing_col.dtype()
4387 );
4388 }
4389
4390 #[cfg(feature = "polars")]
4391 #[test]
4392 fn test_samples_dataframe_mask_binary_column() {
4393 let mut ann = Annotation::new();
4394 ann.set_name(Some("test".to_string()));
4395 let pixels = vec![0u8, 255, 128, 64];
4397 let mask_data = MaskData::encode(&pixels, 2, 2, 8).unwrap();
4398 ann.set_mask(Some(mask_data));
4399
4400 let sample = Sample {
4401 image_name: Some("test.jpg".to_string()),
4402 annotations: vec![ann],
4403 ..Default::default()
4404 };
4405
4406 let df = samples_dataframe(&[sample]).unwrap();
4407
4408 let mask_col = df.column("mask").unwrap();
4410 assert_eq!(
4411 mask_col.dtype(),
4412 &polars::prelude::DataType::Binary,
4413 "mask column should be Binary"
4414 );
4415 assert_eq!(mask_col.null_count(), 0, "mask value should not be null");
4416 }
4417
4418 #[test]
4423 fn test_annotation_type_seg_alias() {
4424 assert_eq!(
4425 AnnotationType::try_from("seg").unwrap(),
4426 AnnotationType::Polygon,
4427 "\"seg\" should map to Polygon for server round-trip"
4428 );
4429 }
4430
4431 #[cfg(feature = "polars")]
4436 #[test]
4437 fn test_samples_dataframe_timing_partial() {
4438 let mut ann = Annotation::new();
4440 ann.set_name(Some("test".to_string()));
4441 ann.set_label(Some("person".to_string()));
4442
4443 let sample = Sample {
4444 image_name: Some("test.jpg".to_string()),
4445 annotations: vec![ann],
4446 timing: Some(Timing {
4447 load: Some(1000),
4448 ..Default::default()
4449 }),
4450 ..Default::default()
4451 };
4452
4453 let df = samples_dataframe(&[sample]).unwrap();
4454
4455 assert!(
4457 df.column("timing").is_ok(),
4458 "timing column should be present when partial data exists"
4459 );
4460 }
4461
4462 #[cfg(feature = "polars")]
4463 #[test]
4464 fn test_samples_dataframe_timing_all_none_omitted() {
4465 let mut ann = Annotation::new();
4467 ann.set_name(Some("test".to_string()));
4468 ann.set_label(Some("person".to_string()));
4469
4470 let sample = Sample {
4471 image_name: Some("test.jpg".to_string()),
4472 annotations: vec![ann],
4473 timing: None,
4474 ..Default::default()
4475 };
4476
4477 let df = samples_dataframe(&[sample]).unwrap();
4478
4479 assert!(
4480 df.column("timing").is_err(),
4481 "timing column should be omitted when all samples have timing: None"
4482 );
4483 }
4484
4485 #[cfg(feature = "polars")]
4490 #[test]
4491 fn test_samples_dataframe_score_zero_survives() {
4492 let mut ann = Annotation::new();
4494 ann.set_name(Some("test".to_string()));
4495 ann.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
4496 ann.set_box2d_score(Some(0.0));
4497
4498 let sample = Sample {
4499 image_name: Some("test.jpg".to_string()),
4500 annotations: vec![ann],
4501 ..Default::default()
4502 };
4503
4504 let df = samples_dataframe(&[sample]).unwrap();
4505
4506 let scores = df.column("box2d_score").unwrap();
4507 let val = scores.f32().unwrap().get(0);
4508 assert_eq!(val, Some(0.0), "score of 0.0 should survive as non-null");
4509 }
4510
4511 #[cfg(feature = "polars")]
4512 #[test]
4513 fn test_samples_dataframe_score_one_survives() {
4514 let mut ann = Annotation::new();
4515 ann.set_name(Some("test".to_string()));
4516 ann.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
4517 ann.set_box2d_score(Some(1.0));
4518
4519 let sample = Sample {
4520 image_name: Some("test.jpg".to_string()),
4521 annotations: vec![ann],
4522 ..Default::default()
4523 };
4524
4525 let df = samples_dataframe(&[sample]).unwrap();
4526
4527 let scores = df.column("box2d_score").unwrap();
4528 let val = scores.f32().unwrap().get(0);
4529 assert_eq!(val, Some(1.0), "score of 1.0 should survive as non-null");
4530 }
4531}
4532
4533#[cfg(test)]
4534mod versioning_deser_tests {
4535 use super::*;
4536
4537 #[test]
4538 fn test_annotation_set_deserializes_from_tag_scoped_response() {
4539 let json = r#"{"id": 42, "name": "Default", "description": "Default set"}"#;
4542 let result: Result<AnnotationSet, _> = serde_json::from_str(json);
4543 assert!(
4544 result.is_ok(),
4545 "tag-scoped annotation set response must deserialize: {:?}",
4546 result.err()
4547 );
4548 let annset = result.unwrap();
4549 assert_eq!(annset.name(), "Default");
4550 assert_eq!(annset.description(), "Default set");
4551 assert_eq!(annset.created(), None);
4552 }
4553
4554 #[test]
4555 fn test_annotation_set_deserializes_from_head_response() {
4556 let json = r#"{"id": 42, "dataset_id": 1, "name": "Default", "description": "Default set", "date": "2026-01-01T00:00:00Z"}"#;
4563 let result: Result<AnnotationSet, _> = serde_json::from_str(json);
4564 assert!(
4565 result.is_ok(),
4566 "HEAD annotation set response must deserialize: {:?}",
4567 result.err()
4568 );
4569 let annset = result.unwrap();
4570 assert!(annset.created().is_some());
4571 }
4572
4573 #[test]
4574 fn test_label_deserializes_from_tag_scoped_response() {
4575 let json = r#"{"id": 7, "name": "circle", "index": 0, "color": 16711680}"#;
4578 let result: Result<Label, _> = serde_json::from_str(json);
4579 assert!(
4580 result.is_ok(),
4581 "tag-scoped label response must deserialize: {:?}",
4582 result.err()
4583 );
4584 let label = result.unwrap();
4585 assert_eq!(label.name(), "circle");
4586 assert_eq!(label.color(), Some(16711680));
4587 assert_eq!(label.dataset_id(), None);
4588 }
4589
4590 #[test]
4591 fn test_label_deserializes_from_head_response() {
4592 let json = r#"{"id": 7, "dataset_id": 1, "name": "circle", "index": 0}"#;
4596 let result: Result<Label, _> = serde_json::from_str(json);
4597 assert!(
4598 result.is_ok(),
4599 "HEAD label response must deserialize: {:?}",
4600 result.err()
4601 );
4602 let label = result.unwrap();
4603 assert!(label.dataset_id().is_some());
4604 assert_eq!(label.color(), None);
4605 }
4606
4607 #[test]
4608 fn test_dataset_deserializes_tag_fields() {
4609 let json = r#"{
4614 "id": 1, "project_id": 1, "name": "My Dataset",
4615 "description": "", "cloud_key": "k", "createdAt": "2026-01-01T00:00:00Z",
4616 "tag_id": 42, "tag": "v1.0", "tag_description": "Release candidate"
4617 }"#;
4618 let dataset: Dataset = serde_json::from_str(json).unwrap();
4619 assert_eq!(dataset.tag_id(), Some(42));
4620 assert_eq!(dataset.tag(), "v1.0");
4621 assert_eq!(dataset.tag_description(), "Release candidate");
4622 }
4623
4624 #[test]
4625 fn test_dataset_deserializes_without_tag_fields() {
4626 let json = r#"{
4629 "id": 1, "project_id": 1, "name": "My Dataset",
4630 "description": "", "cloud_key": "k", "createdAt": "2026-01-01T00:00:00Z"
4631 }"#;
4632 let dataset: Dataset = serde_json::from_str(json).unwrap();
4633 assert_eq!(dataset.tag_id(), None);
4634 assert_eq!(dataset.tag(), "");
4635 assert_eq!(dataset.tag_description(), "");
4636 }
4637}