nx_shadow_integration.nx source
↩ module page · 112 lines · 4526 B
1// nx_shadow_integration.nx -- Best Compositing (shadow half).
2//
3// Real subjects in real scenes cast shadows onto environment objects.
4// AI renders often drop the shadow → subject reads "pasted on".
5// Detector measures: in the region around the subject's silhouette
6// edge, is there a darkening band ON the environment side that
7// matches what a subject-cast shadow would look like?
8//
9// Math:
10// 1. Define a band along the subject's outer edge on the BG side.
11// 2. Compare mean luminance INSIDE that band vs background baseline
12// luminance further away.
13// 3. Real shadow: band is 10-25% darker than baseline.
14// 4. No shadow: band is similar to baseline.
15//
16// Inputs: gray image + subject_mask + env_baseline_bbox.
17//
18// Output flat-array:
19// result[0] band_mean_Y
20// result[1] baseline_mean_Y
21// result[2] shadow_depth_pct (baseline - band) * 100 / baseline
22// result[3] verdict
23
24// nx_safety_envelope:
25// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
26// sil_target: SIL1
27// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
28// verdict: NOT_YET_EVALUATED
29
30import "nx_syscalls.nx"
31import "nx_image.nx"
32
33const NX_SHADOW_RES_BAND_Y: i64 = 0
34const NX_SHADOW_RES_BASELINE_Y: i64 = 1
35const NX_SHADOW_RES_DEPTH_PCT: i64 = 2
36const NX_SHADOW_RES_VERDICT: i64 = 3
37const NX_SHADOW_RES_FIELDS: i64 = 4
38
39const NX_SHADOW_V_MISSING: i64 = 0 // < 5% depth
40const NX_SHADOW_V_WEAK: i64 = 1 // 5-10%
41const NX_SHADOW_V_NATURAL: i64 = 2 // 10-25%
42const NX_SHADOW_V_OVERDARK: i64 = 3 // > 25%
43
44// Mean luminance over bbox where mask differs from match_value.
45// pass_through_value 0: count pixels where mask == 0 (env)
46// pass_through_value 255: count pixels where mask == 255 (subject)
47func nx_shadow_mean_y_masked(rgb: *Image, mask: *Image,
48 x0: i64, y0: i64, x1: i64, y1: i64,
49 match_value: i64) -> i64 {
50 var sum: i64 = 0
51 var n: i64 = 0
52 var y: i64 = y0
53 while y <= y1 {
54 if y >= 0 { if y < rgb.height {
55 var x: i64 = x0
56 while x <= x1 {
57 if x >= 0 { if x < rgb.width {
58 let m: i64 = nx_image_get(mask, x, y, 0)
59 var include: i64 = 0
60 if match_value == 0 { if m == 0 { include = 1 } }
61 if match_value == 255 { if m == 255 { include = 1 } }
62 if include != 0 {
63 let r: i64 = nx_image_get(rgb, x, y, 0)
64 let g: i64 = nx_image_get(rgb, x, y, 1)
65 let b: i64 = nx_image_get(rgb, x, y, 2)
66 sum = sum + (77 * r + 150 * g + 29 * b) / 256
67 n = n + 1
68 }
69 } }
70 x = x + 1
71 }
72 } }
73 y = y + 1
74 }
75 if n == 0 { return -1 }
76 return sum / n
77}
78
79// Score shadow integration.
80// bbox_pack: [band_x0, band_y0, band_x1, band_y1,
81// baseline_x0, baseline_y0, baseline_x1, baseline_y1]
82// The band is a thin region adjacent to the subject's silhouette.
83// The baseline is further out, same env material.
84func nx_shadow_integration(rgb: *Image, subject_mask: *Image,
85 bbox_pack: *i64, result: *i64) -> i64 {
86 var i: i64 = 0
87 while i < NX_SHADOW_RES_FIELDS { result[i] = 0; i = i + 1 }
88
89 // Band: env pixels (mask == 0) within shadow band.
90 let band_y: i64 = nx_shadow_mean_y_masked(rgb, subject_mask,
91 bbox_pack[0], bbox_pack[1],
92 bbox_pack[2], bbox_pack[3], 0)
93 let base_y: i64 = nx_shadow_mean_y_masked(rgb, subject_mask,
94 bbox_pack[4], bbox_pack[5],
95 bbox_pack[6], bbox_pack[7], 0)
96 if band_y < 0 { return 1 }
97 if base_y < 0 { return 2 }
98 if base_y == 0 { return 3 }
99
100 result[NX_SHADOW_RES_BAND_Y] = band_y
101 result[NX_SHADOW_RES_BASELINE_Y] = base_y
102
103 let depth_pct: i64 = ((base_y - band_y) * 100) / base_y
104 result[NX_SHADOW_RES_DEPTH_PCT] = depth_pct
105
106 var verdict: i64 = NX_SHADOW_V_MISSING
107 if depth_pct >= 5 { verdict = NX_SHADOW_V_WEAK }
108 if depth_pct >= 10 { verdict = NX_SHADOW_V_NATURAL }
109 if depth_pct > 25 { verdict = NX_SHADOW_V_OVERDARK }
110 result[NX_SHADOW_RES_VERDICT] = verdict
111 return 0
112}