| Let the media plane cross to the phone, camera and all fd0e21a nandi 18d ago | 1 | //! 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). |
| 12 | pub 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 | |
| 38 | fn 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 | |
| 50 | fn 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. |
| 83 | fn 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. |
| 115 | fn 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 | |
| 151 | fn 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. |
| 180 | pub 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)] |
| 195 | mod 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 | } |