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
461#[derive(Deserialize)]
470pub struct AnnotationSet {
471 id: AnnotationSetID,
472 #[serde(default)]
473 dataset_id: Option<DatasetID>,
474 name: String,
475 description: String,
476 #[serde(rename = "date", default)]
477 created: Option<DateTime<Utc>>,
478}
479
480impl Display for AnnotationSet {
481 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
482 write!(f, "{} {}", self.id, self.name)
483 }
484}
485
486impl AnnotationSet {
487 pub fn id(&self) -> AnnotationSetID {
488 self.id
489 }
490
491 pub fn dataset_id(&self) -> Option<DatasetID> {
496 self.dataset_id
497 }
498
499 pub(crate) fn backfill_dataset_id(&mut self, dataset_id: DatasetID) {
503 if self.dataset_id.is_none() {
504 self.dataset_id = Some(dataset_id);
505 }
506 }
507
508 pub fn name(&self) -> &str {
509 &self.name
510 }
511
512 pub fn description(&self) -> &str {
513 &self.description
514 }
515
516 pub fn created(&self) -> Option<DateTime<Utc>> {
520 self.created
521 }
522
523 pub async fn dataset(&self, client: &Client) -> Result<Dataset, Error> {
524 client
525 .dataset(self.dataset_id.ok_or_else(|| {
526 Error::InvalidParameters(
527 "annotation set has no dataset_id (tag-scoped query result)".to_string(),
528 )
529 })?)
530 .await
531 }
532}
533
534#[derive(Clone, Debug, Default, PartialEq)]
539pub struct Timing {
540 pub load: Option<i64>,
542 pub preprocess: Option<i64>,
544 pub inference: Option<i64>,
546 pub decode: Option<i64>,
548}
549
550#[derive(Serialize, Clone, Debug)]
557pub struct Sample {
558 #[serde(skip_serializing_if = "Option::is_none")]
559 pub id: Option<SampleID>,
560 #[serde(
565 alias = "group_name",
566 rename(serialize = "group", deserialize = "group_name"),
567 skip_serializing_if = "Option::is_none"
568 )]
569 pub group: Option<String>,
570 #[serde(skip_serializing_if = "Option::is_none")]
571 pub sequence_name: Option<String>,
572 #[serde(skip_serializing_if = "Option::is_none")]
573 pub sequence_uuid: Option<String>,
574 #[serde(skip_serializing_if = "Option::is_none")]
575 pub sequence_description: Option<String>,
576 #[serde(
577 default,
578 skip_serializing_if = "Option::is_none",
579 deserialize_with = "deserialize_frame_number"
580 )]
581 pub frame_number: Option<u32>,
582 #[serde(skip_serializing_if = "Option::is_none")]
583 pub uuid: Option<String>,
584 #[serde(skip_serializing_if = "Option::is_none")]
585 pub image_name: Option<String>,
586 #[serde(skip_serializing_if = "Option::is_none")]
587 pub image_url: Option<String>,
588 #[serde(skip_serializing_if = "Option::is_none")]
589 pub width: Option<u32>,
590 #[serde(skip_serializing_if = "Option::is_none")]
591 pub height: Option<u32>,
592 #[serde(skip_serializing_if = "Option::is_none")]
593 pub date: Option<DateTime<Utc>>,
594 #[serde(skip_serializing_if = "Option::is_none")]
595 pub source: Option<String>,
596 #[serde(skip_serializing_if = "Option::is_none", rename(serialize = "sensors"))]
601 pub location: Option<Location>,
602 #[serde(skip_serializing_if = "Option::is_none")]
604 pub degradation: Option<String>,
605 #[serde(default, skip_serializing_if = "Option::is_none")]
607 pub neg_label_indices: Option<Vec<u32>>,
608 #[serde(default, skip_serializing_if = "Option::is_none")]
610 pub not_exhaustive_label_indices: Option<Vec<u32>>,
611 #[serde(
616 default,
617 skip_serializing_if = "Vec::is_empty",
618 serialize_with = "serialize_files"
619 )]
620 pub files: Vec<SampleFile>,
621 #[serde(
624 default,
625 skip_serializing_if = "Vec::is_empty",
626 serialize_with = "serialize_annotations"
627 )]
628 pub annotations: Vec<Annotation>,
629 #[serde(skip)]
632 pub timing: Option<Timing>,
633}
634
635fn deserialize_frame_number<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
638where
639 D: serde::Deserializer<'de>,
640{
641 use serde::Deserialize;
642
643 let value = Option::<i32>::deserialize(deserializer)?;
644 Ok(value.and_then(|v| if v < 0 { None } else { Some(v as u32) }))
645}
646
647fn is_valid_url(s: &str) -> bool {
650 s.starts_with("http://") || s.starts_with("https://")
651}
652
653fn serialize_files<S>(files: &[SampleFile], serializer: S) -> Result<S::Ok, S::Error>
656where
657 S: serde::Serializer,
658{
659 use serde::Serialize;
660 let map: HashMap<String, String> = files
661 .iter()
662 .filter_map(|f| {
663 f.filename()
664 .map(|filename| (f.file_type().to_string(), filename.to_string()))
665 })
666 .collect();
667 map.serialize(serializer)
668}
669
670fn serialize_annotations<S>(annotations: &Vec<Annotation>, serializer: S) -> Result<S::Ok, S::Error>
674where
675 S: serde::Serializer,
676{
677 serde::Serialize::serialize(annotations, serializer)
678}
679
680fn deserialize_annotations<'de, D>(deserializer: D) -> Result<Vec<Annotation>, D::Error>
683where
684 D: serde::Deserializer<'de>,
685{
686 use serde::Deserialize;
687
688 #[derive(Deserialize)]
689 #[serde(untagged)]
690 enum AnnotationsFormat {
691 Vec(Vec<Annotation>),
692 Map(HashMap<String, Vec<Annotation>>),
693 }
694
695 let value = Option::<AnnotationsFormat>::deserialize(deserializer)?;
696 Ok(value
697 .map(|v| match v {
698 AnnotationsFormat::Vec(annotations) => annotations,
699 AnnotationsFormat::Map(map) => convert_annotations_map_to_vec(map),
700 })
701 .unwrap_or_default())
702}
703
704#[derive(Debug, Default)]
707struct SensorsData {
708 files: Vec<SampleFile>,
709 location: Option<Location>,
710}
711
712fn deserialize_sensors_data(value: Option<serde_json::Value>) -> SensorsData {
714 use serde_json::Value;
715
716 fn create_sample_file(file_type: String, value: String) -> SampleFile {
719 if is_valid_url(&value) {
720 SampleFile::with_url(file_type, value)
721 } else {
722 SampleFile::with_data(file_type, value)
723 }
724 }
725
726 fn create_sample_file_from_value(file_type: String, value: Value) -> Option<SampleFile> {
728 match value {
729 Value::String(s) => Some(create_sample_file(file_type, s)),
730 Value::Object(_) | Value::Array(_) => {
731 serde_json::to_string(&value)
733 .ok()
734 .map(|data| SampleFile::with_data(file_type, data))
735 }
736 _ => None,
737 }
738 }
739
740 fn extract_location(map: &serde_json::Map<String, Value>) -> Option<Location> {
742 let gps = map
743 .get("gps")
744 .and_then(|v| serde_json::from_value::<GpsData>(v.clone()).ok());
745 let imu = map
746 .get("imu")
747 .and_then(|v| serde_json::from_value::<ImuData>(v.clone()).ok());
748
749 if gps.is_some() || imu.is_some() {
750 Some(Location { gps, imu })
751 } else {
752 None
753 }
754 }
755
756 let mut result = SensorsData::default();
757
758 match value {
759 None => result,
760 Some(Value::Array(arr)) => {
761 for item in arr {
763 if let Value::Object(map) = item {
764 if map.contains_key("type") {
766 if let Ok(file) =
768 serde_json::from_value::<SampleFile>(Value::Object(map.clone()))
769 {
770 result.files.push(file);
771 }
772 } else {
773 if let Some(loc) = extract_location(&map) {
775 if let Some(ref mut existing) = result.location {
777 if loc.gps.is_some() {
778 existing.gps = loc.gps;
779 }
780 if loc.imu.is_some() {
781 existing.imu = loc.imu;
782 }
783 } else {
784 result.location = Some(loc);
785 }
786 } else {
787 for (file_type, value) in map {
789 if let Some(file) = create_sample_file_from_value(file_type, value)
790 {
791 result.files.push(file);
792 }
793 }
794 }
795 }
796 }
797 }
798 result
799 }
800 Some(Value::Object(map)) => {
801 if let Some(loc) = extract_location(&map) {
803 result.location = Some(loc);
804 }
805
806 for (key, value) in map {
808 if key != "gps"
809 && key != "imu"
810 && let Some(file) = create_sample_file_from_value(key, value)
811 {
812 result.files.push(file);
813 }
814 }
815 result
816 }
817 Some(_) => result,
818 }
819}
820
821#[derive(Deserialize)]
825struct SampleRaw {
826 #[serde(default)]
827 id: Option<SampleID>,
828 #[serde(alias = "group_name")]
829 group: Option<String>,
830 sequence_name: Option<String>,
831 sequence_uuid: Option<String>,
832 sequence_description: Option<String>,
833 #[serde(default, deserialize_with = "deserialize_frame_number")]
834 frame_number: Option<u32>,
835 uuid: Option<String>,
836 image_name: Option<String>,
837 image_url: Option<String>,
838 width: Option<u32>,
839 height: Option<u32>,
840 date: Option<DateTime<Utc>>,
841 source: Option<String>,
842 degradation: Option<String>,
843 #[serde(default)]
844 neg_label_indices: Option<Vec<u32>>,
845 #[serde(default)]
846 not_exhaustive_label_indices: Option<Vec<u32>>,
847 #[serde(default, alias = "sensors")]
849 sensors: Option<serde_json::Value>,
850 #[serde(default, deserialize_with = "deserialize_annotations")]
851 annotations: Vec<Annotation>,
852}
853
854impl From<SampleRaw> for Sample {
855 fn from(raw: SampleRaw) -> Self {
856 let sensors_data = deserialize_sensors_data(raw.sensors);
857
858 Sample {
859 id: raw.id,
860 group: raw.group,
861 sequence_name: raw.sequence_name,
862 sequence_uuid: raw.sequence_uuid,
863 sequence_description: raw.sequence_description,
864 frame_number: raw.frame_number,
865 uuid: raw.uuid,
866 image_name: raw.image_name,
867 image_url: raw.image_url,
868 width: raw.width,
869 height: raw.height,
870 date: raw.date,
871 source: raw.source,
872 location: sensors_data.location,
873 degradation: raw.degradation,
874 neg_label_indices: raw.neg_label_indices,
875 not_exhaustive_label_indices: raw.not_exhaustive_label_indices,
876 files: sensors_data.files,
877 annotations: raw.annotations,
878 timing: None,
879 }
880 }
881}
882
883impl<'de> serde::Deserialize<'de> for Sample {
884 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
885 where
886 D: serde::Deserializer<'de>,
887 {
888 let raw = SampleRaw::deserialize(deserializer)?;
889 Ok(Sample::from(raw))
890 }
891}
892
893impl Display for Sample {
894 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
895 write!(
896 f,
897 "{} {}",
898 self.id
899 .map(|id| id.to_string())
900 .unwrap_or_else(|| "unknown".to_string()),
901 self.image_name().unwrap_or("unknown")
902 )
903 }
904}
905
906impl Default for Sample {
907 fn default() -> Self {
908 Self::new()
909 }
910}
911
912impl Sample {
913 pub fn new() -> Self {
915 Self {
916 id: None,
917 group: None,
918 sequence_name: None,
919 sequence_uuid: None,
920 sequence_description: None,
921 frame_number: None,
922 uuid: None,
923 image_name: None,
924 image_url: None,
925 width: None,
926 height: None,
927 date: None,
928 source: None,
929 location: None,
930 degradation: None,
931 neg_label_indices: None,
932 not_exhaustive_label_indices: None,
933 files: vec![],
934 annotations: vec![],
935 timing: None,
936 }
937 }
938
939 pub fn id(&self) -> Option<SampleID> {
940 self.id
941 }
942
943 pub fn name(&self) -> Option<String> {
944 self.image_name.as_ref().map(|n| extract_sample_name(n))
945 }
946
947 pub fn group(&self) -> Option<&String> {
948 self.group.as_ref()
949 }
950
951 pub fn sequence_name(&self) -> Option<&String> {
952 self.sequence_name.as_ref()
953 }
954
955 pub fn sequence_uuid(&self) -> Option<&String> {
956 self.sequence_uuid.as_ref()
957 }
958
959 pub fn sequence_description(&self) -> Option<&String> {
960 self.sequence_description.as_ref()
961 }
962
963 pub fn frame_number(&self) -> Option<u32> {
964 self.frame_number
965 }
966
967 pub fn uuid(&self) -> Option<&String> {
968 self.uuid.as_ref()
969 }
970
971 pub fn image_name(&self) -> Option<&str> {
972 self.image_name.as_deref()
973 }
974
975 pub fn image_url(&self) -> Option<&str> {
976 self.image_url.as_deref()
977 }
978
979 pub fn width(&self) -> Option<u32> {
980 self.width
981 }
982
983 pub fn height(&self) -> Option<u32> {
984 self.height
985 }
986
987 pub fn date(&self) -> Option<DateTime<Utc>> {
988 self.date
989 }
990
991 pub fn source(&self) -> Option<&String> {
992 self.source.as_ref()
993 }
994
995 pub fn location(&self) -> Option<&Location> {
996 self.location.as_ref()
997 }
998
999 pub fn files(&self) -> &[SampleFile] {
1000 &self.files
1001 }
1002
1003 pub fn annotations(&self) -> &[Annotation] {
1004 &self.annotations
1005 }
1006
1007 pub fn with_annotations(mut self, annotations: Vec<Annotation>) -> Self {
1008 self.annotations = annotations;
1009 self
1010 }
1011
1012 pub fn with_frame_number(mut self, frame_number: Option<u32>) -> Self {
1013 self.frame_number = frame_number;
1014 self
1015 }
1016
1017 pub async fn download(
1024 &self,
1025 client: &Client,
1026 file_type: FileType,
1027 ) -> Result<Option<Vec<u8>>, Error> {
1028 use base64::{Engine, engine::general_purpose::STANDARD};
1029
1030 if file_type == FileType::Image {
1032 if let Some(url) = self.image_url.as_deref()
1033 && is_valid_url(url)
1034 {
1035 return Ok(Some(client.download(url).await?));
1036 }
1037 return Ok(None);
1038 }
1039
1040 let file = resolve_file(&file_type, &self.files);
1042
1043 match file {
1044 Some(f) => {
1045 if let Some(url) = f.url() {
1047 return Ok(Some(client.download(url).await?));
1048 }
1049
1050 if let Some(data) = f.data() {
1052 let decoded = if let Ok(bytes) = STANDARD.decode(data) {
1059 if let Ok(text) = String::from_utf8(bytes.clone()) {
1061 if text.starts_with('{') {
1062 text
1064 } else {
1065 return Ok(Some(bytes));
1067 }
1068 } else {
1069 return Ok(Some(bytes));
1071 }
1072 } else {
1073 data.to_string()
1075 };
1076
1077 let content = if decoded.starts_with('{') {
1079 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&decoded) {
1080 if let Some(obj) = json.as_object() {
1081 obj.values()
1082 .next()
1083 .and_then(|v| v.as_str())
1084 .map(|s| s.to_string())
1085 .unwrap_or(decoded)
1086 } else {
1087 decoded
1088 }
1089 } else {
1090 decoded
1091 }
1092 } else {
1093 decoded
1094 };
1095
1096 return Ok(Some(content.as_bytes().to_vec()));
1097 }
1098
1099 Ok(None)
1100 }
1101 None => Ok(None),
1102 }
1103 }
1104}
1105
1106#[derive(Serialize, Deserialize, Clone, Debug)]
1114pub struct SampleFile {
1115 r#type: String,
1116 #[serde(skip_serializing_if = "Option::is_none")]
1117 url: Option<String>,
1118 #[serde(skip_serializing_if = "Option::is_none")]
1119 filename: Option<String>,
1120 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1122 data: Option<String>,
1123 #[serde(skip)]
1126 bytes: Option<Vec<u8>>,
1127}
1128
1129impl SampleFile {
1130 pub fn with_url(file_type: String, url: String) -> Self {
1132 Self {
1133 r#type: file_type,
1134 url: Some(url),
1135 filename: None,
1136 data: None,
1137 bytes: None,
1138 }
1139 }
1140
1141 pub fn with_filename(file_type: String, filename: String) -> Self {
1143 Self {
1144 r#type: file_type,
1145 url: None,
1146 filename: Some(filename),
1147 data: None,
1148 bytes: None,
1149 }
1150 }
1151
1152 pub fn with_data(file_type: String, data: String) -> Self {
1154 Self {
1155 r#type: file_type,
1156 url: None,
1157 filename: None,
1158 data: Some(data),
1159 bytes: None,
1160 }
1161 }
1162
1163 pub fn with_bytes(file_type: String, filename: String, bytes: Vec<u8>) -> Self {
1173 Self {
1174 r#type: file_type,
1175 url: None,
1176 filename: Some(filename),
1177 data: None,
1178 bytes: Some(bytes),
1179 }
1180 }
1181
1182 pub fn file_type(&self) -> &str {
1183 &self.r#type
1184 }
1185
1186 pub fn url(&self) -> Option<&str> {
1187 self.url.as_deref()
1188 }
1189
1190 pub fn filename(&self) -> Option<&str> {
1191 self.filename.as_deref()
1192 }
1193
1194 pub fn data(&self) -> Option<&str> {
1196 self.data.as_deref()
1197 }
1198
1199 pub fn bytes(&self) -> Option<&[u8]> {
1201 self.bytes.as_deref()
1202 }
1203}
1204
1205#[derive(Serialize, Deserialize, Clone, Debug)]
1210pub struct Location {
1211 #[serde(skip_serializing_if = "Option::is_none")]
1212 pub gps: Option<GpsData>,
1213 #[serde(skip_serializing_if = "Option::is_none")]
1214 pub imu: Option<ImuData>,
1215}
1216
1217#[derive(Serialize, Deserialize, Clone, Debug)]
1219pub struct GpsData {
1220 pub lat: f64,
1221 pub lon: f64,
1222}
1223
1224impl GpsData {
1225 pub fn validate(&self) -> Result<(), String> {
1255 validate_gps_coordinates(self.lat, self.lon)
1256 }
1257}
1258
1259#[derive(Serialize, Deserialize, Clone, Debug)]
1261pub struct ImuData {
1262 pub roll: f64,
1263 pub pitch: f64,
1264 pub yaw: f64,
1265}
1266
1267impl ImuData {
1268 pub fn validate(&self) -> Result<(), String> {
1301 validate_imu_orientation(self.roll, self.pitch, self.yaw)
1302 }
1303}
1304
1305#[allow(dead_code)]
1306pub trait TypeName {
1307 fn type_name() -> String;
1308}
1309
1310#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
1311pub struct Box3d {
1312 x: f32,
1313 y: f32,
1314 z: f32,
1315 w: f32,
1316 h: f32,
1317 l: f32,
1318}
1319
1320impl TypeName for Box3d {
1321 fn type_name() -> String {
1322 "box3d".to_owned()
1323 }
1324}
1325
1326impl Box3d {
1327 pub fn new(cx: f32, cy: f32, cz: f32, width: f32, height: f32, length: f32) -> Self {
1328 Self {
1329 x: cx,
1330 y: cy,
1331 z: cz,
1332 w: width,
1333 h: height,
1334 l: length,
1335 }
1336 }
1337
1338 pub fn width(&self) -> f32 {
1339 self.w
1340 }
1341
1342 pub fn height(&self) -> f32 {
1343 self.h
1344 }
1345
1346 pub fn length(&self) -> f32 {
1347 self.l
1348 }
1349
1350 pub fn cx(&self) -> f32 {
1351 self.x
1352 }
1353
1354 pub fn cy(&self) -> f32 {
1355 self.y
1356 }
1357
1358 pub fn cz(&self) -> f32 {
1359 self.z
1360 }
1361
1362 pub fn left(&self) -> f32 {
1363 self.x - self.w / 2.0
1364 }
1365
1366 pub fn top(&self) -> f32 {
1367 self.y - self.h / 2.0
1368 }
1369
1370 pub fn front(&self) -> f32 {
1371 self.z - self.l / 2.0
1372 }
1373}
1374
1375#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
1376pub struct Box2d {
1377 h: f32,
1378 w: f32,
1379 x: f32,
1380 y: f32,
1381}
1382
1383impl TypeName for Box2d {
1384 fn type_name() -> String {
1385 "box2d".to_owned()
1386 }
1387}
1388
1389impl Box2d {
1390 pub fn new(left: f32, top: f32, width: f32, height: f32) -> Self {
1391 Self {
1392 x: left,
1393 y: top,
1394 w: width,
1395 h: height,
1396 }
1397 }
1398
1399 pub fn width(&self) -> f32 {
1400 self.w
1401 }
1402
1403 pub fn height(&self) -> f32 {
1404 self.h
1405 }
1406
1407 pub fn left(&self) -> f32 {
1408 self.x
1409 }
1410
1411 pub fn top(&self) -> f32 {
1412 self.y
1413 }
1414
1415 pub fn cx(&self) -> f32 {
1416 self.x + self.w / 2.0
1417 }
1418
1419 pub fn cy(&self) -> f32 {
1420 self.y + self.h / 2.0
1421 }
1422}
1423
1424#[derive(Clone, Debug, PartialEq)]
1425pub struct Polygon {
1426 pub rings: Vec<Vec<(f32, f32)>>,
1427}
1428
1429impl TypeName for Polygon {
1430 fn type_name() -> String {
1431 "polygon".to_owned()
1432 }
1433}
1434
1435impl Polygon {
1436 pub fn new(rings: Vec<Vec<(f32, f32)>>) -> Self {
1437 Self { rings }
1438 }
1439}
1440
1441impl serde::Serialize for Polygon {
1442 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1443 where
1444 S: serde::Serializer,
1445 {
1446 serde::Serialize::serialize(&self.rings, serializer)
1447 }
1448}
1449
1450impl<'de> serde::Deserialize<'de> for Polygon {
1451 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1452 where
1453 D: serde::Deserializer<'de>,
1454 {
1455 let value = serde_json::Value::deserialize(deserializer)?;
1457
1458 let polygon_value = if let Some(obj) = value.as_object() {
1460 obj.get("rings")
1462 .or_else(|| obj.get("polygon"))
1463 .cloned()
1464 .unwrap_or(serde_json::Value::Null)
1465 } else {
1466 value
1468 };
1469
1470 let rings = parse_polygon_value(&polygon_value);
1472
1473 Ok(Self { rings })
1474 }
1475}
1476
1477fn parse_polygon_value(value: &serde_json::Value) -> Vec<Vec<(f32, f32)>> {
1485 let Some(outer_array) = value.as_array() else {
1486 return vec![];
1487 };
1488
1489 let mut result = Vec::new();
1490
1491 for ring in outer_array {
1492 let Some(ring_array) = ring.as_array() else {
1493 continue;
1494 };
1495
1496 let is_3d = ring_array
1498 .first()
1499 .map(|first| first.is_array())
1500 .unwrap_or(false);
1501
1502 let points: Vec<(f32, f32)> = if is_3d {
1503 ring_array
1505 .iter()
1506 .filter_map(|point| {
1507 let arr = point.as_array()?;
1508 if arr.len() >= 2 {
1509 let x = arr[0].as_f64()? as f32;
1510 let y = arr[1].as_f64()? as f32;
1511 if x.is_finite() && y.is_finite() {
1512 Some((x, y))
1513 } else {
1514 None
1515 }
1516 } else {
1517 None
1518 }
1519 })
1520 .collect()
1521 } else {
1522 ring_array
1524 .chunks(2)
1525 .filter_map(|chunk| {
1526 if chunk.len() >= 2 {
1527 let x = chunk[0].as_f64()? as f32;
1528 let y = chunk[1].as_f64()? as f32;
1529 if x.is_finite() && y.is_finite() {
1530 Some((x, y))
1531 } else {
1532 None
1533 }
1534 } else {
1535 None
1536 }
1537 })
1538 .collect()
1539 };
1540
1541 if points.len() >= 3 {
1543 result.push(points);
1544 }
1545 }
1546
1547 result
1548}
1549
1550#[derive(Deserialize)]
1555struct AnnotationRaw {
1556 #[serde(default)]
1557 sample_id: Option<SampleID>,
1558 #[serde(default)]
1559 name: Option<String>,
1560 #[serde(default)]
1561 sequence_name: Option<String>,
1562 #[serde(default)]
1563 frame_number: Option<u32>,
1564 #[serde(rename = "group_name", default)]
1565 group: Option<String>,
1566 #[serde(rename = "object_reference", alias = "object_id", default)]
1567 object_id: Option<String>,
1568 #[serde(default)]
1569 label_name: Option<String>,
1570 #[serde(default)]
1571 label_index: Option<u64>,
1572 #[serde(default)]
1573 iscrowd: Option<bool>,
1574 #[serde(default)]
1575 category_frequency: Option<String>,
1576 #[serde(default)]
1578 box2d: Option<Box2d>,
1579 #[serde(default)]
1580 box3d: Option<Box3d>,
1581 #[serde(default, alias = "mask")]
1582 polygon: Option<Polygon>,
1583 #[serde(default)]
1585 x: Option<f64>,
1586 #[serde(default)]
1587 y: Option<f64>,
1588 #[serde(default)]
1589 w: Option<f64>,
1590 #[serde(default)]
1591 h: Option<f64>,
1592}
1593
1594#[derive(Serialize, Clone, Debug)]
1595pub struct Annotation {
1596 #[serde(skip_serializing_if = "Option::is_none")]
1597 sample_id: Option<SampleID>,
1598 #[serde(skip_serializing_if = "Option::is_none")]
1599 name: Option<String>,
1600 #[serde(skip_serializing_if = "Option::is_none")]
1601 sequence_name: Option<String>,
1602 #[serde(skip_serializing_if = "Option::is_none")]
1603 frame_number: Option<u32>,
1604 #[serde(rename = "group_name", skip_serializing_if = "Option::is_none")]
1608 group: Option<String>,
1609 #[serde(
1613 rename = "object_reference",
1614 alias = "object_id",
1615 skip_serializing_if = "Option::is_none"
1616 )]
1617 object_id: Option<String>,
1618 #[serde(skip_serializing_if = "Option::is_none")]
1619 label_name: Option<String>,
1620 #[serde(skip_serializing_if = "Option::is_none")]
1621 label_index: Option<u64>,
1622 #[serde(default, skip_serializing_if = "Option::is_none")]
1624 iscrowd: Option<bool>,
1625 #[serde(default, skip_serializing_if = "Option::is_none")]
1627 category_frequency: Option<String>,
1628 #[serde(skip_serializing_if = "Option::is_none")]
1629 box2d: Option<Box2d>,
1630 #[serde(skip_serializing_if = "Option::is_none")]
1631 box3d: Option<Box3d>,
1632 #[serde(rename(serialize = "mask"), skip_serializing_if = "Option::is_none")]
1641 polygon: Option<Polygon>,
1642 #[serde(skip)]
1644 mask: Option<MaskData>,
1645 #[serde(skip_serializing_if = "Option::is_none")]
1647 box2d_score: Option<f32>,
1648 #[serde(skip_serializing_if = "Option::is_none")]
1650 box3d_score: Option<f32>,
1651 #[serde(skip_serializing_if = "Option::is_none")]
1653 polygon_score: Option<f32>,
1654 #[serde(skip_serializing_if = "Option::is_none")]
1656 mask_score: Option<f32>,
1657}
1658
1659impl<'de> serde::Deserialize<'de> for Annotation {
1660 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1661 where
1662 D: serde::Deserializer<'de>,
1663 {
1664 let raw: AnnotationRaw = serde::Deserialize::deserialize(deserializer)?;
1666
1667 let box2d = raw.box2d.or_else(|| match (raw.x, raw.y, raw.w, raw.h) {
1669 (Some(x), Some(y), Some(w), Some(h)) if w > 0.0 && h > 0.0 => {
1670 Some(Box2d::new(x as f32, y as f32, w as f32, h as f32))
1671 }
1672 _ => None,
1673 });
1674
1675 Ok(Annotation {
1676 sample_id: raw.sample_id,
1677 name: raw.name,
1678 sequence_name: raw.sequence_name,
1679 frame_number: raw.frame_number,
1680 group: raw.group,
1681 object_id: raw.object_id,
1682 label_name: raw.label_name,
1683 label_index: raw.label_index,
1684 iscrowd: raw.iscrowd,
1685 category_frequency: raw.category_frequency,
1686 box2d,
1687 box3d: raw.box3d,
1688 polygon: raw.polygon,
1689 mask: None,
1690 box2d_score: None,
1691 box3d_score: None,
1692 polygon_score: None,
1693 mask_score: None,
1694 })
1695 }
1696}
1697
1698impl Default for Annotation {
1699 fn default() -> Self {
1700 Self::new()
1701 }
1702}
1703
1704impl Annotation {
1705 pub fn new() -> Self {
1706 Self {
1707 sample_id: None,
1708 name: None,
1709 sequence_name: None,
1710 frame_number: None,
1711 group: None,
1712 object_id: None,
1713 label_name: None,
1714 label_index: None,
1715 iscrowd: None,
1716 category_frequency: None,
1717 box2d: None,
1718 box3d: None,
1719 polygon: None,
1720 mask: None,
1721 box2d_score: None,
1722 box3d_score: None,
1723 polygon_score: None,
1724 mask_score: None,
1725 }
1726 }
1727
1728 pub fn set_sample_id(&mut self, sample_id: Option<SampleID>) {
1729 self.sample_id = sample_id;
1730 }
1731
1732 pub fn sample_id(&self) -> Option<SampleID> {
1733 self.sample_id
1734 }
1735
1736 pub fn set_name(&mut self, name: Option<String>) {
1737 self.name = name;
1738 }
1739
1740 pub fn name(&self) -> Option<&String> {
1741 self.name.as_ref()
1742 }
1743
1744 pub fn set_sequence_name(&mut self, sequence_name: Option<String>) {
1745 self.sequence_name = sequence_name;
1746 }
1747
1748 pub fn sequence_name(&self) -> Option<&String> {
1749 self.sequence_name.as_ref()
1750 }
1751
1752 pub fn set_frame_number(&mut self, frame_number: Option<u32>) {
1753 self.frame_number = frame_number;
1754 }
1755
1756 pub fn frame_number(&self) -> Option<u32> {
1757 self.frame_number
1758 }
1759
1760 pub fn set_group(&mut self, group: Option<String>) {
1761 self.group = group;
1762 }
1763
1764 pub fn group(&self) -> Option<&String> {
1765 self.group.as_ref()
1766 }
1767
1768 pub fn object_id(&self) -> Option<&String> {
1769 self.object_id.as_ref()
1770 }
1771
1772 pub fn set_object_id(&mut self, object_id: Option<String>) {
1773 self.object_id = object_id;
1774 }
1775
1776 pub fn label(&self) -> Option<&String> {
1777 self.label_name.as_ref()
1778 }
1779
1780 pub fn set_label(&mut self, label_name: Option<String>) {
1781 self.label_name = label_name;
1782 }
1783
1784 pub fn label_index(&self) -> Option<u64> {
1785 self.label_index
1786 }
1787
1788 pub fn set_label_index(&mut self, label_index: Option<u64>) {
1789 self.label_index = label_index;
1790 }
1791
1792 pub fn iscrowd(&self) -> Option<bool> {
1793 self.iscrowd
1794 }
1795
1796 pub fn set_iscrowd(&mut self, iscrowd: Option<bool>) {
1797 self.iscrowd = iscrowd;
1798 }
1799
1800 pub fn category_frequency(&self) -> Option<&String> {
1801 self.category_frequency.as_ref()
1802 }
1803
1804 pub fn set_category_frequency(&mut self, category_frequency: Option<String>) {
1805 self.category_frequency = category_frequency;
1806 }
1807
1808 pub fn box2d(&self) -> Option<&Box2d> {
1809 self.box2d.as_ref()
1810 }
1811
1812 pub fn set_box2d(&mut self, box2d: Option<Box2d>) {
1813 self.box2d = box2d;
1814 }
1815
1816 pub fn box3d(&self) -> Option<&Box3d> {
1817 self.box3d.as_ref()
1818 }
1819
1820 pub fn set_box3d(&mut self, box3d: Option<Box3d>) {
1821 self.box3d = box3d;
1822 }
1823
1824 pub fn polygon(&self) -> Option<&Polygon> {
1825 self.polygon.as_ref()
1826 }
1827
1828 pub fn set_polygon(&mut self, polygon: Option<Polygon>) {
1829 self.polygon = polygon;
1830 }
1831
1832 pub fn mask(&self) -> Option<&MaskData> {
1833 self.mask.as_ref()
1834 }
1835
1836 pub fn set_mask(&mut self, mask: Option<MaskData>) {
1837 self.mask = mask;
1838 }
1839
1840 pub fn box2d_score(&self) -> Option<f32> {
1841 self.box2d_score
1842 }
1843
1844 pub fn set_box2d_score(&mut self, score: Option<f32>) {
1845 self.box2d_score = score;
1846 }
1847
1848 pub fn box3d_score(&self) -> Option<f32> {
1849 self.box3d_score
1850 }
1851
1852 pub fn set_box3d_score(&mut self, score: Option<f32>) {
1853 self.box3d_score = score;
1854 }
1855
1856 pub fn polygon_score(&self) -> Option<f32> {
1857 self.polygon_score
1858 }
1859
1860 pub fn set_polygon_score(&mut self, score: Option<f32>) {
1861 self.polygon_score = score;
1862 }
1863
1864 pub fn mask_score(&self) -> Option<f32> {
1865 self.mask_score
1866 }
1867
1868 pub fn set_mask_score(&mut self, score: Option<f32>) {
1869 self.mask_score = score;
1870 }
1871}
1872
1873#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
1882pub struct Label {
1883 id: u64,
1884 #[serde(default)]
1885 dataset_id: Option<DatasetID>,
1886 index: u64,
1887 name: String,
1888 #[serde(default)]
1889 color: Option<u64>,
1890}
1891
1892impl Label {
1893 pub fn id(&self) -> u64 {
1894 self.id
1895 }
1896
1897 pub fn dataset_id(&self) -> Option<DatasetID> {
1902 self.dataset_id
1903 }
1904
1905 pub(crate) fn backfill_dataset_id(&mut self, dataset_id: DatasetID) {
1909 if self.dataset_id.is_none() {
1910 self.dataset_id = Some(dataset_id);
1911 }
1912 }
1913
1914 pub fn index(&self) -> u64 {
1915 self.index
1916 }
1917
1918 pub fn name(&self) -> &str {
1919 &self.name
1920 }
1921
1922 pub fn color(&self) -> Option<u64> {
1925 self.color
1926 }
1927
1928 pub async fn remove(&self, client: &Client) -> Result<(), Error> {
1929 client.remove_label(self.id()).await
1930 }
1931
1932 pub async fn set_name(&mut self, client: &Client, name: &str) -> Result<(), Error> {
1933 self.name = name.to_string();
1934 client.update_label(self).await
1935 }
1936
1937 pub async fn set_index(&mut self, client: &Client, index: u64) -> Result<(), Error> {
1938 self.index = index;
1939 client.update_label(self).await
1940 }
1941}
1942
1943impl Display for Label {
1944 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1945 write!(f, "{}", self.name())
1946 }
1947}
1948
1949#[derive(Serialize, Clone, Debug)]
1950pub struct NewLabelObject {
1951 pub name: String,
1952}
1953
1954#[derive(Serialize, Clone, Debug)]
1955pub struct NewLabel {
1956 pub dataset_id: DatasetID,
1957 pub labels: Vec<NewLabelObject>,
1958}
1959
1960#[derive(Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
1990pub struct Group {
1991 pub id: u64,
1996
1997 pub name: String,
2001}
2002
2003#[cfg(feature = "polars")]
2004fn extract_annotation_name(ann: &Annotation) -> Option<(String, Option<u32>)> {
2005 use std::path::Path;
2006
2007 let name = ann.name.as_ref()?;
2008 let name = Path::new(name).file_stem()?.to_str()?;
2009
2010 match &ann.sequence_name {
2013 Some(sequence) => Some((sequence.clone(), ann.frame_number)),
2014 None => Some((name.to_string(), None)),
2015 }
2016}
2017
2018#[cfg(feature = "polars")]
2022fn convert_polygon_to_nested_series(polygon: &Polygon) -> Series {
2023 let ring_series: Vec<Option<Series>> = polygon
2024 .rings
2025 .iter()
2026 .map(|ring| {
2027 let coords: Vec<f32> = ring.iter().flat_map(|&(x, y)| [x, y]).collect();
2028 Some(Series::new("".into(), coords))
2029 })
2030 .collect();
2031 Series::new("".into(), ring_series)
2032}
2033
2034#[cfg(feature = "polars")]
2084pub fn samples_dataframe(samples: &[Sample]) -> Result<DataFrame, Error> {
2085 let mut names: Vec<String> = Vec::new();
2087 let mut frames: Vec<Option<u32>> = Vec::new();
2088 let mut objects: Vec<Option<String>> = Vec::new();
2089 let mut labels: Vec<Option<String>> = Vec::new();
2090 let mut label_indices: Vec<Option<u64>> = Vec::new();
2091 let mut groups: Vec<Option<String>> = Vec::new();
2092 let mut polygons: Vec<Option<Series>> = Vec::new();
2093 let mut boxes2d: Vec<Option<Series>> = Vec::new();
2094 let mut boxes3d: Vec<Option<Series>> = Vec::new();
2095 let mut mask_bytes: Vec<Option<Vec<u8>>> = Vec::new();
2096 let mut box2d_scores: Vec<Option<f32>> = Vec::new();
2097 let mut box3d_scores: Vec<Option<f32>> = Vec::new();
2098 let mut polygon_scores: Vec<Option<f32>> = Vec::new();
2099 let mut mask_scores: Vec<Option<f32>> = Vec::new();
2100 let mut sizes: Vec<Option<Vec<u32>>> = Vec::new();
2101 let mut locations: Vec<Option<Vec<f32>>> = Vec::new();
2102 let mut poses: Vec<Option<Vec<f32>>> = Vec::new();
2103 let mut degradations: Vec<Option<String>> = Vec::new();
2104 let mut iscrowds: Vec<Option<bool>> = Vec::new();
2105 let mut category_frequencies: Vec<Option<String>> = Vec::new();
2106 let mut neg_label_indices_vec: Vec<Option<Vec<u32>>> = Vec::new();
2107 let mut not_exhaustive_label_indices_vec: Vec<Option<Vec<u32>>> = Vec::new();
2108 let mut timing_load: Vec<Option<i64>> = Vec::new();
2109 let mut timing_preprocess: Vec<Option<i64>> = Vec::new();
2110 let mut timing_inference: Vec<Option<i64>> = Vec::new();
2111 let mut timing_decode: Vec<Option<i64>> = Vec::new();
2112
2113 for sample in samples {
2114 let size = match (sample.width, sample.height) {
2116 (Some(w), Some(h)) => Some(vec![w, h]),
2117 _ => None,
2118 };
2119
2120 let location = sample.location.as_ref().and_then(|loc| {
2121 loc.gps
2122 .as_ref()
2123 .map(|gps| vec![gps.lat as f32, gps.lon as f32])
2124 });
2125
2126 let pose = sample.location.as_ref().and_then(|loc| {
2127 loc.imu
2128 .as_ref()
2129 .map(|imu| vec![imu.yaw as f32, imu.pitch as f32, imu.roll as f32])
2130 });
2131
2132 let degradation = sample.degradation.clone();
2133
2134 let t_load = sample.timing.as_ref().and_then(|t| t.load);
2136 let t_preprocess = sample.timing.as_ref().and_then(|t| t.preprocess);
2137 let t_inference = sample.timing.as_ref().and_then(|t| t.inference);
2138 let t_decode = sample.timing.as_ref().and_then(|t| t.decode);
2139
2140 macro_rules! push_sample_fields {
2142 () => {
2143 sizes.push(size.clone());
2144 locations.push(location.clone());
2145 poses.push(pose.clone());
2146 degradations.push(degradation.clone());
2147 neg_label_indices_vec.push(sample.neg_label_indices.clone());
2148 not_exhaustive_label_indices_vec.push(sample.not_exhaustive_label_indices.clone());
2149 timing_load.push(t_load);
2150 timing_preprocess.push(t_preprocess);
2151 timing_inference.push(t_inference);
2152 timing_decode.push(t_decode);
2153 };
2154 }
2155
2156 if sample.annotations.is_empty() {
2157 let (name, frame) = match extract_annotation_name_from_sample(sample) {
2159 Some(nf) => nf,
2160 None => continue,
2161 };
2162
2163 names.push(name);
2164 frames.push(frame);
2165 objects.push(None);
2166 labels.push(None);
2167 label_indices.push(None);
2168 groups.push(sample.group.clone());
2169 polygons.push(None);
2170 boxes2d.push(None);
2171 boxes3d.push(None);
2172 mask_bytes.push(None);
2173 box2d_scores.push(None);
2174 box3d_scores.push(None);
2175 polygon_scores.push(None);
2176 mask_scores.push(None);
2177 iscrowds.push(None);
2178 category_frequencies.push(None);
2179 push_sample_fields!();
2180 } else {
2181 for ann in &sample.annotations {
2183 let (name, frame) = match extract_annotation_name(ann) {
2184 Some(nf) => nf,
2185 None => continue,
2186 };
2187
2188 let polygon = ann.polygon.as_ref().map(convert_polygon_to_nested_series);
2189
2190 let box2d = ann
2191 .box2d
2192 .as_ref()
2193 .map(|b| Series::new("box2d".into(), [b.cx(), b.cy(), b.width(), b.height()]));
2194
2195 let box3d = ann
2196 .box3d
2197 .as_ref()
2198 .map(|b| Series::new("box3d".into(), [b.x, b.y, b.z, b.w, b.h, b.l]));
2199
2200 names.push(name);
2201 frames.push(frame);
2202 objects.push(ann.object_id().cloned());
2203 labels.push(ann.label_name.clone());
2204 label_indices.push(ann.label_index);
2205 groups.push(sample.group.clone());
2206 polygons.push(polygon);
2207 boxes2d.push(box2d);
2208 boxes3d.push(box3d);
2209 mask_bytes.push(ann.mask.as_ref().map(|m| m.as_bytes().to_vec()));
2210 box2d_scores.push(ann.box2d_score());
2211 box3d_scores.push(ann.box3d_score());
2212 polygon_scores.push(ann.polygon_score());
2213 mask_scores.push(ann.mask_score());
2214 iscrowds.push(ann.iscrowd);
2215 category_frequencies.push(ann.category_frequency.clone());
2216 push_sample_fields!();
2217 }
2218 }
2219 }
2220
2221 let names_col: Column = Series::new("name".into(), names).into();
2223 let frames_col: Column = Series::new("frame".into(), frames).into();
2224 let objects_col: Column = Series::new("object_id".into(), objects).into();
2225
2226 let labels_col: Column = Series::new("label".into(), labels)
2232 .cast(&DataType::Categorical(
2233 Categories::new("labels".into(), "labels".into(), CategoricalPhysical::U16),
2234 Arc::new(CategoricalMapping::with_hasher(
2235 u16::MAX as usize,
2236 Default::default(),
2237 )),
2238 ))?
2239 .into();
2240
2241 let label_indices_col: Column = Series::new("label_index".into(), label_indices).into();
2242
2243 let groups_col: Column = Series::new("group".into(), groups)
2245 .cast(&DataType::Categorical(
2246 Categories::new("groups".into(), "groups".into(), CategoricalPhysical::U8),
2247 Arc::new(CategoricalMapping::with_hasher(
2248 u8::MAX as usize,
2249 Default::default(),
2250 )),
2251 ))?
2252 .into();
2253
2254 let polygons_col: Column = if polygons.iter().all(|p| p.is_none()) {
2259 Series::new_null("polygon".into(), polygons.len()).into()
2261 } else {
2262 let typed_polygons: Vec<Option<Series>> = polygons
2265 .into_iter()
2266 .map(|opt| {
2267 opt.map(|s| {
2268 s.cast(&DataType::List(Box::new(DataType::Float32)))
2269 .unwrap_or(s)
2270 })
2271 })
2272 .collect();
2273 Series::new("polygon".into(), &typed_polygons)
2274 .cast(&DataType::List(Box::new(DataType::List(Box::new(
2275 DataType::Float32,
2276 )))))?
2277 .into()
2278 };
2279
2280 let boxes2d_col: Column = Series::new("box2d".into(), boxes2d)
2281 .cast(&DataType::Array(Box::new(DataType::Float32), 4))?
2282 .into();
2283 let boxes3d_col: Column = Series::new("box3d".into(), boxes3d)
2284 .cast(&DataType::Array(Box::new(DataType::Float32), 6))?
2285 .into();
2286
2287 let mask_col: Column = Series::new("mask".into(), mask_bytes).into();
2289
2290 let box2d_score_col: Column = Series::new("box2d_score".into(), box2d_scores).into();
2292 let box3d_score_col: Column = Series::new("box3d_score".into(), box3d_scores).into();
2293 let polygon_score_col: Column = Series::new("polygon_score".into(), polygon_scores).into();
2294 let mask_score_col: Column = Series::new("mask_score".into(), mask_scores).into();
2295
2296 let size_series: Vec<Option<Series>> = sizes
2298 .into_iter()
2299 .map(|opt_vec| opt_vec.map(|vec| Series::new("size".into(), vec)))
2300 .collect();
2301 let sizes_col: Column = Series::new("size".into(), size_series)
2302 .cast(&DataType::Array(Box::new(DataType::UInt32), 2))?
2303 .into();
2304
2305 let location_series: Vec<Option<Series>> = locations
2306 .into_iter()
2307 .map(|opt_vec| opt_vec.map(|vec| Series::new("location".into(), vec)))
2308 .collect();
2309 let locations_col: Column = Series::new("location".into(), location_series)
2310 .cast(&DataType::Array(Box::new(DataType::Float32), 2))?
2311 .into();
2312
2313 let pose_series: Vec<Option<Series>> = poses
2314 .into_iter()
2315 .map(|opt_vec| opt_vec.map(|vec| Series::new("pose".into(), vec)))
2316 .collect();
2317 let poses_col: Column = Series::new("pose".into(), pose_series)
2318 .cast(&DataType::Array(Box::new(DataType::Float32), 3))?
2319 .into();
2320
2321 let degradations_col: Column = Series::new("degradation".into(), degradations).into();
2322
2323 let iscrowds_col: Column = Series::new("iscrowd".into(), iscrowds).into();
2325
2326 let category_frequencies_col: Column =
2327 Series::new("category_frequency".into(), category_frequencies)
2328 .cast(&DataType::Categorical(
2329 Categories::new(
2330 "cat_freq".into(),
2331 "cat_freq".into(),
2332 CategoricalPhysical::U8,
2333 ),
2334 Arc::new(CategoricalMapping::with_hasher(
2335 u8::MAX as usize,
2336 Default::default(),
2337 )),
2338 ))?
2339 .into();
2340
2341 let neg_label_indices_series: Vec<Option<Series>> = neg_label_indices_vec
2342 .into_iter()
2343 .map(|opt_vec| opt_vec.map(|vec| Series::new("neg_label_indices".into(), vec)))
2344 .collect();
2345 let neg_label_indices_col: Column =
2346 Series::new("neg_label_indices".into(), neg_label_indices_series)
2347 .cast(&DataType::List(Box::new(DataType::UInt32)))?
2348 .into();
2349
2350 let not_exhaustive_label_indices_series: Vec<Option<Series>> = not_exhaustive_label_indices_vec
2351 .into_iter()
2352 .map(|opt_vec| opt_vec.map(|vec| Series::new("not_exhaustive_label_indices".into(), vec)))
2353 .collect();
2354 let not_exhaustive_label_indices_col: Column = Series::new(
2355 "not_exhaustive_label_indices".into(),
2356 not_exhaustive_label_indices_series,
2357 )
2358 .cast(&DataType::List(Box::new(DataType::UInt32)))?
2359 .into();
2360
2361 let timing_col: Column = StructChunked::from_series(
2363 "timing".into(),
2364 frames_col.len(),
2365 [
2366 Series::new("load".into(), &timing_load),
2367 Series::new("preprocess".into(), &timing_preprocess),
2368 Series::new("inference".into(), &timing_inference),
2369 Series::new("decode".into(), &timing_decode),
2370 ]
2371 .iter(),
2372 )?
2373 .into_series()
2374 .into();
2375
2376 let all_columns: Vec<Column> = vec![
2378 names_col,
2379 frames_col,
2380 objects_col,
2381 labels_col,
2382 label_indices_col,
2383 groups_col,
2384 polygons_col,
2385 boxes2d_col,
2386 boxes3d_col,
2387 mask_col,
2388 box2d_score_col,
2389 box3d_score_col,
2390 polygon_score_col,
2391 mask_score_col,
2392 sizes_col,
2393 locations_col,
2394 poses_col,
2395 degradations_col,
2396 iscrowds_col,
2397 category_frequencies_col,
2398 neg_label_indices_col,
2399 not_exhaustive_label_indices_col,
2400 timing_col,
2401 ];
2402
2403 let height = all_columns.first().map(|c| c.len()).unwrap_or(0);
2404
2405 let non_empty_columns: Vec<Column> = all_columns
2406 .into_iter()
2407 .filter(|col| col.name() == "name" || !is_all_null_column(col))
2408 .collect();
2409
2410 Ok(DataFrame::new(height, non_empty_columns)?)
2411}
2412
2413#[cfg(feature = "polars")]
2417fn is_all_null_column(col: &Column) -> bool {
2418 if col.is_empty() {
2419 return true;
2420 }
2421 if col.null_count() == col.len() {
2422 return true;
2423 }
2424 if let DataType::Struct(..) = col.dtype()
2426 && let Ok(s) = col.as_materialized_series().struct_()
2427 {
2428 return s
2429 .fields_as_series()
2430 .iter()
2431 .all(|field| field.null_count() == field.len());
2432 }
2433 false
2434}
2435
2436#[cfg(feature = "polars")]
2438fn extract_annotation_name_from_sample(sample: &Sample) -> Option<(String, Option<u32>)> {
2439 use std::path::Path;
2440
2441 let name = sample.image_name.as_ref()?;
2442 let name = Path::new(name).file_stem()?.to_str()?;
2443
2444 match &sample.sequence_name {
2447 Some(sequence) => Some((sequence.clone(), sample.frame_number)),
2448 None => Some((name.to_string(), None)),
2449 }
2450}
2451
2452fn extract_sample_name(image_name: &str) -> String {
2465 let name = image_name
2467 .rsplit_once('.')
2468 .and_then(|(name, _)| {
2469 if name.is_empty() {
2471 None
2472 } else {
2473 Some(name.to_string())
2474 }
2475 })
2476 .unwrap_or_else(|| image_name.to_string());
2477
2478 name.rsplit_once(".camera")
2480 .and_then(|(name, _)| {
2481 if name.is_empty() {
2483 None
2484 } else {
2485 Some(name.to_string())
2486 }
2487 })
2488 .unwrap_or_else(|| name.clone())
2489}
2490
2491fn resolve_file<'a>(file_type: &FileType, files: &'a [SampleFile]) -> Option<&'a SampleFile> {
2500 match file_type {
2501 FileType::Image => None, FileType::All => None, file => {
2504 let type_names = file_type_names(file);
2506 files
2507 .iter()
2508 .find(|f| type_names.contains(&f.r#type.as_str()))
2509 }
2510 }
2511}
2512
2513fn file_type_names(file_type: &FileType) -> Vec<&'static str> {
2516 match file_type {
2517 FileType::Image => vec!["image"],
2518 FileType::LidarPcd => vec!["lidar.pcd"],
2519 FileType::LidarDepth => vec!["lidar.depth", "depth.png", "depthmap"],
2520 FileType::LidarReflect => vec!["lidar.reflect"],
2521 FileType::RadarPcd => vec!["radar.pcd", "pcd"],
2522 FileType::RadarCube => vec!["radar.png", "cube"],
2523 FileType::All => vec![],
2524 }
2525}
2526
2527fn convert_annotations_map_to_vec(map: HashMap<String, Vec<Annotation>>) -> Vec<Annotation> {
2540 let mut all_annotations = Vec::new();
2541 if let Some(bbox_anns) = map.get("bbox") {
2542 all_annotations.extend(bbox_anns.clone());
2543 }
2544 if let Some(box3d_anns) = map.get("box3d") {
2545 all_annotations.extend(box3d_anns.clone());
2546 }
2547 if let Some(mask_anns) = map.get("mask") {
2548 all_annotations.extend(mask_anns.clone());
2549 }
2550 all_annotations
2551}
2552
2553fn validate_gps_coordinates(lat: f64, lon: f64) -> Result<(), String> {
2573 if !lat.is_finite() {
2574 return Err(format!("GPS latitude is not finite: {}", lat));
2575 }
2576 if !lon.is_finite() {
2577 return Err(format!("GPS longitude is not finite: {}", lon));
2578 }
2579 if !(-90.0..=90.0).contains(&lat) {
2580 return Err(format!("GPS latitude out of range [-90, 90]: {}", lat));
2581 }
2582 if !(-180.0..=180.0).contains(&lon) {
2583 return Err(format!("GPS longitude out of range [-180, 180]: {}", lon));
2584 }
2585 Ok(())
2586}
2587
2588fn validate_imu_orientation(roll: f64, pitch: f64, yaw: f64) -> Result<(), String> {
2607 if !roll.is_finite() {
2608 return Err(format!("IMU roll is not finite: {}", roll));
2609 }
2610 if !pitch.is_finite() {
2611 return Err(format!("IMU pitch is not finite: {}", pitch));
2612 }
2613 if !yaw.is_finite() {
2614 return Err(format!("IMU yaw is not finite: {}", yaw));
2615 }
2616 if !(-180.0..=180.0).contains(&roll) {
2617 return Err(format!("IMU roll out of range [-180, 180]: {}", roll));
2618 }
2619 if !(-90.0..=90.0).contains(&pitch) {
2620 return Err(format!("IMU pitch out of range [-90, 90]: {}", pitch));
2621 }
2622 if !(-180.0..=180.0).contains(&yaw) {
2623 return Err(format!("IMU yaw out of range [-180, 180]: {}", yaw));
2624 }
2625 Ok(())
2626}
2627
2628#[cfg(feature = "polars")]
2656pub fn unflatten_polygon_coordinates(coords: &[f32]) -> Vec<Vec<(f32, f32)>> {
2657 let mut polygons = Vec::new();
2658 let mut current_polygon = Vec::new();
2659 let mut i = 0;
2660
2661 while i < coords.len() {
2662 if coords[i].is_nan() {
2663 if !current_polygon.is_empty() {
2665 polygons.push(std::mem::take(&mut current_polygon));
2666 }
2667 i += 1;
2668 } else if i + 1 < coords.len() && !coords[i + 1].is_nan() {
2669 current_polygon.push((coords[i], coords[i + 1]));
2671 i += 2;
2672 } else if i + 1 < coords.len() && coords[i + 1].is_nan() {
2673 i += 1;
2676 } else {
2677 i += 1;
2679 }
2680 }
2681
2682 if !current_polygon.is_empty() {
2684 polygons.push(current_polygon);
2685 }
2686
2687 polygons
2688}
2689
2690#[cfg(test)]
2691mod tests {
2692 use super::*;
2693
2694 fn flatten_annotation_map(
2703 map: std::collections::HashMap<String, Vec<Annotation>>,
2704 ) -> Vec<Annotation> {
2705 let mut all_annotations = Vec::new();
2706
2707 for key in ["bbox", "box3d", "mask"] {
2709 if let Some(mut anns) = map.get(key).cloned() {
2710 all_annotations.append(&mut anns);
2711 }
2712 }
2713
2714 all_annotations
2715 }
2716
2717 fn annotation_group_field_name() -> &'static str {
2719 "group_name"
2720 }
2721
2722 fn annotation_object_id_field_name() -> &'static str {
2724 "object_reference"
2725 }
2726
2727 fn annotation_object_id_alias() -> &'static str {
2729 "object_id"
2730 }
2731
2732 fn validate_annotation_field_names(
2735 json_str: &str,
2736 expected_group: bool,
2737 expected_object_ref: bool,
2738 ) -> Result<(), String> {
2739 if expected_group && !json_str.contains("\"group_name\"") {
2740 return Err("Missing expected field: group_name".to_string());
2741 }
2742 if expected_object_ref && !json_str.contains("\"object_reference\"") {
2743 return Err("Missing expected field: object_reference".to_string());
2744 }
2745 Ok(())
2746 }
2747
2748 #[test]
2750 fn test_file_type_conversions() {
2751 let api_cases = vec![
2753 (FileType::Image, "image"),
2754 (FileType::LidarPcd, "lidar.pcd"),
2755 (FileType::LidarDepth, "lidar.depth"),
2756 (FileType::LidarReflect, "lidar.reflect"),
2757 (FileType::RadarPcd, "radar.pcd"),
2758 (FileType::RadarCube, "radar.png"),
2759 ];
2760
2761 let ext_cases = vec![
2763 (FileType::Image, "jpg"),
2764 (FileType::LidarPcd, "lidar.pcd"),
2765 (FileType::LidarDepth, "lidar.png"),
2766 (FileType::LidarReflect, "lidar.jpg"),
2767 (FileType::RadarPcd, "radar.pcd"),
2768 (FileType::RadarCube, "radar.png"),
2769 ];
2770
2771 for (file_type, expected_str) in &api_cases {
2773 assert_eq!(file_type.to_string(), *expected_str);
2774 }
2775
2776 for (file_type, expected_ext) in &ext_cases {
2778 assert_eq!(file_type.file_extension(), *expected_ext);
2779 }
2780
2781 assert_eq!(
2783 FileType::try_from("lidar.depth").unwrap(),
2784 FileType::LidarDepth
2785 );
2786 assert_eq!(
2787 FileType::try_from("lidar.png").unwrap(),
2788 FileType::LidarDepth
2789 );
2790 assert_eq!(
2791 FileType::try_from("depth.png").unwrap(),
2792 FileType::LidarDepth
2793 );
2794 assert_eq!(
2795 FileType::try_from("lidar.reflect").unwrap(),
2796 FileType::LidarReflect
2797 );
2798 assert_eq!(
2799 FileType::try_from("lidar.jpg").unwrap(),
2800 FileType::LidarReflect
2801 );
2802 assert_eq!(
2803 FileType::try_from("lidar.jpeg").unwrap(),
2804 FileType::LidarReflect
2805 );
2806
2807 assert!(FileType::try_from("invalid").is_err());
2809
2810 for (file_type, _) in &api_cases {
2812 let s = file_type.to_string();
2813 let parsed = FileType::try_from(s.as_str()).unwrap();
2814 assert_eq!(parsed, *file_type);
2815 }
2816 }
2817
2818 #[test]
2820 fn test_annotation_type_conversions() {
2821 let cases = vec![
2822 (AnnotationType::Box2d, "box2d"),
2823 (AnnotationType::Box3d, "box3d"),
2824 (AnnotationType::Polygon, "polygon"),
2825 (AnnotationType::Mask, "mask"),
2826 ];
2827
2828 for (ann_type, expected_str) in &cases {
2830 assert_eq!(ann_type.to_string(), *expected_str);
2831 }
2832
2833 assert_eq!(
2835 AnnotationType::try_from("box2d").unwrap(),
2836 AnnotationType::Box2d
2837 );
2838 assert_eq!(
2839 AnnotationType::try_from("box3d").unwrap(),
2840 AnnotationType::Box3d
2841 );
2842 assert_eq!(
2843 AnnotationType::try_from("polygon").unwrap(),
2844 AnnotationType::Polygon
2845 );
2846 assert_eq!(
2848 AnnotationType::try_from("mask").unwrap(),
2849 AnnotationType::Polygon
2850 );
2851 assert_eq!(
2853 AnnotationType::try_from("raster").unwrap(),
2854 AnnotationType::Mask
2855 );
2856
2857 assert_eq!(
2859 AnnotationType::from("box2d".to_string()),
2860 AnnotationType::Box2d
2861 );
2862 assert_eq!(
2863 AnnotationType::from("box3d".to_string()),
2864 AnnotationType::Box3d
2865 );
2866 assert_eq!(
2867 AnnotationType::from("polygon".to_string()),
2868 AnnotationType::Polygon
2869 );
2870 assert_eq!(
2872 AnnotationType::from("mask".to_string()),
2873 AnnotationType::Polygon
2874 );
2875
2876 assert_eq!(
2878 AnnotationType::from("invalid".to_string()),
2879 AnnotationType::Box2d
2880 );
2881
2882 assert!(AnnotationType::try_from("invalid").is_err());
2884
2885 assert_eq!(
2890 AnnotationType::try_from(AnnotationType::Box2d.to_string().as_str()).unwrap(),
2891 AnnotationType::Box2d
2892 );
2893 assert_eq!(
2894 AnnotationType::try_from(AnnotationType::Box3d.to_string().as_str()).unwrap(),
2895 AnnotationType::Box3d
2896 );
2897 assert_eq!(
2898 AnnotationType::try_from(AnnotationType::Polygon.to_string().as_str()).unwrap(),
2899 AnnotationType::Polygon
2900 );
2901 }
2902
2903 #[test]
2904 fn test_annotation_type_as_server_type() {
2905 assert_eq!(AnnotationType::Box2d.as_server_type(), "box2d");
2910 assert_eq!(AnnotationType::Box3d.as_server_type(), "box3d");
2911 assert_eq!(AnnotationType::Polygon.as_server_type(), "mask");
2912 assert_eq!(AnnotationType::Mask.as_server_type(), "mask");
2913
2914 assert_ne!(
2917 AnnotationType::Polygon.as_server_type(),
2918 AnnotationType::Polygon.to_string().as_str()
2919 );
2920 assert_eq!(
2921 AnnotationType::Box2d.as_server_type(),
2922 AnnotationType::Box2d.to_string().as_str()
2923 );
2924 }
2925
2926 #[test]
2928 fn test_extract_sample_name_with_extension_and_camera() {
2929 assert_eq!(extract_sample_name("scene_001.camera.jpg"), "scene_001");
2930 }
2931
2932 #[test]
2933 fn test_extract_sample_name_multiple_dots() {
2934 assert_eq!(extract_sample_name("image.v2.camera.png"), "image.v2");
2935 }
2936
2937 #[test]
2938 fn test_extract_sample_name_extension_only() {
2939 assert_eq!(extract_sample_name("test.jpg"), "test");
2940 }
2941
2942 #[test]
2943 fn test_extract_sample_name_no_extension() {
2944 assert_eq!(extract_sample_name("test"), "test");
2945 }
2946
2947 #[test]
2948 fn test_extract_sample_name_edge_case_dot_prefix() {
2949 assert_eq!(extract_sample_name(".jpg"), ".jpg");
2950 }
2951
2952 #[test]
2954 fn test_resolve_file_image_type_returns_none() {
2955 let files = vec![];
2957 let result = resolve_file(&FileType::Image, &files);
2958 assert!(result.is_none());
2959 }
2960
2961 #[test]
2962 fn test_resolve_file_lidar_pcd() {
2963 let files = vec![
2964 SampleFile::with_url(
2965 "lidar.pcd".to_string(),
2966 "https://example.com/file.pcd".to_string(),
2967 ),
2968 SampleFile::with_url(
2969 "radar.pcd".to_string(),
2970 "https://example.com/radar.pcd".to_string(),
2971 ),
2972 ];
2973 let result = resolve_file(&FileType::LidarPcd, &files);
2974 assert!(result.is_some());
2975 assert_eq!(result.unwrap().url(), Some("https://example.com/file.pcd"));
2976 }
2977
2978 #[test]
2979 fn test_resolve_file_not_found() {
2980 let files = vec![SampleFile::with_url(
2981 "lidar.pcd".to_string(),
2982 "https://example.com/file.pcd".to_string(),
2983 )];
2984 let result = resolve_file(&FileType::RadarPcd, &files);
2986 assert!(result.is_none());
2987 }
2988
2989 #[test]
2990 fn test_resolve_file_lidar_depth() {
2991 let files = vec![SampleFile::with_url(
2993 "lidar.depth".to_string(),
2994 "https://example.com/depth.png".to_string(),
2995 )];
2996 let result = resolve_file(&FileType::LidarDepth, &files);
2997 assert!(result.is_some());
2998 assert_eq!(result.unwrap().url(), Some("https://example.com/depth.png"));
2999 }
3000
3001 #[test]
3002 fn test_resolve_file_lidar_reflect() {
3003 let files = vec![SampleFile::with_url(
3005 "lidar.reflect".to_string(),
3006 "https://example.com/reflect.png".to_string(),
3007 )];
3008 let result = resolve_file(&FileType::LidarReflect, &files);
3009 assert!(result.is_some());
3010 assert_eq!(
3011 result.unwrap().url(),
3012 Some("https://example.com/reflect.png")
3013 );
3014 }
3015
3016 #[test]
3017 fn test_resolve_file_radar_cube() {
3018 let files = vec![SampleFile::with_url(
3020 "radar.png".to_string(),
3021 "https://example.com/radar.png".to_string(),
3022 )];
3023 let result = resolve_file(&FileType::RadarCube, &files);
3024 assert!(result.is_some());
3025 assert_eq!(result.unwrap().url(), Some("https://example.com/radar.png"));
3026 }
3027
3028 #[test]
3029 fn test_resolve_file_with_inline_data() {
3030 let files = vec![SampleFile::with_data(
3032 "radar.pcd".to_string(),
3033 "SGVsbG8gV29ybGQ=".to_string(), )];
3035 let result = resolve_file(&FileType::RadarPcd, &files);
3036 assert!(result.is_some());
3037 let file = result.unwrap();
3038 assert!(file.url().is_none());
3039 assert_eq!(file.data(), Some("SGVsbG8gV29ybGQ="));
3040 }
3041
3042 #[test]
3043 fn test_convert_annotations_map_to_vec_with_bbox() {
3044 let mut map = HashMap::new();
3045 let bbox_ann = Annotation::new();
3046 map.insert("bbox".to_string(), vec![bbox_ann.clone()]);
3047
3048 let annotations = convert_annotations_map_to_vec(map);
3049 assert_eq!(annotations.len(), 1);
3050 }
3051
3052 #[test]
3053 fn test_convert_annotations_map_to_vec_all_types() {
3054 let mut map = HashMap::new();
3055 map.insert("bbox".to_string(), vec![Annotation::new()]);
3056 map.insert("box3d".to_string(), vec![Annotation::new()]);
3057 map.insert("mask".to_string(), vec![Annotation::new()]);
3058
3059 let annotations = convert_annotations_map_to_vec(map);
3060 assert_eq!(annotations.len(), 3);
3061 }
3062
3063 #[test]
3064 fn test_convert_annotations_map_to_vec_empty() {
3065 let map = HashMap::new();
3066 let annotations = convert_annotations_map_to_vec(map);
3067 assert_eq!(annotations.len(), 0);
3068 }
3069
3070 #[test]
3071 fn test_convert_annotations_map_to_vec_unknown_type_ignored() {
3072 let mut map = HashMap::new();
3073 map.insert("unknown".to_string(), vec![Annotation::new()]);
3074
3075 let annotations = convert_annotations_map_to_vec(map);
3076 assert_eq!(annotations.len(), 0);
3078 }
3079
3080 #[test]
3082 fn test_annotation_group_field_name() {
3083 assert_eq!(annotation_group_field_name(), "group_name");
3084 }
3085
3086 #[test]
3087 fn test_annotation_object_id_field_name() {
3088 assert_eq!(annotation_object_id_field_name(), "object_reference");
3089 }
3090
3091 #[test]
3092 fn test_annotation_object_id_alias() {
3093 assert_eq!(annotation_object_id_alias(), "object_id");
3094 }
3095
3096 #[test]
3097 fn test_validate_annotation_field_names_success() {
3098 let json = r#"{"group_name":"train","object_reference":"obj1"}"#;
3099 assert!(validate_annotation_field_names(json, true, true).is_ok());
3100 }
3101
3102 #[test]
3103 fn test_validate_annotation_field_names_missing_group() {
3104 let json = r#"{"object_reference":"obj1"}"#;
3105 let result = validate_annotation_field_names(json, true, false);
3106 assert!(result.is_err());
3107 assert!(result.unwrap_err().contains("group_name"));
3108 }
3109
3110 #[test]
3111 fn test_validate_annotation_field_names_missing_object_ref() {
3112 let json = r#"{"group_name":"train"}"#;
3113 let result = validate_annotation_field_names(json, false, true);
3114 assert!(result.is_err());
3115 assert!(result.unwrap_err().contains("object_reference"));
3116 }
3117
3118 #[test]
3119 fn test_annotation_serialization_field_names() {
3120 let mut ann = Annotation::new();
3122 ann.set_group(Some("train".to_string()));
3123 ann.set_object_id(Some("obj1".to_string()));
3124
3125 let json = serde_json::to_string(&ann).unwrap();
3126 assert!(validate_annotation_field_names(&json, true, true).is_ok());
3128 }
3129
3130 #[test]
3132 fn test_validate_gps_coordinates_valid() {
3133 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()); }
3138
3139 #[test]
3140 fn test_validate_gps_coordinates_invalid_latitude() {
3141 let result = validate_gps_coordinates(91.0, 0.0);
3142 assert!(result.is_err());
3143 assert!(result.unwrap_err().contains("latitude out of range"));
3144
3145 let result = validate_gps_coordinates(-91.0, 0.0);
3146 assert!(result.is_err());
3147 assert!(result.unwrap_err().contains("latitude out of range"));
3148 }
3149
3150 #[test]
3151 fn test_validate_gps_coordinates_invalid_longitude() {
3152 let result = validate_gps_coordinates(0.0, 181.0);
3153 assert!(result.is_err());
3154 assert!(result.unwrap_err().contains("longitude out of range"));
3155
3156 let result = validate_gps_coordinates(0.0, -181.0);
3157 assert!(result.is_err());
3158 assert!(result.unwrap_err().contains("longitude out of range"));
3159 }
3160
3161 #[test]
3162 fn test_validate_gps_coordinates_non_finite() {
3163 let result = validate_gps_coordinates(f64::NAN, 0.0);
3164 assert!(result.is_err());
3165 assert!(result.unwrap_err().contains("not finite"));
3166
3167 let result = validate_gps_coordinates(0.0, f64::INFINITY);
3168 assert!(result.is_err());
3169 assert!(result.unwrap_err().contains("not finite"));
3170 }
3171
3172 #[test]
3173 fn test_validate_imu_orientation_valid() {
3174 assert!(validate_imu_orientation(0.0, 0.0, 0.0).is_ok());
3175 assert!(validate_imu_orientation(45.0, 30.0, 90.0).is_ok());
3176 assert!(validate_imu_orientation(180.0, 90.0, -180.0).is_ok()); assert!(validate_imu_orientation(-180.0, -90.0, 180.0).is_ok()); }
3179
3180 #[test]
3181 fn test_validate_imu_orientation_invalid_roll() {
3182 let result = validate_imu_orientation(181.0, 0.0, 0.0);
3183 assert!(result.is_err());
3184 assert!(result.unwrap_err().contains("roll out of range"));
3185
3186 let result = validate_imu_orientation(-181.0, 0.0, 0.0);
3187 assert!(result.is_err());
3188 }
3189
3190 #[test]
3191 fn test_validate_imu_orientation_invalid_pitch() {
3192 let result = validate_imu_orientation(0.0, 91.0, 0.0);
3193 assert!(result.is_err());
3194 assert!(result.unwrap_err().contains("pitch out of range"));
3195
3196 let result = validate_imu_orientation(0.0, -91.0, 0.0);
3197 assert!(result.is_err());
3198 }
3199
3200 #[test]
3201 fn test_validate_imu_orientation_non_finite() {
3202 let result = validate_imu_orientation(f64::NAN, 0.0, 0.0);
3203 assert!(result.is_err());
3204 assert!(result.unwrap_err().contains("not finite"));
3205
3206 let result = validate_imu_orientation(0.0, f64::INFINITY, 0.0);
3207 assert!(result.is_err());
3208
3209 let result = validate_imu_orientation(0.0, 0.0, f64::NEG_INFINITY);
3210 assert!(result.is_err());
3211 }
3212
3213 #[test]
3215 #[cfg(feature = "polars")]
3216 fn test_unflatten_polygon_coordinates_single_polygon() {
3217 let coords = vec![1.0, 2.0, 3.0, 4.0];
3218 let result = unflatten_polygon_coordinates(&coords);
3219
3220 assert_eq!(result.len(), 1);
3221 assert_eq!(result[0].len(), 2);
3222 assert_eq!(result[0][0], (1.0, 2.0));
3223 assert_eq!(result[0][1], (3.0, 4.0));
3224 }
3225
3226 #[test]
3227 #[cfg(feature = "polars")]
3228 fn test_unflatten_polygon_coordinates_multiple_polygons() {
3229 let coords = vec![1.0, 2.0, 3.0, 4.0, f32::NAN, 5.0, 6.0, 7.0, 8.0];
3230 let result = unflatten_polygon_coordinates(&coords);
3231
3232 assert_eq!(result.len(), 2);
3233 assert_eq!(result[0].len(), 2);
3234 assert_eq!(result[0][0], (1.0, 2.0));
3235 assert_eq!(result[0][1], (3.0, 4.0));
3236 assert_eq!(result[1].len(), 2);
3237 assert_eq!(result[1][0], (5.0, 6.0));
3238 assert_eq!(result[1][1], (7.0, 8.0));
3239 }
3240
3241 #[test]
3242 #[cfg(feature = "polars")]
3243 fn test_unflatten_polygon_coordinates_roundtrip() {
3244 let flat = vec![1.0, 2.0, 3.0, 4.0, f32::NAN, 5.0, 6.0, 7.0, 8.0];
3246 let result = unflatten_polygon_coordinates(&flat);
3247
3248 let expected = vec![vec![(1.0, 2.0), (3.0, 4.0)], vec![(5.0, 6.0), (7.0, 8.0)]];
3249 assert_eq!(result, expected);
3250 }
3251
3252 #[test]
3254 fn test_flatten_annotation_map_all_types() {
3255 use std::collections::HashMap;
3256
3257 let mut map = HashMap::new();
3258
3259 let mut bbox_ann = Annotation::new();
3261 bbox_ann.set_label(Some("bbox_label".to_string()));
3262
3263 let mut box3d_ann = Annotation::new();
3264 box3d_ann.set_label(Some("box3d_label".to_string()));
3265
3266 let mut mask_ann = Annotation::new();
3267 mask_ann.set_label(Some("mask_label".to_string()));
3268
3269 map.insert("bbox".to_string(), vec![bbox_ann.clone()]);
3270 map.insert("box3d".to_string(), vec![box3d_ann.clone()]);
3271 map.insert("mask".to_string(), vec![mask_ann.clone()]);
3272
3273 let result = flatten_annotation_map(map);
3274
3275 assert_eq!(result.len(), 3);
3276 assert_eq!(result[0].label(), Some(&"bbox_label".to_string()));
3278 assert_eq!(result[1].label(), Some(&"box3d_label".to_string()));
3279 assert_eq!(result[2].label(), Some(&"mask_label".to_string()));
3280 }
3281
3282 #[test]
3283 fn test_flatten_annotation_map_single_type() {
3284 use std::collections::HashMap;
3285
3286 let mut map = HashMap::new();
3287 let mut bbox_ann = Annotation::new();
3288 bbox_ann.set_label(Some("test".to_string()));
3289 map.insert("bbox".to_string(), vec![bbox_ann]);
3290
3291 let result = flatten_annotation_map(map);
3292
3293 assert_eq!(result.len(), 1);
3294 assert_eq!(result[0].label(), Some(&"test".to_string()));
3295 }
3296
3297 #[test]
3298 fn test_flatten_annotation_map_empty() {
3299 use std::collections::HashMap;
3300
3301 let map = HashMap::new();
3302 let result = flatten_annotation_map(map);
3303
3304 assert_eq!(result.len(), 0);
3305 }
3306
3307 #[test]
3308 fn test_flatten_annotation_map_deterministic_order() {
3309 use std::collections::HashMap;
3310
3311 let mut map = HashMap::new();
3312
3313 let mut bbox_ann = Annotation::new();
3314 bbox_ann.set_label(Some("bbox".to_string()));
3315
3316 let mut box3d_ann = Annotation::new();
3317 box3d_ann.set_label(Some("box3d".to_string()));
3318
3319 let mut mask_ann = Annotation::new();
3320 mask_ann.set_label(Some("mask".to_string()));
3321
3322 map.insert("mask".to_string(), vec![mask_ann]);
3324 map.insert("box3d".to_string(), vec![box3d_ann]);
3325 map.insert("bbox".to_string(), vec![bbox_ann]);
3326
3327 let result = flatten_annotation_map(map);
3328
3329 assert_eq!(result.len(), 3);
3331 assert_eq!(result[0].label(), Some(&"bbox".to_string()));
3332 assert_eq!(result[1].label(), Some(&"box3d".to_string()));
3333 assert_eq!(result[2].label(), Some(&"mask".to_string()));
3334 }
3335
3336 #[test]
3338 fn test_box2d_construction_and_accessors() {
3339 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
3341 assert_eq!(
3342 (bbox.left(), bbox.top(), bbox.width(), bbox.height()),
3343 (10.0, 20.0, 100.0, 50.0)
3344 );
3345
3346 assert_eq!((bbox.cx(), bbox.cy()), (60.0, 45.0)); let bbox = Box2d::new(0.0, 0.0, 640.0, 480.0);
3351 assert_eq!(
3352 (bbox.left(), bbox.top(), bbox.width(), bbox.height()),
3353 (0.0, 0.0, 640.0, 480.0)
3354 );
3355 assert_eq!((bbox.cx(), bbox.cy()), (320.0, 240.0));
3356 }
3357
3358 #[test]
3359 fn test_box2d_center_calculation() {
3360 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
3361
3362 assert_eq!(bbox.cx(), 60.0); assert_eq!(bbox.cy(), 45.0); }
3366
3367 #[test]
3368 fn test_box2d_zero_dimensions() {
3369 let bbox = Box2d::new(10.0, 20.0, 0.0, 0.0);
3370
3371 assert_eq!(bbox.cx(), 10.0);
3373 assert_eq!(bbox.cy(), 20.0);
3374 }
3375
3376 #[test]
3377 fn test_box2d_negative_dimensions() {
3378 let bbox = Box2d::new(100.0, 100.0, -50.0, -50.0);
3379
3380 assert_eq!(bbox.width(), -50.0);
3382 assert_eq!(bbox.height(), -50.0);
3383 assert_eq!(bbox.cx(), 75.0); assert_eq!(bbox.cy(), 75.0); }
3386
3387 #[test]
3389 fn test_box3d_construction_and_accessors() {
3390 let bbox = Box3d::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
3392 assert_eq!((bbox.cx(), bbox.cy(), bbox.cz()), (1.0, 2.0, 3.0));
3393 assert_eq!(
3394 (bbox.width(), bbox.height(), bbox.length()),
3395 (4.0, 5.0, 6.0)
3396 );
3397
3398 let bbox = Box3d::new(10.0, 20.0, 30.0, 4.0, 6.0, 8.0);
3400 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);
3404 assert_eq!((bbox.cx(), bbox.cy(), bbox.cz()), (0.0, 0.0, 0.0));
3405 assert_eq!(
3406 (bbox.width(), bbox.height(), bbox.length()),
3407 (2.0, 3.0, 4.0)
3408 );
3409 assert_eq!((bbox.left(), bbox.top(), bbox.front()), (-1.0, -1.5, -2.0));
3410 }
3411
3412 #[test]
3413 fn test_box3d_center_calculation() {
3414 let bbox = Box3d::new(10.0, 20.0, 30.0, 100.0, 50.0, 40.0);
3415
3416 assert_eq!(bbox.cx(), 10.0);
3418 assert_eq!(bbox.cy(), 20.0);
3419 assert_eq!(bbox.cz(), 30.0);
3420 }
3421
3422 #[test]
3423 fn test_box3d_zero_dimensions() {
3424 let bbox = Box3d::new(5.0, 10.0, 15.0, 0.0, 0.0, 0.0);
3425
3426 assert_eq!(bbox.cx(), 5.0);
3428 assert_eq!(bbox.cy(), 10.0);
3429 assert_eq!(bbox.cz(), 15.0);
3430 assert_eq!((bbox.left(), bbox.top(), bbox.front()), (5.0, 10.0, 15.0));
3431 }
3432
3433 #[test]
3434 fn test_box3d_negative_dimensions() {
3435 let bbox = Box3d::new(100.0, 100.0, 100.0, -50.0, -50.0, -50.0);
3436
3437 assert_eq!(bbox.width(), -50.0);
3439 assert_eq!(bbox.height(), -50.0);
3440 assert_eq!(bbox.length(), -50.0);
3441 assert_eq!(
3442 (bbox.left(), bbox.top(), bbox.front()),
3443 (125.0, 125.0, 125.0)
3444 );
3445 }
3446
3447 #[test]
3449 fn test_polygon_creation_and_deserialization() {
3450 let rings = vec![vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]];
3452 let polygon = Polygon::new(rings.clone());
3453 assert_eq!(polygon.rings, rings);
3454
3455 let legacy = serde_json::json!({
3457 "polygon": {
3458 "polygon": [[
3459 [0.0_f32, 0.0_f32],
3460 [1.0_f32, 0.0_f32],
3461 [1.0_f32, 1.0_f32]
3462 ]]
3463 }
3464 });
3465
3466 #[derive(serde::Deserialize)]
3467 struct Wrapper {
3468 polygon: Polygon,
3469 }
3470
3471 let parsed: Wrapper = serde_json::from_value(legacy).unwrap();
3472 assert_eq!(parsed.polygon.rings.len(), 1);
3473 assert_eq!(parsed.polygon.rings[0].len(), 3);
3474 }
3475
3476 #[test]
3478 fn test_sample_construction_and_accessors() {
3479 let sample = Sample::new();
3481 assert_eq!(sample.id(), None);
3482 assert_eq!(sample.image_name(), None);
3483 assert_eq!(sample.width(), None);
3484 assert_eq!(sample.height(), None);
3485
3486 let mut sample = Sample::new();
3488 sample.image_name = Some("test.jpg".to_string());
3489 sample.width = Some(1920);
3490 sample.height = Some(1080);
3491 sample.group = Some("group1".to_string());
3492
3493 assert_eq!(sample.image_name(), Some("test.jpg"));
3494 assert_eq!(sample.width(), Some(1920));
3495 assert_eq!(sample.height(), Some(1080));
3496 assert_eq!(sample.group(), Some(&"group1".to_string()));
3497 }
3498
3499 #[test]
3500 fn test_sample_name_extraction_from_image_name() {
3501 let mut sample = Sample::new();
3502
3503 sample.image_name = Some("test_image.jpg".to_string());
3505 assert_eq!(sample.name(), Some("test_image".to_string()));
3506
3507 sample.image_name = Some("test_image.camera.jpg".to_string());
3509 assert_eq!(sample.name(), Some("test_image".to_string()));
3510
3511 sample.image_name = Some("test_image".to_string());
3513 assert_eq!(sample.name(), Some("test_image".to_string()));
3514 }
3515
3516 #[test]
3518 fn test_annotation_construction_and_setters() {
3519 let ann = Annotation::new();
3521 assert_eq!(ann.sample_id(), None);
3522 assert_eq!(ann.label(), None);
3523 assert_eq!(ann.box2d(), None);
3524 assert_eq!(ann.box3d(), None);
3525 assert_eq!(ann.polygon(), None);
3526
3527 let mut ann = Annotation::new();
3529 ann.set_label(Some("car".to_string()));
3530 assert_eq!(ann.label(), Some(&"car".to_string()));
3531
3532 ann.set_label_index(Some(42));
3533 assert_eq!(ann.label_index(), Some(42));
3534
3535 let bbox = Box2d::new(10.0, 20.0, 100.0, 50.0);
3537 ann.set_box2d(Some(bbox.clone()));
3538 assert!(ann.box2d().is_some());
3539 assert_eq!(ann.box2d().unwrap().left(), 10.0);
3540 }
3541
3542 #[test]
3544 fn test_sample_file_with_url_and_filename() {
3545 let file = SampleFile::with_url(
3547 "lidar.pcd".to_string(),
3548 "https://example.com/file.pcd".to_string(),
3549 );
3550 assert_eq!(file.file_type(), "lidar.pcd");
3551 assert_eq!(file.url(), Some("https://example.com/file.pcd"));
3552 assert_eq!(file.filename(), None);
3553
3554 let file = SampleFile::with_filename("image".to_string(), "test.jpg".to_string());
3556 assert_eq!(file.file_type(), "image");
3557 assert_eq!(file.filename(), Some("test.jpg"));
3558 assert_eq!(file.url(), None);
3559 }
3560
3561 #[test]
3563 fn test_sample_deserializes_gps_imu_from_sensors() {
3564 use serde_json::json;
3565
3566 let sample_json = json!({
3568 "id": 123,
3569 "image_name": "test.jpg",
3570 "sensors": [
3571 {"gps": {"lat": 37.7749, "lon": -122.4194}},
3572 {"imu": {"roll": 1.5, "pitch": 2.5, "yaw": 3.5}},
3573 {"radar.pcd": "https://example.com/radar.pcd"}
3574 ]
3575 });
3576
3577 let sample: Sample = serde_json::from_value(sample_json).unwrap();
3578
3579 assert!(sample.location.is_some());
3581 let location = sample.location.as_ref().unwrap();
3582
3583 assert!(location.gps.is_some());
3585 let gps = location.gps.as_ref().unwrap();
3586 assert!((gps.lat - 37.7749).abs() < 0.0001);
3587 assert!((gps.lon - (-122.4194)).abs() < 0.0001);
3588
3589 assert!(location.imu.is_some());
3591 let imu = location.imu.as_ref().unwrap();
3592 assert!((imu.roll - 1.5).abs() < 0.0001);
3593 assert!((imu.pitch - 2.5).abs() < 0.0001);
3594 assert!((imu.yaw - 3.5).abs() < 0.0001);
3595
3596 assert_eq!(sample.files.len(), 1);
3598 assert_eq!(sample.files[0].file_type(), "radar.pcd");
3599 assert_eq!(sample.files[0].url(), Some("https://example.com/radar.pcd"));
3600 }
3601
3602 #[test]
3603 fn test_sample_deserializes_gps_only() {
3604 use serde_json::json;
3605
3606 let sample_json = json!({
3608 "id": 456,
3609 "sensors": [
3610 {"gps": {"lat": 40.7128, "lon": -74.0060}}
3611 ]
3612 });
3613
3614 let sample: Sample = serde_json::from_value(sample_json).unwrap();
3615
3616 assert!(sample.location.is_some());
3617 let location = sample.location.as_ref().unwrap();
3618
3619 assert!(location.gps.is_some());
3620 assert!(location.imu.is_none());
3621
3622 let gps = location.gps.as_ref().unwrap();
3623 assert!((gps.lat - 40.7128).abs() < 0.0001);
3624 assert!((gps.lon - (-74.0060)).abs() < 0.0001);
3625 }
3626
3627 #[test]
3628 fn test_sample_deserializes_without_location() {
3629 use serde_json::json;
3630
3631 let sample_json = json!({
3633 "id": 789,
3634 "sensors": [
3635 {"radar.pcd": "https://example.com/radar.pcd"},
3636 {"lidar.pcd": "https://example.com/lidar.pcd"}
3637 ]
3638 });
3639
3640 let sample: Sample = serde_json::from_value(sample_json).unwrap();
3641
3642 assert!(sample.location.is_none());
3644
3645 assert_eq!(sample.files.len(), 2);
3647 }
3648
3649 #[test]
3651 fn test_label_deserialization_and_accessors() {
3652 use serde_json::json;
3653
3654 let label_json = json!({
3656 "id": 123,
3657 "dataset_id": 456,
3658 "index": 5,
3659 "name": "car"
3660 });
3661
3662 let label: Label = serde_json::from_value(label_json).unwrap();
3663 assert_eq!(label.id(), 123);
3664 assert_eq!(label.index(), 5);
3665 assert_eq!(label.name(), "car");
3666 assert_eq!(label.to_string(), "car");
3667 assert_eq!(format!("{}", label), "car");
3668
3669 let label_json = json!({
3671 "id": 1,
3672 "dataset_id": 100,
3673 "index": 0,
3674 "name": "person"
3675 });
3676
3677 let label: Label = serde_json::from_value(label_json).unwrap();
3678 assert_eq!(format!("{}", label), "person");
3679 }
3680
3681 #[test]
3683 fn test_annotation_serialization_with_mask_and_box() {
3684 let polygon = vec![vec![
3685 (0.0_f32, 0.0_f32),
3686 (1.0_f32, 0.0_f32),
3687 (1.0_f32, 1.0_f32),
3688 ]];
3689
3690 let mut annotation = Annotation::new();
3691 annotation.set_label(Some("test".to_string()));
3692 annotation.set_box2d(Some(Box2d::new(10.0, 20.0, 30.0, 40.0)));
3693 annotation.set_polygon(Some(Polygon::new(polygon)));
3694
3695 let mut sample = Sample::new();
3696 sample.annotations.push(annotation);
3697
3698 let json = serde_json::to_value(&sample).unwrap();
3699 let annotations = json
3700 .get("annotations")
3701 .and_then(|value| value.as_array())
3702 .expect("annotations serialized as array");
3703 assert_eq!(annotations.len(), 1);
3704
3705 let annotation_json = annotations[0].as_object().expect("annotation object");
3706 assert!(annotation_json.contains_key("box2d"));
3707 assert!(
3712 annotation_json.contains_key("mask"),
3713 "Annotation must serialise polygon under 'mask' key for samples.populate2; got keys: {:?}",
3714 annotation_json.keys().collect::<Vec<_>>()
3715 );
3716 assert!(!annotation_json.contains_key("polygon"));
3717 assert!(!annotation_json.contains_key("x"));
3718 assert!(
3719 annotation_json
3720 .get("mask")
3721 .and_then(|value| value.as_array())
3722 .is_some()
3723 );
3724 }
3725
3726 #[test]
3727 fn test_frame_number_negative_one_deserializes_as_none() {
3728 let json = r#"{
3731 "uuid": "test-uuid",
3732 "frame_number": -1
3733 }"#;
3734
3735 let sample: Sample = serde_json::from_str(json).unwrap();
3736 assert_eq!(sample.frame_number, None);
3737 }
3738
3739 #[test]
3740 fn test_frame_number_positive_value_deserializes_correctly() {
3741 let json = r#"{
3743 "uuid": "test-uuid",
3744 "frame_number": 5
3745 }"#;
3746
3747 let sample: Sample = serde_json::from_str(json).unwrap();
3748 assert_eq!(sample.frame_number, Some(5));
3749 }
3750
3751 #[test]
3752 fn test_frame_number_null_deserializes_as_none() {
3753 let json = r#"{
3755 "uuid": "test-uuid",
3756 "frame_number": null
3757 }"#;
3758
3759 let sample: Sample = serde_json::from_str(json).unwrap();
3760 assert_eq!(sample.frame_number, None);
3761 }
3762
3763 #[test]
3764 fn test_frame_number_missing_deserializes_as_none() {
3765 let json = r#"{
3767 "uuid": "test-uuid"
3768 }"#;
3769
3770 let sample: Sample = serde_json::from_str(json).unwrap();
3771 assert_eq!(sample.frame_number, None);
3772 }
3773
3774 #[cfg(feature = "polars")]
3779 #[test]
3780 fn test_samples_dataframe_preserves_group_for_samples_without_annotations() {
3781 use polars::prelude::*;
3782
3783 let mut sample_with_ann = Sample::new();
3785 sample_with_ann.image_name = Some("annotated.jpg".to_string());
3786 sample_with_ann.group = Some("train".to_string());
3787 let mut annotation = Annotation::new();
3788 annotation.set_label(Some("car".to_string()));
3789 annotation.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
3790 annotation.set_name(Some("annotated".to_string()));
3791 sample_with_ann.annotations = vec![annotation];
3792
3793 let mut sample_no_ann = Sample::new();
3795 sample_no_ann.image_name = Some("unannotated.jpg".to_string());
3796 sample_no_ann.group = Some("val".to_string()); sample_no_ann.annotations = vec![]; let samples = vec![sample_with_ann, sample_no_ann];
3800
3801 let df = samples_dataframe(&samples).expect("Failed to create DataFrame");
3803
3804 assert_eq!(df.height(), 2, "Expected 2 rows (one per sample)");
3806
3807 let groups_col = df.column("group").expect("group column should exist");
3809 let groups_cast = groups_col.cast(&DataType::String).expect("cast to string");
3810 let groups = groups_cast.str().expect("as str");
3811
3812 let names_col = df.column("name").expect("name column should exist");
3814 let names_cast = names_col.cast(&DataType::String).expect("cast to string");
3815 let names = names_cast.str().expect("as str");
3816
3817 let mut found_unannotated = false;
3818 for idx in 0..df.height() {
3819 if let Some(name) = names.get(idx)
3820 && name == "unannotated"
3821 {
3822 found_unannotated = true;
3823 let group = groups.get(idx);
3824 assert_eq!(
3825 group,
3826 Some("val"),
3827 "CRITICAL: Sample 'unannotated' without annotations must have group 'val'"
3828 );
3829 }
3830 }
3831
3832 assert!(
3833 found_unannotated,
3834 "Did not find 'unannotated' sample in DataFrame - \
3835 this means samples without annotations are not being included"
3836 );
3837 }
3838
3839 #[cfg(feature = "polars")]
3840 #[test]
3841 fn test_samples_dataframe_includes_all_samples_even_without_annotations() {
3842 let mut sample1 = Sample::new();
3846 sample1.image_name = Some("with_ann.jpg".to_string());
3847 sample1.group = Some("train".to_string());
3848 let mut ann = Annotation::new();
3849 ann.set_label(Some("person".to_string()));
3850 ann.set_box2d(Some(Box2d::new(0.0, 0.0, 0.5, 0.5)));
3851 ann.set_name(Some("with_ann".to_string()));
3852 sample1.annotations = vec![ann];
3853
3854 let mut sample2 = Sample::new();
3855 sample2.image_name = Some("no_ann_train.jpg".to_string());
3856 sample2.group = Some("train".to_string());
3857 sample2.annotations = vec![];
3858
3859 let mut sample3 = Sample::new();
3860 sample3.image_name = Some("no_ann_val.jpg".to_string());
3861 sample3.group = Some("val".to_string());
3862 sample3.annotations = vec![];
3863
3864 let samples = vec![sample1, sample2, sample3];
3865
3866 let df = samples_dataframe(&samples).expect("Failed to create DataFrame");
3867
3868 assert_eq!(
3870 df.height(),
3871 3,
3872 "Expected 3 rows (samples without annotations should create one row each)"
3873 );
3874
3875 let groups_col = df.column("group").expect("group column");
3877 let groups_cast = groups_col.cast(&polars::prelude::DataType::String).unwrap();
3878 let groups = groups_cast.str().unwrap();
3879
3880 let mut train_count = 0;
3881 let mut val_count = 0;
3882
3883 for idx in 0..df.height() {
3884 match groups.get(idx) {
3885 Some("train") => train_count += 1,
3886 Some("val") => val_count += 1,
3887 other => panic!(
3888 "Unexpected group value at row {}: {:?}. \
3889 All samples should have their group preserved.",
3890 idx, other
3891 ),
3892 }
3893 }
3894
3895 assert_eq!(train_count, 2, "Expected 2 samples in 'train' group");
3896 assert_eq!(val_count, 1, "Expected 1 sample in 'val' group");
3897 }
3898
3899 #[cfg(feature = "polars")]
3900 #[test]
3901 fn test_samples_dataframe_group_is_not_null_for_samples_with_group() {
3902 let mut sample = Sample::new();
3906 sample.image_name = Some("test.jpg".to_string());
3907 sample.group = Some("test_group".to_string());
3908 sample.annotations = vec![];
3909
3910 let df = samples_dataframe(&[sample]).expect("Failed to create DataFrame");
3911
3912 let groups_col = df.column("group").expect("group column");
3913
3914 assert_eq!(
3916 groups_col.null_count(),
3917 0,
3918 "Sample with group='test_group' but no annotations has NULL group in DataFrame. \
3919 This is a bug in samples_dataframe - group must be preserved!"
3920 );
3921 }
3922
3923 #[cfg(feature = "polars")]
3924 #[test]
3925 fn test_samples_dataframe_group_consistent_across_all_rows_for_same_image() {
3926 use polars::prelude::*;
3927
3928 let mut sample = Sample::new();
3932 sample.image_name = Some("multi_ann.jpg".to_string());
3933 sample.group = Some("train".to_string());
3934
3935 let mut ann1 = Annotation::new();
3937 ann1.set_label(Some("car".to_string()));
3938 ann1.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
3939 ann1.set_name(Some("multi_ann".to_string()));
3940
3941 let mut ann2 = Annotation::new();
3942 ann2.set_label(Some("truck".to_string()));
3943 ann2.set_box2d(Some(Box2d::new(0.5, 0.6, 0.2, 0.2)));
3944 ann2.set_name(Some("multi_ann".to_string()));
3945
3946 let mut ann3 = Annotation::new();
3947 ann3.set_label(Some("bus".to_string()));
3948 ann3.set_box2d(Some(Box2d::new(0.7, 0.8, 0.1, 0.1)));
3949 ann3.set_name(Some("multi_ann".to_string()));
3950
3951 sample.annotations = vec![ann1, ann2, ann3];
3952
3953 let df = samples_dataframe(&[sample]).expect("Failed to create DataFrame");
3954
3955 assert_eq!(df.height(), 3, "Expected 3 rows (one per annotation)");
3957
3958 let groups_col = df.column("group").expect("group column");
3960 let groups_cast = groups_col.cast(&DataType::String).expect("cast to string");
3961 let groups = groups_cast.str().expect("as str");
3962
3963 assert_eq!(groups_col.null_count(), 0, "No rows should have null group");
3965
3966 for idx in 0..df.height() {
3968 let group = groups.get(idx);
3969 assert_eq!(
3970 group,
3971 Some("train"),
3972 "Row {} should have group 'train', got {:?}. \
3973 All rows for the same image must have identical group values.",
3974 idx,
3975 group
3976 );
3977 }
3978 }
3979
3980 #[cfg(feature = "polars")]
3981 #[test]
3982 fn test_samples_dataframe_lvis_columns() {
3983 let mut ann = Annotation::new();
3984 ann.set_name(Some("test".to_string()));
3985 ann.set_label(Some("person".to_string()));
3986 ann.set_label_index(Some(1));
3987 ann.set_iscrowd(Some(false));
3988 ann.set_category_frequency(Some("f".to_string()));
3989
3990 let sample = Sample {
3991 image_name: Some("test.jpg".to_string()),
3992 width: Some(640),
3993 height: Some(480),
3994 annotations: vec![ann],
3995 neg_label_indices: Some(vec![5, 12]),
3996 not_exhaustive_label_indices: Some(vec![3]),
3997 ..Default::default()
3998 };
3999
4000 let df = samples_dataframe(&[sample]).unwrap();
4001
4002 assert!(df.column("iscrowd").is_ok(), "iscrowd column missing");
4004 assert!(
4005 df.column("category_frequency").is_ok(),
4006 "category_frequency column missing"
4007 );
4008 assert!(
4009 df.column("neg_label_indices").is_ok(),
4010 "neg_label_indices column missing"
4011 );
4012 assert!(
4013 df.column("not_exhaustive_label_indices").is_ok(),
4014 "not_exhaustive_label_indices column missing"
4015 );
4016
4017 assert!(
4019 df.column("polygon").is_err(),
4020 "polygon column should be dropped (all null)"
4021 );
4022 assert!(
4023 df.column("box2d").is_err(),
4024 "box2d column should be dropped (all null)"
4025 );
4026 }
4027
4028 #[test]
4029 fn test_annotation_serialization_skips_lvis_fields() {
4030 let ann = Annotation::new();
4031 let json = serde_json::to_string(&ann).unwrap();
4032 assert!(
4033 !json.contains("iscrowd"),
4034 "iscrowd should be omitted when None"
4035 );
4036 assert!(
4037 !json.contains("category_frequency"),
4038 "category_frequency should be omitted when None"
4039 );
4040 }
4041
4042 #[test]
4043 fn test_sample_serialization_skips_lvis_fields() {
4044 let sample = Sample::new();
4045 let json = serde_json::to_string(&sample).unwrap();
4046 assert!(
4047 !json.contains("neg_label_indices"),
4048 "neg_label_indices should be omitted when None"
4049 );
4050 assert!(
4051 !json.contains("not_exhaustive_label_indices"),
4052 "not_exhaustive_label_indices should be omitted when None"
4053 );
4054 }
4055
4056 #[test]
4057 fn test_annotation_score_fields() {
4058 let mut ann = Annotation::default();
4059 assert!(ann.box2d_score.is_none());
4060 assert!(ann.polygon_score.is_none());
4061 assert!(ann.mask_score.is_none());
4062 ann.box2d_score = Some(0.95);
4063 ann.polygon_score = Some(0.87);
4064 ann.mask_score = Some(0.42);
4065 assert_eq!(ann.box2d_score, Some(0.95));
4066 assert_eq!(ann.polygon_score, Some(0.87));
4067 assert_eq!(ann.mask_score, Some(0.42));
4068 }
4069
4070 #[test]
4071 fn test_timing_struct() {
4072 let timing = Timing {
4073 load: Some(1_000_000),
4074 preprocess: Some(2_000_000),
4075 inference: Some(50_000_000),
4076 decode: Some(3_000_000),
4077 };
4078 assert_eq!(timing.inference, Some(50_000_000));
4079
4080 let default = Timing::default();
4081 assert!(default.load.is_none());
4082 }
4083
4084 #[test]
4085 fn test_sample_timing() {
4086 let mut sample = Sample::default();
4087 assert!(sample.timing.is_none());
4088 sample.timing = Some(Timing {
4089 load: Some(1_000_000),
4090 ..Default::default()
4091 });
4092 assert!(sample.timing.is_some());
4093 }
4094
4095 #[cfg(feature = "polars")]
4100 #[test]
4101 fn test_samples_dataframe_polygon_column() {
4102 let mut ann = Annotation::new();
4103 ann.set_name(Some("test".to_string()));
4104 ann.set_polygon(Some(Polygon::new(vec![vec![
4105 (0.1, 0.2),
4106 (0.3, 0.4),
4107 (0.5, 0.6),
4108 ]])));
4109
4110 let sample = Sample {
4111 image_name: Some("test.jpg".to_string()),
4112 annotations: vec![ann],
4113 ..Default::default()
4114 };
4115
4116 let df = samples_dataframe(&[sample]).unwrap();
4117
4118 assert!(df.column("polygon").is_ok(), "Should have polygon column");
4120
4121 if let Ok(mask_col) = df.column("mask") {
4124 assert_eq!(
4126 mask_col.dtype(),
4127 &polars::prelude::DataType::Binary,
4128 "mask column must be Binary type (PNG bytes), not float list"
4129 );
4130 }
4131 }
4132
4133 #[cfg(feature = "polars")]
4134 #[test]
4135 fn test_samples_dataframe_column_presence_drops_all_null() {
4136 let sample = Sample {
4138 image_name: Some("test.jpg".to_string()),
4139 ..Default::default()
4140 };
4141
4142 let df = samples_dataframe(&[sample]).unwrap();
4143
4144 assert!(df.column("name").is_ok(), "name column must always exist");
4146
4147 assert!(
4149 df.column("polygon").is_err(),
4150 "All-null polygon should be dropped"
4151 );
4152 assert!(
4153 df.column("box2d").is_err(),
4154 "All-null box2d should be dropped"
4155 );
4156 assert!(
4157 df.column("box3d").is_err(),
4158 "All-null box3d should be dropped"
4159 );
4160 assert!(
4161 df.column("mask").is_err(),
4162 "All-null mask should be dropped"
4163 );
4164 assert!(
4165 df.column("box2d_score").is_err(),
4166 "All-null score columns should be dropped"
4167 );
4168 assert!(
4169 df.column("timing").is_err(),
4170 "All-null timing should be dropped"
4171 );
4172 }
4173
4174 #[cfg(feature = "polars")]
4175 #[test]
4176 fn test_samples_dataframe_size_column() {
4177 let sample1 = Sample {
4179 image_name: Some("img1.jpg".to_string()),
4180 width: Some(1920),
4181 height: Some(1080),
4182 ..Default::default()
4183 };
4184 let sample2 = Sample {
4185 image_name: Some("img2.jpg".to_string()),
4186 width: Some(640),
4187 height: Some(480),
4188 ..Default::default()
4189 };
4190
4191 let df = samples_dataframe(&[sample1, sample2]).unwrap();
4192
4193 let size_col = df
4195 .column("size")
4196 .expect("size column should be present when width/height are set");
4197 assert_eq!(size_col.len(), 2);
4198
4199 let arr = size_col.array().expect("size column should be Array dtype");
4201 let row0 = arr.get_as_series(0).unwrap();
4202 let row0_vals: Vec<u32> = row0.u32().unwrap().into_no_null_iter().collect();
4203 assert_eq!(row0_vals, vec![1920, 1080]);
4204
4205 let row1 = arr.get_as_series(1).unwrap();
4206 let row1_vals: Vec<u32> = row1.u32().unwrap().into_no_null_iter().collect();
4207 assert_eq!(row1_vals, vec![640, 480]);
4208 }
4209
4210 #[cfg(feature = "polars")]
4211 #[test]
4212 fn test_samples_dataframe_size_column_partial() {
4213 let sample1 = Sample {
4215 image_name: Some("img1.jpg".to_string()),
4216 width: Some(1920),
4217 height: Some(1080),
4218 ..Default::default()
4219 };
4220 let sample2 = Sample {
4221 image_name: Some("img2.jpg".to_string()),
4222 ..Default::default()
4224 };
4225
4226 let df = samples_dataframe(&[sample1, sample2]).unwrap();
4227
4228 let size_col = df
4230 .column("size")
4231 .expect("size column should be present when at least one sample has dimensions");
4232 assert_eq!(size_col.len(), 2);
4233 assert_eq!(size_col.null_count(), 1, "one row should be null");
4234 }
4235
4236 #[cfg(feature = "polars")]
4237 #[test]
4238 fn test_samples_dataframe_score_columns() {
4239 let mut ann = Annotation::new();
4240 ann.set_name(Some("test".to_string()));
4241 ann.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
4242 ann.set_box2d_score(Some(0.95));
4243 ann.set_polygon(Some(Polygon::new(vec![vec![
4244 (0.0, 0.0),
4245 (1.0, 0.0),
4246 (1.0, 1.0),
4247 ]])));
4248 ann.set_polygon_score(Some(0.87));
4249
4250 let sample = Sample {
4251 image_name: Some("test.jpg".to_string()),
4252 annotations: vec![ann],
4253 ..Default::default()
4254 };
4255
4256 let df = samples_dataframe(&[sample]).unwrap();
4257
4258 assert!(
4260 df.column("box2d_score").is_ok(),
4261 "box2d_score column missing"
4262 );
4263 assert!(
4264 df.column("polygon_score").is_ok(),
4265 "polygon_score column missing"
4266 );
4267
4268 assert!(
4270 df.column("box3d_score").is_err(),
4271 "box3d_score should be dropped (all null)"
4272 );
4273 assert!(
4274 df.column("mask_score").is_err(),
4275 "mask_score should be dropped (all null)"
4276 );
4277
4278 let box2d_scores = df.column("box2d_score").unwrap();
4280 let val = box2d_scores.f32().unwrap().get(0);
4281 assert_eq!(val, Some(0.95));
4282 }
4283
4284 #[cfg(feature = "polars")]
4285 #[test]
4286 fn test_samples_dataframe_timing_column() {
4287 let mut ann = Annotation::new();
4288 ann.set_name(Some("test".to_string()));
4289 ann.set_label(Some("person".to_string()));
4290
4291 let sample = Sample {
4292 image_name: Some("test.jpg".to_string()),
4293 annotations: vec![ann],
4294 timing: Some(Timing {
4295 load: Some(1_000_000),
4296 preprocess: Some(2_000_000),
4297 inference: Some(50_000_000),
4298 decode: Some(3_000_000),
4299 }),
4300 ..Default::default()
4301 };
4302
4303 let df = samples_dataframe(&[sample]).unwrap();
4304
4305 assert!(df.column("timing").is_ok(), "timing column missing");
4307
4308 let timing_col = df.column("timing").unwrap();
4310 assert!(
4311 matches!(timing_col.dtype(), polars::prelude::DataType::Struct(..)),
4312 "timing column should be Struct type, got {:?}",
4313 timing_col.dtype()
4314 );
4315 }
4316
4317 #[cfg(feature = "polars")]
4318 #[test]
4319 fn test_samples_dataframe_mask_binary_column() {
4320 let mut ann = Annotation::new();
4321 ann.set_name(Some("test".to_string()));
4322 let pixels = vec![0u8, 255, 128, 64];
4324 let mask_data = MaskData::encode(&pixels, 2, 2, 8).unwrap();
4325 ann.set_mask(Some(mask_data));
4326
4327 let sample = Sample {
4328 image_name: Some("test.jpg".to_string()),
4329 annotations: vec![ann],
4330 ..Default::default()
4331 };
4332
4333 let df = samples_dataframe(&[sample]).unwrap();
4334
4335 let mask_col = df.column("mask").unwrap();
4337 assert_eq!(
4338 mask_col.dtype(),
4339 &polars::prelude::DataType::Binary,
4340 "mask column should be Binary"
4341 );
4342 assert_eq!(mask_col.null_count(), 0, "mask value should not be null");
4343 }
4344
4345 #[test]
4350 fn test_annotation_type_seg_alias() {
4351 assert_eq!(
4352 AnnotationType::try_from("seg").unwrap(),
4353 AnnotationType::Polygon,
4354 "\"seg\" should map to Polygon for server round-trip"
4355 );
4356 }
4357
4358 #[cfg(feature = "polars")]
4363 #[test]
4364 fn test_samples_dataframe_timing_partial() {
4365 let mut ann = Annotation::new();
4367 ann.set_name(Some("test".to_string()));
4368 ann.set_label(Some("person".to_string()));
4369
4370 let sample = Sample {
4371 image_name: Some("test.jpg".to_string()),
4372 annotations: vec![ann],
4373 timing: Some(Timing {
4374 load: Some(1000),
4375 ..Default::default()
4376 }),
4377 ..Default::default()
4378 };
4379
4380 let df = samples_dataframe(&[sample]).unwrap();
4381
4382 assert!(
4384 df.column("timing").is_ok(),
4385 "timing column should be present when partial data exists"
4386 );
4387 }
4388
4389 #[cfg(feature = "polars")]
4390 #[test]
4391 fn test_samples_dataframe_timing_all_none_omitted() {
4392 let mut ann = Annotation::new();
4394 ann.set_name(Some("test".to_string()));
4395 ann.set_label(Some("person".to_string()));
4396
4397 let sample = Sample {
4398 image_name: Some("test.jpg".to_string()),
4399 annotations: vec![ann],
4400 timing: None,
4401 ..Default::default()
4402 };
4403
4404 let df = samples_dataframe(&[sample]).unwrap();
4405
4406 assert!(
4407 df.column("timing").is_err(),
4408 "timing column should be omitted when all samples have timing: None"
4409 );
4410 }
4411
4412 #[cfg(feature = "polars")]
4417 #[test]
4418 fn test_samples_dataframe_score_zero_survives() {
4419 let mut ann = Annotation::new();
4421 ann.set_name(Some("test".to_string()));
4422 ann.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
4423 ann.set_box2d_score(Some(0.0));
4424
4425 let sample = Sample {
4426 image_name: Some("test.jpg".to_string()),
4427 annotations: vec![ann],
4428 ..Default::default()
4429 };
4430
4431 let df = samples_dataframe(&[sample]).unwrap();
4432
4433 let scores = df.column("box2d_score").unwrap();
4434 let val = scores.f32().unwrap().get(0);
4435 assert_eq!(val, Some(0.0), "score of 0.0 should survive as non-null");
4436 }
4437
4438 #[cfg(feature = "polars")]
4439 #[test]
4440 fn test_samples_dataframe_score_one_survives() {
4441 let mut ann = Annotation::new();
4442 ann.set_name(Some("test".to_string()));
4443 ann.set_box2d(Some(Box2d::new(0.1, 0.2, 0.3, 0.4)));
4444 ann.set_box2d_score(Some(1.0));
4445
4446 let sample = Sample {
4447 image_name: Some("test.jpg".to_string()),
4448 annotations: vec![ann],
4449 ..Default::default()
4450 };
4451
4452 let df = samples_dataframe(&[sample]).unwrap();
4453
4454 let scores = df.column("box2d_score").unwrap();
4455 let val = scores.f32().unwrap().get(0);
4456 assert_eq!(val, Some(1.0), "score of 1.0 should survive as non-null");
4457 }
4458}
4459
4460#[cfg(test)]
4461mod versioning_deser_tests {
4462 use super::*;
4463
4464 #[test]
4465 fn test_annotation_set_deserializes_from_tag_scoped_response() {
4466 let json = r#"{"id": 42, "name": "Default", "description": "Default set"}"#;
4469 let result: Result<AnnotationSet, _> = serde_json::from_str(json);
4470 assert!(
4471 result.is_ok(),
4472 "tag-scoped annotation set response must deserialize: {:?}",
4473 result.err()
4474 );
4475 let annset = result.unwrap();
4476 assert_eq!(annset.name(), "Default");
4477 assert_eq!(annset.description(), "Default set");
4478 assert_eq!(annset.created(), None);
4479 }
4480
4481 #[test]
4482 fn test_annotation_set_deserializes_from_head_response() {
4483 let json = r#"{"id": 42, "dataset_id": 1, "name": "Default", "description": "Default set", "date": "2026-01-01T00:00:00Z"}"#;
4490 let result: Result<AnnotationSet, _> = serde_json::from_str(json);
4491 assert!(
4492 result.is_ok(),
4493 "HEAD annotation set response must deserialize: {:?}",
4494 result.err()
4495 );
4496 let annset = result.unwrap();
4497 assert!(annset.created().is_some());
4498 }
4499
4500 #[test]
4501 fn test_label_deserializes_from_tag_scoped_response() {
4502 let json = r#"{"id": 7, "name": "circle", "index": 0, "color": 16711680}"#;
4505 let result: Result<Label, _> = serde_json::from_str(json);
4506 assert!(
4507 result.is_ok(),
4508 "tag-scoped label response must deserialize: {:?}",
4509 result.err()
4510 );
4511 let label = result.unwrap();
4512 assert_eq!(label.name(), "circle");
4513 assert_eq!(label.color(), Some(16711680));
4514 assert_eq!(label.dataset_id(), None);
4515 }
4516
4517 #[test]
4518 fn test_label_deserializes_from_head_response() {
4519 let json = r#"{"id": 7, "dataset_id": 1, "name": "circle", "index": 0}"#;
4523 let result: Result<Label, _> = serde_json::from_str(json);
4524 assert!(
4525 result.is_ok(),
4526 "HEAD label response must deserialize: {:?}",
4527 result.err()
4528 );
4529 let label = result.unwrap();
4530 assert!(label.dataset_id().is_some());
4531 assert_eq!(label.color(), None);
4532 }
4533
4534 #[test]
4535 fn test_dataset_deserializes_tag_fields() {
4536 let json = r#"{
4541 "id": 1, "project_id": 1, "name": "My Dataset",
4542 "description": "", "cloud_key": "k", "createdAt": "2026-01-01T00:00:00Z",
4543 "tag_id": 42, "tag": "v1.0", "tag_description": "Release candidate"
4544 }"#;
4545 let dataset: Dataset = serde_json::from_str(json).unwrap();
4546 assert_eq!(dataset.tag_id(), Some(42));
4547 assert_eq!(dataset.tag(), "v1.0");
4548 assert_eq!(dataset.tag_description(), "Release candidate");
4549 }
4550
4551 #[test]
4552 fn test_dataset_deserializes_without_tag_fields() {
4553 let json = r#"{
4556 "id": 1, "project_id": 1, "name": "My Dataset",
4557 "description": "", "cloud_key": "k", "createdAt": "2026-01-01T00:00:00Z"
4558 }"#;
4559 let dataset: Dataset = serde_json::from_str(json).unwrap();
4560 assert_eq!(dataset.tag_id(), None);
4561 assert_eq!(dataset.tag(), "");
4562 assert_eq!(dataset.tag_description(), "");
4563 }
4564}