nx_haar_cascade.nx source
↩ module page · 330 lines · 13538 B
1// nx_haar_cascade.nx -- Viola-Jones attentional cascade EVALUATOR (R-FACE-2).
2// The detector engine: a weak classifier is a Haar feature + threshold +
3// polarity; a stage sums weighted weak votes vs a stage threshold; the cascade
4// runs stages in order and EARLY-REJECTS the moment any stage fails (so the
5// vast non-object majority is discarded after 1-2 cheap stages).
6//
7// This evaluates a GIVEN cascade. The cascade PARAMETERS (thresholds/polarities/
8// alphas/stage thresholds) come from TRAINING (AdaBoost over a labeled face /
9// non-face set) -- a separate data-dependent rung. This rung is the machinery,
10// validated with a hand-built toy cascade.
11//
12// Layout (caller-supplied flat i64 arrays):
13// weak row (8 i64): [kind, x, y, w, h, threshold, polarity(+1/-1), alpha]
14// stage row (3 i64): [weak_offset(in rows), weak_count, stage_threshold]
15//
16// genealogy_id: viola_jones_2001_attentional_cascade + freund_schapire_adaboost_1997
17// lineage_id: weak_classifier + boosted_stage + early_reject_cascade
18// license_tier: ORIGINAL
19import "syscalls.nx"
20import "nx_image.nx"
21import "nx_integral.nx"
22import "nx_haar.nx"
23
24const NX_WEAK_FIELDS: i64 = 8
25const NX_STAGE_FIELDS: i64 = 3
26
27// One weak classifier: 1 if the feature votes "object", else 0.
28// vote = (polarity * feature) < (polarity * threshold)
29func nx_weak_eval(ii: *IntegralImage, weak: *i64) -> i64 {
30 let f: i64 = nx_haar_eval(ii, weak[0], weak[1], weak[2], weak[3], weak[4])
31 let pol: i64 = weak[6]
32 if pol * f < pol * weak[5] { return 1 }
33 return 0
34}
35
36// One stage: pass(1)/fail(0). Pass if sum of alpha over firing weaks >= stage_thresh.
37func nx_stage_eval(ii: *IntegralImage, weaks: *i64, n: i64, stage_thresh: i64) -> i64 {
38 var s: i64 = 0
39 var i: i64 = 0
40 while i < n {
41 let row: *i64 = (weaks as i64 + i * NX_WEAK_FIELDS * 8) as *i64
42 if nx_weak_eval(ii, row) == 1 { s = s + row[7] }
43 i = i + 1
44 }
45 if s >= stage_thresh { return 1 }
46 return 0
47}
48
49// Full cascade: 1 if ALL stages pass (early reject on first failure).
50func nx_haar_cascade_eval(ii: *IntegralImage, stages: *i64, n_stages: i64, weaks: *i64) -> i64 {
51 var st: i64 = 0
52 while st < n_stages {
53 let meta: *i64 = (stages as i64 + st * NX_STAGE_FIELDS * 8) as *i64
54 let wrow: *i64 = (weaks as i64 + meta[0] * NX_WEAK_FIELDS * 8) as *i64
55 if nx_stage_eval(ii, wrow, meta[1], meta[2]) == 0 { return 0 }
56 st = st + 1
57 }
58 return 1
59}
60
61// ===== R-FACE-3 (2026-09-16, aesthetictwin AT30): the PUBLISHED-DATA cascade shape =====================================
62// The toy layout above is the machinery rung; the field's trained cascades (OpenCV 3+ XML, loaded by nx_haar_xml) carry
63// rect-list features with weights, stumps or small trees, per-stage thresholds, and a per-window VARIANCE NORMALISATION.
64// One arithmetic, fixed-point Q24, integer sums off nx_integral. Features are scaled to the window (OpenCV 1.x style: the
65// integral image is built once, the rects are rounded per scale and rect 0's weight recomputed so the feature stays
66// zero-mean after rounding) and a node compares sum_k weight_k x rectsum_k < threshold x sqrt(area x sqsum - sum^2),
67// which is the published rule with both sides carried in Q24. Windows step by the scale in pixels, as the field does.
68struct HaarCascade {
69 hdr: *i64,
70 stages: *i64,
71 weaks: *i64,
72 nodes: *i64,
73 leaves: *i64,
74 feats: *i64,
75}
76const HC_BYTES: i64 = 48
77const HC_HDR_N: i64 = 8
78const HC_H_WINW: i64 = 0
79const HC_H_WINH: i64 = 1
80const HC_H_NSTAGES: i64 = 2
81const HC_H_NWEAK: i64 = 3
82const HC_H_NNODES: i64 = 4
83const HC_H_NLEAVES: i64 = 5
84const HC_H_NFEATS: i64 = 6
85const HC_H_MAXRECTS: i64 = 7
86const HC_STAGE_FIELDS: i64 = 3
87const HC_S_FIRST: i64 = 0
88const HC_S_N: i64 = 1
89const HC_S_THR: i64 = 2
90const HC_WEAK_FIELDS: i64 = 4
91const HC_W_FIRST_NODE: i64 = 0
92const HC_W_N_NODES: i64 = 1
93const HC_W_FIRST_LEAF: i64 = 2
94const HC_W_N_LEAVES: i64 = 3
95const HC_NODE_FIELDS: i64 = 4
96const HC_N_LEFT: i64 = 0
97const HC_N_RIGHT: i64 = 1
98const HC_N_FEAT: i64 = 2
99const HC_N_THR: i64 = 3
100const HC_MAX_RECTS: i64 = 3
101const HC_RECT_FIELDS: i64 = 5
102const HC_R_X: i64 = 0
103const HC_R_Y: i64 = 1
104const HC_R_W: i64 = 2
105const HC_R_H: i64 = 3
106const HC_R_WEIGHT: i64 = 4
107const HC_F_N: i64 = 0
108const HC_F_RECTS: i64 = 1
109const HC_FEAT_FIELDS: i64 = 16
110const HC_Q16: i64 = 65536
111const HC_Q16_HALF: i64 = 32768
112const HC_Q24: i64 = 16777216
113const HC_NORM_INSET: i64 = 1
114const HC_PERMIL: i64 = 1000
115const HC_DET_FIELDS: i64 = 4
116const HC_GRP_FIELDS: i64 = 5
117const HC_G_X: i64 = 0
118const HC_G_Y: i64 = 1
119const HC_G_W: i64 = 2
120const HC_G_H: i64 = 3
121const HC_G_VOTES: i64 = 4
122const HC_I64: i64 = 8
123
124func hc_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
125func hc_min(a: i64, b: i64) -> i64 { if a < b { return a } return b }
126func hc_round_q16(v: i64, s_q16: i64) -> i64 { return (v * s_q16 + HC_Q16_HALF) / HC_Q16 }
127func hc_isqrt(v: i64) -> i64 {
128 if v <= 0 { return 0 }
129 var x: i64 = v
130 var y: i64 = (x + 1) / 2
131 while y < x { x = y; y = (x + v / x) / 2 }
132 return x
133}
134// the features at one window scale (Q16), rect 0's weight recomputed so the rounded feature stays zero-mean
135func hc_scale_features(c: *HaarCascade, s_q16: i64, out: *i64) -> i64 {
136 let nf: i64 = c.hdr[HC_H_NFEATS]
137 var f: i64 = 0
138 while f < nf {
139 let fo: i64 = f * HC_FEAT_FIELDS
140 let nr: i64 = c.feats[fo + HC_F_N]
141 out[fo + HC_F_N] = nr
142 var sum0: i64 = 0
143 var area0: i64 = 1
144 var k: i64 = 0
145 while k < nr {
146 let ro: i64 = fo + HC_F_RECTS + k * HC_RECT_FIELDS
147 var rw: i64 = hc_round_q16(c.feats[ro + HC_R_W], s_q16)
148 var rh: i64 = hc_round_q16(c.feats[ro + HC_R_H], s_q16)
149 if rw < 1 { rw = 1 }
150 if rh < 1 { rh = 1 }
151 out[ro + HC_R_X] = hc_round_q16(c.feats[ro + HC_R_X], s_q16)
152 out[ro + HC_R_Y] = hc_round_q16(c.feats[ro + HC_R_Y], s_q16)
153 out[ro + HC_R_W] = rw
154 out[ro + HC_R_H] = rh
155 out[ro + HC_R_WEIGHT] = c.feats[ro + HC_R_WEIGHT]
156 if k == 0 { area0 = rw * rh } else { sum0 = sum0 + c.feats[ro + HC_R_WEIGHT] * rw * rh }
157 k = k + 1
158 }
159 out[fo + HC_F_RECTS + HC_R_WEIGHT] = (0 - sum0) / area0
160 f = f + 1
161 }
162 return nf
163}
164// one feature's weighted rect sum at window origin (x, y), from the scaled feature table
165func hc_feature(sf: *i64, f: i64, ii: *IntegralImage, x: i64, y: i64) -> i64 {
166 let fo: i64 = f * HC_FEAT_FIELDS
167 let nr: i64 = sf[fo + HC_F_N]
168 var v: i64 = 0
169 var k: i64 = 0
170 while k < nr {
171 let ro: i64 = fo + HC_F_RECTS + k * HC_RECT_FIELDS
172 let rx: i64 = x + sf[ro + HC_R_X]
173 let ry: i64 = y + sf[ro + HC_R_Y]
174 v = v + sf[ro + HC_R_WEIGHT] * nx_integral_rect_sum(ii, rx, ry, rx + sf[ro + HC_R_W] - 1, ry + sf[ro + HC_R_H] - 1)
175 k = k + 1
176 }
177 return v
178}
179// the window's normaliser: sqrt(area x sqsum - sum^2) over the inset rect, 1 for a flat window (the published rule)
180func hc_window_norm(c: *HaarCascade, ii: *IntegralImage, sq: *IntegralImage, s_q16: i64, x: i64, y: i64) -> i64 {
181 let inset: i64 = hc_round_q16(HC_NORM_INSET, s_q16)
182 var nw: i64 = hc_round_q16(c.hdr[HC_H_WINW] - 2 * HC_NORM_INSET, s_q16)
183 var nh: i64 = hc_round_q16(c.hdr[HC_H_WINH] - 2 * HC_NORM_INSET, s_q16)
184 if nw < 1 { nw = 1 }
185 if nh < 1 { nh = 1 }
186 let x0: i64 = x + inset
187 let y0: i64 = y + inset
188 let area: i64 = nw * nh
189 let sum: i64 = nx_integral_rect_sum(ii, x0, y0, x0 + nw - 1, y0 + nh - 1)
190 let sqs: i64 = nx_integral_rect_sum(sq, x0, y0, x0 + nw - 1, y0 + nh - 1)
191 let nf: i64 = area * sqs - sum * sum
192 if nf > 0 { return hc_isqrt(nf) }
193 return 1
194}
195// the whole cascade at one window: 1 = every stage passed
196func hc_eval_window(c: *HaarCascade, sf: *i64, ii: *IntegralImage, sq: *IntegralImage, s_q16: i64, x: i64, y: i64) -> i64 {
197 let norm: i64 = hc_window_norm(c, ii, sq, s_q16, x, y)
198 let ns: i64 = c.hdr[HC_H_NSTAGES]
199 var st: i64 = 0
200 while st < ns {
201 let so: i64 = st * HC_STAGE_FIELDS
202 var ssum: i64 = 0
203 var wk: i64 = c.stages[so + HC_S_FIRST]
204 let wend: i64 = wk + c.stages[so + HC_S_N]
205 while wk < wend {
206 let wo: i64 = wk * HC_WEAK_FIELDS
207 var idx: i64 = 0
208 var walking: i64 = 1
209 while walking == 1 {
210 let no: i64 = (c.weaks[wo + HC_W_FIRST_NODE] + idx) * HC_NODE_FIELDS
211 let val: i64 = hc_feature(sf, c.nodes[no + HC_N_FEAT], ii, x, y)
212 var nxt: i64 = 0
213 if val < c.nodes[no + HC_N_THR] * norm { nxt = c.nodes[no + HC_N_LEFT] } else { nxt = c.nodes[no + HC_N_RIGHT] }
214 if nxt > 0 { idx = nxt } else { ssum = ssum + c.leaves[c.weaks[wo + HC_W_FIRST_LEAF] + (0 - nxt)]; walking = 0 }
215 }
216 wk = wk + 1
217 }
218 if ssum < c.stages[so + HC_S_THR] { return 0 }
219 st = st + 1
220 }
221 return 1
222}
223// raw detections (x, y, w, h rows) over the region [rx0, rx1) x [ry0, ry1): window sizes from min_size to max_size, the scale
224// stepped by scale_permil, the window stepped by the scale in pixels. Returns the count; capped[0] = 1 when the table filled
225func hc_detect(c: *HaarCascade, ii: *IntegralImage, sq: *IntegralImage, rx0: i64, ry0: i64, rx1: i64, ry1: i64,
226 min_size: i64, max_size: i64, scale_permil: i64, out: *i64, cap: i64, capped: *i64) -> i64 {
227 let winw: i64 = c.hdr[HC_H_WINW]
228 let winh: i64 = c.hdr[HC_H_WINH]
229 var s_q16: i64 = min_size * HC_Q16 / winw
230 if s_q16 < HC_Q16 { s_q16 = HC_Q16 }
231 let sf: *i64 = sys_mmap(c.hdr[HC_H_NFEATS] * HC_FEAT_FIELDS * HC_I64) as *i64
232 var n: i64 = 0
233 capped[0] = 0
234 var go: i64 = 1
235 while go == 1 {
236 let ww: i64 = hc_round_q16(winw, s_q16)
237 let wh: i64 = hc_round_q16(winh, s_q16)
238 if ww > max_size { go = 0 }
239 if ww + 1 > rx1 - rx0 { go = 0 }
240 if wh + 1 > ry1 - ry0 { go = 0 }
241 if go == 1 {
242 hc_scale_features(c, s_q16, sf)
243 var step: i64 = hc_round_q16(1, s_q16)
244 if step < 1 { step = 1 }
245 var y: i64 = ry0
246 while y + wh + 1 <= ry1 {
247 var x: i64 = rx0
248 while x + ww + 1 <= rx1 {
249 if hc_eval_window(c, sf, ii, sq, s_q16, x, y) == 1 {
250 if n < cap {
251 let o: i64 = n * HC_DET_FIELDS
252 out[o] = x
253 out[o + 1] = y
254 out[o + 2] = ww
255 out[o + 3] = wh
256 n = n + 1
257 } else { capped[0] = 1 }
258 }
259 x = x + step
260 }
261 y = y + step
262 }
263 s_q16 = s_q16 * scale_permil / HC_PERMIL
264 }
265 }
266 return n
267}
268// group overlapping detections (the field's rectangle grouping): rows [x, y, w, h, votes]; a group survives with at least
269// min_neighbors votes; eps_permil is the position and size tolerance relative to the smaller of the two boxes
270func hc_group(dets: *i64, n: i64, min_neighbors: i64, eps_permil: i64, groups: *i64, gcap: i64) -> i64 {
271 var ng: i64 = 0
272 var i: i64 = 0
273 while i < n {
274 let o: i64 = i * HC_DET_FIELDS
275 let x: i64 = dets[o]
276 let y: i64 = dets[o + 1]
277 let w: i64 = dets[o + 2]
278 let h: i64 = dets[o + 3]
279 var hit: i64 = 0 - 1
280 var g: i64 = 0
281 while g < ng {
282 let go: i64 = g * HC_GRP_FIELDS
283 let d: i64 = eps_permil * (hc_min(w, groups[go + HC_G_W]) + hc_min(h, groups[go + HC_G_H])) / (2 * HC_PERMIL)
284 var same: i64 = 1
285 if hc_abs(x - groups[go + HC_G_X]) > d { same = 0 }
286 if hc_abs(y - groups[go + HC_G_Y]) > d { same = 0 }
287 if hc_abs(w - groups[go + HC_G_W]) > d { same = 0 }
288 if same == 1 { hit = g; g = ng } else { g = g + 1 }
289 }
290 if hit >= 0 {
291 let go2: i64 = hit * HC_GRP_FIELDS
292 let v: i64 = groups[go2 + HC_G_VOTES]
293 groups[go2 + HC_G_X] = (groups[go2 + HC_G_X] * v + x) / (v + 1)
294 groups[go2 + HC_G_Y] = (groups[go2 + HC_G_Y] * v + y) / (v + 1)
295 groups[go2 + HC_G_W] = (groups[go2 + HC_G_W] * v + w) / (v + 1)
296 groups[go2 + HC_G_H] = (groups[go2 + HC_G_H] * v + h) / (v + 1)
297 groups[go2 + HC_G_VOTES] = v + 1
298 } else {
299 if ng < gcap {
300 let go3: i64 = ng * HC_GRP_FIELDS
301 groups[go3 + HC_G_X] = x
302 groups[go3 + HC_G_Y] = y
303 groups[go3 + HC_G_W] = w
304 groups[go3 + HC_G_H] = h
305 groups[go3 + HC_G_VOTES] = 1
306 ng = ng + 1
307 }
308 }
309 i = i + 1
310 }
311 var k: i64 = 0
312 var g2: i64 = 0
313 while g2 < ng {
314 if groups[g2 * HC_GRP_FIELDS + HC_G_VOTES] >= min_neighbors {
315 var f: i64 = 0
316 while f < HC_GRP_FIELDS { groups[k * HC_GRP_FIELDS + f] = groups[g2 * HC_GRP_FIELDS + f]; f = f + 1 }
317 k = k + 1
318 }
319 g2 = g2 + 1
320 }
321 return k
322}
323// the group with the most votes, or -1
324func hc_best_group(groups: *i64, ng: i64) -> i64 {
325 var best: i64 = 0 - 1
326 var bv: i64 = 0
327 var g: i64 = 0
328 while g < ng { if groups[g * HC_GRP_FIELDS + HC_G_VOTES] > bv { bv = groups[g * HC_GRP_FIELDS + HC_G_VOTES]; best = g } g = g + 1 }
329 return best
330}