use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use super::bbox::BBoxXYXY;
use super::ids::{AnnotationId, CategoryId, ImageId, LicenseId};
use super::space::Pixel;
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Dataset {
#[serde(default)]
pub info: DatasetInfo,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub licenses: Vec<License>,
pub images: Vec<Image>,
pub categories: Vec<Category>,
pub annotations: Vec<Annotation>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct DatasetInfo {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub year: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contributor: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub date_created: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub attributes: BTreeMap<String, String>,
}
impl DatasetInfo {
pub fn is_empty(&self) -> bool {
self.name.is_none()
&& self.version.is_none()
&& self.description.is_none()
&& self.url.is_none()
&& self.year.is_none()
&& self.contributor.is_none()
&& self.date_created.is_none()
&& self.attributes.is_empty()
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct License {
pub id: LicenseId,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
impl License {
pub fn new(id: impl Into<LicenseId>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
url: None,
}
}
pub fn with_url(
id: impl Into<LicenseId>,
name: impl Into<String>,
url: impl Into<String>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
url: Some(url.into()),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Image {
pub id: ImageId,
pub file_name: String,
pub width: u32,
pub height: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license_id: Option<LicenseId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub date_captured: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub attributes: BTreeMap<String, String>,
}
impl Image {
pub fn new(
id: impl Into<ImageId>,
file_name: impl Into<String>,
width: u32,
height: u32,
) -> Self {
Self {
id: id.into(),
file_name: file_name.into(),
width,
height,
license_id: None,
date_captured: None,
attributes: BTreeMap::new(),
}
}
pub fn with_license(mut self, license_id: impl Into<LicenseId>) -> Self {
self.license_id = Some(license_id.into());
self
}
pub fn with_date_captured(mut self, date: impl Into<String>) -> Self {
self.date_captured = Some(date.into());
self
}
}
impl From<u64> for ImageId {
fn from(id: u64) -> Self {
ImageId::new(id)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Category {
pub id: CategoryId,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supercategory: Option<String>,
}
impl Category {
pub fn new(id: impl Into<CategoryId>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
supercategory: None,
}
}
pub fn with_supercategory(
id: impl Into<CategoryId>,
name: impl Into<String>,
supercategory: impl Into<String>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
supercategory: Some(supercategory.into()),
}
}
}
impl From<u64> for CategoryId {
fn from(id: u64) -> Self {
CategoryId::new(id)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Annotation {
pub id: AnnotationId,
pub image_id: ImageId,
pub category_id: CategoryId,
pub bbox: BBoxXYXY<Pixel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub confidence: Option<f64>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub attributes: BTreeMap<String, String>,
}
impl Annotation {
pub fn new(
id: impl Into<AnnotationId>,
image_id: impl Into<ImageId>,
category_id: impl Into<CategoryId>,
bbox: BBoxXYXY<Pixel>,
) -> Self {
Self {
id: id.into(),
image_id: image_id.into(),
category_id: category_id.into(),
bbox,
confidence: None,
attributes: BTreeMap::new(),
}
}
pub fn with_confidence(mut self, confidence: f64) -> Self {
self.confidence = Some(confidence);
self
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
}
impl From<u64> for AnnotationId {
fn from(id: u64) -> Self {
AnnotationId::new(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dataset_creation() {
let dataset = Dataset {
info: DatasetInfo {
name: Some("Test Dataset".into()),
..Default::default()
},
licenses: vec![],
images: vec![Image::new(1u64, "image001.jpg", 640, 480)],
categories: vec![Category::new(1u64, "person")],
annotations: vec![Annotation::new(
1u64,
1u64,
1u64,
BBoxXYXY::from_xyxy(10.0, 20.0, 100.0, 200.0),
)],
};
assert_eq!(dataset.images.len(), 1);
assert_eq!(dataset.categories.len(), 1);
assert_eq!(dataset.annotations.len(), 1);
}
#[test]
fn test_annotation_builder_pattern() {
let annotation =
Annotation::new(1u64, 1u64, 1u64, BBoxXYXY::from_xyxy(0.0, 0.0, 50.0, 50.0))
.with_confidence(0.95)
.with_attribute("occluded", "false")
.with_attribute("truncated", "true");
assert_eq!(annotation.confidence, Some(0.95));
assert_eq!(annotation.attributes.len(), 2);
assert_eq!(
annotation.attributes.get("occluded"),
Some(&"false".to_string())
);
}
}