mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2025-01-23 17:38:20 +00:00
280 lines
7.7 KiB
Rust
280 lines
7.7 KiB
Rust
// Copyright (C) 2020 Julien Bardagi <julien.bardagi@gmail.com>
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
// option. This file may not be copied, modified, or distributed
|
|
// except according to those terms.
|
|
//
|
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
|
|
|
// Reference used for implementation: https://en.wikipedia.org/wiki/HSL_and_HSV
|
|
|
|
// Since the standard 'clamp' feature is still considered unstable, we provide
|
|
// a subsititute implementaion here so we can still build with the stable toolchain.
|
|
// Source: https://github.com/rust-lang/rust/issues/44095#issuecomment-624879262
|
|
pub trait Clamp: Sized {
|
|
fn clamp<L, U>(self, lower: L, upper: U) -> Self
|
|
where
|
|
L: Into<Option<Self>>,
|
|
U: Into<Option<Self>>;
|
|
}
|
|
|
|
impl Clamp for f32 {
|
|
fn clamp<L, U>(self, lower: L, upper: U) -> Self
|
|
where
|
|
L: Into<Option<Self>>,
|
|
U: Into<Option<Self>>,
|
|
{
|
|
let below = match lower.into() {
|
|
None => self,
|
|
Some(lower) => self.max(lower),
|
|
};
|
|
match upper.into() {
|
|
None => below,
|
|
Some(upper) => below.min(upper),
|
|
}
|
|
}
|
|
}
|
|
|
|
const EPSILON: f32 = 0.00001;
|
|
|
|
// Converts a RGB pixel to HSV
|
|
#[inline]
|
|
pub fn from_rgb(in_p: &[u8; 3]) -> [f32; 3] {
|
|
let r = in_p[0] as f32 / 255.0;
|
|
let g = in_p[1] as f32 / 255.0;
|
|
let b = 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 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] {
|
|
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[0] + 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[2] + m) * 255.0).clamp(0.0, 255.0) as u8,
|
|
]
|
|
}
|
|
|
|
// 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 {
|
|
|
|
fn is_equivalent(hsv: &[f32; 3], expected: &[f32; 3], eps: f32) -> bool {
|
|
// We handle hue being circular here
|
|
let ref_hue_offset = 180.0 - expected[0];
|
|
let mut shifted_hue = hsv[0] + ref_hue_offset;
|
|
|
|
if shifted_hue < 0.0 {
|
|
shifted_hue += 360.0;
|
|
}
|
|
|
|
shifted_hue %= 360.0;
|
|
|
|
(shifted_hue - 180.0).abs() < eps
|
|
&& (hsv[1] - expected[1]).abs() < eps
|
|
&& (hsv[2] - expected[2]).abs() < eps
|
|
}
|
|
|
|
const RGB_WHITE: [u8; 3] = [255, 255, 255];
|
|
const RGB_BLACK: [u8; 3] = [0, 0, 0];
|
|
const RGB_RED: [u8; 3] = [255, 0, 0];
|
|
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];
|
|
const HSV_GREEN: [f32; 3] = [120.0, 1.0, 1.0];
|
|
const HSV_BLUE: [f32; 3] = [240.0, 1.0, 1.0];
|
|
|
|
#[test]
|
|
fn test_from_rgb() {
|
|
use super::*;
|
|
|
|
assert!(is_equivalent(&from_rgb(&RGB_WHITE), &HSV_WHITE, EPSILON));
|
|
assert!(is_equivalent(&from_rgb(&RGB_BLACK), &HSV_BLACK, EPSILON));
|
|
assert!(is_equivalent(&from_rgb(&RGB_RED), &HSV_RED, EPSILON));
|
|
assert!(is_equivalent(&from_rgb(&RGB_GREEN), &HSV_GREEN, EPSILON));
|
|
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::*;
|
|
|
|
assert!(to_rgb(&HSV_WHITE) == RGB_WHITE);
|
|
assert!(to_rgb(&HSV_BLACK) == RGB_BLACK);
|
|
assert!(to_rgb(&HSV_RED) == RGB_RED);
|
|
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);
|
|
}
|
|
}
|