///|
priv struct CocoMaskAnnotation {
id : Json
image_id : Int32
category_id : Int32
segmentation : Json
area : Double
iscrowd : Json?
ignore : Json?
} derive(FromJson)
///|
priv struct CocoMaskPrediction {
image_id : Int32
category_id : Int32
segmentation : Json
score : Double
} derive(FromJson)
///|
/// Reads compressed/uncompressed RLE or polygon-ring annotations.
/// bbox is not required and does not affect mask scoring. Annotation area is kept.
pub fn parse_coco_mask_dataset(source : String) -> MaskDataset 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: " +
error.to_string(),
)
}
let images : Array[Image] = []
let categories : Array[Category] = []
let truths : Array[MaskGroundTruth] = []
let image_ids : Map[Int32, Image] = Map([])
let category_ids : Map[Int32, Bool] = Map([])
let truth_ids : Map[Int64, Bool] = Map([])
for index, entry in raw.images {
let item = try {
let image : CocoImage = @json.from_json(entry) catch {
error => raise InvalidInput(error.to_string())
}
Image::new(image.id, image.width, image.height)
} catch {
error =>
raise InvalidInput(
"images[" + index.to_string() + "]: " + error.message(),
)
}
if image_ids.contains(item.id) {
raise InvalidInput(
"images[" + index.to_string() + "].id duplicates " + item.id.to_string(),
)
}
images.push(item)
image_ids[item.id] = item
}
for index, entry in raw.categories {
let item = try {
let category : CocoCategory = @json.from_json(entry) catch {
error => raise InvalidInput(error.to_string())
}
Category::new(category.id, category.name)
} catch {
error =>
raise InvalidInput(
"categories[" + index.to_string() + "]: " + error.message(),
)
}
if category_ids.contains(item.id) {
raise InvalidInput(
"categories[" +
index.to_string() +
"].id duplicates " +
item.id.to_string(),
)
}
categories.push(item)
category_ids[item.id] = true
}
for index, entry in raw.annotations {
let location = "annotations[" + index.to_string() + "]"
let item = try {
let annotation : CocoMaskAnnotation = @json.from_json(entry) catch {
error => raise InvalidInput(error.to_string())
}
let image = match image_ids.get(annotation.image_id) {
Some(image) => image
None => raise InvalidInput("image_id references unknown image")
}
MaskGroundTruth::new(
parse_annotation_id(annotation.id, "id"),
annotation.image_id,
annotation.category_id,
parse_segmentation_value(
annotation.segmentation,
image.height,
image.width,
),
area=annotation.area,
iscrowd=parse_flag(annotation.iscrowd, "iscrowd"),
ignore=parse_flag(annotation.ignore, "ignore"),
)
} catch {
error => raise InvalidInput(location + ": " + error.message())
}
if truth_ids.contains(item.id()) {
raise InvalidInput(location + ".id duplicates " + item.id().to_string())
}
match image_ids.get(item.image_id()) {
Some(image) => validate_mask_canvas(item.mask(), image, location)
None =>
raise InvalidInput(location + ".image_id references unknown image")
}
if !category_ids.contains(item.category_id()) {
raise InvalidInput(location + ".category_id references unknown category")
}
truths.push(item)
truth_ids[item.id()] = true
}
MaskDataset::new(images, categories, truths)
}
///|
/// Stable detection IDs are zero-based input positions. bbox and supplied area
/// fields are ignored: segmentation pixel area controls unmatched area filtering.
/// This original entry point accepts RLE only. For polygons, use the dataset-aware
/// entry point below so canvas size comes from the declared image.
pub fn parse_coco_mask_detections(
source : String,
) -> Array[MaskDetection] raise EvalError {
parse_mask_predictions(source, None)
}
///|
/// Reads polygon or RLE predictions, validating references and canvas sizes
/// against the same ground-truth dataset used for evaluation.
pub fn parse_coco_mask_detections_with_dataset(
source : String,
dataset : MaskDataset,
) -> Array[MaskDetection] raise EvalError {
parse_mask_predictions(source, Some(dataset))
}
///|
fn parse_mask_predictions(
source : String,
dataset : MaskDataset?,
) -> Array[MaskDetection] 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: " + error.to_string())
}
let predictions : Array[MaskDetection] = []
let images : Map[Int32, Image] = Map([])
let categories : Map[Int32, Bool] = Map([])
match dataset {
Some(value) => {
for image in value.images() {
images[image.id] = image
}
for category in value.categories() {
categories[category.id] = true
}
}
None => ()
}
for index, entry in raw {
let item = try {
let prediction : CocoMaskPrediction = @json.from_json(entry) catch {
error => raise InvalidInput(error.to_string())
}
let mask = match dataset {
Some(_) => {
let image = match images.get(prediction.image_id) {
Some(image) => image
None => raise InvalidInput("image_id references unknown image")
}
if !categories.contains(prediction.category_id) {
raise InvalidInput("category_id references unknown category")
}
parse_segmentation_value(
prediction.segmentation,
image.height,
image.width,
)
}
None => {
if prediction.segmentation is Array(_) {
raise InvalidInput(
"polygon predictions require image dimensions; use parse_coco_mask_detections_with_dataset",
)
}
parse_rle_value(prediction.segmentation)
}
}
MaskDetection::new(
index,
prediction.image_id,
prediction.category_id,
mask,
prediction.score,
)
} catch {
error =>
raise InvalidInput(
"detections[" + index.to_string() + "]: " + error.message(),
)
}
predictions.push(item)
}
predictions
}