mod cvat;
mod dota;
mod oid;
mod voc;
mod yolo;
pub use cvat::{CvatImportStats, CvatStats, coco_to_cvat, cvat_to_coco};
pub use dota::{DotaStats, coco_to_dota, dota_to_coco};
pub use oid::{OidStats, coco_to_oid, oid_results_to_anns, oid_to_coco, read_class_descriptions};
pub use voc::{VocStats, coco_to_voc, voc_to_coco};
pub use yolo::{YoloStats, coco_to_yolo, yolo_to_coco};
use std::collections::HashMap;
use std::io;
use std::path::Path;
use crate::types::{Annotation, Dataset};
#[derive(Debug, thiserror::Error)]
pub enum ConvertError {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error(
"image `{0}` has zero or unknown width/height, which this conversion needs to scale coordinates"
)]
MissingImageDimensions(String),
#[error("data.yaml not found in YOLO directory")]
MissingDataYaml,
#[error("parse error: {0}")]
ParseError(String),
#[error("XML error: {0}")]
XmlError(String),
#[error(
"images `{first}` and `{second}` share the file stem `{stem}`; per-image outputs are keyed by stem and would overwrite each other — rename one image"
)]
StemCollision {
stem: String,
first: String,
second: String,
},
#[error(
"annotation {ann_id} references category id {category_id}, which is not in the dataset's categories"
)]
UnknownCategory {
ann_id: u64,
category_id: u64,
},
}
impl From<quick_xml::Error> for ConvertError {
fn from(e: quick_xml::Error) -> Self {
ConvertError::XmlError(e.to_string())
}
}
impl ConvertError {
pub(crate) fn with_path(self, path: &Path) -> Self {
match self {
ConvertError::ParseError(msg) => {
ConvertError::ParseError(format!("{}: {msg}", path.display()))
}
ConvertError::XmlError(msg) => {
ConvertError::XmlError(format!("{}: {msg}", path.display()))
}
other => other,
}
}
}
pub(crate) fn parse_err(path: &Path, detail: impl std::fmt::Display) -> ConvertError {
ConvertError::ParseError(format!("{}: {detail}", path.display()))
}
pub(crate) fn line_err(
path: &Path,
line_no: usize,
detail: impl std::fmt::Display,
) -> ConvertError {
ConvertError::ParseError(format!("{}: line {line_no}: {detail}", path.display()))
}
pub(crate) fn anns_by_image(dataset: &Dataset) -> HashMap<u64, Vec<&Annotation>> {
let mut map: HashMap<u64, Vec<&Annotation>> = HashMap::new();
for ann in &dataset.annotations {
map.entry(ann.image_id).or_default().push(ann);
}
map
}
pub(crate) fn at_byte(err: ConvertError, pos: u64) -> ConvertError {
match err {
ConvertError::ParseError(msg) => {
ConvertError::ParseError(format!("near byte {pos}: {msg}"))
}
other => other,
}
}
pub(crate) fn file_stem(file_name: &str) -> &str {
Path::new(file_name)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(file_name)
}
pub(crate) fn utf8_stem(path: &Path) -> Result<&str, ConvertError> {
path.file_stem().and_then(|s| s.to_str()).ok_or_else(|| {
ConvertError::ParseError(format!(
"label file `{}` has a non-UTF-8 name; cannot derive an image file name from it",
path.display()
))
})
}
pub(crate) fn check_unique_stems(dataset: &Dataset) -> Result<(), ConvertError> {
let mut seen: HashMap<&str, &str> = HashMap::new();
for img in &dataset.images {
let stem = file_stem(&img.file_name);
if let Some(first) = seen.insert(stem, &img.file_name) {
return Err(ConvertError::StemCollision {
stem: stem.to_string(),
first: first.to_string(),
second: img.file_name.clone(),
});
}
}
Ok(())
}
pub const IMAGE_EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "bmp", "tif", "tiff"];
pub(crate) fn lookup_image_dims(
image_dims: &HashMap<String, (u32, u32)>,
stem: &str,
) -> Option<(u32, u32)> {
let found = image_dims.get(stem).or_else(|| {
IMAGE_EXTENSIONS
.iter()
.find_map(|ext| image_dims.get(&format!("{stem}.{ext}")))
});
match found {
Some(&(w, h)) if w > 0 && h > 0 => Some((w, h)),
_ => None,
}
}
pub(crate) fn write_text_element<W: std::io::Write>(
writer: &mut quick_xml::writer::Writer<W>,
tag: &str,
text: &str,
) -> Result<(), quick_xml::Error> {
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
writer.write_event(Event::Start(BytesStart::new(tag)))?;
writer.write_event(Event::Text(BytesText::new(text)))?;
writer.write_event(Event::End(BytesEnd::new(tag)))?;
Ok(())
}