-
Notifications
You must be signed in to change notification settings - Fork 74
Implement YUV to RGB #291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Implement YUV to RGB #291
Changes from all commits
f4b2f35
01095a3
c50c996
a544a2a
bfb585d
5183bd7
6fa4622
0ecc958
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
mod gray; | ||
mod hsv; | ||
mod yuv; | ||
|
||
pub use gray::{bgr_from_rgb, gray_from_rgb, gray_from_rgb_u8, rgb_from_gray}; | ||
pub use hsv::hsv_from_rgb; | ||
pub use yuv::{rgb_from_yuv, yuv_from_rgb}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,293 @@ | ||
use crate::parallel; | ||
use kornia_image::{Image, ImageError}; | ||
|
||
/// Convert an RGB image to an YUV image. | ||
/// | ||
/// The input image is assumed to have 3 channels in the order R, G, B. in the range [0, 255]. | ||
/// | ||
/// # Arguments | ||
/// | ||
/// * `src` - The input RGB image assumed to have 3 channels. | ||
/// * `dst` - The output YUV image. | ||
/// | ||
/// # Returns | ||
/// | ||
/// The YUV image with the following channels: | ||
/// | ||
/// * Y: The luminance channel in the range [0, 1]. | ||
/// * U: The chrominance-blue channel in the range [-0.436, +0.436]. | ||
/// * V: The chrominance-red channel in the range [-0.615, +0.615]. | ||
/// | ||
/// Precondition: the input image must have 3 channels. | ||
/// Precondition: the output image must have 3 channels. | ||
/// Precondition: the input and output images must have the same size. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// use kornia_image::{Image, ImageSize}; | ||
/// use kornia_imgproc::color::yuv_from_rgb; | ||
/// | ||
/// let image = Image::<f32, 3>::new( | ||
/// ImageSize { | ||
/// width: 4, | ||
/// height: 5, | ||
/// }, | ||
/// vec![0f32; 4 * 5 * 3], | ||
/// ) | ||
/// .unwrap(); | ||
/// | ||
/// let mut yuv = Image::from_size_val(image.size(), 0.0).unwrap(); | ||
/// | ||
/// yuv_from_rgb(&image, &mut yuv).unwrap(); | ||
/// | ||
/// assert_eq!(yuv.num_channels(), 3); | ||
/// assert_eq!(yuv.size().width, 4); | ||
/// assert_eq!(yuv.size().height, 5); | ||
/// ``` | ||
pub fn yuv_from_rgb(src: &Image<f32, 3>, dst: &mut Image<f32, 3>) -> Result<(), ImageError> { | ||
if src.size() != dst.size() { | ||
return Err(ImageError::InvalidImageSize( | ||
src.cols(), | ||
src.rows(), | ||
dst.cols(), | ||
dst.rows(), | ||
)); | ||
} | ||
|
||
// compute the YUV values | ||
parallel::par_iter_rows(src, dst, |src_pixel, dst_pixel| { | ||
// Normalize the input to the range [0, 1] | ||
let r = src_pixel[0] / 255.; | ||
let g = src_pixel[1] / 255.; | ||
let b = src_pixel[2] / 255.; | ||
|
||
let y = 0.299 * r + 0.587 * g + 0.114 * b; | ||
let u = -0.147 * r - 0.289 * g + 0.436 * b; | ||
let v = 0.615 * r - 0.515 * g - 0.100 * b; | ||
|
||
dst_pixel[0] = y; | ||
dst_pixel[1] = u; | ||
dst_pixel[2] = v; | ||
}); | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Convert a YUV image to an RGB image. | ||
/// | ||
/// The input image is assumed to have 3 channels in the order Y, U, V. Where Y is in range[0, 1]. | ||
/// U is in range [-0.436, +0.436] and V is in range [-0.615, +0.615]. | ||
/// | ||
/// # Arguments | ||
/// | ||
/// * `src` - The input YUV image. | ||
/// * `dst` - The output RGB image. | ||
/// | ||
/// # Returns | ||
/// | ||
/// The RGB image with the following channels: | ||
/// | ||
/// * R: The red channel in the range [0, 255]. | ||
/// * G: The green channel in the range [0, 255]. | ||
/// * B: The blue channel in the range [0, 255]. | ||
/// | ||
/// Precondition: the input image must have 3 channels. | ||
/// Precondition: the output image must have 3 channels. | ||
/// Precondition: the input and output images must have the same size. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// use kornia_image::{Image, ImageSize}; | ||
/// use kornia_imgproc::color::rgb_from_yuv; | ||
/// | ||
/// let image = Image::<f32, 3>::new( | ||
/// ImageSize { | ||
/// width: 4, | ||
/// height: 5, | ||
/// }, | ||
/// vec![0f32; 4 * 5 * 3], | ||
/// ) | ||
/// .unwrap(); | ||
/// | ||
/// let mut rgb = Image::from_size_val(image.size(), 0.0).unwrap(); | ||
/// | ||
/// rgb_from_yuv(&image, &mut rgb).unwrap(); | ||
/// | ||
/// assert_eq!(rgb.num_channels(), 3); | ||
/// assert_eq!(rgb.size().width, 4); | ||
/// assert_eq!(rgb.size().height, 5); | ||
/// ``` | ||
pub fn rgb_from_yuv(src: &Image<f32, 3>, dst: &mut Image<f32, 3>) -> Result<(), ImageError> { | ||
if src.size() != dst.size() { | ||
return Err(ImageError::InvalidImageSize( | ||
src.cols(), | ||
src.rows(), | ||
dst.cols(), | ||
dst.rows(), | ||
)); | ||
} | ||
|
||
// compute the RGB values | ||
parallel::par_iter_rows(src, dst, |src_pixel, dst_pixel| { | ||
// Transform the input to the range [0, 255] | ||
let y = src_pixel[0] * 255.; | ||
omar-abdelgawad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let u = src_pixel[1] * 255.; | ||
let v = src_pixel[2] * 255.; | ||
|
||
let r = y + 1.140 * v; | ||
let g = y - 0.396 * u - 0.581 * v; | ||
let b = y + 2.029 * u; | ||
|
||
dst_pixel[0] = r; | ||
dst_pixel[1] = g; | ||
dst_pixel[2] = b; | ||
}); | ||
|
||
Ok(()) | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use kornia_image::{Image, ImageError, ImageSize}; | ||
use num_traits::Pow; | ||
const RGB_TEST_DATA: [f32; 18] = [ | ||
0.0, 128.0, 255.0, 255.0, 128.0, 0.0, 128.0, 255.0, 0.0, 255.0, 0.0, 128.0, 0.0, 128.0, | ||
255.0, 255.0, 128.0, 0.0, | ||
]; | ||
// corresponding YUV values to the RGB_TEST_DATA | ||
const YUV_TEST_DATA: [f32; 18] = [ | ||
0.4087, 0.2909, -0.3585, 0.5937, -0.2921, 0.3565, 0.7371, -0.3628, -0.2063, 0.3562, 0.0719, | ||
0.5648, 0.4087, 0.2909, -0.3585, 0.5937, -0.2921, 0.3565, | ||
]; | ||
const RGB_FROM_YUV_DATA: [f32; 18] = [ | ||
0.002548, 127.956985, 254.7287, 255.02805, 128.0725, 0.262405, 127.98908, 255.16042, | ||
0.249588, 255.01837, -0.107399, 128.03171, 0.002548, 127.956985, 254.7287, 255.02805, | ||
128.0725, 0.262405, | ||
]; | ||
const WIDTH: usize = 2; | ||
const HEIGHT: usize = 3; | ||
omar-abdelgawad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#[test] | ||
fn yuv_from_rgb() -> Result<(), ImageError> { | ||
let image = Image::<f32, 3>::new( | ||
ImageSize { | ||
width: WIDTH, | ||
height: HEIGHT, | ||
}, | ||
RGB_TEST_DATA.to_vec(), | ||
)?; | ||
let expected = YUV_TEST_DATA; | ||
|
||
let mut yuv = Image::from_size_val(image.size(), 0.0)?; | ||
|
||
super::yuv_from_rgb(&image, &mut yuv)?; | ||
|
||
assert_eq!(yuv.num_channels(), 3); | ||
assert_eq!(yuv.size(), image.size()); | ||
|
||
for (a, b) in yuv.as_slice().iter().zip(expected.iter()) { | ||
assert!((a - b).pow(2) < 1e-6f32); | ||
} | ||
Ok(()) | ||
} | ||
omar-abdelgawad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#[test] | ||
fn rgb_from_yuv() -> Result<(), ImageError> { | ||
let image = Image::<f32, 3>::new( | ||
ImageSize { | ||
width: WIDTH, | ||
height: HEIGHT, | ||
}, | ||
YUV_TEST_DATA.to_vec(), | ||
)?; | ||
let expected = RGB_FROM_YUV_DATA; | ||
|
||
let mut rgb = Image::from_size_val(image.size(), 0.0)?; | ||
|
||
super::rgb_from_yuv(&image, &mut rgb)?; | ||
|
||
assert_eq!(rgb.num_channels(), 3); | ||
assert_eq!(rgb.size(), image.size()); | ||
|
||
for (a, b) in rgb.as_slice().iter().zip(expected.iter()) { | ||
assert!((a - b).pow(2) < 1e-6f32); | ||
} | ||
Ok(()) | ||
} | ||
omar-abdelgawad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#[test] | ||
fn yuv_inverse_relation_test() -> Result<(), ImageError> { | ||
let image = Image::<f32, 3>::new( | ||
ImageSize { | ||
width: 2, | ||
height: 3, | ||
}, | ||
RGB_TEST_DATA.to_vec(), | ||
)?; | ||
|
||
let mut yuv = Image::from_size_val(image.size(), 0.0)?; | ||
let mut rgb = Image::from_size_val(image.size(), 0.0)?; | ||
|
||
super::yuv_from_rgb(&image, &mut yuv)?; | ||
super::rgb_from_yuv(&yuv, &mut rgb)?; | ||
|
||
for (a, b) in rgb.as_slice().iter().zip(image.as_slice().iter()) { | ||
assert!((a - b).pow(2) < 1e-1f32); | ||
} | ||
Ok(()) | ||
} | ||
|
||
#[test] | ||
fn yuv_utils_rs_comparison() -> Result<(), ImageError> { | ||
use yuvutils_rs::{ | ||
rgb_to_yuv444, YuvChromaSubsampling, YuvConversionMode, YuvPlanarImageMut, YuvRange, | ||
YuvStandardMatrix, | ||
}; | ||
let rgb_image = Image::<f32, 3>::new( | ||
ImageSize { | ||
width: WIDTH, | ||
height: HEIGHT, | ||
}, | ||
RGB_TEST_DATA.to_vec(), | ||
)?; | ||
let mut yuv = Image::from_size_val(rgb_image.size(), 0.0)?; | ||
super::yuv_from_rgb(&rgb_image, &mut yuv)?; | ||
super::parallel::par_iter_rows(&yuv.clone(), &mut yuv, |src_p, dst_p| { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why this ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is because of the output range I was asking about earlier:
These ranges are the same as in kornia and since the yuv_utils crate uses u8, I have to transform the range back to [0,255] to compare them. |
||
dst_p[0] = src_p[0] * 255.0; | ||
dst_p[1] = (src_p[1] + 0.436) * ((255.0) / (0.436 * 2.0)); | ||
dst_p[2] = (src_p[2] + 0.615) * ((255.0) / (0.615 * 2.0)); | ||
}); | ||
// Create an RGB buffer for yuv | ||
let rgb_data = RGB_TEST_DATA.iter().map(|x| *x as u8).collect::<Vec<u8>>(); | ||
let mut planar_image_444 = YuvPlanarImageMut::<u8>::alloc( | ||
WIDTH as u32, | ||
HEIGHT as u32, | ||
YuvChromaSubsampling::Yuv444, | ||
); | ||
rgb_to_yuv444( | ||
&mut planar_image_444, | ||
&rgb_data.as_slice(), | ||
WIDTH as u32 * 3, | ||
YuvRange::Full, | ||
YuvStandardMatrix::Bt601, | ||
YuvConversionMode::Balanced, | ||
) | ||
.unwrap(); | ||
// construct valid representation with y,u,v planes | ||
let yuv_utils_rs_data = planar_image_444 | ||
.y_plane | ||
.borrow() | ||
.iter() | ||
.zip(planar_image_444.u_plane.borrow().iter()) | ||
.zip(planar_image_444.v_plane.borrow().iter()) | ||
.flat_map(|((y, u), v)| vec![*y, *u, *v]) | ||
.collect::<Vec<u8>>(); | ||
for (&a, &b) in yuv.as_slice().iter().zip(yuv_utils_rs_data.iter()) { | ||
let b = b as f32; | ||
assert!((a - b).pow(2) <= 4e-1f32); | ||
} | ||
Ok(()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@andrew-shc feel free to add comments. Especially on the input types / conversion to match other open discussion in this topic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please point out the discussion link so that I may be able to add the comments myself instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, here #296