mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2025-01-24 09:58:13 +00:00
videofx: Add colordetect video filter
This new video filter is able to detect the dominant color in a video frame. When the color has changed from the previous frame the filter posts an Element message on the bus, the associated structure is named `colordetect` and has two fields: * a string field named `dominant-color` * a list field containing the whole color palette, stored as uint values, sorted by dominance, with more dominant colors first
This commit is contained in:
parent
e82678586f
commit
b423febfbe
5 changed files with 447 additions and 1 deletions
|
@ -11,6 +11,10 @@ edition = "2021"
|
||||||
cairo-rs = { git = "https://github.com/gtk-rs/gtk-rs-core", features=["use_glib"] }
|
cairo-rs = { git = "https://github.com/gtk-rs/gtk-rs-core", features=["use_glib"] }
|
||||||
atomic_refcell = "0.1"
|
atomic_refcell = "0.1"
|
||||||
once_cell = "1.0"
|
once_cell = "1.0"
|
||||||
|
# color-thief = "0.2"
|
||||||
|
# https://github.com/RazrFalcon/color-thief-rs/pull/4
|
||||||
|
color-thief = { git = "https://github.com/philn/color-thief-rs", branch = "max-colors-respect" }
|
||||||
|
color-name = "1.0.0"
|
||||||
|
|
||||||
[dependencies.gst]
|
[dependencies.gst]
|
||||||
git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"
|
git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"
|
||||||
|
|
348
video/videofx/src/colordetect/imp.rs
Normal file
348
video/videofx/src/colordetect/imp.rs
Normal file
|
@ -0,0 +1,348 @@
|
||||||
|
// Copyright (C) 2022, Igalia S.L
|
||||||
|
// Author: Philippe Normand <philn@igalia.com>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||||
|
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||||
|
// <https://mozilla.org/MPL/2.0/>.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use atomic_refcell::AtomicRefCell;
|
||||||
|
use color_name;
|
||||||
|
use color_thief::{get_palette, Color, ColorFormat};
|
||||||
|
use gst::{glib, subclass::prelude::*};
|
||||||
|
use gst_base::prelude::*;
|
||||||
|
use gst_video::{subclass::prelude::*, VideoFormat};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
const DEFAULT_QUALITY: u32 = 10;
|
||||||
|
const DEFAULT_MAX_COLORS: u32 = 2;
|
||||||
|
|
||||||
|
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||||
|
gst::DebugCategory::new(
|
||||||
|
"colordetect",
|
||||||
|
gst::DebugColorFlags::empty(),
|
||||||
|
Some("Dominant color detection"),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct Settings {
|
||||||
|
quality: u32,
|
||||||
|
max_colors: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Settings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Settings {
|
||||||
|
quality: DEFAULT_QUALITY,
|
||||||
|
max_colors: DEFAULT_MAX_COLORS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
color_format: ColorFormat,
|
||||||
|
out_info: gst_video::VideoInfo,
|
||||||
|
current_color: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ColorDetect {
|
||||||
|
settings: Mutex<Settings>,
|
||||||
|
state: AtomicRefCell<Option<State>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorDetect {
|
||||||
|
fn detect_color(
|
||||||
|
&self,
|
||||||
|
element: &super::ColorDetect,
|
||||||
|
buf: &mut gst::BufferRef,
|
||||||
|
) -> Result<Option<(String, Vec<Color>)>, gst::FlowError> {
|
||||||
|
let mut state_guard = self.state.borrow_mut();
|
||||||
|
let state = state_guard.as_mut().ok_or_else(|| {
|
||||||
|
gst::element_error!(element, gst::CoreError::Negotiation, ["Have no state yet"]);
|
||||||
|
gst::FlowError::NotNegotiated
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let settings = *self.settings.lock().unwrap();
|
||||||
|
let frame =
|
||||||
|
gst_video::VideoFrameRef::from_buffer_ref_readable(buf, &state.out_info).unwrap();
|
||||||
|
let palette = get_palette(
|
||||||
|
frame.plane_data(0).unwrap(),
|
||||||
|
state.color_format,
|
||||||
|
settings.quality as u8,
|
||||||
|
settings.max_colors as u8,
|
||||||
|
)
|
||||||
|
.map_err(|_| gst::FlowError::Error)?;
|
||||||
|
|
||||||
|
let dominant_color = palette[0];
|
||||||
|
let dominant_color_name =
|
||||||
|
color_name::Color::similar([dominant_color.r, dominant_color.g, dominant_color.b])
|
||||||
|
.to_lowercase();
|
||||||
|
if state
|
||||||
|
.current_color
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |current_color| current_color != &dominant_color_name)
|
||||||
|
{
|
||||||
|
let name = dominant_color_name.clone();
|
||||||
|
state.current_color = Some(dominant_color_name);
|
||||||
|
return Ok(Some((name, palette)));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn color_changed(
|
||||||
|
&self,
|
||||||
|
element: &super::ColorDetect,
|
||||||
|
dominant_color_name: &str,
|
||||||
|
palette: Vec<Color>,
|
||||||
|
) {
|
||||||
|
gst::debug!(
|
||||||
|
CAT,
|
||||||
|
obj: element,
|
||||||
|
"Dominant color changed to {}",
|
||||||
|
dominant_color_name
|
||||||
|
);
|
||||||
|
let palette_colors = gst::List::new(
|
||||||
|
palette
|
||||||
|
.iter()
|
||||||
|
.map(|c| ((c.r as u32) << 16) | ((c.g as u32) << 8) | (c.b as u32)),
|
||||||
|
);
|
||||||
|
|
||||||
|
element
|
||||||
|
.post_message(
|
||||||
|
gst::message::Element::builder(
|
||||||
|
gst::structure::Structure::builder("colordetect")
|
||||||
|
.field("dominant-color", dominant_color_name)
|
||||||
|
.field("palette", palette_colors)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.expect("Element without bus. Should not happen!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for ColorDetect {
|
||||||
|
const NAME: &'static str = "GstColorDetect";
|
||||||
|
type Type = super::ColorDetect;
|
||||||
|
type ParentType = gst_base::BaseTransform;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for ColorDetect {
|
||||||
|
fn properties() -> &'static [glib::ParamSpec] {
|
||||||
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||||
|
vec![
|
||||||
|
glib::ParamSpecUInt::new(
|
||||||
|
"quality",
|
||||||
|
"Quality of an output colors",
|
||||||
|
"A step in pixels to improve performance",
|
||||||
|
0,
|
||||||
|
10,
|
||||||
|
DEFAULT_QUALITY,
|
||||||
|
glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_PLAYING,
|
||||||
|
),
|
||||||
|
glib::ParamSpecUInt::new(
|
||||||
|
"max-colors",
|
||||||
|
"Number of colors in the output palette",
|
||||||
|
"Actual colors count can be lower depending on the image",
|
||||||
|
2,
|
||||||
|
255,
|
||||||
|
DEFAULT_MAX_COLORS,
|
||||||
|
glib::ParamFlags::READWRITE | gst::PARAM_FLAG_MUTABLE_PLAYING,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
PROPERTIES.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_property(
|
||||||
|
&self,
|
||||||
|
obj: &Self::Type,
|
||||||
|
_id: usize,
|
||||||
|
value: &glib::Value,
|
||||||
|
pspec: &glib::ParamSpec,
|
||||||
|
) {
|
||||||
|
match pspec.name() {
|
||||||
|
"quality" => {
|
||||||
|
let mut settings = self.settings.lock().unwrap();
|
||||||
|
let quality = value.get().expect("type checked upstream");
|
||||||
|
if settings.quality != quality {
|
||||||
|
gst::info!(
|
||||||
|
CAT,
|
||||||
|
obj: obj,
|
||||||
|
"Changing quality from {} to {}",
|
||||||
|
settings.quality,
|
||||||
|
quality
|
||||||
|
);
|
||||||
|
settings.quality = quality;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"max-colors" => {
|
||||||
|
let mut settings = self.settings.lock().unwrap();
|
||||||
|
let max_colors = value.get().expect("type checked upstream");
|
||||||
|
if settings.max_colors != max_colors {
|
||||||
|
gst::info!(
|
||||||
|
CAT,
|
||||||
|
obj: obj,
|
||||||
|
"Changing max_colors from {} to {}",
|
||||||
|
settings.max_colors,
|
||||||
|
max_colors
|
||||||
|
);
|
||||||
|
settings.max_colors = max_colors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||||
|
match pspec.name() {
|
||||||
|
"quality" => {
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
settings.quality.to_value()
|
||||||
|
}
|
||||||
|
"max-colors" => {
|
||||||
|
let settings = self.settings.lock().unwrap();
|
||||||
|
settings.max_colors.to_value()
|
||||||
|
}
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GstObjectImpl for ColorDetect {}
|
||||||
|
|
||||||
|
impl ElementImpl for ColorDetect {
|
||||||
|
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||||
|
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||||
|
gst::subclass::ElementMetadata::new(
|
||||||
|
"Dominant color detection",
|
||||||
|
"Filter/Video",
|
||||||
|
"Detects the dominant color of a video",
|
||||||
|
"Philippe Normand <philn@igalia.com>",
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(&*ELEMENT_METADATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||||
|
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||||
|
let formats = gst::List::new([
|
||||||
|
VideoFormat::Rgb.to_str(),
|
||||||
|
VideoFormat::Rgba.to_str(),
|
||||||
|
VideoFormat::Argb.to_str(),
|
||||||
|
VideoFormat::Bgr.to_str(),
|
||||||
|
VideoFormat::Bgra.to_str(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let caps = gst::Caps::builder("video/x-raw")
|
||||||
|
.field("format", &formats)
|
||||||
|
.field("width", gst::IntRange::new(1, i32::MAX))
|
||||||
|
.field("height", gst::IntRange::new(1, i32::MAX))
|
||||||
|
.field(
|
||||||
|
"framerate",
|
||||||
|
gst::FractionRange::new(
|
||||||
|
gst::Fraction::new(0, 1),
|
||||||
|
gst::Fraction::new(i32::MAX, 1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
let sink_pad_template = gst::PadTemplate::new(
|
||||||
|
"sink",
|
||||||
|
gst::PadDirection::Sink,
|
||||||
|
gst::PadPresence::Always,
|
||||||
|
&caps,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let src_pad_template = gst::PadTemplate::new(
|
||||||
|
"src",
|
||||||
|
gst::PadDirection::Src,
|
||||||
|
gst::PadPresence::Always,
|
||||||
|
&caps,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
vec![sink_pad_template, src_pad_template]
|
||||||
|
});
|
||||||
|
|
||||||
|
PAD_TEMPLATES.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BaseTransformImpl for ColorDetect {
|
||||||
|
const MODE: gst_base::subclass::BaseTransformMode =
|
||||||
|
gst_base::subclass::BaseTransformMode::AlwaysInPlace;
|
||||||
|
const PASSTHROUGH_ON_SAME_CAPS: bool = false;
|
||||||
|
const TRANSFORM_IP_ON_PASSTHROUGH: bool = false;
|
||||||
|
|
||||||
|
fn stop(&self, element: &Self::Type) -> Result<(), gst::ErrorMessage> {
|
||||||
|
*self.state.borrow_mut() = None;
|
||||||
|
gst::info!(CAT, obj: element, "Stopped");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_caps(
|
||||||
|
&self,
|
||||||
|
element: &Self::Type,
|
||||||
|
incaps: &gst::Caps,
|
||||||
|
outcaps: &gst::Caps,
|
||||||
|
) -> Result<(), gst::LoggableError> {
|
||||||
|
let in_info = match gst_video::VideoInfo::from_caps(incaps) {
|
||||||
|
Err(_) => return Err(gst::loggable_error!(CAT, "Failed to parse input caps")),
|
||||||
|
Ok(info) => info,
|
||||||
|
};
|
||||||
|
|
||||||
|
let out_info = match gst_video::VideoInfo::from_caps(outcaps) {
|
||||||
|
Err(_) => return Err(gst::loggable_error!(CAT, "Failed to parse output caps")),
|
||||||
|
Ok(info) => info,
|
||||||
|
};
|
||||||
|
|
||||||
|
gst::debug!(
|
||||||
|
CAT,
|
||||||
|
obj: element,
|
||||||
|
"Configured for caps {} to {}",
|
||||||
|
incaps,
|
||||||
|
outcaps
|
||||||
|
);
|
||||||
|
|
||||||
|
let color_format = match in_info.format() {
|
||||||
|
VideoFormat::Rgb => ColorFormat::Rgb,
|
||||||
|
VideoFormat::Rgba => ColorFormat::Rgba,
|
||||||
|
VideoFormat::Argb => ColorFormat::Argb,
|
||||||
|
VideoFormat::Bgr => ColorFormat::Bgr,
|
||||||
|
VideoFormat::Bgra => ColorFormat::Bgra,
|
||||||
|
_ => unimplemented!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let previous_color = match self.state.borrow().as_ref() {
|
||||||
|
Some(state) => state.current_color.clone(),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
*self.state.borrow_mut() = Some(State {
|
||||||
|
color_format,
|
||||||
|
out_info,
|
||||||
|
current_color: previous_color,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform_ip(
|
||||||
|
&self,
|
||||||
|
element: &Self::Type,
|
||||||
|
buf: &mut gst::BufferRef,
|
||||||
|
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||||
|
if let Some((dominant_color_name, palette)) = self.detect_color(element, buf)? {
|
||||||
|
self.color_changed(element, &dominant_color_name, palette);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(gst::FlowSuccess::Ok)
|
||||||
|
}
|
||||||
|
}
|
26
video/videofx/src/colordetect/mod.rs
Normal file
26
video/videofx/src/colordetect/mod.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright (C) 2022, Igalia S.L
|
||||||
|
// Author: Philippe Normand <philn@igalia.com>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||||
|
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||||
|
// <https://mozilla.org/MPL/2.0/>.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use gst::glib;
|
||||||
|
use gst::prelude::*;
|
||||||
|
|
||||||
|
pub mod imp;
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct ColorDetect(ObjectSubclass<imp::ColorDetect>) @extends gst_base::BaseTransform, gst::Element, gst::Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||||
|
gst::Element::register(
|
||||||
|
Some(plugin),
|
||||||
|
"colordetect",
|
||||||
|
gst::Rank::None,
|
||||||
|
ColorDetect::static_type(),
|
||||||
|
)
|
||||||
|
}
|
|
@ -9,9 +9,11 @@
|
||||||
#![allow(clippy::non_send_fields_in_send_ty)]
|
#![allow(clippy::non_send_fields_in_send_ty)]
|
||||||
|
|
||||||
mod border;
|
mod border;
|
||||||
|
mod colordetect;
|
||||||
|
|
||||||
fn plugin_init(plugin: &gst::Plugin) -> Result<(), gst::glib::BoolError> {
|
fn plugin_init(plugin: &gst::Plugin) -> Result<(), gst::glib::BoolError> {
|
||||||
border::register(plugin)
|
border::register(plugin)?;
|
||||||
|
colordetect::register(plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
gst::plugin_define!(
|
gst::plugin_define!(
|
||||||
|
|
66
video/videofx/tests/colordetect.rs
Normal file
66
video/videofx/tests/colordetect.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
// Copyright (C) 2022 Philippe Normand <philn@igalia.com>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||||
|
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||||
|
// <https://mozilla.org/MPL/2.0/>.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use gst::prelude::*;
|
||||||
|
|
||||||
|
fn init() {
|
||||||
|
use std::sync::Once;
|
||||||
|
static INIT: Once = Once::new();
|
||||||
|
|
||||||
|
INIT.call_once(|| {
|
||||||
|
gst::init().unwrap();
|
||||||
|
gstvideofx::plugin_register_static().expect("Failed to register videofx plugin");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_red_color() {
|
||||||
|
init();
|
||||||
|
let pipeline = gst::Pipeline::new(None);
|
||||||
|
|
||||||
|
let src = gst::ElementFactory::make("videotestsrc", None).unwrap();
|
||||||
|
src.set_property_from_str("pattern", "red");
|
||||||
|
src.set_property("num-buffers", &2i32);
|
||||||
|
|
||||||
|
let filter = gst::ElementFactory::make("colordetect", None).unwrap();
|
||||||
|
let sink = gst::ElementFactory::make("fakevideosink", None).unwrap();
|
||||||
|
|
||||||
|
pipeline
|
||||||
|
.add_many(&[&src, &filter, &sink])
|
||||||
|
.expect("failed to add elements to the pipeline");
|
||||||
|
gst::Element::link_many(&[&src, &filter, &sink]).expect("failed to link the elements");
|
||||||
|
|
||||||
|
pipeline
|
||||||
|
.set_state(gst::State::Playing)
|
||||||
|
.expect("Unable to set the pipeline to the `Playing` state");
|
||||||
|
|
||||||
|
let mut detected_color: Option<String> = None;
|
||||||
|
let bus = pipeline.bus().unwrap();
|
||||||
|
for msg in bus.iter_timed(gst::ClockTime::NONE) {
|
||||||
|
use gst::MessageView;
|
||||||
|
match msg.view() {
|
||||||
|
MessageView::Element(elt) => {
|
||||||
|
if let Some(s) = elt.structure() {
|
||||||
|
if s.name() == "colordetect" {
|
||||||
|
// The video source emits 2 red frames, but we should
|
||||||
|
// receive only one message because the dominant color
|
||||||
|
// doesn't change.
|
||||||
|
assert_eq!(detected_color.as_deref(), None);
|
||||||
|
detected_color = Some(s.get::<String>("dominant-color").unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MessageView::Eos(..) => break,
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline.set_state(gst::State::Null).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(detected_color.as_deref(), Some("red"));
|
||||||
|
}
|
Loading…
Reference in a new issue