| Clear the clippy backlog the new CI enforces 4956d1e nandi 13d ago | 1 | // 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 ago | 15 | //! 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). |
| 26 | pub 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 | |
| 52 | fn 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 | |
| 64 | fn 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. |
| 97 | fn 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. |
| 129 | fn 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 | |
| 165 | fn 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. |
| 194 | pub 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)] |
| 209 | mod 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 | } |