1package hints
2
3const (
4 // maxRegions is the protocol limit of regions per request.
5 maxRegions = 64
6 // wholeMaxSide is the longest frame side sent whole when the frame is not
7 // 16:9: up to there the square letterbox of the detector loses little.
8 wholeMaxSide = 2560
9 // tileMaxH is the tallest tile; its width is 16/9 of it.
10 tileMaxH = 1440
11 // tileCutMargin is the distance, in physical pixels, to a tile's inner
12 // edge under which a box counts as cut by it.
13 tileCutMargin = 2
14)
15
16// region is a crop of the frame, in physical pixels.
17type region struct{ x, y, w, h int }
18
19// tileRegions returns the crops to detect separately on a width x height
20// frame, or nil for one whole-frame detection (a 16:9 frame, or one whose
21// long side is at most wholeMaxSide). Otherwise the frame is covered by exact
22// 16:9 tiles (w*9 == h*16; explicit crops always use square letterboxing) of
23// height min(height, 1440) rounded down to a multiple of 9, overlapping by at
24// least 10 % of the tile side on each axis; the last tile of a row or column
25// is aligned to the frame edge. Tiles are listed row by row. A frame that
26// would need more than maxRegions tiles is detected whole.
27func tileRegions(width, height int) []region {
28 if width <= 0 || height <= 0 || width*9 == height*16 || max(width, height) <= wholeMaxSide {
29 return nil
30 }
31 th := min(height, tileMaxH) / 9 * 9
32 tw := th / 9 * 16
33 if tw > width { // a tall frame: the width bounds the tile
34 tw = width / 16 * 16
35 th = tw / 16 * 9
36 }
37 if tw == 0 || th == 0 {
38 return nil
39 }
40 xs, ys := tileStarts(width, tw), tileStarts(height, th)
41 if len(xs)*len(ys) > maxRegions {
42 return nil
43 }
44 out := make([]region, 0, len(xs)*len(ys))
45 for _, y := range ys {
46 for _, x := range xs {
47 out = append(out, region{x, y, tw, th})
48 }
49 }
50 return out
51}
52
53// tileStarts returns the start offsets of tiles of size t covering length l,
54// evenly spread, the last one flush with the end, with consecutive tiles
55// overlapping by at least a tenth of t.
56func tileStarts(l, t int) []int {
57 if l <= t {
58 return []int{0}
59 }
60 d := l - t
61 maxStep := t * 9 / 10
62 n := (d+maxStep-1)/maxStep + 1
63 out := make([]int, n)
64 for i := range out {
65 out[i] = (d*i + n - 2) / (n - 1) // ceil(d*i/(n-1)): steps never exceed maxStep
66 }
67 return out
68}
69
70// dropCutBoxes drops the boxes that a tile's inner edge cut (an edge of the
71// tile that is not an edge of the frame) when another tile saw the same
72// element uncut: the cut box would otherwise survive as a smaller box nested
73// in the whole one. A box is "the same element" when the other box covers
74// nestRatio of it. Without regions the boxes are returned as they are.
75func dropCutBoxes(dets []detection, regions []region) []detection {
76 if len(regions) == 0 {
77 return dets
78 }
79 fw, fh := 0, 0
80 for _, r := range regions {
81 fw, fh = max(fw, r.x+r.w), max(fh, r.y+r.h)
82 }
83 cut := func(d detection) bool {
84 if d.region < 0 || d.region >= len(regions) {
85 return false
86 }
87 r := regions[d.region]
88 m := float32(tileCutMargin)
89 return (r.x > 0 && d.x1 < float32(r.x)+m) || (r.x+r.w < fw && d.x2 > float32(r.x+r.w)-m) ||
90 (r.y > 0 && d.y1 < float32(r.y)+m) || (r.y+r.h < fh && d.y2 > float32(r.y+r.h)-m)
91 }
92 isCut := make([]bool, len(dets))
93 for i, d := range dets {
94 isCut[i] = cut(d)
95 }
96 out := make([]detection, 0, len(dets))
97 for i, d := range dets {
98 drop := false
99 if isCut[i] {
100 for j, o := range dets {
101 if j != i && !isCut[j] && o.region != d.region && covered(d, o) {
102 drop = true
103 break
104 }
105 }
106 }
107 if !drop {
108 out = append(out, d)
109 }
110 }
111 return out
112}
113
114// covered reports whether o covers at least nestRatio of d's area.
115func covered(d, o detection) bool {
116 w := min(d.x2, o.x2) - max(d.x1, o.x1)
117 h := min(d.y2, o.y2) - max(d.y1, o.y1)
118 if w <= 0 || h <= 0 {
119 return false
120 }
121 return float64(w*h) >= nestRatio*float64((d.x2-d.x1)*(d.y2-d.y1))
122}