hsv: added support for more RGB and BGR formats to hsvdetector and hsvfilter

This commit is contained in:
Blaxar Waldarax 2020-12-23 19:58:41 +01:00 committed by Sebastian Dröge
parent 6b5e536ca6
commit e9f0a3b8ac
3 changed files with 577 additions and 94 deletions

View file

@ -82,6 +82,93 @@ impl ObjectSubclass for HsvDetector {
type ParentType = gst_base::BaseTransform;
}
fn video_input_formats() -> Vec<glib::SendValue> {
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<glib::SendValue> {
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<CF, DF>(
&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<Vec<glib::ParamSpec>> = 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::<i32>::new(0, i32::MAX)),
("height", &gst::IntRange::<i32>::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::<i32>::new(0, i32::MAX)),
("height", &gst::IntRange::<i32>::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<gst::FlowSuccess, gst::FlowError> {
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)
}

View file

@ -77,6 +77,55 @@ impl ObjectSubclass for HsvFilter {
type ParentType = gst_base::BaseTransform;
}
impl HsvFilter {
#[inline]
fn hsv_filter<CF, FF>(
&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<Vec<glib::ParamSpec>> = 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::<i32>::new(0, i32::MAX)),
("height", &gst::IntRange::<i32>::new(0, i32::MAX)),
@ -346,8 +406,6 @@ impl BaseTransformImpl for HsvFilter {
element: &Self::Type,
buf: &mut gst::BufferRef,
) -> Result<gst::FlowSuccess, gst::FlowError> {
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)

View file

@ -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);
}
}