use serde::Serialize;
use std::fmt;
#[derive(Clone, Debug, Default, Serialize)]
pub struct ConversionReport {
pub from: String,
pub to: String,
pub input: ConversionCounts,
pub output: ConversionCounts,
pub issues: Vec<ConversionIssue>,
}
impl ConversionReport {
pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
Self {
from: from.into(),
to: to.into(),
..Default::default()
}
}
pub fn add(&mut self, issue: ConversionIssue) {
self.issues.push(issue);
}
pub fn warning_count(&self) -> usize {
self.issues
.iter()
.filter(|i| i.severity == ConversionSeverity::Warning)
.count()
}
pub fn info_count(&self) -> usize {
self.issues
.iter()
.filter(|i| i.severity == ConversionSeverity::Info)
.count()
}
pub fn is_lossy(&self) -> bool {
self.warning_count() > 0
}
}
impl fmt::Display for ConversionReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
" {} images, {} categories, {} annotations",
self.input.images, self.input.categories, self.input.annotations
)?;
if self.output != self.input {
writeln!(
f,
" output: {} images, {} categories, {} annotations",
self.output.images, self.output.categories, self.output.annotations
)?;
}
if !self.issues.is_empty() {
let warnings = self.warning_count();
let infos = self.info_count();
if warnings > 0 {
writeln!(f)?;
writeln!(f, "Warnings ({}):", warnings)?;
for issue in self
.issues
.iter()
.filter(|i| i.severity == ConversionSeverity::Warning)
{
writeln!(f, " - [{}] {}", issue.code.as_str(), issue.message)?;
}
}
if infos > 0 {
writeln!(f)?;
writeln!(f, "Notes ({}):", infos)?;
for issue in self
.issues
.iter()
.filter(|i| i.severity == ConversionSeverity::Info)
{
writeln!(f, " - [{}] {}", issue.code.as_str(), issue.message)?;
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct ConversionCounts {
pub images: usize,
pub categories: usize,
pub annotations: usize,
}
#[derive(Clone, Debug, Serialize)]
pub struct ConversionIssue {
pub severity: ConversionSeverity,
pub stage: ConversionStage,
pub code: ConversionIssueCode,
pub message: String,
}
impl ConversionIssue {
pub fn warning(code: ConversionIssueCode, message: impl Into<String>) -> Self {
Self {
severity: ConversionSeverity::Warning,
stage: ConversionStage::Analysis,
code,
message: message.into(),
}
}
pub fn reader_info(code: ConversionIssueCode, message: impl Into<String>) -> Self {
Self {
severity: ConversionSeverity::Info,
stage: ConversionStage::SourceReader,
code,
message: message.into(),
}
}
pub fn writer_info(code: ConversionIssueCode, message: impl Into<String>) -> Self {
Self {
severity: ConversionSeverity::Info,
stage: ConversionStage::TargetWriter,
code,
message: message.into(),
}
}
pub fn info(code: ConversionIssueCode, message: impl Into<String>) -> Self {
Self {
severity: ConversionSeverity::Info,
stage: ConversionStage::Analysis,
code,
message: message.into(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ConversionStage {
Analysis,
SourceReader,
TargetWriter,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ConversionSeverity {
Warning,
Info,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ConversionIssueCode {
DropDatasetInfo,
DropLicenses,
DropImageMetadata,
DropCategorySupercategory,
DropAnnotationConfidence,
DropAnnotationAttributes,
DropImagesWithoutAnnotations,
DropDatasetInfoName,
CocoAttributesMayNotBePreserved,
CocoWriterDeterministicOrder,
CocoWriterScoreMapping,
CocoWriterAreaIscrowdMapping,
CocoWriterEmptySegmentation,
CocoReaderAttributeMapping,
HfMetadataLost,
HfAttributesLost,
HfConfidenceLost,
HfReaderObjectContainerPrecedence,
HfReaderBboxFormatDependence,
LabelStudioWriterConfidenceRouting,
YoloWriterDeterministicOrder,
YoloWriterNoImageCopy,
YoloWriterDataYamlPolicy,
TfodReaderIdAssignment,
TfodWriterRowOrder,
YoloReaderIdAssignment,
YoloReaderClassMapSource,
YoloReaderSplitHandling,
YoloWriterClassOrder,
YoloWriterEmptyLabelFiles,
YoloWriterFloatPrecision,
VocReaderIdAssignment,
VocReaderAttributeMapping,
VocReaderCoordinatePolicy,
VocReaderDepthHandling,
VocWriterFileLayout,
VocWriterNoImageCopy,
VocWriterBoolNormalization,
LabelStudioRotationDropped,
LabelStudioReaderIdAssignment,
LabelStudioReaderImageRefPolicy,
LabelStudioWriterFromToDefaults,
CvatReaderIdAssignment,
CvatReaderAttributePolicy,
CvatWriterMetaDefaults,
CvatWriterDeterministicOrder,
CvatWriterImageIdReassignment,
CvatWriterSourceDefault,
CvatWriterDropUnusedCategories,
HfReaderCategoryResolution,
HfWriterDeterministicOrder,
LabelmeReaderIdAssignment,
LabelmeReaderPathPolicy,
LabelmePolygonEnvelopeApplied,
LabelmeWriterFileLayout,
LabelmeWriterRectanglePolicy,
LabelmeWriterNoImageCopy,
CreatemlReaderIdAssignment,
CreatemlReaderImageResolution,
CreatemlWriterDeterministicOrder,
CreatemlWriterCoordinateMapping,
CreatemlWriterNoImageCopy,
KittiReaderIdAssignment,
KittiReaderFieldMapping,
KittiReaderImageResolution,
KittiWriterFileLayout,
KittiWriterDefaultFieldValues,
KittiWriterDeterministicOrder,
KittiWriterNoImageCopy,
ViaReaderIdAssignment,
ViaReaderLabelResolution,
ViaReaderImageResolution,
ViaWriterDeterministicOrder,
ViaWriterLabelAttributeKey,
ViaWriterNoImageCopy,
RetinanetReaderIdAssignment,
RetinanetReaderImageResolution,
RetinanetReaderEmptyRowHandling,
RetinanetWriterDeterministicOrder,
RetinanetWriterEmptyRows,
RetinanetWriterNoImageCopy,
}
impl ConversionIssueCode {
pub const ALL: &'static [ConversionIssueCode] = &[
Self::DropDatasetInfo,
Self::DropLicenses,
Self::DropImageMetadata,
Self::DropCategorySupercategory,
Self::DropAnnotationConfidence,
Self::DropAnnotationAttributes,
Self::DropImagesWithoutAnnotations,
Self::DropDatasetInfoName,
Self::CocoAttributesMayNotBePreserved,
Self::CocoWriterDeterministicOrder,
Self::CocoWriterScoreMapping,
Self::CocoWriterAreaIscrowdMapping,
Self::CocoWriterEmptySegmentation,
Self::CocoReaderAttributeMapping,
Self::HfMetadataLost,
Self::HfAttributesLost,
Self::HfConfidenceLost,
Self::HfReaderObjectContainerPrecedence,
Self::HfReaderBboxFormatDependence,
Self::LabelStudioWriterConfidenceRouting,
Self::YoloWriterDeterministicOrder,
Self::YoloWriterNoImageCopy,
Self::YoloWriterDataYamlPolicy,
Self::TfodReaderIdAssignment,
Self::TfodWriterRowOrder,
Self::YoloReaderIdAssignment,
Self::YoloReaderClassMapSource,
Self::YoloReaderSplitHandling,
Self::YoloWriterClassOrder,
Self::YoloWriterEmptyLabelFiles,
Self::YoloWriterFloatPrecision,
Self::VocReaderIdAssignment,
Self::VocReaderAttributeMapping,
Self::VocReaderCoordinatePolicy,
Self::VocReaderDepthHandling,
Self::VocWriterFileLayout,
Self::VocWriterNoImageCopy,
Self::VocWriterBoolNormalization,
Self::LabelStudioRotationDropped,
Self::LabelStudioReaderIdAssignment,
Self::LabelStudioReaderImageRefPolicy,
Self::LabelStudioWriterFromToDefaults,
Self::CvatReaderIdAssignment,
Self::CvatReaderAttributePolicy,
Self::CvatWriterMetaDefaults,
Self::CvatWriterDeterministicOrder,
Self::CvatWriterImageIdReassignment,
Self::CvatWriterSourceDefault,
Self::CvatWriterDropUnusedCategories,
Self::HfReaderCategoryResolution,
Self::HfWriterDeterministicOrder,
Self::LabelmeReaderIdAssignment,
Self::LabelmeReaderPathPolicy,
Self::LabelmePolygonEnvelopeApplied,
Self::LabelmeWriterFileLayout,
Self::LabelmeWriterRectanglePolicy,
Self::LabelmeWriterNoImageCopy,
Self::CreatemlReaderIdAssignment,
Self::CreatemlReaderImageResolution,
Self::CreatemlWriterDeterministicOrder,
Self::CreatemlWriterCoordinateMapping,
Self::CreatemlWriterNoImageCopy,
Self::KittiReaderIdAssignment,
Self::KittiReaderFieldMapping,
Self::KittiReaderImageResolution,
Self::KittiWriterFileLayout,
Self::KittiWriterDefaultFieldValues,
Self::KittiWriterDeterministicOrder,
Self::KittiWriterNoImageCopy,
Self::ViaReaderIdAssignment,
Self::ViaReaderLabelResolution,
Self::ViaReaderImageResolution,
Self::ViaWriterDeterministicOrder,
Self::ViaWriterLabelAttributeKey,
Self::ViaWriterNoImageCopy,
Self::RetinanetReaderIdAssignment,
Self::RetinanetReaderImageResolution,
Self::RetinanetReaderEmptyRowHandling,
Self::RetinanetWriterDeterministicOrder,
Self::RetinanetWriterEmptyRows,
Self::RetinanetWriterNoImageCopy,
];
pub fn as_str(&self) -> &'static str {
match self {
Self::DropDatasetInfo => "drop_dataset_info",
Self::DropLicenses => "drop_licenses",
Self::DropImageMetadata => "drop_image_metadata",
Self::DropCategorySupercategory => "drop_category_supercategory",
Self::DropAnnotationConfidence => "drop_annotation_confidence",
Self::DropAnnotationAttributes => "drop_annotation_attributes",
Self::DropImagesWithoutAnnotations => "drop_images_without_annotations",
Self::DropDatasetInfoName => "drop_dataset_info_name",
Self::CocoAttributesMayNotBePreserved => "coco_attributes_may_not_be_preserved",
Self::CocoWriterDeterministicOrder => "coco_writer_deterministic_order",
Self::CocoWriterScoreMapping => "coco_writer_score_mapping",
Self::CocoWriterAreaIscrowdMapping => "coco_writer_area_iscrowd_mapping",
Self::CocoWriterEmptySegmentation => "coco_writer_empty_segmentation",
Self::CocoReaderAttributeMapping => "coco_reader_attribute_mapping",
Self::HfMetadataLost => "hf_metadata_lost",
Self::HfAttributesLost => "hf_attributes_lost",
Self::HfConfidenceLost => "hf_confidence_lost",
Self::HfReaderObjectContainerPrecedence => "hf_reader_object_container_precedence",
Self::HfReaderBboxFormatDependence => "hf_reader_bbox_format_dependence",
Self::LabelStudioWriterConfidenceRouting => "label_studio_writer_confidence_routing",
Self::YoloWriterDeterministicOrder => "yolo_writer_deterministic_order",
Self::YoloWriterNoImageCopy => "yolo_writer_no_image_copy",
Self::YoloWriterDataYamlPolicy => "yolo_writer_data_yaml_policy",
Self::TfodReaderIdAssignment => "tfod_reader_id_assignment",
Self::TfodWriterRowOrder => "tfod_writer_row_order",
Self::YoloReaderIdAssignment => "yolo_reader_id_assignment",
Self::YoloReaderClassMapSource => "yolo_reader_class_map_source",
Self::YoloReaderSplitHandling => "yolo_reader_split_handling",
Self::YoloWriterClassOrder => "yolo_writer_class_order",
Self::YoloWriterEmptyLabelFiles => "yolo_writer_empty_label_files",
Self::YoloWriterFloatPrecision => "yolo_writer_float_precision",
Self::VocReaderIdAssignment => "voc_reader_id_assignment",
Self::VocReaderAttributeMapping => "voc_reader_attribute_mapping",
Self::VocReaderCoordinatePolicy => "voc_reader_coordinate_policy",
Self::VocReaderDepthHandling => "voc_reader_depth_handling",
Self::VocWriterFileLayout => "voc_writer_file_layout",
Self::VocWriterNoImageCopy => "voc_writer_no_image_copy",
Self::VocWriterBoolNormalization => "voc_writer_bool_normalization",
Self::LabelStudioRotationDropped => "label_studio_rotation_dropped",
Self::LabelStudioReaderIdAssignment => "label_studio_reader_id_assignment",
Self::LabelStudioReaderImageRefPolicy => "label_studio_reader_image_ref_policy",
Self::LabelStudioWriterFromToDefaults => "label_studio_writer_from_to_defaults",
Self::CvatReaderIdAssignment => "cvat_reader_id_assignment",
Self::CvatReaderAttributePolicy => "cvat_reader_attribute_policy",
Self::CvatWriterMetaDefaults => "cvat_writer_meta_defaults",
Self::CvatWriterDeterministicOrder => "cvat_writer_deterministic_order",
Self::CvatWriterImageIdReassignment => "cvat_writer_image_id_reassignment",
Self::CvatWriterSourceDefault => "cvat_writer_source_default",
Self::CvatWriterDropUnusedCategories => "cvat_writer_drop_unused_categories",
Self::HfReaderCategoryResolution => "hf_reader_category_resolution",
Self::HfWriterDeterministicOrder => "hf_writer_deterministic_order",
Self::LabelmeReaderIdAssignment => "labelme_reader_id_assignment",
Self::LabelmeReaderPathPolicy => "labelme_reader_path_policy",
Self::LabelmePolygonEnvelopeApplied => "labelme_polygon_envelope_applied",
Self::LabelmeWriterFileLayout => "labelme_writer_file_layout",
Self::LabelmeWriterRectanglePolicy => "labelme_writer_rectangle_policy",
Self::LabelmeWriterNoImageCopy => "labelme_writer_no_image_copy",
Self::CreatemlReaderIdAssignment => "createml_reader_id_assignment",
Self::CreatemlReaderImageResolution => "createml_reader_image_resolution",
Self::CreatemlWriterDeterministicOrder => "createml_writer_deterministic_order",
Self::CreatemlWriterCoordinateMapping => "createml_writer_coordinate_mapping",
Self::CreatemlWriterNoImageCopy => "createml_writer_no_image_copy",
Self::KittiReaderIdAssignment => "kitti_reader_id_assignment",
Self::KittiReaderFieldMapping => "kitti_reader_field_mapping",
Self::KittiReaderImageResolution => "kitti_reader_image_resolution",
Self::KittiWriterFileLayout => "kitti_writer_file_layout",
Self::KittiWriterDefaultFieldValues => "kitti_writer_default_field_values",
Self::KittiWriterDeterministicOrder => "kitti_writer_deterministic_order",
Self::KittiWriterNoImageCopy => "kitti_writer_no_image_copy",
Self::ViaReaderIdAssignment => "via_reader_id_assignment",
Self::ViaReaderLabelResolution => "via_reader_label_resolution",
Self::ViaReaderImageResolution => "via_reader_image_resolution",
Self::ViaWriterDeterministicOrder => "via_writer_deterministic_order",
Self::ViaWriterLabelAttributeKey => "via_writer_label_attribute_key",
Self::ViaWriterNoImageCopy => "via_writer_no_image_copy",
Self::RetinanetReaderIdAssignment => "retinanet_reader_id_assignment",
Self::RetinanetReaderImageResolution => "retinanet_reader_image_resolution",
Self::RetinanetReaderEmptyRowHandling => "retinanet_reader_empty_row_handling",
Self::RetinanetWriterDeterministicOrder => "retinanet_writer_deterministic_order",
Self::RetinanetWriterEmptyRows => "retinanet_writer_empty_rows",
Self::RetinanetWriterNoImageCopy => "retinanet_writer_no_image_copy",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_report_is_not_lossy() {
let report = ConversionReport::new("coco", "ir-json");
assert!(!report.is_lossy());
assert_eq!(report.warning_count(), 0);
assert_eq!(report.info_count(), 0);
}
#[test]
fn warning_makes_report_lossy() {
let mut report = ConversionReport::new("ir-json", "tfod");
report.add(ConversionIssue::warning(
ConversionIssueCode::DropDatasetInfo,
"dataset info will be dropped",
));
assert!(report.is_lossy());
assert_eq!(report.warning_count(), 1);
}
#[test]
fn info_does_not_make_report_lossy() {
let mut report = ConversionReport::new("tfod", "coco");
report.add(ConversionIssue::info(
ConversionIssueCode::TfodReaderIdAssignment,
"IDs assigned by lexicographic order",
));
assert!(!report.is_lossy());
assert_eq!(report.info_count(), 1);
}
#[test]
fn report_serializes_to_json() {
let mut report = ConversionReport::new("coco", "tfod");
report.input = ConversionCounts {
images: 10,
categories: 3,
annotations: 50,
};
report.add(ConversionIssue::warning(
ConversionIssueCode::DropLicenses,
"2 license(s) will be dropped",
));
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains("\"from\":\"coco\""));
assert!(json.contains("\"severity\":\"warning\""));
assert!(json.contains("\"code\":\"drop_licenses\""));
}
#[test]
fn as_str_matches_serde_json_for_all_codes() {
for code in ConversionIssueCode::ALL {
let json = serde_json::to_value(code).unwrap();
assert_eq!(
json.as_str().unwrap(),
code.as_str(),
"as_str() and serde disagree for {:?}",
code
);
}
}
#[test]
fn all_codes_have_unique_str() {
let mut seen = std::collections::HashSet::new();
for code in ConversionIssueCode::ALL {
assert!(
seen.insert(code.as_str()),
"duplicate as_str() value: {}",
code.as_str()
);
}
}
#[test]
fn all_codes_documented_in_conversion_md() {
let docs =
std::fs::read_to_string("docs/conversion.md").expect("docs/conversion.md should exist");
let mut missing = Vec::new();
for code in ConversionIssueCode::ALL {
if !docs.contains(code.as_str()) {
missing.push(code.as_str());
}
}
assert!(
missing.is_empty(),
"docs/conversion.md is missing these issue codes: {:?}",
missing
);
}
#[test]
fn text_display_includes_stable_codes() {
let mut report = ConversionReport::new("coco", "tfod");
report.input = ConversionCounts {
images: 5,
categories: 2,
annotations: 10,
};
report.output = report.input.clone();
report.add(ConversionIssue::warning(
ConversionIssueCode::DropDatasetInfo,
"dataset info will be dropped",
));
report.add(ConversionIssue::info(
ConversionIssueCode::TfodWriterRowOrder,
"rows ordered by annotation ID",
));
let text = format!("{}", report);
assert!(
text.contains("[drop_dataset_info]"),
"text should contain warning code"
);
assert!(
text.contains("[tfod_writer_row_order]"),
"text should contain info code"
);
}
}