use std::collections::HashMap;
use std::fs;
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use crate::geometry::{corners_to_obb, obb_to_aabb, obb_to_corners};
use crate::types::{Annotation, Category, Dataset, Image};
use super::{ConvertError, anns_by_image, file_stem, line_err};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DotaStats {
pub images: usize,
pub annotations: usize,
pub skipped_no_obb: usize,
}
pub fn coco_to_dota(dataset: &Dataset, output_dir: &Path) -> Result<DotaStats, ConvertError> {
super::check_unique_stems(dataset)?;
fs::create_dir_all(output_dir)?;
let cat_map = crate::types::cat_id_to_name(dataset);
let grouped = anns_by_image(dataset);
let mut stats = DotaStats::default();
for img in &dataset.images {
let stem = file_stem(&img.file_name);
let path = output_dir.join(format!("{stem}.txt"));
let mut file = fs::File::create(&path)?;
if let Some(anns) = grouped.get(&img.id) {
for ann in anns {
let obb = match &ann.obb {
Some(o) => o,
None => {
stats.skipped_no_obb += 1;
continue;
}
};
let cat_name =
*cat_map
.get(&ann.category_id)
.ok_or(ConvertError::UnknownCategory {
ann_id: ann.id,
category_id: ann.category_id,
})?;
let difficulty = i32::from(ann.iscrowd);
let corners = obb_to_corners(obb);
writeln!(
file,
"{:.1} {:.1} {:.1} {:.1} {:.1} {:.1} {:.1} {:.1} {} {}",
corners[0].0,
corners[0].1,
corners[1].0,
corners[1].1,
corners[2].0,
corners[2].1,
corners[3].0,
corners[3].1,
cat_name,
difficulty,
)?;
stats.annotations += 1;
}
}
stats.images += 1;
}
Ok(stats)
}
pub fn dota_to_coco(
label_dir: &Path,
categories: Option<Vec<String>>,
image_dims: &HashMap<String, (u32, u32)>,
) -> Result<Dataset, ConvertError> {
let mut cat_name_to_id: HashMap<String, u64> = HashMap::new();
let mut cat_list: Vec<Category> = Vec::new();
if let Some(ref cats) = categories {
for (i, name) in cats.iter().enumerate() {
let id = (i + 1) as u64;
cat_name_to_id.insert(name.clone(), id);
cat_list.push(Category {
id,
name: name.clone(),
..Default::default()
});
}
}
let mut images: Vec<Image> = Vec::new();
let mut annotations: Vec<Annotation> = Vec::new();
let mut img_id: u64 = 1;
let mut ann_id: u64 = 1;
let mut entries: Vec<_> = fs::read_dir(label_dir)?
.filter_map(std::result::Result::ok)
.filter(|e| e.path().extension().is_some_and(|ext| ext == "txt"))
.collect();
entries.sort_by_key(std::fs::DirEntry::file_name);
for entry in entries {
let path = entry.path();
let stem = super::utf8_stem(&path)?.to_string();
let (width, height) = super::lookup_image_dims(image_dims, &stem).unwrap_or((0, 0));
images.push(Image {
id: img_id,
file_name: stem,
width,
height,
..Default::default()
});
let file = fs::File::open(&path)?;
let reader = BufReader::new(file);
for (line_idx, line) in reader.lines().enumerate() {
let line_no = line_idx + 1;
let line = line?;
let line = line.trim();
if line.is_empty()
|| line.starts_with('#')
|| line.starts_with("imagesource:")
|| line.starts_with("gsd:")
{
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 9 || parts.len() > 10 {
return Err(line_err(
&path,
line_no,
format!(
"expected `x1 y1 x2 y2 x3 y3 x4 y4 category [difficulty]` (9 or 10 fields), got {} in: {line}",
parts.len()
),
));
}
let coords: Vec<f64> = parts[..8]
.iter()
.map(|s| {
s.parse::<f64>().map_err(|_| {
line_err(&path, line_no, format!("invalid corner coordinate: {s}"))
})
})
.collect::<Result<_, _>>()?;
let cat_name = parts[8].to_string();
let difficulty: u8 = match parts.get(9) {
Some(s) => s
.parse()
.map_err(|_| line_err(&path, line_no, format!("invalid difficulty: {s}")))?,
None => 0,
};
let category_id = if let Some(&id) = cat_name_to_id.get(&cat_name) {
id
} else {
let id = (cat_list.len() + 1) as u64;
cat_name_to_id.insert(cat_name.clone(), id);
cat_list.push(Category {
id,
name: cat_name,
..Default::default()
});
id
};
let obb =
corners_to_obb(&coords).map_err(|e| line_err(&path, line_no, e.to_string()))?;
annotations.push(Annotation {
id: ann_id,
image_id: img_id,
category_id,
bbox: Some(obb_to_aabb(&obb)),
area: Some(obb[2] * obb[3]),
iscrowd: difficulty > 0,
obb: Some(obb),
..Default::default()
});
ann_id += 1;
}
img_id += 1;
}
Ok(Dataset {
info: None,
images,
annotations,
categories: cat_list,
licenses: vec![],
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use tempfile::TempDir;
const EPS: f64 = 0.2;
#[test]
fn test_corners_to_obb_axis_aligned() {
let coords = [0.0, 0.0, 4.0, 0.0, 4.0, 3.0, 0.0, 3.0];
let obb = corners_to_obb(&coords).unwrap();
assert!((obb[0] - 2.0).abs() < 1e-9, "cx"); assert!((obb[1] - 1.5).abs() < 1e-9, "cy"); assert!((obb[2] - 4.0).abs() < 1e-9, "w"); assert!((obb[3] - 3.0).abs() < 1e-9, "h"); assert!(obb[4].abs() < 1e-9, "angle should be 0"); }
#[test]
fn test_dota_round_trip() {
let dataset = Dataset {
info: None,
images: vec![Image {
id: 1,
file_name: "test.png".into(),
width: 800,
height: 600,
..Default::default()
}],
annotations: vec![Annotation {
id: 1,
image_id: 1,
category_id: 1,
bbox: Some([10.0, 10.0, 40.0, 20.0]),
area: Some(800.0),
obb: Some([30.0, 20.0, 40.0, 20.0, 0.0]),
..Default::default()
}],
categories: vec![Category {
id: 1,
name: "vehicle".into(),
..Default::default()
}],
licenses: vec![],
};
let tmp = TempDir::new().unwrap();
let label_dir = tmp.path().join("labels");
let stats = coco_to_dota(&dataset, &label_dir).unwrap();
assert_eq!(stats.images, 1);
assert_eq!(stats.annotations, 1);
let mut dims = HashMap::new();
dims.insert("test".into(), (800, 600));
let result = dota_to_coco(&label_dir, None, &dims).unwrap();
assert_eq!(result.annotations.len(), 1);
let ann = &result.annotations[0];
let obb = ann.obb.unwrap();
assert!((obb[0] - 30.0).abs() < EPS, "cx: {}", obb[0]);
assert!((obb[1] - 20.0).abs() < EPS, "cy: {}", obb[1]);
assert!((obb[2] - 40.0).abs() < EPS, "w: {}", obb[2]);
assert!((obb[3] - 20.0).abs() < EPS, "h: {}", obb[3]);
assert!(obb[4].abs() < EPS, "angle: {}", obb[4]);
assert_eq!(result.categories.len(), 1);
assert_eq!(result.categories[0].name, "vehicle");
assert_eq!(result.images[0].file_name, "test");
assert_eq!(
(result.images[0].width, result.images[0].height),
(800, 600)
);
}
}