nx_finger_count.nx source
↩ module page · 267 lines · 9172 B
1// nx_finger_count.nx -- Tier 6 anatomical-coherence finger-counter.
2//
3// THE canonical generative-AI failure mode: six fingers per hand,
4// or three, or seven, or two fingers fused into a paddle. Stable
5// Diffusion / Flux / Midjourney / DALL-E all produce this with
6// alarming frequency because they have no anatomical prior.
7// Substrate's bits-up answer: count fingers DETERMINISTICALLY via
8// horizontal scan-line crossings on a binary hand mask.
9//
10// Method:
11// Input: binary hand mask (0 = bg, 1 = hand), w x h, plus a
12// scan_y row index that cuts horizontally THROUGH the
13// fingers (above the palm, below the fingertips). Caller
14// determines scan_y from hand-orientation upstream.
15// For x = 0..w-1: track run-length of consecutive 1 pixels along
16// scan_y. Each contiguous run >= min_run_width is one
17// finger.
18// verdict by run count vs anatomical norm (5 per hand):
19// 5 -> OK
20// 6+ -> EXTRA (the famous AI failure)
21// 4 -> MISSING (or fused)
22// 3 or less -> FUSED / OCCLUSION
23// NOISE detected (many tiny runs) -> NOISY
24//
25// Caller responsibility: provide a HAND-segmented binary mask + a
26// VALID scan_y. Substrate provides the deterministic counter.
27//
28// Reference: Suk/Park 2010 "Hand gesture recognition based on
29// digital image processing using MATLAB" -- scan-line approach for
30// finger counting in segmented hand regions. Convex-hull-defect
31// approach (Hawley/Eichmann 2014) is more robust to fingertip
32// occlusion; queued for nx_finger_count_v2.
33//
34// genealogy_id: suk_park_2010_scanline
35// lineage_id: substrate_anatomy_coherence_v1
36
37// nx_safety_envelope:
38// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
39// sil_target: SIL1
40// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
41// verdict: NOT_YET_EVALUATED
42
43import "nx_syscalls.nx"
44import "nx_runtime.nx"
45import "nx_tier.nx"
46
47// Sealed-verdict IDs.
48const NX_FC_VERDICT_OK: nx_int = 0
49const NX_FC_VERDICT_EXTRA: nx_int = 1
50const NX_FC_VERDICT_MISSING: nx_int = 2
51const NX_FC_VERDICT_FUSED: nx_int = 3
52const NX_FC_VERDICT_NOISY: nx_int = 4
53
54// Anatomical norm.
55const NX_FC_EXPECTED_FINGERS: nx_int = 5
56
57// Default minimum-run-width in pixels (used when caller passes 0).
58const NX_FC_DEFAULT_MIN_RUN: nx_int = 3
59const NX_FC_MAX_RUN_THR: nx_int = 1024
60const NX_FC_NOISE_RUN_LIMIT: nx_int = 16 // > this many tiny runs -> NOISY
61
62const NX_FC_MAX_DIM: nx_int = 16384
63
64// ===== result struct ==============================================
65
66struct NxFingerCountResult {
67 n_runs: nx_int,
68 n_tiny_runs: nx_int,
69 longest_run: nx_int,
70 expected: nx_int,
71 delta: nx_int,
72 verdict: nx_int,
73 scan_y: nx_int,
74 min_run_width: nx_int,
75}
76
77const NX_FC_RESULT_BYTES: nx_size = 64
78
79// ===== compute ====================================================
80
81func nx_finger_count_scanline(
82 mask: *nx_int, w: nx_int, h: nx_int,
83 scan_y: nx_int, min_run_width: nx_int) -> *NxFingerCountResult {
84
85 if w <= 0 { return 0 as *NxFingerCountResult }
86 if h <= 0 { return 0 as *NxFingerCountResult }
87 if w > NX_FC_MAX_DIM { return 0 as *NxFingerCountResult }
88 if h > NX_FC_MAX_DIM { return 0 as *NxFingerCountResult }
89 if scan_y < 0 { return 0 as *NxFingerCountResult }
90 if scan_y >= h { return 0 as *NxFingerCountResult }
91 var thr: nx_int = min_run_width
92 if thr <= 0 { thr = NX_FC_DEFAULT_MIN_RUN }
93 if thr > NX_FC_MAX_RUN_THR { thr = NX_FC_MAX_RUN_THR }
94
95 let r_ptr: *u8 = sys_mmap(NX_FC_RESULT_BYTES)
96 let r: *NxFingerCountResult = r_ptr as *NxFingerCountResult
97
98 var n_runs: nx_int = 0
99 var n_tiny: nx_int = 0
100 var longest: nx_int = 0
101 var cur_run: nx_int = 0
102 var x: nx_int = 0
103 let row_base: nx_int = scan_y * w
104 while x < w {
105 let v: nx_int = mask[row_base + x]
106 if v != 0 {
107 cur_run = cur_run + 1
108 } else {
109 if cur_run > 0 {
110 if cur_run > longest { longest = cur_run }
111 if cur_run >= thr {
112 n_runs = n_runs + 1
113 } else {
114 n_tiny = n_tiny + 1
115 }
116 cur_run = 0
117 }
118 }
119 x = x + 1
120 }
121 // Close trailing run.
122 if cur_run > 0 {
123 if cur_run > longest { longest = cur_run }
124 if cur_run >= thr {
125 n_runs = n_runs + 1
126 } else {
127 n_tiny = n_tiny + 1
128 }
129 }
130
131 let delta: nx_int = n_runs - NX_FC_EXPECTED_FINGERS
132
133 var verdict: nx_int = NX_FC_VERDICT_OK
134 if n_tiny > NX_FC_NOISE_RUN_LIMIT {
135 verdict = NX_FC_VERDICT_NOISY
136 } else {
137 if n_runs == NX_FC_EXPECTED_FINGERS {
138 verdict = NX_FC_VERDICT_OK
139 } else {
140 if n_runs > NX_FC_EXPECTED_FINGERS {
141 verdict = NX_FC_VERDICT_EXTRA
142 } else {
143 if n_runs == 4 {
144 verdict = NX_FC_VERDICT_MISSING
145 } else {
146 verdict = NX_FC_VERDICT_FUSED
147 }
148 }
149 }
150 }
151
152 r.n_runs = n_runs
153 r.n_tiny_runs = n_tiny
154 r.longest_run = longest
155 r.expected = NX_FC_EXPECTED_FINGERS
156 r.delta = delta
157 r.verdict = verdict
158 r.scan_y = scan_y
159 r.min_run_width = thr
160 return r
161}
162
163// ===== self-test ==================================================
164//
165// Build small synthetic hand masks at scan_y rows. Each test uses
166// a 40-wide, 1-tall row (h=1, scan_y=0). Pattern uses ASCII-style
167// arrays where '1' = hand pixel, '0' = bg.
168
169func _fc_set_run(mask: *nx_int, x0: nx_int, n: nx_int) -> nx_int {
170 var i: nx_int = 0
171 while i < n {
172 mask[x0 + i] = 1
173 i = i + 1
174 }
175 return 0
176}
177
178func _fc_zero_row(mask: *nx_int, w: nx_int) -> nx_int {
179 var i: nx_int = 0
180 while i < w {
181 mask[i] = 0
182 i = i + 1
183 }
184 return 0
185}
186
187func main() -> nx_int {
188 // ---- OK hand: 5 fingers, each 4px wide, 2px gap ----
189 //
190 // Layout (40 cols): GFFF GFFF GFFF GFFF GFFF (G=gap 2, F=finger 4)
191 // Positions: finger starts at 2, 8, 14, 20, 26. Each is 4 wide.
192 let m_ok: *nx_int = (sys_mmap(320)) as *nx_int
193 _fc_zero_row(m_ok, 40)
194 _fc_set_run(m_ok, 2, 4)
195 _fc_set_run(m_ok, 8, 4)
196 _fc_set_run(m_ok, 14, 4)
197 _fc_set_run(m_ok, 20, 4)
198 _fc_set_run(m_ok, 26, 4)
199
200 let r_ok: *NxFingerCountResult = nx_finger_count_scanline(m_ok, 40, 1, 0, 3)
201 if r_ok == (0 as *NxFingerCountResult) { return 1 }
202 if r_ok.n_runs != 5 { return 2 }
203 if r_ok.verdict != NX_FC_VERDICT_OK { return 3 }
204 if r_ok.longest_run != 4 { return 4 }
205 if r_ok.delta != 0 { return 5 }
206
207 // ---- EXTRA: 6 fingers ----
208 let m_ex: *nx_int = (sys_mmap(320)) as *nx_int
209 _fc_zero_row(m_ex, 40)
210 _fc_set_run(m_ex, 2, 4)
211 _fc_set_run(m_ex, 8, 4)
212 _fc_set_run(m_ex, 14, 4)
213 _fc_set_run(m_ex, 20, 4)
214 _fc_set_run(m_ex, 26, 4)
215 _fc_set_run(m_ex, 32, 4)
216 let r_ex: *NxFingerCountResult = nx_finger_count_scanline(m_ex, 40, 1, 0, 3)
217 if r_ex.n_runs != 6 { return 10 }
218 if r_ex.verdict != NX_FC_VERDICT_EXTRA { return 11 }
219 if r_ex.delta != 1 { return 12 }
220
221 // ---- MISSING (or fused into 4): 4 fingers ----
222 let m_mi: *nx_int = (sys_mmap(320)) as *nx_int
223 _fc_zero_row(m_mi, 40)
224 _fc_set_run(m_mi, 2, 4)
225 _fc_set_run(m_mi, 8, 4)
226 _fc_set_run(m_mi, 14, 4)
227 _fc_set_run(m_mi, 20, 4)
228 let r_mi: *NxFingerCountResult = nx_finger_count_scanline(m_mi, 40, 1, 0, 3)
229 if r_mi.n_runs != 4 { return 20 }
230 if r_mi.verdict != NX_FC_VERDICT_MISSING { return 21 }
231
232 // ---- FUSED: 3 fingers (e.g. two pairs fused into single wide run)
233 let m_fu: *nx_int = (sys_mmap(320)) as *nx_int
234 _fc_zero_row(m_fu, 40)
235 _fc_set_run(m_fu, 2, 4)
236 _fc_set_run(m_fu, 8, 10) // fused chunk
237 _fc_set_run(m_fu, 22, 4)
238 let r_fu: *NxFingerCountResult = nx_finger_count_scanline(m_fu, 40, 1, 0, 3)
239 if r_fu.n_runs != 3 { return 30 }
240 if r_fu.verdict != NX_FC_VERDICT_FUSED { return 31 }
241 if r_fu.longest_run != 10 { return 32 }
242
243 // ---- NOISY: > 16 tiny single-pixel runs interleaved.
244 // Use a 40-wide row with alternating 1010101010...
245 // -> 20 single-pixel runs. All under min_run_width=3 -> tiny.
246 let m_no: *nx_int = (sys_mmap(320)) as *nx_int
247 _fc_zero_row(m_no, 40)
248 var i: nx_int = 0
249 while i < 40 {
250 if i % 2 == 0 { m_no[i] = 1 }
251 i = i + 1
252 }
253 let r_no: *NxFingerCountResult = nx_finger_count_scanline(m_no, 40, 1, 0, 3)
254 if r_no.n_runs != 0 { return 40 }
255 if r_no.n_tiny_runs != 20 { return 41 }
256 if r_no.verdict != NX_FC_VERDICT_NOISY { return 42 }
257
258 // ---- input validation ----
259 let r_bad: *NxFingerCountResult = nx_finger_count_scanline(
260 m_ok, 0, 1, 0, 3)
261 if r_bad != (0 as *NxFingerCountResult) { return 50 }
262 let r_bad_y: *NxFingerCountResult = nx_finger_count_scanline(
263 m_ok, 40, 1, 5, 3)
264 if r_bad_y != (0 as *NxFingerCountResult) { return 51 }
265
266 return 0
267}