|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| #![cfg(target_os = "android")]
|
|
|
| use std::ffi::{c_char, c_int, c_void};
|
| use std::sync::atomic::{AtomicBool, Ordering};
|
| use std::time::{Duration, Instant};
|
|
|
| use crate::source::FrameSource;
|
|
|
|
|
|
|
| const REOPEN_INTERVAL: Duration = Duration::from_secs(1);
|
|
|
|
|
|
|
| #[repr(C)]
|
| struct ACameraManager(#[allow(dead_code)] [u8; 0]);
|
| #[repr(C)]
|
| struct ACameraDevice(#[allow(dead_code)] [u8; 0]);
|
| #[repr(C)]
|
| struct ACameraMetadata(#[allow(dead_code)] [u8; 0]);
|
| #[repr(C)]
|
| struct ACameraCaptureSession(#[allow(dead_code)] [u8; 0]);
|
| #[repr(C)]
|
| struct ACaptureRequest(#[allow(dead_code)] [u8; 0]);
|
| #[repr(C)]
|
| struct ACameraOutputTarget(#[allow(dead_code)] [u8; 0]);
|
| #[repr(C)]
|
| struct ACaptureSessionOutput(#[allow(dead_code)] [u8; 0]);
|
| #[repr(C)]
|
| struct ACaptureSessionOutputContainer(#[allow(dead_code)] [u8; 0]);
|
| #[repr(C)]
|
| struct AImageReader(#[allow(dead_code)] [u8; 0]);
|
| #[repr(C)]
|
| struct AImage(#[allow(dead_code)] [u8; 0]);
|
| #[repr(C)]
|
| struct ANativeWindow(#[allow(dead_code)] [u8; 0]);
|
|
|
| #[repr(C)]
|
| struct ACameraIdList {
|
| num_cameras: c_int,
|
| camera_ids: *mut *const c_char,
|
| }
|
|
|
| #[repr(C)]
|
| struct ACameraMetadataConstEntry {
|
| tag: u32,
|
| kind: u8,
|
| count: u32,
|
| data: *const u8,
|
| }
|
|
|
| #[repr(C)]
|
| struct ACameraDeviceStateCallbacks {
|
| context: *mut c_void,
|
| on_disconnected: extern "C" fn(*mut c_void, *mut ACameraDevice),
|
| on_error: extern "C" fn(*mut c_void, *mut ACameraDevice, c_int),
|
| }
|
|
|
| #[repr(C)]
|
| struct ACameraCaptureSessionStateCallbacks {
|
| context: *mut c_void,
|
| on_closed: extern "C" fn(*mut c_void, *mut ACameraCaptureSession),
|
| on_ready: extern "C" fn(*mut c_void, *mut ACameraCaptureSession),
|
| on_active: extern "C" fn(*mut c_void, *mut ACameraCaptureSession),
|
| }
|
|
|
| const AIMAGE_FORMAT_YUV_420_888: c_int = 0x23;
|
| const TEMPLATE_PREVIEW: c_int = 1;
|
| const ACAMERA_OK: c_int = 0;
|
| const AMEDIA_OK: c_int = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| const ACAMERA_LENS_FACING: u32 = (8 << 16) + 5;
|
| const ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS: u32 = (13 << 16) + 10;
|
|
|
|
|
|
|
|
|
|
|
| const LENS_FACING_BACK: u8 = 1;
|
|
|
|
|
|
|
| fn camera_error(code: c_int) -> &'static str {
|
| match code {
|
| -10001 => "invalid parameter",
|
| -10002 => "camera disconnected",
|
| -10003 => "not enough memory",
|
| -10004 => "metadata not found",
|
| -10005 => "camera device error",
|
| -10006 => "camera service error",
|
| -10007 => "session closed",
|
| -10008 => "invalid operation",
|
| -10009 => "stream configure failed",
|
| -10010 => "camera in use",
|
| -10011 => "max cameras in use",
|
| -10012 => "camera disabled",
|
| -10013 => "permission denied",
|
| -10014 => "unsupported operation",
|
| _ => "unknown",
|
| }
|
| }
|
|
|
| #[link(name = "camera2ndk")]
|
| unsafe extern "C" {
|
| fn ACameraManager_create() -> *mut ACameraManager;
|
| fn ACameraManager_delete(m: *mut ACameraManager);
|
| fn ACameraManager_getCameraIdList(m: *mut ACameraManager, out: *mut *mut ACameraIdList) -> c_int;
|
| fn ACameraManager_deleteCameraIdList(l: *mut ACameraIdList);
|
| fn ACameraManager_getCameraCharacteristics(
|
| m: *mut ACameraManager,
|
| id: *const c_char,
|
| out: *mut *mut ACameraMetadata,
|
| ) -> c_int;
|
| fn ACameraManager_openCamera(
|
| m: *mut ACameraManager,
|
| id: *const c_char,
|
| cb: *mut ACameraDeviceStateCallbacks,
|
| out: *mut *mut ACameraDevice,
|
| ) -> c_int;
|
| fn ACameraMetadata_getConstEntry(
|
| md: *const ACameraMetadata,
|
| tag: u32,
|
| entry: *mut ACameraMetadataConstEntry,
|
| ) -> c_int;
|
| fn ACameraMetadata_free(md: *mut ACameraMetadata);
|
| fn ACameraDevice_close(d: *mut ACameraDevice) -> c_int;
|
| fn ACameraDevice_createCaptureRequest(
|
| d: *mut ACameraDevice,
|
| template: c_int,
|
| out: *mut *mut ACaptureRequest,
|
| ) -> c_int;
|
| fn ACameraDevice_createCaptureSession(
|
| d: *mut ACameraDevice,
|
| outputs: *const ACaptureSessionOutputContainer,
|
| cb: *const ACameraCaptureSessionStateCallbacks,
|
| session: *mut *mut ACameraCaptureSession,
|
| ) -> c_int;
|
| fn ACaptureSessionOutputContainer_create(
|
| out: *mut *mut ACaptureSessionOutputContainer,
|
| ) -> c_int;
|
| fn ACaptureSessionOutputContainer_add(
|
| c: *mut ACaptureSessionOutputContainer,
|
| o: *const ACaptureSessionOutput,
|
| ) -> c_int;
|
| fn ACaptureSessionOutputContainer_free(c: *mut ACaptureSessionOutputContainer);
|
| fn ACaptureSessionOutput_create(
|
| w: *mut ANativeWindow,
|
| out: *mut *mut ACaptureSessionOutput,
|
| ) -> c_int;
|
| fn ACaptureSessionOutput_free(o: *mut ACaptureSessionOutput);
|
| fn ACameraOutputTarget_create(w: *mut ANativeWindow, out: *mut *mut ACameraOutputTarget)
|
| -> c_int;
|
| fn ACameraOutputTarget_free(t: *mut ACameraOutputTarget);
|
| fn ACaptureRequest_addTarget(r: *mut ACaptureRequest, t: *const ACameraOutputTarget) -> c_int;
|
| fn ACaptureRequest_free(r: *mut ACaptureRequest);
|
| fn ACameraCaptureSession_setRepeatingRequest(
|
| s: *mut ACameraCaptureSession,
|
| cb: *mut c_void,
|
| num: c_int,
|
| requests: *mut *mut ACaptureRequest,
|
| seq: *mut c_int,
|
| ) -> c_int;
|
| fn ACameraCaptureSession_close(s: *mut ACameraCaptureSession);
|
| }
|
|
|
| #[link(name = "mediandk")]
|
| unsafe extern "C" {
|
| fn AImageReader_new(
|
| width: c_int,
|
| height: c_int,
|
| format: c_int,
|
| max_images: c_int,
|
| out: *mut *mut AImageReader,
|
| ) -> c_int;
|
| fn AImageReader_delete(r: *mut AImageReader);
|
| fn AImageReader_getWindow(r: *mut AImageReader, out: *mut *mut ANativeWindow) -> c_int;
|
| fn AImageReader_acquireLatestImage(r: *mut AImageReader, out: *mut *mut AImage) -> c_int;
|
| fn AImage_delete(i: *mut AImage);
|
| fn AImage_getWidth(i: *const AImage, out: *mut i32) -> c_int;
|
| fn AImage_getHeight(i: *const AImage, out: *mut i32) -> c_int;
|
| fn AImage_getPlaneRowStride(i: *const AImage, plane: c_int, out: *mut i32) -> c_int;
|
| fn AImage_getPlanePixelStride(i: *const AImage, plane: c_int, out: *mut i32) -> c_int;
|
| fn AImage_getPlaneData(
|
| i: *const AImage,
|
| plane: c_int,
|
| data: *mut *mut u8,
|
| len: *mut c_int,
|
| ) -> c_int;
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| static DEVICE_ERROR: AtomicBool = AtomicBool::new(false);
|
|
|
| extern "C" fn on_disconnected(_ctx: *mut c_void, _d: *mut ACameraDevice) {
|
| log::warn!("camera disconnected");
|
| DEVICE_ERROR.store(true, Ordering::Release);
|
| }
|
|
|
| extern "C" fn on_error(_ctx: *mut c_void, _d: *mut ACameraDevice, err: c_int) {
|
|
|
|
|
|
|
| log::warn!(
|
| "camera device error {err}{}",
|
| if err == 3 { " (disabled — headset removed?)" } else { "" }
|
| );
|
| DEVICE_ERROR.store(true, Ordering::Release);
|
| }
|
| extern "C" fn on_session(_ctx: *mut c_void, _s: *mut ACameraCaptureSession) {}
|
|
|
|
|
| #[derive(Clone, Copy, Debug)]
|
| pub enum Eye {
|
| Left,
|
| Right,
|
| }
|
|
|
| impl Eye {
|
| fn position(self) -> u8 {
|
| match self {
|
| Eye::Left => 0,
|
| Eye::Right => 1,
|
| }
|
| }
|
| }
|
|
|
|
|
| pub struct PassthroughCamera {
|
| manager: *mut ACameraManager,
|
| device: *mut ACameraDevice,
|
| session: *mut ACameraCaptureSession,
|
| request: *mut ACaptureRequest,
|
| target: *mut ACameraOutputTarget,
|
| output: *mut ACaptureSessionOutput,
|
| container: *mut ACaptureSessionOutputContainer,
|
| reader: *mut AImageReader,
|
| size: usize,
|
| buf: Vec<u8>,
|
| have_frame: bool,
|
|
|
| eye: Eye,
|
| capture: (i32, i32),
|
| last_reopen: Instant,
|
| }
|
|
|
|
|
|
|
| unsafe impl Send for PassthroughCamera {}
|
|
|
| impl PassthroughCamera {
|
|
|
|
|
|
|
|
|
| pub fn new(size: usize, eye: Eye, capture: (i32, i32)) -> Result<Self, String> {
|
| unsafe {
|
| let manager = ACameraManager_create();
|
| if manager.is_null() {
|
| return Err("ACameraManager_create returned null".into());
|
| }
|
|
|
| let mut list: *mut ACameraIdList = std::ptr::null_mut();
|
| if ACameraManager_getCameraIdList(manager, &mut list) != ACAMERA_OK || list.is_null() {
|
| ACameraManager_delete(manager);
|
| return Err("could not enumerate cameras — is HEADSET_CAMERA granted?".into());
|
| }
|
| let ids = std::slice::from_raw_parts((*list).camera_ids, (*list).num_cameras as usize);
|
| log::info!("{} cameras visible", ids.len());
|
|
|
| let candidates = Self::candidates(manager, ids, eye, capture);
|
| if candidates.is_empty() {
|
| ACameraManager_deleteCameraIdList(list);
|
| ACameraManager_delete(manager);
|
| return Err("no outward-facing camera offers the requested format".into());
|
| }
|
|
|
|
|
| let mut reader: *mut AImageReader = std::ptr::null_mut();
|
| if AImageReader_new(
|
| capture.0,
|
| capture.1,
|
| AIMAGE_FORMAT_YUV_420_888,
|
|
|
|
|
| 4,
|
| &mut reader,
|
| ) != AMEDIA_OK
|
| {
|
| ACameraManager_deleteCameraIdList(list);
|
| ACameraManager_delete(manager);
|
| return Err("AImageReader_new failed".into());
|
| }
|
| let mut window: *mut ANativeWindow = std::ptr::null_mut();
|
| AImageReader_getWindow(reader, &mut window);
|
|
|
|
|
| let mut callbacks = ACameraDeviceStateCallbacks {
|
| context: std::ptr::null_mut(),
|
| on_disconnected,
|
| on_error,
|
| };
|
|
|
|
|
|
|
|
|
| let mut device: *mut ACameraDevice = std::ptr::null_mut();
|
| let mut last = ACAMERA_OK;
|
| for &id in &candidates {
|
| let status = ACameraManager_openCamera(manager, id, &mut callbacks, &mut device);
|
| if status == ACAMERA_OK && !device.is_null() {
|
| log::info!("opened camera {:?}", std::ffi::CStr::from_ptr(id));
|
| break;
|
| }
|
| log::warn!(
|
| "camera {:?} would not open: {} ({status})",
|
| std::ffi::CStr::from_ptr(id),
|
| camera_error(status)
|
| );
|
| last = status;
|
| device = std::ptr::null_mut();
|
| }
|
| ACameraManager_deleteCameraIdList(list);
|
| if device.is_null() {
|
| AImageReader_delete(reader);
|
| ACameraManager_delete(manager);
|
| return Err(format!(
|
| "no camera would open; last error {} ({last}){}",
|
| camera_error(last),
|
| if last == -10013 {
|
| " — HEADSET_CAMERA is a runtime permission and must be \
|
| granted, and passthrough must be enabled"
|
| } else {
|
| ""
|
| }
|
| ));
|
| }
|
|
|
|
|
| let mut container: *mut ACaptureSessionOutputContainer = std::ptr::null_mut();
|
| ACaptureSessionOutputContainer_create(&mut container);
|
| let mut output: *mut ACaptureSessionOutput = std::ptr::null_mut();
|
| ACaptureSessionOutput_create(window, &mut output);
|
| ACaptureSessionOutputContainer_add(container, output);
|
|
|
| let session_cb = ACameraCaptureSessionStateCallbacks {
|
| context: std::ptr::null_mut(),
|
| on_closed: on_session,
|
| on_ready: on_session,
|
| on_active: on_session,
|
| };
|
| let mut session: *mut ACameraCaptureSession = std::ptr::null_mut();
|
| if ACameraDevice_createCaptureSession(device, container, &session_cb, &mut session)
|
| != ACAMERA_OK
|
| {
|
| return Err("createCaptureSession failed".into());
|
| }
|
|
|
| let mut request: *mut ACaptureRequest = std::ptr::null_mut();
|
| ACameraDevice_createCaptureRequest(device, TEMPLATE_PREVIEW, &mut request);
|
| let mut target: *mut ACameraOutputTarget = std::ptr::null_mut();
|
| ACameraOutputTarget_create(window, &mut target);
|
| ACaptureRequest_addTarget(request, target);
|
|
|
| let mut requests = [request];
|
| if ACameraCaptureSession_setRepeatingRequest(
|
| session,
|
| std::ptr::null_mut(),
|
| 1,
|
| requests.as_mut_ptr(),
|
| std::ptr::null_mut(),
|
| ) != ACAMERA_OK
|
| {
|
| return Err("setRepeatingRequest failed".into());
|
| }
|
|
|
| log::info!(
|
| "passthrough camera streaming {}x{} -> {size}x{size}",
|
| capture.0,
|
| capture.1
|
| );
|
| Ok(Self {
|
| manager,
|
| device,
|
| session,
|
| request,
|
| target,
|
| output,
|
| container,
|
| reader,
|
| size,
|
|
|
|
|
| buf: vec![128; size * size * 3],
|
| have_frame: false,
|
| eye,
|
| capture,
|
| last_reopen: Instant::now(),
|
| })
|
| }
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| unsafe fn candidates(
|
| manager: *mut ACameraManager,
|
| ids: &[*const c_char],
|
| eye: Eye,
|
| capture: (i32, i32),
|
| ) -> Vec<*const c_char> {
|
| unsafe {
|
| let mut matching = Vec::new();
|
| for &id in ids {
|
| let mut md: *mut ACameraMetadata = std::ptr::null_mut();
|
| if ACameraManager_getCameraCharacteristics(manager, id, &mut md) != ACAMERA_OK {
|
| continue;
|
| }
|
|
|
| let mut entry = ACameraMetadataConstEntry {
|
| tag: 0,
|
| kind: 0,
|
| count: 0,
|
| data: std::ptr::null(),
|
| };
|
| let facing = (ACameraMetadata_getConstEntry(md, ACAMERA_LENS_FACING, &mut entry)
|
| == ACAMERA_OK
|
| && entry.count > 0)
|
| .then(|| *entry.data);
|
|
|
| let mut supported = false;
|
| let mut entry = ACameraMetadataConstEntry {
|
| tag: 0,
|
| kind: 0,
|
| count: 0,
|
| data: std::ptr::null(),
|
| };
|
| if ACameraMetadata_getConstEntry(
|
| md,
|
| ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS,
|
| &mut entry,
|
| ) == ACAMERA_OK
|
| && !entry.data.is_null()
|
| {
|
|
|
| let values =
|
| std::slice::from_raw_parts(entry.data as *const i32, entry.count as usize);
|
| supported = values.chunks_exact(4).any(|c| {
|
| c[0] == AIMAGE_FORMAT_YUV_420_888
|
| && c[1] == capture.0
|
| && c[2] == capture.1
|
| && c[3] == 0
|
| });
|
| }
|
| ACameraMetadata_free(md);
|
|
|
| log::info!(
|
| "camera {:?}: facing={facing:?} offers {}x{} YUV420: {supported}",
|
| std::ffi::CStr::from_ptr(id),
|
| capture.0,
|
| capture.1
|
| );
|
| if supported {
|
| matching.push((id, facing));
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| let (mut outward, inward): (Vec<_>, Vec<_>) = matching
|
| .into_iter()
|
| .partition(|&(_, facing)| facing == Some(LENS_FACING_BACK));
|
|
|
| let wanted = eye.position() as usize;
|
| if wanted < outward.len() {
|
| outward.swap(0, wanted);
|
| }
|
| let ordered: Vec<*const c_char> = outward
|
| .into_iter()
|
| .chain(inward)
|
| .map(|(id, _)| id)
|
| .collect();
|
|
|
| if ordered.is_empty() {
|
| log::warn!(
|
| "no camera advertises {}x{} YUV420; trying all of them",
|
| capture.0,
|
| capture.1
|
| );
|
| return ids.to_vec();
|
| }
|
| ordered
|
| }
|
| }
|
|
|
|
|
| fn pump(&mut self) -> bool {
|
| unsafe {
|
| let mut image: *mut AImage = std::ptr::null_mut();
|
| if AImageReader_acquireLatestImage(self.reader, &mut image) != AMEDIA_OK
|
| || image.is_null()
|
| {
|
| return false;
|
| }
|
|
|
| let (mut w, mut h) = (0i32, 0i32);
|
| AImage_getWidth(image, &mut w);
|
| AImage_getHeight(image, &mut h);
|
|
|
| let plane = |index: c_int| -> Option<(*mut u8, usize, usize)> {
|
| let (mut data, mut len) = (std::ptr::null_mut(), 0);
|
| if AImage_getPlaneData(image, index, &mut data, &mut len) != AMEDIA_OK {
|
| return None;
|
| }
|
| let (mut row, mut pixel) = (0i32, 0i32);
|
| AImage_getPlaneRowStride(image, index, &mut row);
|
| AImage_getPlanePixelStride(image, index, &mut pixel);
|
| Some((data, row as usize, pixel.max(1) as usize))
|
| };
|
|
|
| let (Some((y_data, y_row, _)), Some((u_data, u_row, u_pix)), Some((v_data, v_row, v_pix))) =
|
| (plane(0), plane(1), plane(2))
|
| else {
|
| AImage_delete(image);
|
| return false;
|
| };
|
|
|
| yuv420_to_square_rgb(
|
| YuvPlanes {
|
| y: y_data,
|
| y_row,
|
| u: u_data,
|
| u_row,
|
| u_pix,
|
| v: v_data,
|
| v_row,
|
| v_pix,
|
| },
|
| w as usize,
|
| h as usize,
|
| self.size,
|
| &mut self.buf,
|
| );
|
| AImage_delete(image);
|
| self.have_frame = true;
|
| true
|
| }
|
| }
|
| }
|
|
|
| impl Drop for PassthroughCamera {
|
| fn drop(&mut self) {
|
| unsafe {
|
| if !self.session.is_null() {
|
| ACameraCaptureSession_close(self.session);
|
| }
|
| if !self.request.is_null() {
|
| ACaptureRequest_free(self.request);
|
| }
|
| if !self.target.is_null() {
|
| ACameraOutputTarget_free(self.target);
|
| }
|
| if !self.container.is_null() {
|
| ACaptureSessionOutputContainer_free(self.container);
|
| }
|
| if !self.output.is_null() {
|
| ACaptureSessionOutput_free(self.output);
|
| }
|
| if !self.device.is_null() {
|
| ACameraDevice_close(self.device);
|
| }
|
| if !self.reader.is_null() {
|
| AImageReader_delete(self.reader);
|
| }
|
| if !self.manager.is_null() {
|
| ACameraManager_delete(self.manager);
|
| }
|
| }
|
| }
|
| }
|
|
|
| impl FrameSource for PassthroughCamera {
|
| fn size(&self) -> usize {
|
| self.size
|
| }
|
|
|
| fn next_frame(&mut self) -> Option<&[u8]> {
|
|
|
|
|
|
|
| if DEVICE_ERROR.load(Ordering::Acquire)
|
| && self.last_reopen.elapsed() >= REOPEN_INTERVAL
|
| {
|
| self.last_reopen = Instant::now();
|
| match Self::new(self.size, self.eye, self.capture) {
|
| Ok(mut fresh) => {
|
|
|
|
|
| std::mem::swap(&mut fresh.buf, &mut self.buf);
|
| fresh.have_frame = self.have_frame;
|
| DEVICE_ERROR.store(false, Ordering::Release);
|
|
|
| *self = fresh;
|
| log::info!("camera reopened");
|
| }
|
| Err(e) => log::warn!("camera reopen failed: {e}"),
|
| }
|
| }
|
|
|
|
|
|
|
| self.pump();
|
| self.have_frame.then_some(&self.buf[..])
|
| }
|
| }
|
|
|
| struct YuvPlanes {
|
| y: *mut u8,
|
| y_row: usize,
|
| u: *mut u8,
|
| u_row: usize,
|
| u_pix: usize,
|
| v: *mut u8,
|
| v_row: usize,
|
| v_pix: usize,
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| unsafe fn yuv420_to_square_rgb(
|
| p: YuvPlanes,
|
| width: usize,
|
| height: usize,
|
| size: usize,
|
| out: &mut [u8],
|
| ) {
|
| for oy in 0..size {
|
| let sy0 = oy * height / size;
|
| let sy1 = ((oy + 1) * height / size).max(sy0 + 1).min(height);
|
| for ox in 0..size {
|
| let sx0 = ox * width / size;
|
| let sx1 = ((ox + 1) * width / size).max(sx0 + 1).min(width);
|
|
|
| let mut acc = 0u32;
|
| let mut n = 0u32;
|
| for sy in sy0..sy1 {
|
| let row = unsafe { p.y.add(sy * p.y_row) };
|
| for sx in sx0..sx1 {
|
| acc += unsafe { *row.add(sx) } as u32;
|
| n += 1;
|
| }
|
| }
|
| let luma = (acc / n.max(1)) as f32;
|
|
|
|
|
| let cx = ((sx0 + sx1) / 2) / 2;
|
| let cy = ((sy0 + sy1) / 2) / 2;
|
| let u = unsafe { *p.u.add(cy * p.u_row + cx * p.u_pix) } as f32 - 128.0;
|
| let v = unsafe { *p.v.add(cy * p.v_row + cx * p.v_pix) } as f32 - 128.0;
|
|
|
|
|
| let r = luma + 1.402 * v;
|
| let g = luma - 0.344_136 * u - 0.714_136 * v;
|
| let b = luma + 1.772 * u;
|
|
|
| let o = (oy * size + ox) * 3;
|
| out[o] = r.clamp(0.0, 255.0) as u8;
|
| out[o + 1] = g.clamp(0.0, 255.0) as u8;
|
| out[o + 2] = b.clamp(0.0, 255.0) as u8;
|
| }
|
| }
|
| }
|
|
|