Add the Image Color Palette node (#1311)

* Add image color palette node

* Add max size of palette

* Code review cleanup

---------

Co-authored-by: 0hypercube <0hypercube@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Henry Barreto 2023-12-09 20:21:41 -03:00 committed by GitHub
parent fe4b9ef8bb
commit cbda811480
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 148 additions and 0 deletions

View file

@ -38,6 +38,8 @@ pub enum FrontendGraphDataType {
GraphicGroup, GraphicGroup,
#[serde(rename = "artboard")] #[serde(rename = "artboard")]
Artboard, Artboard,
#[serde(rename = "palette")]
Palette,
} }
impl FrontendGraphDataType { impl FrontendGraphDataType {
pub const fn with_tagged_value(value: &TaggedValue) -> Self { pub const fn with_tagged_value(value: &TaggedValue) -> Self {
@ -52,6 +54,7 @@ impl FrontendGraphDataType {
TaggedValue::RcSubpath(_) | TaggedValue::Subpaths(_) | TaggedValue::VectorData(_) => Self::Subpath, TaggedValue::RcSubpath(_) | TaggedValue::Subpaths(_) | TaggedValue::VectorData(_) => Self::Subpath,
TaggedValue::GraphicGroup(_) => Self::GraphicGroup, TaggedValue::GraphicGroup(_) => Self::GraphicGroup,
TaggedValue::Artboard(_) => Self::Artboard, TaggedValue::Artboard(_) => Self::Artboard,
TaggedValue::Palette(_) => Self::Palette,
_ => Self::General, _ => Self::General,
} }
} }

View file

@ -2323,6 +2323,18 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
properties: node_properties::color_overlay_properties, properties: node_properties::color_overlay_properties,
..Default::default() ..Default::default()
}, },
DocumentNodeDefinition {
name: "Image Color Palette",
category: "Image Adjustments",
implementation: NodeImplementation::proto("graphene_std::image_color_palette::ImageColorPaletteNode<_>"),
inputs: vec![
DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true),
DocumentInputType::value("Max Size", TaggedValue::U32(8), true),
],
outputs: vec![DocumentOutputType::new("Colors", FrontendGraphDataType::Color)],
properties: node_properties::image_color_palette,
..Default::default()
},
] ]
} }

View file

@ -1904,3 +1904,9 @@ pub fn color_overlay_properties(document_node: &DocumentNode, node_id: NodeId, _
vec![color, blend_mode, LayoutGroup::Row { widgets: opacity }] vec![color, blend_mode, LayoutGroup::Row { widgets: opacity }]
} }
pub fn image_color_palette(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let size = number_widget(document_node, node_id, 1, "Max Size", NumberInput::default().int().min(1.).max(28.), true);
vec![LayoutGroup::Row { widgets: size }]
}

View file

@ -716,6 +716,7 @@ impl NodeGraphExecutor {
TaggedValue::OptionalColor(render_object) => Self::render(render_object, transform, responses), TaggedValue::OptionalColor(render_object) => Self::render(render_object, transform, responses),
TaggedValue::VectorData(render_object) => Self::render(render_object, transform, responses), TaggedValue::VectorData(render_object) => Self::render(render_object, transform, responses),
TaggedValue::ImageFrame(render_object) => Self::render(render_object, transform, responses), TaggedValue::ImageFrame(render_object) => Self::render(render_object, transform, responses),
TaggedValue::Palette(render_object) => Self::render(render_object, transform, responses),
_ => { _ => {
return Err(format!("Invalid node graph output type: {node_graph_output:#?}")); return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
} }

View file

@ -617,6 +617,26 @@ impl GraphicElementRendered for Option<Color> {
fn add_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {} fn add_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {}
} }
impl GraphicElementRendered for Vec<Color> {
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
for (index, &color) in self.iter().enumerate() {
render.leaf_tag("rect", |attributes| {
attributes.push("width", "100");
attributes.push("height", "100");
attributes.push("x", (index * 120).to_string());
attributes.push("y", "40");
attributes.push("fill", format!("#{}", color.rgba_hex()));
});
}
}
fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> {
None
}
fn add_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {}
}
/// A segment of an svg string to allow for embedding blob urls /// A segment of an svg string to allow for embedding blob urls
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum SvgSegment { pub enum SvgSegment {

View file

@ -1128,4 +1128,12 @@ mod index_node {
ImageFrame::empty() ImageFrame::empty()
} }
} }
#[node_macro::node_impl(IndexNode)]
pub fn index_node(input: Vec<Color>, index: u32) -> Option<Color> {
if index as usize >= input.len() {
warn!("Index of colors is out of range: index is {index} and length is {}", input.len());
}
input.into_iter().nth(index as usize)
}
} }

View file

@ -67,6 +67,7 @@ pub enum TaggedValue {
SurfaceFrame(graphene_core::SurfaceFrame), SurfaceFrame(graphene_core::SurfaceFrame),
Footprint(graphene_core::transform::Footprint), Footprint(graphene_core::transform::Footprint),
RenderOutput(RenderOutput), RenderOutput(RenderOutput),
Palette(Vec<Color>),
} }
#[allow(clippy::derived_hash_with_manual_eq)] #[allow(clippy::derived_hash_with_manual_eq)]
@ -138,6 +139,7 @@ impl Hash for TaggedValue {
Self::SurfaceFrame(surface_id) => surface_id.hash(state), Self::SurfaceFrame(surface_id) => surface_id.hash(state),
Self::Footprint(footprint) => footprint.hash(state), Self::Footprint(footprint) => footprint.hash(state),
Self::RenderOutput(render_output) => render_output.hash(state), Self::RenderOutput(render_output) => render_output.hash(state),
Self::Palette(palette) => palette.hash(state),
} }
} }
} }
@ -196,6 +198,7 @@ impl<'a> TaggedValue {
TaggedValue::SurfaceFrame(x) => Box::new(x), TaggedValue::SurfaceFrame(x) => Box::new(x),
TaggedValue::Footprint(x) => Box::new(x), TaggedValue::Footprint(x) => Box::new(x),
TaggedValue::RenderOutput(x) => Box::new(x), TaggedValue::RenderOutput(x) => Box::new(x),
TaggedValue::Palette(x) => Box::new(x),
} }
} }
@ -265,6 +268,7 @@ impl<'a> TaggedValue {
TaggedValue::SurfaceFrame(_) => concrete!(graphene_core::SurfaceFrame), TaggedValue::SurfaceFrame(_) => concrete!(graphene_core::SurfaceFrame),
TaggedValue::Footprint(_) => concrete!(graphene_core::transform::Footprint), TaggedValue::Footprint(_) => concrete!(graphene_core::transform::Footprint),
TaggedValue::RenderOutput(_) => concrete!(RenderOutput), TaggedValue::RenderOutput(_) => concrete!(RenderOutput),
TaggedValue::Palette(_) => concrete!(Vec<Color>),
} }
} }
@ -326,6 +330,7 @@ impl<'a> TaggedValue {
Ok(TaggedValue::SurfaceFrame(frame.into())) Ok(TaggedValue::SurfaceFrame(frame.into()))
} }
x if x == TypeId::of::<graphene_core::transform::Footprint>() => Ok(TaggedValue::Footprint(*downcast(input).unwrap())), x if x == TypeId::of::<graphene_core::transform::Footprint>() => Ok(TaggedValue::Footprint(*downcast(input).unwrap())),
x if x == TypeId::of::<Vec<Color>>() => Ok(TaggedValue::Palette(*downcast(input).unwrap())),
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))), _ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))),
} }
} }

View file

@ -0,0 +1,83 @@
use graphene_core::raster::ImageFrame;
use graphene_core::Color;
use graphene_core::Node;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImageColorPaletteNode<MaxSize> {
max_size: MaxSize,
}
#[node_macro::node_fn(ImageColorPaletteNode)]
fn image_color_palette(frame: ImageFrame<Color>, max_size: u32) -> Vec<Color> {
const GRID: f32 = 3.0;
let bins = GRID * GRID * GRID;
let mut histogram: Vec<usize> = vec![0; (bins + 1.0) as usize];
let mut colors: Vec<Vec<Color>> = vec![vec![]; (bins + 1.0) as usize];
for pixel in frame.image.data.iter() {
let r = pixel.r() * GRID;
let g = pixel.g() * GRID;
let b = pixel.b() * GRID;
let bin = (r * GRID + g * GRID + b * GRID) as usize;
histogram[bin] += 1;
colors[bin].push(pixel.to_gamma_srgb());
}
let shorted = histogram.iter().enumerate().filter(|(_, &count)| count > 0).map(|(i, _)| i).collect::<Vec<usize>>();
let mut palette = vec![];
for i in shorted.iter().take(max_size as usize) {
let list = colors[*i].clone();
let mut r = 0.0;
let mut g = 0.0;
let mut b = 0.0;
let mut a = 0.0;
for color in list.iter() {
r += color.r();
g += color.g();
b += color.b();
a += color.a();
}
r /= list.len() as f32;
g /= list.len() as f32;
b /= list.len() as f32;
a /= list.len() as f32;
let color = Color::from_rgbaf32(r, g, b, a).unwrap();
palette.push(color);
}
return palette;
}
#[cfg(test)]
mod test {
use graphene_core::{raster::Image, value::CopiedNode};
use super::*;
#[test]
fn test_image_color_palette() {
assert_eq!(
ImageColorPaletteNode { max_size: CopiedNode(1u32) }.eval(ImageFrame {
image: Image {
width: 100,
height: 100,
data: vec![Color::from_rgbaf32(0.0, 0.0, 0.0, 1.0).unwrap(); 10000],
},
..Default::default()
}),
[Color::from_rgbaf32(0.0, 0.0, 0.0, 1.0).unwrap()]
);
}
}

View file

@ -21,6 +21,8 @@ pub use graphene_core::*;
pub mod image_segmentation; pub mod image_segmentation;
pub mod image_color_palette;
pub mod brush; pub mod brush;
#[cfg(feature = "wasm")] #[cfg(feature = "wasm")]

View file

@ -442,9 +442,11 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
raster_node!(graphene_core::raster::ExtractOpaqueNode<>, params: []), raster_node!(graphene_core::raster::ExtractOpaqueNode<>, params: []),
raster_node!(graphene_core::raster::LevelsNode<_, _, _, _, _>, params: [f32, f32, f32, f32, f32]), raster_node!(graphene_core::raster::LevelsNode<_, _, _, _, _>, params: [f32, f32, f32, f32, f32]),
register_node!(graphene_std::image_segmentation::ImageSegmentationNode<_>, input: ImageFrame<Color>, params: [ImageFrame<Color>]), register_node!(graphene_std::image_segmentation::ImageSegmentationNode<_>, input: ImageFrame<Color>, params: [ImageFrame<Color>]),
register_node!(graphene_std::image_color_palette::ImageColorPaletteNode<_>, input: ImageFrame<Color>, params: [u32]),
register_node!(graphene_core::raster::IndexNode<_>, input: Vec<ImageFrame<Color>>, params: [u32]), register_node!(graphene_core::raster::IndexNode<_>, input: Vec<ImageFrame<Color>>, params: [u32]),
register_node!(graphene_core::raster::adjustments::ColorFillNode<_>, input: ImageFrame<Color>, params: [Color]), register_node!(graphene_core::raster::adjustments::ColorFillNode<_>, input: ImageFrame<Color>, params: [Color]),
register_node!(graphene_core::raster::adjustments::ColorOverlayNode<_, _, _>, input: ImageFrame<Color>, params: [Color, BlendMode, f32]), register_node!(graphene_core::raster::adjustments::ColorOverlayNode<_, _, _>, input: ImageFrame<Color>, params: [Color, BlendMode, f32]),
register_node!(graphene_core::raster::IndexNode<_>, input: Vec<Color>, params: [u32]),
vec![( vec![(
ProtoNodeIdentifier::new("graphene_core::raster::BlendNode<_, _, _, _>"), ProtoNodeIdentifier::new("graphene_core::raster::BlendNode<_, _, _, _>"),
|args| { |args| {
@ -561,8 +563,11 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [bool]), async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [bool]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [String]), async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [String]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [Option<Color>]), async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [Option<Color>]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, params: [Vec<Color>]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => VectorData]), async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => VectorData]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => ImageFrame<Color>]), async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => ImageFrame<Color>]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Option<Color>]),
async_node!(graphene_core::memo::EndLetNode<_, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Vec<Color>]),
async_node!( async_node!(
graphene_core::memo::EndLetNode<_, _>, graphene_core::memo::EndLetNode<_, _>,
input: WasmEditorApi, input: WasmEditorApi,
@ -666,6 +671,9 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => f64, () => Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => f64, () => Arc<WasmSurfaceHandle>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => String, () => Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => String, () => Arc<WasmSurfaceHandle>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => Option<Color>, () => Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => Option<Color>, () => Arc<WasmSurfaceHandle>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Option<Color>, () => Arc<WasmSurfaceHandle>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => Vec<Color>, () => Arc<WasmSurfaceHandle>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Vec<Color>, () => Arc<WasmSurfaceHandle>]),
//register_node!(graphene_core::transform::TranformNode<_, _, _, _, _, _>, input: , output: RenderOutput, fn_params: [Footprint => GraphicGroup, () => Arc<WasmSurfaceHandle>]), //register_node!(graphene_core::transform::TranformNode<_, _, _, _, _, _>, input: , output: RenderOutput, fn_params: [Footprint => GraphicGroup, () => Arc<WasmSurfaceHandle>]),
vec![ vec![
( (