///|
priv struct RankedTruth {
truth : GroundTruth
order : Int32
cache_column : Int32
}
///|
priv struct PreparedImage {
image_id : Int32
truths : Array[RankedTruth]
all_truths : Array[GroundTruth]
candidates : Array[RankedDetection]
overlaps : Array[Array[Double]]?
}
///|
priv struct IndexedCategory {
truths : Map[Int32, Array[RankedTruth]]
candidates : Map[Int32, Array[RankedDetection]]
}
///|
priv struct IndexedInputs {
categories : Map[Int32, IndexedCategory]
all_truths : Map[Int32, Array[GroundTruth]]
}
///|
/// Build category/image indexes once, retaining original input order for ties.
fn index_evaluation_inputs(
dataset : Dataset,
detections : Array[RankedDetection],
) -> IndexedInputs {
let categories : Map[Int32, IndexedCategory] = Map([])
let all_truths : Map[Int32, Array[GroundTruth]] = Map([])
for category in dataset.categories {
categories[category.id] = { truths: Map([]), candidates: Map([]), }
}
for order, truth in dataset.truths {
match all_truths.get(truth.image_id) {
Some(items) => items.push(truth)
None => all_truths[truth.image_id] = [truth]
}
let group = categories[truth.category_id]
match group.truths.get(truth.image_id) {
Some(items) => items.push({ truth, order, cache_column: items.length(), })
None =>
group.truths[truth.image_id] = [{ truth, order, cache_column: 0, }]
}
}
for candidate in detections {
let detection = candidate.detection
let group = categories[detection.category_id]
match group.candidates.get(detection.image_id) {
Some(items) => items.push(candidate)
None => group.candidates[detection.image_id] = [candidate]
}
}
{ categories, all_truths, }
}
///|
/// Empty image/category groups contribute no records or positives to any curve.
/// Cache only complete top-100 DT/GT matrices that fit the remaining pair budget.
fn prepare_category(
inputs : IndexedInputs,
category_id : Int32,
max_iou_cache_pairs : Int32,
) -> Array[PreparedImage] {
let category = inputs.categories[category_id]
let active : Map[Int32, Bool] = Map([])
for image_id in category.truths.keys() {
active[image_id] = true
}
for image_id in category.candidates.keys() {
active[image_id] = true
}
let image_ids = active.keys().to_array()
image_ids.sort()
let mut remaining = max_iou_cache_pairs.to_int64()
let prepared : Array[PreparedImage] = []
for image_id in image_ids {
let truths = category.truths.get(image_id).unwrap_or([])
let candidates = category.candidates.get(image_id).unwrap_or([])
candidates.sort_by(ranked_before)
let count = candidates.length().min(100)
let pairs = count.to_int64() * truths.length().to_int64()
let overlaps = if pairs > 0L && pairs <= remaining {
remaining -= pairs
Some(
Array::makei(count, fn(row) {
Array::makei(truths.length(), fn(column) {
let truth = truths[column].truth
instance_overlap(candidates[row].detection, truth, truth.iscrowd)
})
}),
)
} else {
None
}
prepared.push({
image_id,
truths,
candidates,
all_truths: inputs.all_truths.get(image_id).unwrap_or([]),
overlaps,
})
}
prepared
}
///|
fn PreparedImage::overlap(
self : PreparedImage,
detection_index : Int32,
truth : RankedTruth,
) -> Double {
match self.overlaps {
Some(rows) if detection_index < rows.length() =>
rows[detection_index][truth.cache_column]
_ =>
instance_overlap(
self.candidates[detection_index].detection,
truth.truth,
truth.truth.iscrowd,
)
}
}
///|
fn validate_iou_cache_limit(limit : Int32) -> Unit raise EvalError {
if limit < 0 || limit > 16777216 {
raise InvalidConfig("max_iou_cache_pairs must be in [0,16777216]")
}
}