code wiki / (root) / nx_haar_cascade.nx

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}