code wiki / (root) / nx_motion.nx

nx_motion.nx source

↩ module page · 197 lines · 6488 B

1// nx_motion.nx -- frame-to-frame motion (Lucas-Kanade optical flow). 2// 3// Brightness-constancy + small-motion assumption: 4// I(x, y, t) = I(x + u, y + v, t + 1) 5// Taylor expand: 6// I_x * u + I_y * v + I_t = 0 7// Over an N-pixel window this is N equations in 2 unknowns; solve 8// via 2x2 least-squares (normal equations + Cramer's rule). 9// 10// What this unlocks: 11// + motion detection (frame diff) 12// + tracking (per-pixel flow vectors) 13// + camera-motion estimation (mean flow direction) 14// + scene-cut detection (flow magnitude spike) 15// + activity recognition substrate 16// 17// Returns flow in Q8 sub-pixel precision (1 unit = 1/256 pixel). 18// 5x5 integration window default. 19// 20// genealogy_id: lucas_kanade_1981 + horn_schunck_1981 21// lineage_id: gradient_based_optical_flow + 2x2_least_squares 22 23// nx_safety_envelope: 24// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 25// sil_target: SIL1 26// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 27// verdict: NOT_YET_EVALUATED 28 29import "syscalls.nx" 30import "nx_image.nx" 31 32const NX_MOTION_Q: i64 = 256 // Q8 sub-pixel 33const NX_MOTION_WIN: i64 = 5 // 5x5 integration 34 35// ===== Frame difference ================================================= 36// 37// Absolute difference between two same-size grayscale images. 38 39func nx_motion_frame_diff(a: *Image, b: *Image) -> *Image { 40 let w: i64 = a.width 41 let h: i64 = a.height 42 let dst: *Image = nx_image_alloc(w, h, 1) 43 var y: i64 = 0 44 while y < h { 45 var x: i64 = 0 46 while x < w { 47 let av: i64 = nx_image_get(a, x, y, 0) 48 let bv: i64 = nx_image_get(b, x, y, 0) 49 var d: i64 = av - bv 50 if d < 0 { d = -d } 51 if d > 255 { d = 255 } 52 nx_image_set(dst, x, y, 0, d) 53 x = x + 1 54 } 55 y = y + 1 56 } 57 return dst 58} 59 60// ===== Temporal derivative ============================================ 61// 62// I_t = frame2 - frame1. Signed output in ImageS64. 63 64func nx_motion_temporal_grad(frame1: *Image, frame2: *Image) -> *ImageS64 { 65 let w: i64 = frame1.width 66 let h: i64 = frame1.height 67 let dst: *ImageS64 = nx_image_s64_alloc(w, h) 68 var y: i64 = 0 69 while y < h { 70 var x: i64 = 0 71 while x < w { 72 let v: i64 = nx_image_get(frame2, x, y, 0) - nx_image_get(frame1, x, y, 0) 73 nx_image_s64_set(dst, x, y, v) 74 x = x + 1 75 } 76 y = y + 1 77 } 78 return dst 79} 80 81// ===== Lucas-Kanade per-pixel flow ===================================== 82// 83// Computes flow (u, v) at each pixel by solving the 2x2 normal 84// equations over a 5x5 window of spatial+temporal gradients. 85// 86// Pixels with insufficient texture (det close to zero) get u=v=0. 87// 88// Returns two ImageS64 maps: flow_u (Q8) and flow_v (Q8). 89 90struct FlowField { 91 u: *ImageS64, // Q8 horizontal flow 92 v: *ImageS64, // Q8 vertical flow 93} 94 95func nx_motion_lucas_kanade(frame1: *Image, frame2: *Image) -> *FlowField { 96 let w: i64 = frame1.width 97 let h: i64 = frame1.height 98 99 // Compute spatial + temporal gradients on frame1. 100 let Ix: *ImageS64 = nx_image_sobel_x(frame1) 101 let Iy: *ImageS64 = nx_image_sobel_y(frame1) 102 let It: *ImageS64 = nx_motion_temporal_grad(frame1, frame2) 103 104 let flow_u: *ImageS64 = nx_image_s64_alloc(w, h) 105 let flow_v: *ImageS64 = nx_image_s64_alloc(w, h) 106 107 let half: i64 = NX_MOTION_WIN / 2 108 var y: i64 = half 109 while y < h - half { 110 var x: i64 = half 111 while x < w - half { 112 // Accumulate over 5x5 window. 113 var sxx: i64 = 0 114 var sxy: i64 = 0 115 var syy: i64 = 0 116 var sxt: i64 = 0 117 var syt: i64 = 0 118 var dy: i64 = -half 119 while dy <= half { 120 var dx: i64 = -half 121 while dx <= half { 122 let ix: i64 = nx_image_s64_get(Ix, x + dx, y + dy) 123 let iy: i64 = nx_image_s64_get(Iy, x + dx, y + dy) 124 let it: i64 = nx_image_s64_get(It, x + dx, y + dy) 125 sxx = sxx + ix * ix 126 sxy = sxy + ix * iy 127 syy = syy + iy * iy 128 sxt = sxt + ix * it 129 syt = syt + iy * it 130 dx = dx + 1 131 } 132 dy = dy + 1 133 } 134 // 2x2 system: [sxx sxy] [u] [-sxt] 135 // [sxy syy] [v] = [-syt] 136 // det = sxx * syy - sxy^2 137 let det: i64 = sxx * syy - sxy * sxy 138 if det != 0 { 139 // u = (syy * (-sxt) - sxy * (-syt)) / det 140 // v = (sxx * (-syt) - sxy * (-sxt)) / det 141 // Scale up by Q for sub-pixel precision. 142 let u_num: i64 = (sxy * syt - syy * sxt) * NX_MOTION_Q 143 let v_num: i64 = (sxy * sxt - sxx * syt) * NX_MOTION_Q 144 nx_image_s64_set(flow_u, x, y, u_num / det) 145 nx_image_s64_set(flow_v, x, y, v_num / det) 146 } 147 x = x + 1 148 } 149 y = y + 1 150 } 151 152 let field: *FlowField = (sys_mmap(16)) as *FlowField 153 field.u = flow_u 154 field.v = flow_v 155 return field 156} 157 158// ===== Mean flow ======================================================== 159// 160// Mean (u, v) over high-confidence pixels (those where |flow| >= threshold). 161// Useful for global camera-motion estimate. 162 163func nx_motion_mean_flow(flow: *FlowField, mag_threshold: i64, 164 out_u: *i64, out_v: *i64) -> i64 { 165 let w: i64 = flow.u.width 166 let h: i64 = flow.u.height 167 var sum_u: i64 = 0 168 var sum_v: i64 = 0 169 var n: i64 = 0 170 var y: i64 = 0 171 while y < h { 172 var x: i64 = 0 173 while x < w { 174 let uv: i64 = nx_image_s64_get(flow.u, x, y) 175 let vv: i64 = nx_image_s64_get(flow.v, x, y) 176 var au: i64 = uv 177 if au < 0 { au = -au } 178 var av: i64 = vv 179 if av < 0 { av = -av } 180 if au + av >= mag_threshold { 181 sum_u = sum_u + uv 182 sum_v = sum_v + vv 183 n = n + 1 184 } 185 x = x + 1 186 } 187 y = y + 1 188 } 189 if n == 0 { 190 out_u[0] = 0 191 out_v[0] = 0 192 return 0 193 } 194 out_u[0] = sum_u / n 195 out_v[0] = sum_v / n 196 return n 197}