///|
/// Errors returned by the text-only YOLO and Pascal VOC annotation adapters.
pub(all) enum AdapterError {
InvalidImageSize(message~ : String)
InvalidYolo(field~ : String, message~ : String)
InvalidYoloText(message~ : String)
MissingClassMapping(class_id~ : Int)
InvalidVoc(path~ : String, message~ : String)
} derive(Debug, ToJson)
///|
/// A YOLO bounding box whose centre and dimensions are normalized to `[0, 1]`.
/// `confidence` is optional because training labels normally omit it.
pub(all) struct YoloBox {
class_id : Int
center_x : Double
center_y : Double
width : Double
height : Double
confidence : Double?
} derive(Debug, ToJson)
///|
/// Creates a validated normalized YOLO record.
pub fn YoloBox::try_new(
class_id~ : Int,
center_x~ : Double,
center_y~ : Double,
width~ : Double,
height~ : Double,
confidence? : Double,
) -> Result[YoloBox, AdapterError] {
let box = { class_id, center_x, center_y, width, height, confidence }
match validate_yolo_box(box) {
Ok(_) => Ok(box)
Err(error) => Err(error)
}
}
///|
/// Parses one deterministic YOLO label line (five fields, or six with score).
/// The caller's class mapping is checked so unknown numeric IDs are rejected.
pub fn YoloBox::from_line(
line : String,
image~ : ImageSpec,
classes~ : Array[String],
) -> Result[YoloBox, AdapterError] {
match validate_image(image) {
Err(error) => Err(error)
Ok(_) => {
let fields = yolo_fields(line)
if fields.length() != 5 && fields.length() != 6 {
Err(
InvalidYoloText(
message="expected five fields, or six with confidence",
),
)
} else {
match parse_int(fields[0], "class_id") {
Err(error) => Err(error)
Ok(class_id) =>
match class_for_id(classes, class_id) {
Err(error) => Err(error)
Ok(_) => parse_yolo_values(fields, class_id)
}
}
}
}
}
}
///|
/// Converts this normalized box to the library's pixel-space `Rect`.
pub fn YoloBox::to_rect(
self : YoloBox,
image : ImageSpec,
) -> Result[Rect, AdapterError] {
match validate_image(image) {
Err(error) => Err(error)
Ok(_) =>
match validate_yolo_box(self) {
Err(error) => Err(error)
Ok(_) => {
let image_width = image.width.to_double()
let image_height = image.height.to_double()
Ok(
Rect::new(
x=self.center_x * image_width - self.width * image_width / 2.0,
y=self.center_y * image_height - self.height * image_height / 2.0,
width=self.width * image_width,
height=self.height * image_height,
),
)
}
}
}
}
///|
/// Converts a contained pixel-space rectangle into a normalized YOLO record.
pub fn YoloBox::from_rect(
class_id~ : Int,
rect~ : Rect,
image~ : ImageSpec,
confidence? : Double,
) -> Result[YoloBox, AdapterError] {
match validate_image(image) {
Err(error) => Err(error)
Ok(_) if !valid_rect(rect) =>
Err(
InvalidYolo(
field="rect",
message="must contain finite positive dimensions",
),
)
Ok(_) => {
let image_width = image.width.to_double()
let image_height = image.height.to_double()
YoloBox::try_new(
class_id~,
center_x=(rect.x + rect.width / 2.0) / image_width,
center_y=(rect.y + rect.height / 2.0) / image_height,
width=rect.width / image_width,
height=rect.height / image_height,
confidence?,
)
}
}
}
///|
/// Serializes one record with single ASCII-space separators. The mapping is
/// validated even though standard YOLO syntax stores the numeric ID.
pub fn YoloBox::to_line(
self : YoloBox,
classes : Array[String],
) -> Result[String, AdapterError] {
match validate_yolo_box(self) {
Err(error) => Err(error)
Ok(_) =>
match class_for_id(classes, self.class_id) {
Err(error) => Err(error)
Ok(_) => {
let fields = [
self.class_id.to_string(),
self.center_x.to_string(),
self.center_y.to_string(),
self.width.to_string(),
self.height.to_string(),
]
match self.confidence {
Some(confidence) => {
fields.push(confidence.to_string())
Ok(fields.join(" "))
}
None => Ok(fields.join(" "))
}
}
}
}
}
///|
/// One object in a Pascal VOC annotation. `bbox` is pixel-space and uses the
/// library's zero-based, half-open `Rect` convention.
pub(all) struct VocObject {
name : String
bbox : Rect
} derive(Debug, ToJson)
///|
/// Creates an unvalidated VOC object. `VocAnnotation::try_new` and XML import
/// validate names and boxes before returning a fallible result.
pub fn VocObject::new(name~ : String, bbox~ : Rect) -> VocObject {
{ name, bbox }
}
///|
/// A lightweight Pascal VOC annotation with image dimensions and objects.
pub(all) struct VocAnnotation {
filename : String?
size : ImageSpec
objects : Array[VocObject]
} derive(Debug, ToJson)
///|
/// Creates an unvalidated VOC annotation for convenient literals.
pub fn VocAnnotation::new(
filename? : String,
size~ : ImageSpec,
objects~ : Array[VocObject],
) -> VocAnnotation {
{ filename, size, objects }
}
///|
/// Validates a VOC annotation without reading files or touching the runtime.
pub fn VocAnnotation::try_new(
filename? : String,
size~ : ImageSpec,
objects~ : Array[VocObject],
) -> Result[VocAnnotation, AdapterError] {
let annotation = VocAnnotation::new(filename?, size~, objects~)
match validate_voc(annotation) {
Ok(_) => Ok(annotation)
Err(error) => Err(error)
}
}
///|
/// Parses VOC XML containing an optional XML declaration, standard metadata,
/// ordered `` dimensions, and ordered `