///|
priv struct CocoImage {
id : Int
width : Int
height : Int
} derive(FromJson)
///|
priv struct CocoCategory {
id : Int
name : String
} derive(FromJson)
///|
priv struct CocoAnnotation {
id : Json
image_id : Int
category_id : Int
bbox : Array[Double]
area : Double
iscrowd : Json?
ignore : Json?
} derive(FromJson)
///|
priv struct CocoGroundTruthFile {
images : Array[Json]
categories : Array[Json]
annotations : Array[Json]
} derive(FromJson)
///|
priv struct CocoPrediction {
image_id : Int
category_id : Int
bbox : Array[Double]
score : Double
} derive(FromJson)
///|
fn parse_box(
coordinates : Array[Double],
location : String,
) -> Box raise EvalError {
if coordinates.length() != 4 {
raise InvalidInput(location + ".bbox must contain exactly four numbers")
}
Box::from_xywh(coordinates[0], coordinates[1], coordinates[2], coordinates[3]) catch {
error => raise InvalidInput(location + ".bbox: " + error.message())
}
}
///|
fn parse_annotation_id(
value : Json,
location : String,
) -> Int64 raise EvalError {
guard value is Json::Number(_, ..) else {
raise InvalidInput(location + " must be a 64-bit integer JSON number")
}
let token = value.stringify()
@json.from_json(Json::string(token)) catch {
_ => raise InvalidInput(location + " must be a 64-bit integer JSON number")
}
}
///|
fn parse_flag(value : Json?, location : String) -> Bool raise EvalError {
match value {
None => false
Some(Json::True) => true
Some(Json::False) => false
Some(Json::Number(number, ..)) if number == 0.0 => false
Some(Json::Number(number, ..)) if number == 1.0 => true
_ => raise InvalidInput(location + " must be boolean or 0/1")
}
}
///|
/// Parse the image, category, and annotation fields of a COCO ground-truth file.
pub fn parse_coco_dataset(source : String) -> Dataset raise EvalError {
let json = @json.parse(source) catch {
_ => raise InvalidInput("ground truth is not valid JSON")
}
let raw : CocoGroundTruthFile = @json.from_json(json) catch {
error =>
raise InvalidInput(
"ground truth needs images, categories, and annotations with valid field types: " +
error.to_string(),
)
}
let images : Array[Image] = []
let categories : Array[Category] = []
let truths : Array[GroundTruth] = []
let image_ids : Map[Int, Bool] = Map([])
let category_ids : Map[Int, Bool] = Map([])
let truth_ids : Map[Int64, Bool] = Map([])
for index, entry in raw.images {
let image : CocoImage = @json.from_json(entry) catch {
error => raise InvalidInput("images[\{index}]: " + error.to_string())
}
let item = Image::new(image.id, image.width, image.height) catch {
error => raise InvalidInput("images[\{index}]: " + error.message())
}
if image_ids.contains(item.id()) {
raise InvalidInput("images[\{index}].id duplicates \{item.id()}")
}
image_ids[item.id()] = true
images.push(item)
}
for index, entry in raw.categories {
let category : CocoCategory = @json.from_json(entry) catch {
error => raise InvalidInput("categories[\{index}]: " + error.to_string())
}
let item = Category::new(category.id, category.name) catch {
error => raise InvalidInput("categories[\{index}]: " + error.message())
}
if category_ids.contains(item.id()) {
raise InvalidInput("categories[\{index}].id duplicates \{item.id()}")
}
category_ids[item.id()] = true
categories.push(item)
}
for index, entry in raw.annotations {
let location = "annotations[\{index}]"
let annotation : CocoAnnotation = @json.from_json(entry) catch {
error => raise InvalidInput(location + ": " + error.to_string())
}
let bbox = parse_box(annotation.bbox, location)
let item = GroundTruth::new(
parse_annotation_id(annotation.id, location + ".id"),
annotation.image_id,
annotation.category_id,
bbox,
area=annotation.area,
iscrowd=parse_flag(annotation.iscrowd, location + ".iscrowd"),
ignore=parse_flag(annotation.ignore, location + ".ignore"),
) catch {
error => raise InvalidInput(location + ": " + error.message())
}
if truth_ids.contains(item.id()) {
raise InvalidInput(location + ".id duplicates \{item.id()}")
}
if !image_ids.contains(item.image_id()) {
raise InvalidInput(
location + ".image_id references unknown image \{item.image_id()}",
)
}
if !category_ids.contains(item.category_id()) {
raise InvalidInput(
location +
".category_id references unknown category \{item.category_id()}",
)
}
truth_ids[item.id()] = true
truths.push(item)
}
Dataset::new(images, categories, truths)
}
///|
/// Parse standard COCO detections; their stable IDs are their zero-based positions.
pub fn parse_coco_detections(
source : String,
) -> Array[Detection] raise EvalError {
let json = @json.parse(source) catch {
_ => raise InvalidInput("detections are not valid JSON")
}
let raw : Array[Json] = @json.from_json(json) catch {
error =>
raise InvalidInput(
"detections must be an array of image_id, category_id, bbox, and score records: " +
error.to_string(),
)
}
let predictions : Array[Detection] = []
for index, entry in raw {
let location = "detections[\{index}]"
let prediction : CocoPrediction = @json.from_json(entry) catch {
error => raise InvalidInput(location + ": " + error.to_string())
}
let bbox = parse_box(prediction.bbox, location)
let item = Detection::new(
index,
prediction.image_id,
prediction.category_id,
bbox,
prediction.score,
) catch {
error => raise InvalidInput(location + ": " + error.message())
}
predictions.push(item)
}
predictions
}