use std::collections::HashMap;
use std::fs;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::core::{Detection, Detections};
use crate::dataset::{DatasetImage, DetectionDataset};
use crate::error::Error;
#[derive(Debug, Serialize, Deserialize)]
struct CocoRoot {
images: Vec<CocoImage>,
annotations: Vec<CocoAnnotation>,
categories: Vec<CocoCategory>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CocoImage {
id: u32,
file_name: String,
width: u32,
height: u32,
}
#[derive(Debug, Serialize, Deserialize)]
struct CocoAnnotation {
id: u32,
image_id: u32,
category_id: u32,
bbox: [f32; 4],
#[serde(default)]
score: Option<f32>,
#[serde(default)]
segmentation: Option<Vec<Vec<f32>>>,
#[serde(default)]
area: Option<f32>,
#[serde(default)]
iscrowd: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CocoCategory {
id: u32,
name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
supercategory: Option<String>,
}
pub fn load(annotations_path: &Path, images_dir: &Path) -> crate::Result<DetectionDataset> {
let text = fs::read_to_string(annotations_path)?;
let root: CocoRoot = serde_json::from_str(&text).map_err(|e| Error::Parse(e.to_string()))?;
let mut categories = root.categories.clone();
categories.sort_by_key(|c| c.id);
let classes: Vec<String> = categories.iter().map(|c| c.name.clone()).collect();
let category_index: HashMap<u32, usize> = categories
.iter()
.enumerate()
.map(|(i, c)| (c.id, i))
.collect();
let mut images_by_id: HashMap<u32, DatasetImage> = root
.images
.iter()
.map(|image| {
(
image.id,
DatasetImage {
path: images_dir.join(&image.file_name),
width: image.width,
height: image.height,
detections: Detections::empty(),
},
)
})
.collect();
let mut detections_by_image: HashMap<u32, Vec<Detection>> = HashMap::new();
for annotation in &root.annotations {
let Some(&class_id) = category_index.get(&annotation.category_id) else {
continue;
};
let [x, y, w, h] = annotation.bbox;
let mut detection = Detection::new(
[x, y, x + w, y + h],
annotation.score.unwrap_or(1.0),
class_id,
);
if let Some(first_ring) = annotation.segmentation.as_ref().and_then(|s| s.first()) {
let polygon: Vec<[f32; 2]> = first_ring
.chunks_exact(2)
.map(|pair| [pair[0], pair[1]])
.collect();
if polygon.len() >= 3 {
detection.mask = Some(polygon);
}
}
detections_by_image
.entry(annotation.image_id)
.or_default()
.push(detection);
}
for (image_id, detections) in detections_by_image {
if let Some(image) = images_by_id.get_mut(&image_id) {
image.detections = Detections::new(detections);
}
}
let images = root
.images
.iter()
.filter_map(|image| images_by_id.remove(&image.id))
.collect();
Ok(DetectionDataset::new(classes, images))
}
pub fn save(dataset: &DetectionDataset, path: &Path) -> crate::Result<()> {
let categories: Vec<CocoCategory> = dataset
.classes
.iter()
.enumerate()
.map(|(i, name)| CocoCategory {
id: (i + 1) as u32,
name: name.clone(),
supercategory: None,
})
.collect();
let mut images = Vec::with_capacity(dataset.images.len());
let mut annotations = Vec::new();
let mut next_annotation_id = 1u32;
for (index, image) in dataset.images.iter().enumerate() {
let image_id = (index + 1) as u32;
let file_name = image
.path
.file_name()
.map(|f| f.to_string_lossy().into_owned())
.unwrap_or_default();
images.push(CocoImage {
id: image_id,
file_name,
width: image.width,
height: image.height,
});
for detection in image.detections.iter() {
let [x1, y1, x2, y2] = detection.bbox;
let (w, h) = (x2 - x1, y2 - y1);
annotations.push(CocoAnnotation {
id: next_annotation_id,
image_id,
category_id: (detection.class_id + 1) as u32,
bbox: [x1, y1, w, h],
score: Some(detection.confidence),
segmentation: detection
.mask
.as_ref()
.map(|polygon| vec![polygon.iter().flat_map(|p| [p[0], p[1]]).collect()]),
area: Some(w * h),
iscrowd: 0,
});
next_annotation_id += 1;
}
}
let root = CocoRoot {
images,
annotations,
categories,
};
let text = serde_json::to_string_pretty(&root).map_err(|e| Error::Parse(e.to_string()))?;
fs::write(path, text)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_dataset() -> DetectionDataset {
let mut detection = Detection::new([10.0, 20.0, 60.0, 120.0], 0.95, 0);
detection.mask = Some(vec![
[10.0, 20.0],
[60.0, 20.0],
[60.0, 120.0],
[10.0, 120.0],
]);
DetectionDataset::new(
vec!["cat".to_string(), "dog".to_string()],
vec![DatasetImage {
path: std::path::PathBuf::from("image_0.jpg"),
width: 640,
height: 480,
detections: Detections::new(vec![
detection,
Detection::new([0.0, 0.0, 5.0, 5.0], 0.5, 1),
]),
}],
)
}
#[test]
fn round_trips_through_save_and_load() {
let dir = tempfile::tempdir().unwrap();
let annotations_path = dir.path().join("instances.json");
let images_dir = dir.path().join("images");
let dataset = sample_dataset();
save(&dataset, &annotations_path).unwrap();
let loaded = load(&annotations_path, &images_dir).unwrap();
assert_eq!(loaded.classes, dataset.classes);
assert_eq!(loaded.len(), 1);
assert_eq!(loaded.images[0].width, 640);
assert_eq!(loaded.images[0].height, 480);
assert_eq!(loaded.images[0].path, images_dir.join("image_0.jpg"));
assert_eq!(loaded.images[0].detections.len(), 2);
let boxes = &loaded.images[0].detections;
assert!((boxes.detections[0].bbox[0] - 10.0).abs() < 1e-3);
assert!((boxes.detections[0].bbox[3] - 120.0).abs() < 1e-3);
assert_eq!(boxes.detections[0].class_id, 0);
assert!(boxes.detections[0].mask.is_some());
assert_eq!(boxes.detections[1].class_id, 1);
}
#[test]
fn class_ids_follow_sorted_category_id_order_regardless_of_json_order() {
let dir = tempfile::tempdir().unwrap();
let annotations_path = dir.path().join("instances.json");
let json = r#"{
"images": [{"id": 1, "file_name": "a.jpg", "width": 10, "height": 10}],
"annotations": [
{"id": 1, "image_id": 1, "category_id": 5, "bbox": [0,0,1,1]},
{"id": 2, "image_id": 1, "category_id": 2, "bbox": [1,1,1,1]}
],
"categories": [
{"id": 5, "name": "dog"},
{"id": 2, "name": "cat"}
]
}"#;
fs::write(&annotations_path, json).unwrap();
let loaded = load(&annotations_path, dir.path()).unwrap();
assert_eq!(loaded.classes, vec!["cat".to_string(), "dog".to_string()]);
let class_ids: Vec<usize> = loaded.images[0]
.detections
.iter()
.map(|d| d.class_id)
.collect();
assert!(class_ids.contains(&0)); assert!(class_ids.contains(&1)); }
#[test]
fn malformed_json_returns_a_parse_error() {
let dir = tempfile::tempdir().unwrap();
let annotations_path = dir.path().join("bad.json");
fs::write(&annotations_path, "{ not json").unwrap();
let result = load(&annotations_path, dir.path());
assert!(matches!(result, Err(Error::Parse(_))));
}
}