diff --git a/video/hsv/src/hsvdetector/imp.rs b/video/hsv/src/hsvdetector/imp.rs index a8e2813d..896e8c73 100644 --- a/video/hsv/src/hsvdetector/imp.rs +++ b/video/hsv/src/hsvdetector/imp.rs @@ -82,6 +82,93 @@ impl ObjectSubclass for HsvDetector { type ParentType = gst_base::BaseTransform; } +fn video_input_formats() -> Vec { + let values = [ + gst_video::VideoFormat::Rgbx, + gst_video::VideoFormat::Xrgb, + gst_video::VideoFormat::Bgrx, + gst_video::VideoFormat::Xbgr, + gst_video::VideoFormat::Rgb, + gst_video::VideoFormat::Bgr, + ]; + values.iter().map(|i| i.to_str().to_send_value()).collect() +} + +fn video_output_formats() -> Vec { + let values = [ + gst_video::VideoFormat::Rgba, + gst_video::VideoFormat::Argb, + gst_video::VideoFormat::Bgra, + gst_video::VideoFormat::Abgr, + ]; + values.iter().map(|i| i.to_str().to_send_value()).collect() +} + +impl HsvDetector { + #[inline] + fn hsv_detect( + &self, + in_frame: &gst_video::video_frame::VideoFrameRef<&gst::buffer::BufferRef>, + out_frame: &mut gst_video::video_frame::VideoFrameRef<&mut gst::buffer::BufferRef>, + to_hsv: CF, + apply_alpha: DF, + ) where + CF: Fn(&[u8]) -> [f32; 3], + DF: Fn(&[u8], &mut [u8], u8), + { + let settings = self.settings.lock().unwrap(); + + // Keep the various metadata we need for working with the video frames in + // local variables. This saves some typing below. + let width = in_frame.width() as usize; + let in_stride = in_frame.plane_stride()[0] as usize; + let in_data = in_frame.plane_data(0).unwrap(); + let out_stride = out_frame.plane_stride()[0] as usize; + let out_data = out_frame.plane_data_mut(0).unwrap(); + let nb_input_channels = in_frame.format_info().pixel_stride()[0] as usize; + + assert_eq!(out_data.len() / out_stride, in_data.len() / in_stride); + assert_eq!(in_data.len() % nb_input_channels, 0); + + let in_line_bytes = width * nb_input_channels; + let out_line_bytes = width * 4; + + assert!(in_line_bytes <= in_stride); + assert!(out_line_bytes <= out_stride); + + for (in_line, out_line) in in_data + .chunks_exact(in_stride) + .zip(out_data.chunks_exact_mut(out_stride)) + { + for (in_p, out_p) in in_line[..in_line_bytes] + .chunks_exact(nb_input_channels) + .zip(out_line[..out_line_bytes].chunks_exact_mut(4)) + { + let hsv = to_hsv(in_p); + + // We handle hue being circular here + let ref_hue_offset = 180.0 - settings.hue_ref; + let mut shifted_hue = hsv[0] + ref_hue_offset; + + if shifted_hue < 0.0 { + shifted_hue += 360.0; + } + + shifted_hue %= 360.0; + + if (shifted_hue - 180.0).abs() <= settings.hue_var + && (hsv[1] - settings.saturation_ref).abs() <= settings.saturation_var + && (hsv[2] - settings.value_ref).abs() <= settings.value_var + { + apply_alpha(in_p, out_p, 255); + } else { + apply_alpha(in_p, out_p, 0); + }; + } + } + } +} + impl ObjectImpl for HsvDetector { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { @@ -282,10 +369,7 @@ impl ElementImpl for HsvDetector { let caps = gst::Caps::new_simple( "video/x-raw", &[ - ( - "format", - &gst::List::new(&[&gst_video::VideoFormat::Rgba.to_str()]), - ), + ("format", &gst::List::from_owned(video_output_formats())), ("width", &gst::IntRange::::new(0, i32::MAX)), ("height", &gst::IntRange::::new(0, i32::MAX)), ( @@ -310,10 +394,7 @@ impl ElementImpl for HsvDetector { let caps = gst::Caps::new_simple( "video/x-raw", &[ - ( - "format", - &gst::List::new(&[&gst_video::VideoFormat::Rgbx.to_str()]), - ), + ("format", &gst::List::from_owned(video_input_formats())), ("width", &gst::IntRange::::new(0, i32::MAX)), ("height", &gst::IntRange::::new(0, i32::MAX)), ( @@ -358,7 +439,7 @@ impl BaseTransformImpl for HsvDetector { let mut caps = caps.clone(); for s in caps.make_mut().iter_mut() { - s.set("format", &gst_video::VideoFormat::Rgbx.to_str()); + s.set("format", &gst::List::from_owned(video_input_formats())); } caps @@ -366,7 +447,7 @@ impl BaseTransformImpl for HsvDetector { let mut caps = caps.clone(); for s in caps.make_mut().iter_mut() { - s.set("format", &gst_video::VideoFormat::Rgba.to_str()); + s.set("format", &gst::List::from_owned(video_output_formats())); } caps @@ -439,8 +520,6 @@ impl BaseTransformImpl for HsvDetector { inbuf: &gst::Buffer, outbuf: &mut gst::BufferRef, ) -> Result { - let settings = *self.settings.lock().unwrap(); - let mut state_guard = self.state.borrow_mut(); let state = state_guard.as_mut().ok_or(gst::FlowError::NotNegotiated)?; @@ -468,57 +547,283 @@ impl BaseTransformImpl for HsvDetector { }, )?; - // Keep the various metadata we need for working with the video frames in - // local variables. This saves some typing below. - let width = in_frame.width() as usize; - let in_stride = in_frame.plane_stride()[0] as usize; - let in_data = in_frame.plane_data(0).unwrap(); - let out_stride = out_frame.plane_stride()[0] as usize; - let out_data = out_frame.plane_data_mut(0).unwrap(); - - assert_eq!(in_data.len() % 4, 0); - assert_eq!(out_data.len() / out_stride, in_data.len() / in_stride); - - let in_line_bytes = width * 4; - let out_line_bytes = width * 4; - - assert!(in_line_bytes <= in_stride); - assert!(out_line_bytes <= out_stride); - - for (in_line, out_line) in in_data - .chunks_exact(in_stride) - .zip(out_data.chunks_exact_mut(out_stride)) - { - for (in_p, out_p) in in_line[..in_line_bytes] - .chunks_exact(4) - .zip(out_line[..out_line_bytes].chunks_exact_mut(4)) - { - assert_eq!(out_p.len(), 4); - let hsv = - hsvutils::from_rgb(in_p[..3].try_into().expect("slice with incorrect length")); - - // We handle hue being circular here - let ref_hue_offset = 180.0 - settings.hue_ref; - let mut shifted_hue = hsv[0] + ref_hue_offset; - - if shifted_hue < 0.0 { - shifted_hue += 360.0; + match state.in_info.format() { + gst_video::VideoFormat::Rgbx | gst_video::VideoFormat::Rgb => { + match state.out_info.format() { + gst_video::VideoFormat::Rgba => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_rgb( + in_p[..3].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[..3].copy_from_slice(&in_p[..3]); + out_p[3] = val; + }, + ); + } + gst_video::VideoFormat::Argb => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_rgb( + in_p[..3].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[1..4].copy_from_slice(&in_p[..3]); + out_p[0] = val; + }, + ); + } + gst_video::VideoFormat::Bgra => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_rgb( + in_p[..3].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[0] = in_p[2]; + out_p[1] = in_p[1]; + out_p[2] = in_p[0]; + out_p[3] = val; + }, + ); + } + gst_video::VideoFormat::Abgr => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_rgb( + in_p[..3].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[1] = in_p[2]; + out_p[2] = in_p[1]; + out_p[3] = in_p[0]; + out_p[0] = val; + }, + ); + } + _ => unreachable!(), } - - shifted_hue %= 360.0; - - out_p[..3].copy_from_slice(&in_p[..3]); - - out_p[3] = if (shifted_hue - 180.0).abs() <= settings.hue_var - && (hsv[1] - settings.saturation_ref).abs() <= settings.saturation_var - && (hsv[2] - settings.value_ref).abs() <= settings.value_var - { - 255 - } else { - 0 + } + gst_video::VideoFormat::Xrgb => { + match state.out_info.format() { + gst_video::VideoFormat::Rgba => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_rgb( + in_p[1..4].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[..3].copy_from_slice(&in_p[1..4]); + out_p[3] = val; + }, + ); + } + gst_video::VideoFormat::Argb => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_rgb( + in_p[1..4].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[1..4].copy_from_slice(&in_p[1..4]); + out_p[0] = val; + }, + ); + } + gst_video::VideoFormat::Bgra => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_rgb( + in_p[1..4].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[0] = in_p[3]; + out_p[1] = in_p[2]; + out_p[2] = in_p[1]; + out_p[3] = val; + }, + ); + } + gst_video::VideoFormat::Abgr => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_rgb( + in_p[1..4].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[1] = in_p[3]; + out_p[2] = in_p[2]; + out_p[3] = in_p[1]; + out_p[0] = val; + }, + ); + } + _ => unreachable!(), }; } - } + gst_video::VideoFormat::Bgrx | gst_video::VideoFormat::Bgr => { + match state.out_info.format() { + gst_video::VideoFormat::Rgba => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_bgr( + in_p[..3].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[0] = in_p[2]; + out_p[1] = in_p[1]; + out_p[2] = in_p[0]; + out_p[3] = val; + }, + ); + } + gst_video::VideoFormat::Argb => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_bgr( + in_p[..3].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[1] = in_p[2]; + out_p[2] = in_p[1]; + out_p[3] = in_p[0]; + out_p[0] = val; + }, + ); + } + gst_video::VideoFormat::Bgra => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_bgr( + in_p[..3].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[..3].copy_from_slice(&in_p[..3]); + out_p[3] = val; + }, + ); + } + gst_video::VideoFormat::Abgr => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_bgr( + in_p[..3].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[1..4].copy_from_slice(&in_p[..3]); + out_p[0] = val; + }, + ); + } + _ => unreachable!(), + } + } + gst_video::VideoFormat::Xbgr => match state.out_info.format() { + gst_video::VideoFormat::Rgba => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_bgr( + in_p[1..4].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[0] = in_p[3]; + out_p[1] = in_p[2]; + out_p[2] = in_p[1]; + out_p[3] = val; + }, + ); + } + gst_video::VideoFormat::Argb => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_bgr( + in_p[1..4].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[1] = in_p[3]; + out_p[2] = in_p[2]; + out_p[3] = in_p[1]; + out_p[0] = val; + }, + ); + } + gst_video::VideoFormat::Bgra => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_bgr( + in_p[1..4].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[..3].copy_from_slice(&in_p[1..4]); + out_p[3] = val; + }, + ); + } + gst_video::VideoFormat::Abgr => { + self.hsv_detect( + &in_frame, + &mut out_frame, + |in_p| { + hsvutils::from_bgr( + in_p[1..4].try_into().expect("slice with incorrect length"), + ) + }, + |in_p, out_p, val| { + out_p[1..4].copy_from_slice(&in_p[1..4]); + out_p[0] = val; + }, + ); + } + _ => unreachable!(), + }, + _ => unreachable!(), + }; Ok(gst::FlowSuccess::Ok) } diff --git a/video/hsv/src/hsvfilter/imp.rs b/video/hsv/src/hsvfilter/imp.rs index 062219f5..a527c13f 100644 --- a/video/hsv/src/hsvfilter/imp.rs +++ b/video/hsv/src/hsvfilter/imp.rs @@ -77,6 +77,55 @@ impl ObjectSubclass for HsvFilter { type ParentType = gst_base::BaseTransform; } +impl HsvFilter { + #[inline] + fn hsv_filter( + &self, + frame: &mut gst_video::video_frame::VideoFrameRef<&mut gst::buffer::BufferRef>, + to_hsv: CF, + apply_filter: FF, + ) where + CF: Fn(&[u8]) -> [f32; 3], + FF: Fn(&[f32; 3], &mut [u8]), + { + let settings = *self.settings.lock().unwrap(); + + let width = frame.width() as usize; + let stride = frame.plane_stride()[0] as usize; + let nb_channels = frame.format_info().pixel_stride()[0] as usize; + let data = frame.plane_data_mut(0).unwrap(); + + assert_eq!(data.len() % nb_channels, 0); + + let line_bytes = width * nb_channels; + + for line in data.chunks_exact_mut(stride) { + for p in line[..line_bytes].chunks_exact_mut(nb_channels) { + assert_eq!(p.len(), nb_channels); + + let mut hsv = to_hsv(p); + + hsv[0] = (hsv[0] + settings.hue_shift) % 360.0; + if hsv[0] < 0.0 { + hsv[0] += 360.0; + } + hsv[1] = hsvutils::Clamp::clamp( + settings.saturation_mul * hsv[1] + settings.saturation_off, + 0.0, + 1.0, + ); + hsv[2] = hsvutils::Clamp::clamp( + settings.value_mul * hsv[2] + settings.value_off, + 0.0, + 1.0, + ); + + apply_filter(&hsv, p); + } + } + } +} + impl ObjectImpl for HsvFilter { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { @@ -255,7 +304,18 @@ impl ElementImpl for HsvFilter { &[ ( "format", - &gst::List::new(&[&gst_video::VideoFormat::Rgbx.to_str()]), + &gst::List::new(&[ + &gst_video::VideoFormat::Rgbx.to_str(), + &gst_video::VideoFormat::Xrgb.to_str(), + &gst_video::VideoFormat::Bgrx.to_str(), + &gst_video::VideoFormat::Xbgr.to_str(), + &gst_video::VideoFormat::Rgba.to_str(), + &gst_video::VideoFormat::Argb.to_str(), + &gst_video::VideoFormat::Bgra.to_str(), + &gst_video::VideoFormat::Abgr.to_str(), + &gst_video::VideoFormat::Rgb.to_str(), + &gst_video::VideoFormat::Bgr.to_str(), + ]), ), ("width", &gst::IntRange::::new(0, i32::MAX)), ("height", &gst::IntRange::::new(0, i32::MAX)), @@ -346,8 +406,6 @@ impl BaseTransformImpl for HsvFilter { element: &Self::Type, buf: &mut gst::BufferRef, ) -> Result { - let settings = *self.settings.lock().unwrap(); - let mut state_guard = self.state.borrow_mut(); let state = state_guard.as_mut().ok_or(gst::FlowError::NotNegotiated)?; @@ -361,39 +419,52 @@ impl BaseTransformImpl for HsvFilter { gst::FlowError::Error })?; - let width = frame.width() as usize; - let stride = frame.plane_stride()[0] as usize; - let format = frame.format(); - let data = frame.plane_data_mut(0).unwrap(); - - assert_eq!(format, gst_video::VideoFormat::Rgbx); - assert_eq!(data.len() % 4, 0); - - let line_bytes = width * 4; - - for line in data.chunks_exact_mut(stride) { - for p in line[..line_bytes].chunks_exact_mut(4) { - assert_eq!(p.len(), 4); - - let mut hsv = - hsvutils::from_rgb(p[..3].try_into().expect("slice with incorrect length")); - hsv[0] = (hsv[0] + settings.hue_shift) % 360.0; - if hsv[0] < 0.0 { - hsv[0] += 360.0; - } - hsv[1] = hsvutils::Clamp::clamp( - settings.saturation_mul * hsv[1] + settings.saturation_off, - 0.0, - 1.0, + match state.info.format() { + gst_video::VideoFormat::Rgbx + | gst_video::VideoFormat::Rgba + | gst_video::VideoFormat::Rgb => { + self.hsv_filter( + &mut frame, + |p| hsvutils::from_rgb(p[..3].try_into().expect("slice with incorrect length")), + |hsv, p| { + p[..3].copy_from_slice(&hsvutils::to_rgb(hsv)); + }, ); - hsv[2] = hsvutils::Clamp::clamp( - settings.value_mul * hsv[2] + settings.value_off, - 0.0, - 1.0, - ); - - p[..3].copy_from_slice(&hsvutils::to_rgb(&hsv)); } + gst_video::VideoFormat::Xrgb | gst_video::VideoFormat::Argb => { + self.hsv_filter( + &mut frame, + |p| { + hsvutils::from_rgb(p[1..4].try_into().expect("slice with incorrect length")) + }, + |hsv, p| { + p[1..4].copy_from_slice(&hsvutils::to_rgb(hsv)); + }, + ); + } + gst_video::VideoFormat::Bgrx + | gst_video::VideoFormat::Bgra + | gst_video::VideoFormat::Bgr => { + self.hsv_filter( + &mut frame, + |p| hsvutils::from_bgr(p[..3].try_into().expect("slice with incorrect length")), + |hsv, p| { + p[..3].copy_from_slice(&hsvutils::to_bgr(hsv)); + }, + ); + } + gst_video::VideoFormat::Xbgr | gst_video::VideoFormat::Abgr => { + self.hsv_filter( + &mut frame, + |p| { + hsvutils::from_bgr(p[1..4].try_into().expect("slice with incorrect length")) + }, + |hsv, p| { + p[1..4].copy_from_slice(&hsvutils::to_bgr(hsv)); + }, + ); + } + _ => unreachable!(), } Ok(gst::FlowSuccess::Ok) diff --git a/video/hsv/src/hsvutils.rs b/video/hsv/src/hsvutils.rs index 29479f68..ab2f54ba 100644 --- a/video/hsv/src/hsvutils.rs +++ b/video/hsv/src/hsvutils.rs @@ -83,6 +83,50 @@ pub fn from_rgb(in_p: &[u8; 3]) -> [f32; 3] { ] } +// Converts a BGR pixel to HSV +#[inline] +pub fn from_bgr(in_p: &[u8; 3]) -> [f32; 3] { + let b = in_p[0] as f32 / 255.0; + let g = in_p[1] as f32 / 255.0; + let r = in_p[2] as f32 / 255.0; + + let value: f32 = *in_p + .iter() + .max() + .expect("Cannot find max value from rgb input") as f32 + / 255.0; + let chroma: f32 = value + - (*in_p + .iter() + .min() + .expect("Cannot find min value from rgb input") as f32 + / 255.0); + + let mut hue: f32 = if chroma == 0.0 { + 0.0 + } else if (value - r).abs() < EPSILON { + 60.0 * ((g - b) / chroma) + } else if (value - g).abs() < EPSILON { + 60.0 * (2.0 + ((b - r) / chroma)) + } else if (value - b).abs() < EPSILON { + 60.0 * (4.0 + ((r - g) / chroma)) + } else { + 0.0 + }; + + if hue < 0.0 { + hue += 360.0; + } + + let saturation: f32 = if value == 0.0 { 0.0 } else { chroma / value }; + + [ + hue % 360.0, + saturation.clamp(0.0, 1.0), + value.clamp(0.0, 1.0), + ] +} + // Converts a HSV pixel to RGB #[inline] pub fn to_rgb(in_p: &[f32; 3]) -> [u8; 3] { @@ -118,6 +162,41 @@ pub fn to_rgb(in_p: &[f32; 3]) -> [u8; 3] { ] } +// Converts a HSV pixel to RGB +#[inline] +pub fn to_bgr(in_p: &[f32; 3]) -> [u8; 3] { + let c: f32 = in_p[2] * in_p[1]; + let hue_prime: f32 = in_p[0] / 60.0; + + let x: f32 = c * (1.0 - ((hue_prime % 2.0) - 1.0).abs()); + + let rgb_prime = if hue_prime < 0.0 { + [0.0, 0.0, 0.0] + } else if hue_prime <= 1.0 { + [c, x, 0.0] + } else if hue_prime <= 2.0 { + [x, c, 0.0] + } else if hue_prime <= 3.0 { + [0.0, c, x] + } else if hue_prime <= 4.0 { + [0.0, x, c] + } else if hue_prime <= 5.0 { + [x, 0.0, c] + } else if hue_prime <= 6.0 { + [c, 0.0, x] + } else { + [0.0, 0.0, 0.0] + }; + + let m = in_p[2] - c; + + [ + ((rgb_prime[2] + m) * 255.0).clamp(0.0, 255.0) as u8, + ((rgb_prime[1] + m) * 255.0).clamp(0.0, 255.0) as u8, + ((rgb_prime[0] + m) * 255.0).clamp(0.0, 255.0) as u8, + ] +} + #[cfg(test)] mod tests { @@ -143,6 +222,12 @@ mod tests { const RGB_GREEN: [u8; 3] = [0, 255, 0]; const RGB_BLUE: [u8; 3] = [0, 0, 255]; + const BGR_WHITE: [u8; 3] = [255, 255, 255]; + const BGR_BLACK: [u8; 3] = [0, 0, 0]; + const BGR_RED: [u8; 3] = [0, 0, 255]; + const BGR_GREEN: [u8; 3] = [0, 255, 0]; + const BGR_BLUE: [u8; 3] = [255, 0, 0]; + const HSV_WHITE: [f32; 3] = [0.0, 0.0, 1.0]; const HSV_BLACK: [f32; 3] = [0.0, 0.0, 0.0]; const HSV_RED: [f32; 3] = [0.0, 1.0, 1.0]; @@ -160,6 +245,17 @@ mod tests { assert!(is_equivalent(&from_rgb(&RGB_BLUE), &HSV_BLUE, EPSILON)); } + #[test] + fn test_from_bgr() { + use super::*; + + assert!(is_equivalent(&from_bgr(&BGR_WHITE), &HSV_WHITE, EPSILON)); + assert!(is_equivalent(&from_bgr(&BGR_BLACK), &HSV_BLACK, EPSILON)); + assert!(is_equivalent(&from_bgr(&BGR_RED), &HSV_RED, EPSILON)); + assert!(is_equivalent(&from_bgr(&BGR_GREEN), &HSV_GREEN, EPSILON)); + assert!(is_equivalent(&from_bgr(&BGR_BLUE), &HSV_BLUE, EPSILON)); + } + #[test] fn test_to_rgb() { use super::*; @@ -170,4 +266,15 @@ mod tests { assert!(to_rgb(&HSV_GREEN) == RGB_GREEN); assert!(to_rgb(&HSV_BLUE) == RGB_BLUE); } + + #[test] + fn test_to_bgr() { + use super::*; + + assert!(to_bgr(&HSV_WHITE) == BGR_WHITE); + assert!(to_bgr(&HSV_BLACK) == BGR_BLACK); + assert!(to_bgr(&HSV_RED) == BGR_RED); + assert!(to_bgr(&HSV_GREEN) == BGR_GREEN); + assert!(to_bgr(&HSV_BLUE) == BGR_BLUE); + } }