nx_presence.nx source
↩ module page · 121 lines · 4638 B
1// nx_presence.nx -- person-presence detection (skin-chrominance + blob
2// concentration). R2 of the MEDIA-STUDIO arc, and the TRUE dead-air signal:
3// motion alone is not enough -- a moving curtain or a TV showing scenery is
4// still "dead air" because no PERSON is onscreen. A person presents a
5// coherent region of skin tone; empty rooms, moving non-person objects, and
6// scattered skin-coloured noise do not.
7//
8// Two cheap, sovereign signals (pure integer, no learned weights):
9// skin per-mille : fraction of pixels inside the skin-chrominance gamut.
10// concentration : skin pixels / area of their bounding box, per-mille.
11// A solid blob fills its own box (high); scattered skin-
12// coloured speckle spans a huge box but fills little (low).
13// PRESENT iff skin >= min_skin AND concentration >= min_conc.
14//
15// The skin gamut is the Kovac et al. 2003 uniform-daylight rule (a fixed
16// colour-science test, documented below). The DECISION thresholds
17// (min_skin, min_conc) are CALLER-supplied -- those are the business knobs.
18//
19// genealogy_id: kovac_2003_skin_segmentation + connected_region_density
20// (attribution UNVERIFIED -- record-hint, do not cite as sourced)
21// lineage_id: skin_chrominance_rule + bounding_box_density + threshold
22// nx_safety_envelope:
23// intended_use: "Flag whether a person is onscreen, for dead-air trimming
24// and as the front-end of body/aesthetic analysis."
25// verdict: NOT_YET_EVALUATED
26// license_tier: ORIGINAL
27
28import "syscalls.nx"
29import "nx_image.nx"
30
31const NX_PRES_PRESENT: i64 = 1
32const NX_PRES_ABSENT: i64 = 0
33
34// A subject filling >= this fraction (permille) of the frame with skin counts
35// as present even without a tight blob -- the frame-filling person, which is
36// the common cam/portrait case and which a pure concentration gate wrongly
37// rejects. Data-driven knob; calibrate on real footage. (Proper fix for
38// localization is a connected-component largest-blob, queued as R2-refine.)
39const NX_PRES_MIN_FILL: i64 = 300
40
41// Skin-pixel test, Kovac et al. 2003 (uniform daylight). Pure integer.
42// R>95, G>40, B>20, max-min>15, |R-G|>15, R>G, R>B.
43func nx_skin_is_pixel(r: i64, g: i64, b: i64) -> i64 {
44 if r <= 95 { return 0 }
45 if g <= 40 { return 0 }
46 if b <= 20 { return 0 }
47 var mx: i64 = r
48 if g > mx { mx = g }
49 if b > mx { mx = b }
50 var mn: i64 = r
51 if g < mn { mn = g }
52 if b < mn { mn = b }
53 if mx - mn <= 15 { return 0 }
54 var d: i64 = r - g
55 if d < 0 { d = 0 - d }
56 if d <= 15 { return 0 }
57 if r <= g { return 0 }
58 if r <= b { return 0 }
59 return 1
60}
61
62// Scan an RGB image. Writes:
63// out[0] = skin per-mille (skin pixels / total, *1000)
64// out[1] = concentration per-mille (skin pixels / skin bounding-box area)
65// out[2] = raw skin pixel count
66func nx_presence_scan(img: *Image, out: *i64) -> i64 {
67 let w: i64 = img.width
68 let h: i64 = img.height
69 let total: i64 = w * h
70 var cnt: i64 = 0
71 var minx: i64 = w
72 var maxx: i64 = 0 - 1
73 var miny: i64 = h
74 var maxy: i64 = 0 - 1
75 var y: i64 = 0
76 while y < h {
77 var x: i64 = 0
78 while x < w {
79 let r: i64 = nx_image_get(img, x, y, 0)
80 let g: i64 = nx_image_get(img, x, y, 1)
81 let b: i64 = nx_image_get(img, x, y, 2)
82 if nx_skin_is_pixel(r, g, b) == 1 {
83 cnt = cnt + 1
84 if x < minx { minx = x }
85 if x > maxx { maxx = x }
86 if y < miny { miny = y }
87 if y > maxy { maxy = y }
88 }
89 x = x + 1
90 }
91 y = y + 1
92 }
93 out[2] = cnt
94 out[0] = 0
95 out[1] = 0
96 out[3] = 0 - 1 // bbox minx,miny,maxx,maxy (-1 = none)
97 out[4] = 0 - 1
98 out[5] = 0 - 1
99 out[6] = 0 - 1
100 if total <= 0 { return 0 }
101 out[0] = cnt * 1000 / total
102 if cnt <= 0 { return 0 }
103 out[3] = minx
104 out[4] = miny
105 out[5] = maxx
106 out[6] = maxy
107 let bw: i64 = maxx - minx + 1
108 let bh: i64 = maxy - miny + 1
109 let area: i64 = bw * bh
110 if area <= 0 { return 0 }
111 out[1] = cnt * 1000 / area
112 return 0
113}
114
115// PRESENT iff enough skin AND that skin is concentrated (a blob, not speckle).
116func nx_presence_classify(skin_permille: i64, conc_permille: i64, min_skin: i64, min_conc: i64) -> i64 {
117 if skin_permille < min_skin { return NX_PRES_ABSENT }
118 if conc_permille >= min_conc { return NX_PRES_PRESENT } // compact skin blob
119 if skin_permille >= NX_PRES_MIN_FILL { return NX_PRES_PRESENT } // frame-filling subject
120 return NX_PRES_ABSENT
121}