///|
pub struct ClassMetrics {
class_id : Int
mut expected : Int
mut actual : Int
mut matched : Int
mut mean_iou_per_1000 : Int
} derive(Debug, Eq)
///|
pub fn ClassMetrics::new(class_id~ : Int) -> ClassMetrics {
{ class_id, expected: 0, actual: 0, matched: 0, mean_iou_per_1000: 0 }
}
///|
pub fn ClassMetrics::precision(self : ClassMetrics) -> Int {
if self.actual == 0 {
1000
} else {
self.matched * 1000 / self.actual
}
}
///|
pub fn ClassMetrics::recall(self : ClassMetrics) -> Int {
if self.expected == 0 {
1000
} else {
self.matched * 1000 / self.expected
}
}
///|
pub fn ClassMetrics::f1(self : ClassMetrics) -> Int {
let p = self.precision()
let r = self.recall()
if p + r == 0 {
0
} else {
2 * p * r / (p + r)
}
}
///|
pub struct EvaluationReport {
mut images : Int
classes : Array[ClassMetrics]
mut mask : BinaryMetrics
mean_image_iou_per_1000 : Int
} derive(Debug, Eq)
///|
pub fn EvaluationReport::new() -> EvaluationReport {
{
images: 0,
classes: [],
mask: BinaryMetrics::new(
true_positive=0,
false_positive=0,
false_negative=0,
true_negative=0,
),
mean_image_iou_per_1000: 0,
}
}
///|
fn find_class(classes : Array[ClassMetrics], class_id : Int) -> Int {
for i, metrics in classes {
if metrics.class_id == class_id {
return i
}
}
-1
}
///|
fn ensure_class(report : EvaluationReport, class_id : Int) -> ClassMetrics {
let index = find_class(report.classes, class_id)
if index >= 0 {
report.classes[index]
} else {
let metrics = ClassMetrics::new(class_id~)
report.classes.push(metrics)
metrics
}
}
///|
pub fn EvaluationReport::add_detection_image(
self : EvaluationReport,
expected : Array[Box2D],
actual : Array[Box2D],
threshold_per_1000 : Int,
) -> Unit {
self.images += 1
let matches = match_detections(expected, actual, threshold_per_1000)
for expected_box in expected {
let metrics = ensure_class(self, expected_box.label.id)
metrics.expected += 1
}
for actual_box in actual {
let metrics = ensure_class(self, actual_box.label.id)
metrics.actual += 1
}
for item in matches {
let metrics = ensure_class(self, item.expected_id)
metrics.matched += 1
metrics.mean_iou_per_1000 += item.iou_per_1000
}
for metrics in self.classes {
if metrics.matched > 0 {
metrics.mean_iou_per_1000 = metrics.mean_iou_per_1000 / metrics.matched
}
}
}
///|
pub fn EvaluationReport::add_mask_image(
self : EvaluationReport,
expected : Mask,
actual : Mask,
positive : Int,
) -> Unit {
let metrics = evaluate_mask(expected, actual, positive)
self.mask = BinaryMetrics::new(
true_positive=self.mask.true_positive + metrics.true_positive,
false_positive=self.mask.false_positive + metrics.false_positive,
false_negative=self.mask.false_negative + metrics.false_negative,
true_negative=self.mask.true_negative + metrics.true_negative,
)
}
///|
pub fn EvaluationReport::macro_precision(self : EvaluationReport) -> Int {
if self.classes.length() == 0 {
1000
} else {
let mut total = 0
for metrics in self.classes {
total += metrics.precision()
}
total / self.classes.length()
}
}
///|
pub fn EvaluationReport::macro_recall(self : EvaluationReport) -> Int {
if self.classes.length() == 0 {
1000
} else {
let mut total = 0
for metrics in self.classes {
total += metrics.recall()
}
total / self.classes.length()
}
}
///|
pub fn EvaluationReport::macro_f1(self : EvaluationReport) -> Int {
if self.classes.length() == 0 {
1000
} else {
let mut total = 0
for metrics in self.classes {
total += metrics.f1()
}
total / self.classes.length()
}
}
///|
pub fn EvaluationReport::to_json(self : EvaluationReport) -> String {
let sb = StringBuilder()
sb.write_string(
"{\"images\":\{self.images},\"macro_precision\":\{self.macro_precision()},",
)
sb.write_string(
"\"macro_recall\":\{self.macro_recall()},\"macro_f1\":\{self.macro_f1()},",
)
sb.write_string("\"mask_f1\":\{self.mask.f1()},\"classes\":[")
for i, metrics in self.classes {
if i > 0 {
sb.write_string(",")
}
sb.write_string(
"{\"id\":\{metrics.class_id},\"expected\":\{metrics.expected},\"actual\":\{metrics.actual},\"matched\":\{metrics.matched},\"mean_iou\":\{metrics.mean_iou_per_1000}}",
)
}
sb.write_string("]}")
sb.to_string()
}
///|
pub struct ThresholdPoint {
threshold : Int
precision : Int
recall : Int
f1 : Int
} derive(Debug, Eq)
///|
pub fn detection_threshold_curve(
expected : Array[Box2D],
actual : Array[Box2D],
) -> Array[ThresholdPoint] {
let out : Array[ThresholdPoint] = []
for threshold in [100, 300, 500, 700, 900] {
let metrics = evaluate_detections(expected, actual, threshold)
let precision = metrics.precision()
let recall = metrics.recall()
let f1 = if precision + recall == 0 {
0
} else {
2 * precision * recall / (precision + recall)
}
out.push({ threshold, precision, recall, f1 })
}
out
}
///|
pub fn best_detection_threshold(
expected : Array[Box2D],
actual : Array[Box2D],
) -> Int {
let curve = detection_threshold_curve(expected, actual)
let mut best_threshold = 0
let mut best_f1 = -1
for point in curve {
if point.f1 > best_f1 {
best_f1 = point.f1
best_threshold = point.threshold
}
}
best_threshold
}
///|
pub fn evaluate_scene_pair(
expected : SceneOutput,
actual : SceneOutput,
) -> EvaluationReport {
let report = EvaluationReport::new()
report.add_detection_image(expected.boxes, actual.boxes, 500)
report.add_mask_image(expected.mask, actual.mask, 1)
report
}
///|
pub fn evaluate_scene_batch(
expected : Array[SceneOutput],
actual : Array[SceneOutput],
) -> EvaluationReport {
let report = EvaluationReport::new()
let count = min_i(expected.length(), actual.length())
for i in 0.. ValidationReport {
let result = ValidationReport::new()
if report.images < 0 {
result.error("evaluation image count cannot be negative")
}
for metrics in report.classes {
if metrics.matched > metrics.expected || metrics.matched > metrics.actual {
result.error("class match count exceeds available annotations")
}
}
result
}