nandi/jolt-nativepublic Fork 0
c41903b106d04e5cf80828c49e2d7d456a54739d
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

nv12_orient.rs · 296 lines · 9.0 KBRust Blame HistoryRaw
Let the media plane cross to the phone, camera and all fd0e21a nandi 18d ago1//! Rotate NV12 frames so Camera2 sensor buffers appear upright.
2//!
3//! Phone sensors are usually mounted at 90°/270°. ImageReader delivers
4//! buffers in sensor coordinates; without a CW rotate by
5//! [`CameraCharacteristics.SENSOR_ORIENTATION`] ± display rotation, portrait
6//! video looks sideways on the wire and in local preview.
7
8/// Orient an NV12 frame by rotating `rotation_cw` degrees clockwise (0/90/180/270).
9///
10/// Stride padding is stripped. For 90°/270°, width and height are swapped.
11/// Odd dimensions are rejected (YUV 4:2:0 requires even chroma grid).
12pub fn orient_nv12(
13 y_data: &[u8],
14 uv_data: &[u8],
15 width: u32,
16 height: u32,
17 y_stride: u32,
18 uv_stride: u32,
19 rotation_cw: u32,
20) -> Option<(Vec<u8>, Vec<u8>, u32, u32)> {
21 if width == 0 || height == 0 || !width.is_multiple_of(2) || !height.is_multiple_of(2) {
22 return None;
23 }
24 if y_stride < width || uv_stride < width {
25 return None;
26 }
27 let (y, uv) = pack_nv12(y_data, uv_data, width, height, y_stride, uv_stride)?;
28 let rot = normalize_rotation(rotation_cw);
29 match rot {
30 0 => Some((y, uv, width, height)),
31 90 => Some(rotate_nv12_90_cw(&y, &uv, width, height)),
32 180 => Some(rotate_nv12_180(&y, &uv, width, height)),
33 270 => Some(rotate_nv12_270_cw(&y, &uv, width, height)),
34 _ => Some((y, uv, width, height)),
35 }
36}
37
38fn normalize_rotation(degrees: u32) -> u32 {
39 let d = degrees % 360;
40 // Snap near-miss values from noisy sensors to the nearest cardinal.
41 match d {
42 0..=44 | 316..=359 => 0,
43 45..=134 => 90,
44 135..=224 => 180,
45 225..=315 => 270,
46 _ => 0,
47 }
48}
49
50fn pack_nv12(
51 y_data: &[u8],
52 uv_data: &[u8],
53 width: u32,
54 height: u32,
55 y_stride: u32,
56 uv_stride: u32,
57) -> Option<(Vec<u8>, Vec<u8>)> {
58 let w = width as usize;
59 let h = height as usize;
60 let ys = y_stride as usize;
61 let uvs = uv_stride as usize;
62 let y_need = ys.checked_mul(h)?;
63 let uv_need = uvs.checked_mul(h / 2)?;
64 if y_data.len() < y_need || uv_data.len() < uv_need {
65 return None;
66 }
67 let mut y_out = vec![0u8; w * h];
68 for row in 0..h {
69 let src = row * ys;
70 let dst = row * w;
71 y_out[dst..dst + w].copy_from_slice(&y_data[src..src + w]);
72 }
73 let mut uv_out = vec![0u8; w * (h / 2)];
74 for row in 0..(h / 2) {
75 let src = row * uvs;
76 let dst = row * w;
77 uv_out[dst..dst + w].copy_from_slice(&uv_data[src..src + w]);
78 }
79 Some((y_out, uv_out))
80}
81
82/// 90° CW: dst(dx, dy) = src(dy, H-1-dx); output is H×W.
83fn rotate_nv12_90_cw(y: &[u8], uv: &[u8], width: u32, height: u32) -> (Vec<u8>, Vec<u8>, u32, u32) {
84 let w = width as usize;
85 let h = height as usize;
86 let out_w = h;
87 let out_h = w;
88 let mut y_out = vec![0u8; out_w * out_h];
89 for dy in 0..out_h {
90 for dx in 0..out_w {
91 let sx = dy;
92 let sy = h - 1 - dx;
93 y_out[dy * out_w + dx] = y[sy * w + sx];
94 }
95 }
96 let cw = w / 2;
97 let ch = h / 2;
98 let out_cw = ch;
99 let out_ch = cw;
100 let mut uv_out = vec![0u8; out_w * (out_h / 2)];
101 for dy in 0..out_ch {
102 for dx in 0..out_cw {
103 let sx = dy;
104 let sy = ch - 1 - dx;
105 let src = (sy * cw + sx) * 2;
106 let dst = (dy * out_cw + dx) * 2;
107 uv_out[dst] = uv[src];
108 uv_out[dst + 1] = uv[src + 1];
109 }
110 }
111 (y_out, uv_out, out_w as u32, out_h as u32)
112}
113
114/// 270° CW (= 90° CCW): dst(dx, dy) = src(W-1-dy, dx); output is H×W.
115fn rotate_nv12_270_cw(
116 y: &[u8],
117 uv: &[u8],
118 width: u32,
119 height: u32,
120) -> (Vec<u8>, Vec<u8>, u32, u32) {
121 let w = width as usize;
122 let h = height as usize;
123 let out_w = h;
124 let out_h = w;
125 let mut y_out = vec![0u8; out_w * out_h];
126 for dy in 0..out_h {
127 for dx in 0..out_w {
128 let sx = w - 1 - dy;
129 let sy = dx;
130 y_out[dy * out_w + dx] = y[sy * w + sx];
131 }
132 }
133 let cw = w / 2;
134 let ch = h / 2;
135 let out_cw = ch;
136 let out_ch = cw;
137 let mut uv_out = vec![0u8; out_w * (out_h / 2)];
138 for dy in 0..out_ch {
139 for dx in 0..out_cw {
140 let sx = cw - 1 - dy;
141 let sy = dx;
142 let src = (sy * cw + sx) * 2;
143 let dst = (dy * out_cw + dx) * 2;
144 uv_out[dst] = uv[src];
145 uv_out[dst + 1] = uv[src + 1];
146 }
147 }
148 (y_out, uv_out, out_w as u32, out_h as u32)
149}
150
151fn rotate_nv12_180(y: &[u8], uv: &[u8], width: u32, height: u32) -> (Vec<u8>, Vec<u8>, u32, u32) {
152 let w = width as usize;
153 let h = height as usize;
154 let mut y_out = vec![0u8; w * h];
155 for dy in 0..h {
156 for dx in 0..w {
157 let sx = w - 1 - dx;
158 let sy = h - 1 - dy;
159 y_out[dy * w + dx] = y[sy * w + sx];
160 }
161 }
162 let cw = w / 2;
163 let ch = h / 2;
164 let mut uv_out = vec![0u8; w * (h / 2)];
165 for dy in 0..ch {
166 for dx in 0..cw {
167 let sx = cw - 1 - dx;
168 let sy = ch - 1 - dy;
169 let src = (sy * cw + sx) * 2;
170 let dst = (dy * cw + dx) * 2;
171 uv_out[dst] = uv[src];
172 uv_out[dst + 1] = uv[src + 1];
173 }
174 }
175 (y_out, uv_out, width, height)
176}
177
178/// Camera2 JPEG / buffer orientation: degrees CW to apply so the frame is
179/// upright for the current display rotation.
180pub fn camera2_rotation_degrees(
181 sensor_orientation: u32,
182 display_degrees: u32,
183 front_facing: bool,
184) -> u32 {
185 let sensor = sensor_orientation % 360;
186 let display = display_degrees % 360;
187 if front_facing {
188 (sensor + display) % 360
189 } else {
190 (sensor + 360 - display) % 360
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 fn solid_nv12(w: u32, h: u32, y: u8, u: u8, v: u8) -> (Vec<u8>, Vec<u8>) {
199 let y_plane = vec![y; (w * h) as usize];
200 let mut uv = vec![0u8; (w * (h / 2)) as usize];
201 for i in 0..(uv.len() / 2) {
202 uv[i * 2] = u;
203 uv[i * 2 + 1] = v;
204 }
205 (y_plane, uv)
206 }
207
208 /// Marker at (sx,sy) on a black Y plane — used to verify rotate mapping.
209 fn marker_nv12(w: u32, h: u32, sx: u32, sy: u32) -> (Vec<u8>, Vec<u8>) {
210 let (mut y, uv) = solid_nv12(w, h, 0, 128, 128);
211 y[(sy * w + sx) as usize] = 255;
212 (y, uv)
213 }
214
215 #[test]
216 fn identity_keeps_dims_and_marker() {
217 let (y, uv) = marker_nv12(4, 4, 1, 0);
218 let (oy, _ouv, ow, oh) = orient_nv12(&y, &uv, 4, 4, 4, 4, 0).unwrap();
219 assert_eq!((ow, oh), (4, 4));
220 assert_eq!(oy[1], 255);
221 }
222
223 #[test]
224 fn rotate_90_cw_moves_top_left_edge_marker() {
225 // Marker at top row, x=1 → after 90° CW sits at right column, y=1.
226 // dst(dx,dy)=src(dy,H-1-dx) ⇒ src(1,0) → dx=H-1-0=3, dy=1.
227 let (y, uv) = marker_nv12(4, 4, 1, 0);
228 let (oy, _, ow, oh) = orient_nv12(&y, &uv, 4, 4, 4, 4, 90).unwrap();
229 assert_eq!((ow, oh), (4, 4));
230 assert_eq!(oy[1 * 4 + 3], 255);
231 }
232
233 #[test]
234 fn rotate_90_swaps_rect_dims() {
235 let (y, uv) = solid_nv12(8, 4, 16, 80, 160);
236 let (oy, ouv, ow, oh) = orient_nv12(&y, &uv, 8, 4, 8, 8, 90).unwrap();
237 assert_eq!((ow, oh), (4, 8));
238 assert_eq!(oy.len(), 4 * 8);
239 assert_eq!(ouv.len(), 4 * 4);
240 }
241
242 #[test]
243 fn rotate_180_moves_marker_to_opposite_corner() {
244 let (y, uv) = marker_nv12(4, 4, 0, 0);
245 let (oy, _, ow, oh) = orient_nv12(&y, &uv, 4, 4, 4, 4, 180).unwrap();
246 assert_eq!((ow, oh), (4, 4));
247 assert_eq!(oy[3 * 4 + 3], 255);
248 }
249
250 #[test]
251 fn rotate_270_cw_moves_marker() {
252 // src(1,0) → 270 CW: sx=W-1-dy, sy=dx ⇒ dy=W-1-1=2, dx=0 → (0,2)
253 let (y, uv) = marker_nv12(4, 4, 1, 0);
254 let (oy, _, ow, oh) = orient_nv12(&y, &uv, 4, 4, 4, 4, 270).unwrap();
255 assert_eq!((ow, oh), (4, 4));
256 assert_eq!(oy[2 * 4 + 0], 255);
257 }
258
259 #[test]
260 fn strips_y_stride_padding() {
261 let w = 4u32;
262 let h = 4u32;
263 let y_stride = 8u32;
264 let mut y = vec![0u8; (y_stride * h) as usize];
265 // Put marker at (1,0) in logical coords (byte 1 of row 0).
266 y[1] = 255;
267 let uv = vec![128u8; (w * (h / 2)) as usize];
268 let (oy, _, ow, oh) = orient_nv12(&y, &uv, w, h, y_stride, w, 0).unwrap();
269 assert_eq!((ow, oh), (4, 4));
270 assert_eq!(oy[1], 255);
271 assert_eq!(oy.len(), 16);
272 }
273
274 #[test]
275 fn camera2_back_portrait_sensor_90() {
276 // Typical back camera, phone held in natural portrait (display 0°).
277 assert_eq!(camera2_rotation_degrees(90, 0, false), 90);
278 }
279
280 #[test]
281 fn camera2_front_portrait_sensor_270() {
282 assert_eq!(camera2_rotation_degrees(270, 0, true), 270);
283 }
284
285 #[test]
286 fn camera2_back_landscape_display_90() {
287 // Rotated to landscape: sensor 90 − display 90 = 0 (already upright).
288 assert_eq!(camera2_rotation_degrees(90, 90, false), 0);
289 }
290
291 #[test]
292 fn rejects_odd_dimensions() {
293 let (y, uv) = solid_nv12(4, 4, 0, 128, 128);
294 assert!(orient_nv12(&y, &uv, 3, 4, 3, 3, 0).is_none());
295 }
296}