1package hints
2
3import (
4 "cmp"
5 "slices"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10const (
11 // dupIoU is the IoU above which two boxes are the same element: the
12 // higher score is kept.
13 dupIoU = 0.5
14 // nestRatio is the share of the smaller box that must lie inside the
15 // other for the two to be nested.
16 nestRatio = 0.85
17)
18
19func area(r ports.Rect) int { return r.W * r.H }
20
21// interArea is the area of the intersection of a and b.
22func interArea(a, b ports.Rect) int {
23 w := min(a.X+a.W, b.X+b.W) - max(a.X, b.X)
24 h := min(a.Y+a.H, b.Y+b.H) - max(a.Y, b.Y)
25 if w <= 0 || h <= 0 {
26 return 0
27 }
28 return w * h
29}
30
31// nested reports whether the smaller of a and b lies for at least nestRatio
32// inside the other.
33func nested(a, b ports.Rect) bool {
34 return float64(interArea(a, b)) >= nestRatio*float64(min(area(a), area(b)))
35}
36
37// mergeTargets removes the duplicate and nested boxes the detector returns
38// for one element. It works on logical targets (after rounding), in two
39// passes so that the result does not depend on the processing order:
40//
41// 1. duplicates: by descending score, a box whose IoU with an already kept
42// box is over dupIoU is dropped;
43// 2. nesting: a box that contains another surviving box (that box lies
44// nestRatio inside it) is dropped, whatever the scores: the smallest of
45// a nested chain is kept (a row loses against its icon and its text).
46//
47// The result is ordered by descending score, then position.
48func mergeTargets(in []ports.HintTarget) []ports.HintTarget {
49 s := slices.Clone(in)
50 slices.SortStableFunc(s, func(a, b ports.HintTarget) int {
51 return cmp.Or(cmp.Compare(b.Score, a.Score), cmp.Compare(a.Rect.Y, b.Rect.Y), cmp.Compare(a.Rect.X, b.Rect.X),
52 cmp.Compare(area(a.Rect), area(b.Rect)))
53 })
54 kept := s[:0]
55 for _, t := range s {
56 dup := false
57 for _, k := range kept {
58 inter := interArea(t.Rect, k.Rect)
59 if inter > 0 && float64(inter) > dupIoU*float64(area(t.Rect)+area(k.Rect)-inter) {
60 dup = true
61 break
62 }
63 }
64 if !dup {
65 kept = append(kept, t)
66 }
67 }
68 out := make([]ports.HintTarget, 0, len(kept))
69 for i, t := range kept {
70 contains := false
71 for j, k := range kept {
72 if i != j && area(k.Rect) < area(t.Rect) && nested(t.Rect, k.Rect) {
73 contains = true
74 break
75 }
76 }
77 if !contains {
78 out = append(out, t)
79 }
80 }
81 return out
82}