code wiki / (root) / nx_face_count.nx

nx_face_count.nx source

↩ module page · 171 lines · 6193 B

1// nx_face_count.nx -- face-cardinality detector. 2// Dark-blob CC inside FACE bbox + eye-pair clustering. 3 4// nx_safety_envelope: 5// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 6// sil_target: SIL1 7// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 8// verdict: NOT_YET_EVALUATED 9 10import "nx_syscalls.nx" 11import "nx_image.nx" 12import "nx_features.nx" 13import "nx_region.nx" 14const NX_MAGIC_1024: i64 = 1024 15 16const NX_FC_NO_FACE: i64 = 0 17const NX_FC_SINGLE_FACE: i64 = 1 18const NX_FC_DOUBLE_FACE: i64 = 2 19const NX_FC_MULTI_FACE: i64 = 3 20 21const NX_FC_DARK_THRESH: i64 = 50 22const NX_FC_MIN_EYE_AREA_Q10: i64 = 5 23const NX_FC_MAX_EYE_AREA_Q10: i64 = 100 24const NX_FC_Y_TOLERANCE_Q10: i64 = 100 25const NX_FC_INTER_EYE_MIN_Q10: i64 = 200 26const NX_FC_INTER_EYE_MAX_Q10: i64 = 500 27const NX_FC_MIN_EYE_ABS: i64 = 2 28const NX_FC_MIN_YTOL_ABS: i64 = 2 29const NX_FC_MIN_INTER_ABS: i64 = 3 30 31struct FaceCountVerdict { 32 kind: i64, 33 face_region_count: i64, 34 eye_pair_count: i64, 35 eye_blob_count: i64, 36 severity_q10: i64, 37 fidelity_q10: i64, 38} 39 40func nx_fc_dark_mask(gray: *Image) -> *Image { 41 let dark: *Image = nx_image_alloc(gray.width, gray.height, 1) 42 var y: i64 = 0 43 while y < gray.height { 44 var x: i64 = 0 45 while x < gray.width { 46 var v: i64 = 0 47 if nx_image_get(gray, x, y, 0) < NX_FC_DARK_THRESH { v = 255 } 48 nx_image_set(dark, x, y, 0, v) 49 x = x + 1 50 } 51 y = y + 1 52 } 53 return dark 54} 55 56func nx_face_count_compute(rgb: *Image, verdict: *FaceCountVerdict) -> i64 { 57 verdict.kind = NX_FC_NO_FACE 58 verdict.face_region_count = 0 59 verdict.eye_pair_count = 0 60 verdict.eye_blob_count = 0 61 verdict.severity_q10 = 0 62 verdict.fidelity_q10 = 0 63 64 let set: *RegionSet = nx_region_segment(rgb) 65 let n_faces: i64 = nx_region_count_kind(set, NX_REGION_FACE) 66 verdict.face_region_count = n_faces 67 if n_faces == 0 { return 0 } 68 69 let f_idx: i64 = nx_region_find_kind(set, NX_REGION_FACE) 70 let f_min_x: i64 = nx_region_get_field(set, f_idx, NX_REGION_F_BBOX_MIN_X) 71 let f_min_y: i64 = nx_region_get_field(set, f_idx, NX_REGION_F_BBOX_MIN_Y) 72 let f_max_x: i64 = nx_region_get_field(set, f_idx, NX_REGION_F_BBOX_MAX_X) 73 let f_max_y: i64 = nx_region_get_field(set, f_idx, NX_REGION_F_BBOX_MAX_Y) 74 let f_w: i64 = f_max_x - f_min_x + 1 75 let f_h: i64 = f_max_y - f_min_y + 1 76 let f_area: i64 = f_w * f_h 77 if f_area <= 0 { return 0 } 78 79 var min_eye: i64 = (f_area * NX_FC_MIN_EYE_AREA_Q10) / NX_MAGIC_1024 80 if min_eye < NX_FC_MIN_EYE_ABS { min_eye = NX_FC_MIN_EYE_ABS } 81 let max_eye: i64 = (f_area * NX_FC_MAX_EYE_AREA_Q10) / NX_MAGIC_1024 82 var y_tol: i64 = (f_h * NX_FC_Y_TOLERANCE_Q10) / NX_MAGIC_1024 83 if y_tol < NX_FC_MIN_YTOL_ABS { y_tol = NX_FC_MIN_YTOL_ABS } 84 var inter_min: i64 = (f_w * NX_FC_INTER_EYE_MIN_Q10) / NX_MAGIC_1024 85 if inter_min < NX_FC_MIN_INTER_ABS { inter_min = NX_FC_MIN_INTER_ABS } 86 let inter_max: i64 = (f_w * NX_FC_INTER_EYE_MAX_Q10) / NX_MAGIC_1024 87 88 let gray: *Image = nx_image_to_grayscale(rgb) 89 let dark: *Image = nx_fc_dark_mask(gray) 90 let cc: *CCResult = nx_feat_connected_components(dark) 91 let nblobs: i64 = cc.n_components 92 if nblobs == 0 { return 0 } 93 94 let stats: *i64 = (sys_mmap(nblobs * 7 * 8 + 16)) as *i64 95 nx_feat_component_stats(cc, stats) 96 97 let cand: *i64 = (sys_mmap(nblobs * 2 * 8 + 16)) as *i64 98 var n_cand: i64 = 0 99 var i: i64 = 0 100 while i < nblobs { 101 let area: i64 = stats[i * 7] 102 if area >= min_eye { 103 if area <= max_eye { 104 let cx: i64 = stats[i * 7 + 5] / 1000 105 let cy: i64 = stats[i * 7 + 6] / 1000 106 if cx >= f_min_x { 107 if cx <= f_max_x { 108 if cy >= f_min_y { 109 if cy <= f_max_y { 110 cand[n_cand * 2] = cx 111 cand[n_cand * 2 + 1] = cy 112 n_cand = n_cand + 1 113 } 114 } 115 } 116 } 117 } 118 } 119 i = i + 1 120 } 121 verdict.eye_blob_count = n_cand 122 123 let paired: *i64 = (sys_mmap(n_cand * 8 + 16)) as *i64 124 var k: i64 = 0 125 while k < n_cand { paired[k] = 0; k = k + 1 } 126 var pair_count: i64 = 0 127 i = 0 128 while i < n_cand { 129 if paired[i] == 0 { 130 let cx_i: i64 = cand[i * 2] 131 let cy_i: i64 = cand[i * 2 + 1] 132 var j: i64 = i + 1 133 var found: i64 = 0 134 while j < n_cand { 135 if found == 0 { 136 if paired[j] == 0 { 137 let cx_j: i64 = cand[j * 2] 138 let cy_j: i64 = cand[j * 2 + 1] 139 var dy: i64 = cy_j - cy_i 140 if dy < 0 { dy = -dy } 141 var dx: i64 = cx_j - cx_i 142 if dx < 0 { dx = -dx } 143 if dy <= y_tol { 144 if dx >= inter_min { 145 if dx <= inter_max { 146 paired[i] = 1; paired[j] = 1 147 pair_count = pair_count + 1 148 found = 1 149 } 150 } 151 } 152 } 153 } 154 j = j + 1 155 } 156 } 157 i = i + 1 158 } 159 verdict.eye_pair_count = pair_count 160 if pair_count == 0 { verdict.kind = NX_FC_NO_FACE } 161 if pair_count == 1 { verdict.kind = NX_FC_SINGLE_FACE } 162 if pair_count == 2 { verdict.kind = NX_FC_DOUBLE_FACE } 163 if pair_count >= 3 { verdict.kind = NX_FC_MULTI_FACE } 164 var extra: i64 = pair_count - 1 165 if extra < 0 { extra = 0 } 166 var sev: i64 = extra * 512 167 if sev > NX_MAGIC_1024 { sev = NX_MAGIC_1024 } 168 verdict.severity_q10 = sev 169 verdict.fidelity_q10 = nx_region_get_field(set, f_idx, NX_REGION_F_CONF_Q10) 170 return 0 171}