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