///|
pub(all) struct Detection {
id : String
rect : Rect
label : String
score : Double
} derive(Debug, ToJson)
///|
pub fn Detection::new(
id~ : String,
rect~ : Rect,
label~ : String,
score? : Double = 1.0,
) -> Detection {
{ id, rect, label, score }
}
///|
pub(all) enum MatchKind {
TruePositive
FalsePositive
FalseNegative
LabelMismatch
} derive(Debug, ToJson)
///|
pub(all) struct MatchRecord {
kind : MatchKind
expected : Detection?
actual : Detection?
iou : Double
} derive(Debug, ToJson)
///|
pub fn MatchRecord::is_error(self : MatchRecord) -> Bool {
match self.kind {
TruePositive => false
_ => true
}
}
///|
pub(all) struct MatchSummary {
records : Array[MatchRecord]
true_positive : Int
false_positive : Int
false_negative : Int
label_mismatch : Int
} derive(Debug, ToJson)
///|
pub fn MatchSummary::error_count(self : MatchSummary) -> Int {
self.false_positive + self.false_negative + self.label_mismatch
}
///|
pub fn MatchSummary::precision(self : MatchSummary) -> Double {
let denom = self.true_positive + self.false_positive + self.label_mismatch
if denom == 0 {
0.0
} else {
self.true_positive.to_double() / denom.to_double()
}
}
///|
pub fn MatchSummary::recall(self : MatchSummary) -> Double {
let denom = self.true_positive + self.false_negative + self.label_mismatch
if denom == 0 {
0.0
} else {
self.true_positive.to_double() / denom.to_double()
}
}
///|
pub fn compare_detections(
expected : Array[Detection],
actual : Array[Detection],
iou_threshold? : Double = 0.5,
) -> MatchSummary {
let used_actual = Array::make(actual.length(), false)
let records : Array[MatchRecord] = []
let mut true_positive = 0
let mut false_negative = 0
let mut label_mismatch = 0
for exp in expected {
let candidate = best_unused_match(exp, actual, used_actual, iou_threshold)
match candidate {
None => {
false_negative += 1
records.push({
kind: FalseNegative,
expected: Some(exp),
actual: None,
iou: 0.0,
})
}
Some((index, act, iou)) => {
used_actual[index] = true
if exp.label == act.label {
true_positive += 1
records.push({
kind: TruePositive,
expected: Some(exp),
actual: Some(act),
iou,
})
} else {
label_mismatch += 1
records.push({
kind: LabelMismatch,
expected: Some(exp),
actual: Some(act),
iou,
})
}
}
}
}
for index, act in actual {
if !used_actual[index] {
records.push({
kind: FalsePositive,
expected: None,
actual: Some(act),
iou: 0.0,
})
}
}
let false_positive = records.count_if(record => record.kind is FalsePositive)
{ records, true_positive, false_positive, false_negative, label_mismatch }
}
///|
fn best_unused_match(
expected : Detection,
actual : Array[Detection],
used_actual : Array[Bool],
iou_threshold : Double,
) -> (Int, Detection, Double)? {
let mut best : (Int, Detection, Double)? = None
for index, candidate in actual {
if !used_actual[index] {
let score = expected.rect.iou(candidate.rect)
if score > 0.0 && score >= iou_threshold {
match best {
None => best = Some((index, candidate, score))
Some((_, _, best_score)) =>
if score > best_score {
best = Some((index, candidate, score))
}
}
}
}
}
best
}
///|
pub fn MatchSummary::to_error_layer(
self : MatchSummary,
id? : String = "errors",
) -> Layer {
let mut layer = Layer::new(id~)
for record in self.records {
match record.kind {
TruePositive => ()
FalseNegative =>
match record.expected {
Some(exp) =>
layer = layer.add(
ErrorRegion(
rect=exp.rect,
expected=exp.label,
actual="missing",
severity=1.0,
),
)
None => ()
}
FalsePositive =>
match record.actual {
Some(act) =>
layer = layer.add(
ErrorRegion(
rect=act.rect,
expected="none",
actual=act.label,
severity=0.7,
),
)
None => ()
}
LabelMismatch =>
match (record.expected, record.actual) {
(Some(exp), Some(act)) =>
layer = layer.add(
ErrorRegion(
rect=act.rect,
expected=exp.label,
actual=act.label,
severity=0.85,
),
)
_ => ()
}
}
}
layer
}