///|
/// One COCO-style summary value. -1 means there are no eligible ground truths.
pub struct Metrics {
ap : Double
ap50 : Double
ap75 : Double
ap_small : Double
ap_medium : Double
ap_large : Double
ar1 : Double
ar10 : Double
ar100 : Double
ar_small : Double
ar_medium : Double
ar_large : Double
} derive(Debug, ToJson)
///|
pub extend Metrics with Debug::{to_repr}
///|
pub extend Metrics with ToJson::{to_json}
///|
pub fn Metrics::ap(self : Metrics) -> Double {
self.ap
}
///|
pub fn Metrics::ap50(self : Metrics) -> Double {
self.ap50
}
///|
pub fn Metrics::ap75(self : Metrics) -> Double {
self.ap75
}
///|
pub fn Metrics::ap_small(self : Metrics) -> Double {
self.ap_small
}
///|
pub fn Metrics::ap_medium(self : Metrics) -> Double {
self.ap_medium
}
///|
pub fn Metrics::ap_large(self : Metrics) -> Double {
self.ap_large
}
///|
pub fn Metrics::ar1(self : Metrics) -> Double {
self.ar1
}
///|
pub fn Metrics::ar10(self : Metrics) -> Double {
self.ar10
}
///|
pub fn Metrics::ar100(self : Metrics) -> Double {
self.ar100
}
///|
pub fn Metrics::ar_small(self : Metrics) -> Double {
self.ar_small
}
///|
pub fn Metrics::ar_medium(self : Metrics) -> Double {
self.ar_medium
}
///|
pub fn Metrics::ar_large(self : Metrics) -> Double {
self.ar_large
}
///|
pub struct ClassMetrics {
category_id : Int32
metrics : Metrics
} derive(ToJson)
///|
pub extend ClassMetrics with ToJson::{to_json}
///|
pub fn ClassMetrics::category_id(self : ClassMetrics) -> Int32 {
self.category_id
}
///|
pub fn ClassMetrics::metrics(self : ClassMetrics) -> Metrics {
self.metrics
}
///|
pub struct EvaluationReport {
schema_version : String
evaluator_version : String
iou_type : String
config : EvalConfig
counts : InputCounts
summary : Metrics
classes : Array[ClassMetrics]
diagnostics : Array[MatchDiagnostic]
image_summaries : Array[ImageSummary]
recall_thresholds : Array[Double]
pr_curves : Array[PrecisionRecallCurve]
quality_warnings : Array[QualityWarning]
}
///|
/// Input sizes and detections retained by the all-area maxDets=100 evaluation.
pub struct InputCounts {
images : Int32
categories : Int32
ground_truths : Int32
detections : Int32
evaluated_detections : Int32
} derive(ToJson)
///|
pub extend InputCounts with ToJson::{to_json}
///|
pub fn InputCounts::images(self : InputCounts) -> Int32 {
self.images
}
///|
pub fn InputCounts::categories(self : InputCounts) -> Int32 {
self.categories
}
///|
pub fn InputCounts::ground_truths(self : InputCounts) -> Int32 {
self.ground_truths
}
///|
pub fn InputCounts::detections(self : InputCounts) -> Int32 {
self.detections
}
///|
pub fn InputCounts::evaluated_detections(self : InputCounts) -> Int32 {
self.evaluated_detections
}
///|
/// A nonfatal spatial input warning; scoring uses the original box unchanged.
pub struct QualityWarning {
source : String
record_id : Int64
image_id : Int32
category_id : Int32
kind : String
}
///|
pub impl ToJson for QualityWarning with fn to_json(self) {
Json::object({
"source": @json.to_json(self.source),
"record_id": Json::number(
self.record_id.to_double(),
repr=self.record_id.to_string(),
),
"image_id": @json.to_json(self.image_id),
"category_id": @json.to_json(self.category_id),
"kind": @json.to_json(self.kind),
})
}
///|
pub extend QualityWarning with ToJson::{to_json}
///|
pub fn QualityWarning::source(self : QualityWarning) -> String {
self.source
}
///|
pub fn QualityWarning::record_id(self : QualityWarning) -> Int64 {
self.record_id
}
///|
pub fn QualityWarning::image_id(self : QualityWarning) -> Int32 {
self.image_id
}
///|
pub fn QualityWarning::category_id(self : QualityWarning) -> Int32 {
self.category_id
}
///|
pub fn QualityWarning::kind(self : QualityWarning) -> String {
self.kind
}
///|
pub extend EvaluationReport with ToJson::{to_json}
///|
pub fn EvaluationReport::to_json_string(self : EvaluationReport) -> String {
@json.to_json(self).stringify()
}
///|
pub fn EvaluationReport::summary(self : EvaluationReport) -> Metrics {
self.summary
}
///|
pub fn EvaluationReport::config(self : EvaluationReport) -> EvalConfig {
self.config
}
///|
pub fn EvaluationReport::counts(self : EvaluationReport) -> InputCounts {
self.counts
}
///|
pub fn EvaluationReport::evaluator_version(self : EvaluationReport) -> String {
self.evaluator_version
}
///|
pub fn EvaluationReport::quality_warnings(
self : EvaluationReport,
) -> Array[QualityWarning] {
self.quality_warnings.copy()
}
///|
pub fn EvaluationReport::classes(
self : EvaluationReport,
) -> Array[ClassMetrics] {
self.classes.copy()
}
///|
/// Per-image decisions at IoU=0.50, all areas, maxDets=100 per category.
pub struct ImageSummary {
image_id : Int32
tp : Int32
fp : Int32
fn_count : Int32
ignored : Int32
} derive(ToJson)
///|
pub extend ImageSummary with ToJson::{to_json}
///|
pub fn ImageSummary::image_id(self : ImageSummary) -> Int32 {
self.image_id
}
///|
pub fn ImageSummary::tp(self : ImageSummary) -> Int32 {
self.tp
}
///|
pub fn ImageSummary::fp(self : ImageSummary) -> Int32 {
self.fp
}
///|
pub fn ImageSummary::fn_count(self : ImageSummary) -> Int32 {
self.fn_count
}
///|
pub fn ImageSummary::ignored(self : ImageSummary) -> Int32 {
self.ignored
}
///|
pub fn EvaluationReport::image_summaries(
self : EvaluationReport,
) -> Array[ImageSummary] {
self.image_summaries.copy()
}
///|
/// A 101-point COCO precision envelope at one category and IoU threshold.
/// A value of -1 means this category has no eligible ground truth.
pub struct PrecisionRecallCurve {
category_id : Int32
iou_threshold : Double
precision : Array[Double]
max_recall : Double
ap : Double
} derive(ToJson)
///|
pub extend PrecisionRecallCurve with ToJson::{to_json}
///|
pub fn PrecisionRecallCurve::category_id(self : PrecisionRecallCurve) -> Int32 {
self.category_id
}
///|
pub fn PrecisionRecallCurve::iou_threshold(
self : PrecisionRecallCurve,
) -> Double {
self.iou_threshold
}
///|
pub fn PrecisionRecallCurve::precision(
self : PrecisionRecallCurve,
) -> Array[Double] {
self.precision.copy()
}
///|
pub fn PrecisionRecallCurve::max_recall(self : PrecisionRecallCurve) -> Double {
self.max_recall
}
///|
pub fn PrecisionRecallCurve::ap(self : PrecisionRecallCurve) -> Double {
self.ap
}
///|
pub fn EvaluationReport::recall_thresholds(
self : EvaluationReport,
) -> Array[Double] {
self.recall_thresholds.copy()
}
///|
pub fn EvaluationReport::pr_curves(
self : EvaluationReport,
) -> Array[PrecisionRecallCurve] {
self.pr_curves.copy()
}
///|
/// One detection decision, or one unmatched eligible ground truth, at an IoU threshold.
/// Diagnostics use the all-area range and maxDets=100.
pub struct MatchDiagnostic {
iou_threshold : Double
image_id : Int32
category_id : Int32
detection_id : Int32?
ground_truth_id : Int64?
iou : Double
status : String
reason : String
}
///|
pub impl ToJson for MatchDiagnostic with fn to_json(self) {
Json::object({
"iou_threshold": @json.to_json(self.iou_threshold),
"image_id": @json.to_json(self.image_id),
"category_id": @json.to_json(self.category_id),
"detection_id": match self.detection_id {
Some(id) => @json.to_json(id)
None => Json::null()
},
"ground_truth_id": match self.ground_truth_id {
Some(id) => Json::number(id.to_double(), repr=id.to_string())
None => Json::null()
},
"iou": @json.to_json(self.iou),
"status": @json.to_json(self.status),
"reason": @json.to_json(self.reason),
})
}
///|
pub extend MatchDiagnostic with ToJson::{to_json}
///|
pub fn MatchDiagnostic::iou_threshold(self : MatchDiagnostic) -> Double {
self.iou_threshold
}
///|
pub fn MatchDiagnostic::image_id(self : MatchDiagnostic) -> Int32 {
self.image_id
}
///|
pub fn MatchDiagnostic::category_id(self : MatchDiagnostic) -> Int32 {
self.category_id
}
///|
pub fn MatchDiagnostic::detection_id(self : MatchDiagnostic) -> Int32? {
self.detection_id
}
///|
pub fn MatchDiagnostic::ground_truth_id(self : MatchDiagnostic) -> Int64? {
self.ground_truth_id
}
///|
/// IoU of the chosen match, or zero when no ground truth was matched.
pub fn MatchDiagnostic::iou(self : MatchDiagnostic) -> Double {
self.iou
}
///|
pub fn MatchDiagnostic::status(self : MatchDiagnostic) -> String {
self.status
}
///|
pub fn MatchDiagnostic::reason(self : MatchDiagnostic) -> String {
self.reason
}
///|
pub fn EvaluationReport::diagnostics(
self : EvaluationReport,
) -> Array[MatchDiagnostic] {
self.diagnostics.copy()
}
///|
/// AP and AR average over these IoU thresholds. AP50 and AP75 remain fixed probes.
pub struct EvalConfig {
iou_thresholds : Array[Double]
include_diagnostics : Bool
include_image_summaries : Bool
include_pr_curves : Bool
ignore_policy : String
} derive(ToJson)
///|
pub extend EvalConfig with ToJson::{to_json}
///|
pub fn EvalConfig::new(
iou_thresholds : Array[Double],
include_diagnostics~ : Bool = false,
ignore_policy~ : String = "coco_bbox",
include_image_summaries~ : Bool = false,
include_pr_curves~ : Bool = false,
) -> EvalConfig raise EvalError {
if iou_thresholds.is_empty() || iou_thresholds.length() > 100 {
raise InvalidConfig("IoU thresholds must contain 1 to 100 values")
}
let mut previous = 0.0
for threshold in iou_thresholds {
if !finite(threshold) || threshold <= previous || threshold > 1.0 {
raise InvalidConfig(
"IoU thresholds must be finite, strictly increasing, and in (0,1]",
)
}
previous = threshold
}
if ignore_policy != "coco_bbox" &&
ignore_policy != "coco_segm" &&
ignore_policy != "respect_explicit" {
raise InvalidConfig(
"ignore_policy must be coco_bbox, coco_segm or respect_explicit",
)
}
{
iou_thresholds: iou_thresholds.copy(),
include_diagnostics,
include_image_summaries,
include_pr_curves,
ignore_policy,
}
}
///|
pub fn EvalConfig::coco() -> EvalConfig {
{
iou_thresholds: [
0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.8999999999999999, 0.95,
],
include_diagnostics: false,
include_image_summaries: false,
include_pr_curves: false,
ignore_policy: "coco_bbox",
}
}
///|
pub fn EvalConfig::iou_thresholds(self : EvalConfig) -> Array[Double] {
self.iou_thresholds.copy()
}
///|
pub fn EvalConfig::include_diagnostics(self : EvalConfig) -> Bool {
self.include_diagnostics
}
///|
pub fn EvalConfig::include_image_summaries(self : EvalConfig) -> Bool {
self.include_image_summaries
}
///|
pub fn EvalConfig::include_pr_curves(self : EvalConfig) -> Bool {
self.include_pr_curves
}
///|
pub fn EvalConfig::ignore_policy(self : EvalConfig) -> String {
self.ignore_policy
}
///|
priv struct RankedDetection {
detection : Detection
order : Int32
}
///|
priv struct MatchRecord {
score : Double
order : Int32
matched : Bool
ignored : Bool
}
///|
priv struct CurveValue {
ap : Double
ar : Double
sampled_precision : Array[Double]
}
///|
fn overlap(detection : Box, truth : Box, crowd : Bool) -> Double {
if !crowd {
return detection.iou(truth)
}
let width = (detection.x2.min(truth.x2) - detection.x1.max(truth.x1)).max(0.0)
let height = (detection.y2.min(truth.y2) - detection.y1.max(truth.y1)).max(
0.0,
)
width * height / detection.area()
}
///|
fn in_area(area : Double, lower : Double, upper : Double) -> Bool {
area >= lower && area <= upper
}
///|
fn truth_ignored(
truth : GroundTruth,
lower : Double,
upper : Double,
respect_explicit_ignore : Bool,
) -> Bool {
truth.iscrowd ||
(respect_explicit_ignore && truth.ignore) ||
!in_area(truth.area, lower, upper)
}
///|
fn ranked_before(left : RankedDetection, right : RankedDetection) -> Int32 {
if left.detection.score > right.detection.score {
-1
} else if left.detection.score < right.detection.score {
1
} else {
left.order - right.order
}
}
///|
fn record_before(left : MatchRecord, right : MatchRecord) -> Int32 {
if left.score > right.score {
-1
} else if left.score < right.score {
1
} else {
left.order - right.order
}
}
///|
/// A read-only explanation of an unmatched, non-ignored detection.
fn classify_false_positive(
group : PreparedImage,
detection_index : Int32,
category_id : Int32,
truths : Array[RankedTruth],
used : Array[Bool],
ignored : Array[Bool],
threshold : Double,
respect_explicit_ignore : Bool,
) -> String {
let detection = group.candidates[detection_index].detection
for index in 0..= threshold {
return "duplicate"
}
}
for truth in group.all_truths {
if truth.category_id != category_id &&
!truth.iscrowd &&
(!respect_explicit_ignore || !truth.ignore) &&
instance_overlap(detection, truth, false) >= threshold {
return "wrong_class"
}
}
for index in 0..= 0.1 {
return "localization"
}
}
"background"
}
///|
fn evaluate_curve(
prepared : Array[PreparedImage],
category_id : Int32,
threshold : Double,
lower : Double,
upper : Double,
max_dets : Int32,
trace : Array[MatchDiagnostic]?,
respect_explicit_ignore : Bool,
capture_samples~ : Bool = false,
) -> CurveValue {
let records : Array[MatchRecord] = []
let mut positives = 0
for group in prepared {
let truths = group.truths.copy()
let candidates = group.candidates
truths.sort_by(fn(left, right) {
let left_ignored = truth_ignored(
left.truth,
lower,
upper,
respect_explicit_ignore,
)
let right_ignored = truth_ignored(
right.truth,
lower,
upper,
respect_explicit_ignore,
)
if left_ignored == right_ignored {
left.order - right.order
} else if left_ignored {
1
} else {
-1
}
})
let used = Array::make(truths.length(), false)
let ignored = Array::makei(truths.length(), fn(index) {
truth_ignored(truths[index].truth, lower, upper, respect_explicit_ignore)
})
for flag in ignored {
if !flag {
positives += 1
}
}
let count = candidates.length().min(max_dets)
for detection_index in 0..= 0 && !ignored[best] && ignored[truth_index] {
break
}
let current = group.overlap(detection_index, truths[truth_index])
if current < best_overlap {
continue
}
best_overlap = current
best = truth_index
}
let match_ignored = if best >= 0 {
ignored[best]
} else {
!in_area(instance_detection_area(candidate.detection), lower, upper)
}
if best >= 0 {
used[best] = true
}
match trace {
Some(entries) => {
let ground_truth_id : Int64? = if best >= 0 {
Some(truths[best].truth.id)
} else {
None
}
let reason = if best >= 0 {
let truth = truths[best].truth
if truth.iscrowd {
"crowd"
} else if respect_explicit_ignore && truth.ignore {
"ground_truth_ignore"
} else if !in_area(truth.area, lower, upper) {
"ground_truth_area"
} else {
"matched"
}
} else if match_ignored {
"detection_area"
} else {
classify_false_positive(
group, detection_index, category_id, truths, used, ignored, threshold,
respect_explicit_ignore,
)
}
entries.push({
iou_threshold: threshold,
image_id: group.image_id,
category_id,
detection_id: Some(candidate.detection.id),
ground_truth_id,
iou: if best >= 0 {
best_overlap
} else {
0.0
},
status: if match_ignored {
"ignored"
} else if best >= 0 {
"tp"
} else {
"fp"
},
reason,
})
}
None => ()
}
records.push({
score: candidate.detection.score,
order: records.length(),
matched: best >= 0,
ignored: match_ignored,
})
}
match trace {
Some(entries) =>
for truth_index in 0.. ()
}
}
if positives == 0 {
return {
ap: -1.0,
ar: -1.0,
sampled_precision: if capture_samples {
Array::make(101, -1.0)
} else {
[]
},
}
}
records.sort_by(record_before)
let mut tp = 0
let mut fp = 0
let precision : Array[Double] = []
let recall : Array[Double] = []
for record in records {
if record.ignored {
continue
}
if record.matched {
tp += 1
} else {
fp += 1
}
precision.push(tp.to_double() / (tp + fp).to_double())
recall.push(tp.to_double() / positives.to_double())
}
let ar = tp.to_double() / positives.to_double()
for index = precision.length() - 2; index >= 0; index = index - 1 {
if precision[index] < precision[index + 1] {
precision[index] = precision[index + 1]
}
}
let mut ap = 0.0
let mut index = 0
let sampled_precision : Array[Double] = []
for sample in 0..<=100 {
// COCOeval uses np.linspace(0, 1, 101), whose step is 0.01.
// Multiplication preserves its rounding at samples such as 0.70.
let target = sample.to_double() * 0.01
while index < recall.length() && recall[index] < target {
index += 1
}
let value = if index < precision.length() { precision[index] } else { 0.0 }
ap += value
if capture_samples {
sampled_precision.push(value)
}
}
{ ap: ap / 101.0, ar, sampled_precision, }
}
///|
fn average(values : Array[Double]) -> Double {
let mut sum = 0.0
let mut count = 0
for value in values {
if value >= 0.0 {
sum += value
count += 1
}
}
if count == 0 {
-1.0
} else {
sum / count.to_double()
}
}
///|
fn metrics_for_category(
prepared : Array[PreparedImage],
category_id : Int32,
thresholds : Array[Double],
diagnostics : Array[MatchDiagnostic]?,
curves : Array[PrecisionRecallCurve]?,
respect_explicit_ignore : Bool,
) -> Metrics {
let all_ap : Array[Double] = []
let all_ar1 : Array[Double] = []
let all_ar10 : Array[Double] = []
let all_ar100 : Array[Double] = []
let small_ap : Array[Double] = []
let medium_ap : Array[Double] = []
let large_ap : Array[Double] = []
let small_ar : Array[Double] = []
let medium_ar : Array[Double] = []
let large_ar : Array[Double] = []
let mut ap50 = -1.0
let mut ap75 = -1.0
let mut saw_ap50 = false
let mut saw_ap75 = false
let capture_samples = match curves {
Some(_) => true
None => false
}
for threshold in thresholds {
let all = evaluate_curve(
prepared,
category_id,
threshold,
0.0,
1.0e10,
100,
diagnostics,
respect_explicit_ignore,
capture_samples~,
)
match curves {
Some(entries) =>
entries.push({
category_id,
iou_threshold: threshold,
precision: all.sampled_precision,
max_recall: all.ar,
ap: all.ap,
})
None => ()
}
all_ap.push(all.ap)
all_ar100.push(all.ar)
all_ar1.push(
evaluate_curve(
prepared,
category_id,
threshold,
0.0,
1.0e10,
1,
None,
respect_explicit_ignore,
).ar,
)
all_ar10.push(
evaluate_curve(
prepared,
category_id,
threshold,
0.0,
1.0e10,
10,
None,
respect_explicit_ignore,
).ar,
)
let small = evaluate_curve(
prepared,
category_id,
threshold,
0.0,
1024.0,
100,
None,
respect_explicit_ignore,
)
let medium = evaluate_curve(
prepared,
category_id,
threshold,
1024.0,
9216.0,
100,
None,
respect_explicit_ignore,
)
let large = evaluate_curve(
prepared,
category_id,
threshold,
9216.0,
1.0e10,
100,
None,
respect_explicit_ignore,
)
small_ap.push(small.ap)
medium_ap.push(medium.ap)
large_ap.push(large.ap)
small_ar.push(small.ar)
medium_ar.push(medium.ar)
large_ar.push(large.ar)
if threshold == 0.5 {
ap50 = all.ap
saw_ap50 = true
}
if threshold == 0.75 {
ap75 = all.ap
saw_ap75 = true
}
}
if !saw_ap50 {
ap50 = evaluate_curve(
prepared,
category_id,
0.5,
0.0,
1.0e10,
100,
None,
respect_explicit_ignore,
).ap
}
if !saw_ap75 {
ap75 = evaluate_curve(
prepared,
category_id,
0.75,
0.0,
1.0e10,
100,
None,
respect_explicit_ignore,
).ap
}
{
ap: average(all_ap),
ap50,
ap75,
ap_small: average(small_ap),
ap_medium: average(medium_ap),
ap_large: average(large_ap),
ar1: average(all_ar1),
ar10: average(all_ar10),
ar100: average(all_ar100),
ar_small: average(small_ar),
ar_medium: average(medium_ar),
ar_large: average(large_ar),
}
}
///|
/// Evaluate continuous-coordinate bounding boxes with default COCO thresholds.
pub fn evaluate(
dataset : Dataset,
predictions : Array[Detection],
) -> EvaluationReport raise EvalError {
evaluate_internal(dataset, predictions, EvalConfig::coco())
}
///|
/// Evaluate and retain per-detection decisions for the default IoU thresholds.
pub fn evaluate_with_diagnostics(
dataset : Dataset,
predictions : Array[Detection],
) -> EvaluationReport raise EvalError {
let default = EvalConfig::coco()
evaluate_internal(dataset, predictions, {
iou_thresholds: default.iou_thresholds,
include_diagnostics: true,
include_image_summaries: false,
include_pr_curves: false,
ignore_policy: default.ignore_policy,
})
}
///|
/// Evaluate with custom AP/AR IoU thresholds. Area bins and maxDets remain COCO defaults.
pub fn evaluate_with_config(
dataset : Dataset,
predictions : Array[Detection],
config : EvalConfig,
) -> EvaluationReport raise EvalError {
evaluate_internal(dataset, predictions, config)
}
///|
fn evaluate_internal(
dataset : Dataset,
predictions : Array[Detection],
config : EvalConfig,
iou_type~ : String = "bbox",
max_iou_cache_pairs~ : Int32 = 0,
) -> EvaluationReport raise EvalError {
validate_iou_cache_limit(max_iou_cache_pairs)
if iou_type == "bbox" && config.ignore_policy == "coco_segm" {
raise InvalidConfig("coco_segm policy requires evaluate_masks")
}
let image_ids : Map[Int32, Bool] = Map([])
let images_by_id : Map[Int32, Image] = Map([])
let category_ids : Map[Int32, Bool] = Map([])
let detection_ids : Map[Int32, Bool] = Map([])
for image in dataset.images {
image_ids[image.id] = true
images_by_id[image.id] = image
}
for category in dataset.categories {
category_ids[category.id] = true
}
let detections : Array[RankedDetection] = []
let quality_warnings : Array[QualityWarning] = []
let per_image_category : Map[String, Int32] = Map([])
let mut evaluated_detections = 0
for truth in dataset.truths {
match images_by_id.get(truth.image_id) {
Some(image) =>
if box_outside_image(truth.bbox, image) {
quality_warnings.push({
source: "ground_truth",
record_id: truth.id,
image_id: truth.image_id,
category_id: truth.category_id,
kind: "outside_image",
})
}
None => ()
}
}
for index, prediction in predictions {
if detection_ids.contains(prediction.id) {
raise InvalidInput("duplicate detection id \{prediction.id}")
}
if !image_ids.contains(prediction.image_id) ||
!category_ids.contains(prediction.category_id) {
raise InvalidInput(
"detection \{prediction.id} references unknown image or category",
)
}
detection_ids[prediction.id] = true
detections.push({ detection: prediction, order: index, })
let group = "\{prediction.image_id}:\{prediction.category_id}"
let count = match per_image_category.get(group) {
Some(value) => value
None => 0
}
per_image_category[group] = count + 1
if count < 100 {
evaluated_detections += 1
}
match images_by_id.get(prediction.image_id) {
Some(image) =>
if box_outside_image(prediction.bbox, image) {
quality_warnings.push({
source: "detection",
record_id: prediction.id.to_int64(),
image_id: prediction.image_id,
category_id: prediction.category_id,
kind: "outside_image",
})
}
None => ()
}
}
let classes : Array[ClassMetrics] = []
let diagnostics : Array[MatchDiagnostic] = []
let image_trace : Array[MatchDiagnostic] = []
let pr_curves : Array[PrecisionRecallCurve] = []
let ap : Array[Double] = []
let ap50 : Array[Double] = []
let ap75 : Array[Double] = []
let ap_small : Array[Double] = []
let ap_medium : Array[Double] = []
let ap_large : Array[Double] = []
let ar1 : Array[Double] = []
let ar10 : Array[Double] = []
let ar100 : Array[Double] = []
let ar_small : Array[Double] = []
let ar_medium : Array[Double] = []
let ar_large : Array[Double] = []
let indexed = index_evaluation_inputs(dataset, detections)
for category in dataset.categories {
let prepared = prepare_category(indexed, category.id, max_iou_cache_pairs)
let metrics = metrics_for_category(
prepared,
category.id,
config.iou_thresholds,
if config.include_diagnostics {
Some(diagnostics)
} else {
None
},
if config.include_pr_curves {
Some(pr_curves)
} else {
None
},
config.ignore_policy == "respect_explicit",
)
classes.push({ category_id: category.id, metrics, })
ap.push(metrics.ap)
ap50.push(metrics.ap50)
ap75.push(metrics.ap75)
ap_small.push(metrics.ap_small)
ap_medium.push(metrics.ap_medium)
ap_large.push(metrics.ap_large)
ar1.push(metrics.ar1)
ar10.push(metrics.ar10)
ar100.push(metrics.ar100)
ar_small.push(metrics.ar_small)
ar_medium.push(metrics.ar_medium)
ar_large.push(metrics.ar_large)
if config.include_image_summaries {
evaluate_curve(
prepared,
category.id,
0.5,
0.0,
1.0e10,
100,
Some(image_trace),
config.ignore_policy == "respect_explicit",
)
|> ignore
}
}
let image_summaries : Array[ImageSummary] = []
if config.include_image_summaries {
let counts_by_image : Map[Int32, Array[Int32]] = Map([])
for image in dataset.images {
counts_by_image[image.id] = [0, 0, 0, 0]
}
for entry in image_trace {
match counts_by_image.get(entry.image_id) {
Some(counts) =>
match entry.status {
"tp" => counts[0] += 1
"fp" => counts[1] += 1
"fn" => counts[2] += 1
"ignored" => counts[3] += 1
_ => ()
}
None => ()
}
}
for image in dataset.images {
match counts_by_image.get(image.id) {
Some(counts) =>
image_summaries.push({
image_id: image.id,
tp: counts[0],
fp: counts[1],
fn_count: counts[2],
ignored: counts[3],
})
None => ()
}
}
image_summaries.sort_by(fn(left, right) {
if left.image_id < right.image_id {
-1
} else if left.image_id > right.image_id {
1
} else {
0
}
})
}
let recall_thresholds : Array[Double] = []
if config.include_pr_curves {
for sample in 0..<=100 {
recall_thresholds.push(sample.to_double() * 0.01)
}
}
{
schema_version: "0.1.0",
evaluator_version: "0.2.0",
iou_type,
config,
counts: {
images: dataset.images.length(),
categories: dataset.categories.length(),
ground_truths: dataset.truths.length(),
detections: predictions.length(),
evaluated_detections,
},
summary: {
ap: average(ap),
ap50: average(ap50),
ap75: average(ap75),
ap_small: average(ap_small),
ap_medium: average(ap_medium),
ap_large: average(ap_large),
ar1: average(ar1),
ar10: average(ar10),
ar100: average(ar100),
ar_small: average(ar_small),
ar_medium: average(ar_medium),
ar_large: average(ar_large),
},
classes,
diagnostics,
image_summaries,
recall_thresholds,
pr_curves,
quality_warnings,
}
}
///|
fn box_outside_image(box : Box, image : Image) -> Bool {
box.x1 < 0.0 ||
box.y1 < 0.0 ||
box.x2 > image.width.to_double() ||
box.y2 > image.height.to_double()
}
///|
pub fn EvaluationReport::iou_type(self : EvaluationReport) -> String {
self.iou_type
}
///|
pub impl ToJson for EvaluationReport with fn to_json(self) {
let fields : Map[String, Json] = Map([])
fields["schema_version"] = @json.to_json(self.schema_version)
fields["evaluator_version"] = @json.to_json(self.evaluator_version)
fields["config"] = @json.to_json(self.config)
fields["counts"] = @json.to_json(self.counts)
fields["summary"] = @json.to_json(self.summary)
fields["classes"] = @json.to_json(self.classes)
fields["diagnostics"] = @json.to_json(self.diagnostics)
fields["image_summaries"] = @json.to_json(self.image_summaries)
fields["recall_thresholds"] = @json.to_json(self.recall_thresholds)
fields["pr_curves"] = @json.to_json(self.pr_curves)
fields["quality_warnings"] = @json.to_json(self.quality_warnings)
if self.iou_type == "segm" {
fields["iou_type"] = Json::string("segm")
}
Json::object(fields)
}
///|
// Mask entry points validate equal canvases before the shared matcher runs.
fn instance_overlap(
detection : Detection,
truth : GroundTruth,
crowd : Bool,
) -> Double {
match (detection.mask, truth.mask) {
(Some(left), Some(right)) => {
let common = left.intersection(right).to_double()
let denominator = if crowd {
left.area().to_double()
} else {
left.area().to_double() + right.area().to_double() - common
}
if denominator == 0.0 {
0.0
} else {
common / denominator
}
}
_ => overlap(detection.bbox, truth.bbox, crowd)
}
}
///|
fn instance_detection_area(detection : Detection) -> Double {
match detection.mask {
Some(mask) => mask.area().to_double()
None => detection.bbox.area()
}
}