From 1f0b145740259acaf679ed9879df7e36bfe06ae2 Mon Sep 17 00:00:00 2001 From: Bruno Herbelin Date: Wed, 8 Jun 2022 23:44:19 +0200 Subject: [PATCH] Original implementation of Smooth Image filters Smoothing and noise reduction filters + noise generators. --- CloneSource.cpp | 3 + FrameBufferFilter.cpp | 2 +- FrameBufferFilter.h | 1 + ImGuiVisitor.cpp | 121 ++++++++++++++++++++---------- ImGuiVisitor.h | 1 + ImageFilter.cpp | 63 +++++++++++++--- ImageFilter.h | 35 +++++++++ SessionCreator.cpp | 24 ++++++ SessionCreator.h | 1 + SessionVisitor.cpp | 16 ++++ SessionVisitor.h | 1 + Visitor.h | 2 + rsc/images/icons.dds | Bin 1638528 -> 1638528 bytes rsc/shaders/filters/dilation.glsl | 8 +- rsc/shaders/filters/erosion.glsl | 8 +- rsc/shaders/filters/focus.glsl | 13 ++-- rsc/shaders/filters/grain.glsl | 8 +- rsc/shaders/filters/kuwahara.glsl | 112 +++++++++++++-------------- rsc/shaders/filters/noise.glsl | 43 +++++------ 19 files changed, 311 insertions(+), 151 deletions(-) diff --git a/CloneSource.cpp b/CloneSource.cpp index 1155a56..741487d 100644 --- a/CloneSource.cpp +++ b/CloneSource.cpp @@ -163,6 +163,9 @@ void CloneSource::setFilter(FrameBufferFilter::Type T) case FrameBufferFilter::FILTER_SHARPEN: filter_ = new SharpenFilter; break; + case FrameBufferFilter::FILTER_SMOOTH: + filter_ = new SmoothFilter; + break; case FrameBufferFilter::FILTER_EDGE: filter_ = new EdgeFilter; break; diff --git a/FrameBufferFilter.cpp b/FrameBufferFilter.cpp index 4ee7b0f..efcd847 100644 --- a/FrameBufferFilter.cpp +++ b/FrameBufferFilter.cpp @@ -6,7 +6,7 @@ #include "FrameBufferFilter.h" const char* FrameBufferFilter::type_label[FrameBufferFilter::FILTER_INVALID] = { - "None", "Delay", "Resample", "Blur", "Sharpen", "Edge", "Transparency", "Shader code" + "None", "Delay", "Resample", "Blur", "Sharpen", "Smooth & Noise", "Edge", "Transparency", "Custom shader" }; FrameBufferFilter::FrameBufferFilter() : enabled_(true), input_(nullptr) diff --git a/FrameBufferFilter.h b/FrameBufferFilter.h index faad623..11a0743 100644 --- a/FrameBufferFilter.h +++ b/FrameBufferFilter.h @@ -23,6 +23,7 @@ public: FILTER_RESAMPLE, FILTER_BLUR, FILTER_SHARPEN, + FILTER_SMOOTH, FILTER_EDGE, FILTER_ALPHA, FILTER_IMAGE, diff --git a/ImGuiVisitor.cpp b/ImGuiVisitor.cpp index 6ca8087..c8b2712 100644 --- a/ImGuiVisitor.cpp +++ b/ImGuiVisitor.cpp @@ -851,6 +851,51 @@ void ImGuiVisitor::visit (SharpenFilter& f) } } +void ImGuiVisitor::visit (SmoothFilter& f) +{ + std::ostringstream oss; + oss << "Smooth "; + + // Method selection + if (ImGuiToolkit::IconButton(14, 8)) { + f.setMethod( 0 ); + oss << SmoothFilter::method_label[0]; + Action::manager().store(oss.str()); + } + ImGui::SameLine(0, IMGUI_SAME_LINE); + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + int m = (int) f.method(); + if (ImGui::Combo("Method", &m, SmoothFilter::method_label, IM_ARRAYSIZE(SmoothFilter::method_label) )) { + f.setMethod( m ); + oss << SmoothFilter::method_label[m]; + Action::manager().store(oss.str()); + } + + // List of parameters + std::map filter_parameters = f.program().parameters(); + for (auto param = filter_parameters.begin(); param != filter_parameters.end(); ++param) + { + ImGui::PushID( param->first.c_str() ); + float v = param->second; + if (ImGuiToolkit::IconButton(13, 14)) { + v = 0.f; + f.setProgramParameter(param->first, v); + } + ImGui::SameLine(0, IMGUI_SAME_LINE); + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + if (ImGui::SliderFloat( param->first.c_str(), &v, 0.f, 1.f, "%.2f")) { + f.setProgramParameter(param->first, v); + } + if (ImGui::IsItemDeactivatedAfterEdit()) { + oss << EdgeFilter::method_label[ f.method() ]; + oss << " : " << param->first << " " << std::setprecision(3) <second; + Action::manager().store(oss.str()); + } + ImGui::PopID(); + } +} + + void ImGuiVisitor::visit (EdgeFilter& f) { std::ostringstream oss; @@ -988,47 +1033,47 @@ void ImGuiVisitor::visit (ImageFilter& f) FilteringProgram target; f.setProgram( target ); } - ImGui::SameLine(0, IMGUI_SAME_LINE); - ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); - if (ImGui::BeginCombo("Algorithm", f.program().name().c_str()) ) - { - for (auto p = FilteringProgram::presets.begin(); p != FilteringProgram::presets.end(); ++p){ - if (ImGui::Selectable( p->name().c_str() )) { - // apply the selected filter to the source - f.setProgram( *p ); - } - } - ImGui::EndCombo(); - } +// ImGui::SameLine(0, IMGUI_SAME_LINE); +// ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); +// if (ImGui::BeginCombo("Algorithm", f.program().name().c_str()) ) +// { +// for (auto p = FilteringProgram::presets.begin(); p != FilteringProgram::presets.end(); ++p){ +// if (ImGui::Selectable( p->name().c_str() )) { +// // apply the selected filter to the source +// f.setProgram( *p ); +// } +// } +// ImGui::EndCombo(); +// } - // List of parameters - std::map filter_parameters = f.program().parameters(); - for (auto param = filter_parameters.begin(); param != filter_parameters.end(); ++param) - { - ImGui::PushID( param->first.c_str() ); - float v = param->second; - if (ImGuiToolkit::IconButton(13, 14)) { - v = 0.f; - f.setProgramParameter(param->first, v); - } - ImGui::SameLine(0, IMGUI_SAME_LINE); - ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); - if (ImGui::SliderFloat( param->first.c_str(), &v, 0.f, 1.f, "%.2f")) { - f.setProgramParameter(param->first, v); - } - if (ImGui::IsItemDeactivatedAfterEdit()) { - // TODO UNDO - // std::ostringstream oss; - // oss << "Delay " << std::setprecision(3) << d << " s"; - // Action::manager().store(oss.str()); - } - ImGui::PopID(); - } +// // List of parameters +// std::map filter_parameters = f.program().parameters(); +// for (auto param = filter_parameters.begin(); param != filter_parameters.end(); ++param) +// { +// ImGui::PushID( param->first.c_str() ); +// float v = param->second; +// if (ImGuiToolkit::IconButton(13, 14)) { +// v = 0.f; +// f.setProgramParameter(param->first, v); +// } +// ImGui::SameLine(0, IMGUI_SAME_LINE); +// ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); +// if (ImGui::SliderFloat( param->first.c_str(), &v, 0.f, 1.f, "%.2f")) { +// f.setProgramParameter(param->first, v); +// } +// if (ImGui::IsItemDeactivatedAfterEdit()) { +// // TODO UNDO +// // std::ostringstream oss; +// // oss << "Delay " << std::setprecision(3) << d << " s"; +// // Action::manager().store(oss.str()); +// } +// ImGui::PopID(); +// } // Open Editor - ImGuiToolkit::IconButton(18, 18); +// ImGuiToolkit::IconButton(18, 18); ImGui::SameLine(0, IMGUI_SAME_LINE); - if ( ImGui::Button( ICON_FA_CODE " Edit", ImVec2(IMGUI_RIGHT_ALIGN, 0)) ) + if ( ImGui::Button( ICON_FA_CODE " Open editor", ImVec2(IMGUI_RIGHT_ALIGN, 0)) ) Settings::application.widget.shader_editor = true; ImGui::SameLine(0, IMGUI_SAME_LINE); ImGui::Text("Code"); @@ -1068,7 +1113,7 @@ void ImGuiVisitor::visit (CloneSource& s) ImGui::SameLine(0, IMGUI_SAME_LINE); ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); int type = (int) s.filter()->type(); - if (ImGui::Combo("Filter", &type, FrameBufferFilter::type_label, IM_ARRAYSIZE(FrameBufferFilter::type_label) )) { + if (ImGui::Combo("Filter", &type, FrameBufferFilter::type_label, IM_ARRAYSIZE(FrameBufferFilter::type_label), FrameBufferFilter::FILTER_INVALID)) { s.setFilter( FrameBufferFilter::Type(type) ); oss << ": Filter " << FrameBufferFilter::type_label[type]; Action::manager().store(oss.str()); diff --git a/ImGuiVisitor.h b/ImGuiVisitor.h index be93dd8..2026e75 100644 --- a/ImGuiVisitor.h +++ b/ImGuiVisitor.h @@ -42,6 +42,7 @@ public: void visit (ResampleFilter&) override; void visit (BlurFilter&) override; void visit (SharpenFilter&) override; + void visit (SmoothFilter&) override; void visit (EdgeFilter&) override; void visit (AlphaFilter&) override; void visit (ImageFilter&) override; diff --git a/ImageFilter.cpp b/ImageFilter.cpp index 900b88f..3658682 100644 --- a/ImageFilter.cpp +++ b/ImageFilter.cpp @@ -78,23 +78,13 @@ std::string fragmentFooter = "void main() {\n" std::list< FilteringProgram > FilteringProgram::presets = { FilteringProgram(), - FilteringProgram("Unfocus", "shaders/filters/focus.glsl", "", { { "Factor", 0.5} }), - FilteringProgram("Smooth", "shaders/filters/bilinear.glsl", "", { }), - FilteringProgram("Kuwahara", "shaders/filters/kuwahara.glsl", "", { { "Radius", 1.0} }), - FilteringProgram("Denoise", "shaders/filters/denoise.glsl", "", { { "Threshold", 0.5} }), - FilteringProgram("Noise", "shaders/filters/noise.glsl", "", { { "Amount", 0.25} }), - FilteringProgram("Grain", "shaders/filters/grain.glsl", "", { { "Amount", 0.5} }), + FilteringProgram("Bilateral","shaders/filters/bilinear.glsl", "", { }), FilteringProgram("Pixelate", "shaders/filters/pixelate.glsl", "", { { "Size", 0.5}, { "Sharpen", 0.5} }), - FilteringProgram("Erosion", "shaders/filters/erosion.glsl", "", { { "Radius", 0.5} }), - FilteringProgram("Dilation", "shaders/filters/dilation.glsl", "", { { "Radius", 0.5} }), - FilteringProgram("Openning", "shaders/filters/erosion.glsl", "shaders/filters/dilation.glsl", { { "Radius", 0.5} }), - FilteringProgram("Closing", "shaders/filters/dilation.glsl", "shaders/filters/erosion.glsl", { { "Radius", 0.5} }), FilteringProgram("Bloom", "shaders/filters/bloom.glsl", "", { { "Intensity", 0.5} }), FilteringProgram("Bokeh", "shaders/filters/bokeh.glsl", "", { { "Radius", 1.0} }), FilteringProgram("Chalk", "shaders/filters/talk.glsl", "", { { "Factor", 1.0} }), FilteringProgram("Stippling","shaders/filters/stippling.glsl", "", { { "Factor", 0.5} }), FilteringProgram("Dithering","shaders/filters/dithering.glsl", "", { { "Factor", 0.5} }), - FilteringProgram("Chromakey","shaders/filters/chromakey.glsl", "", { { "Red", 0.05}, { "Green", 0.63}, { "Blue", 0.14}, { "Tolerance", 0.54} }), FilteringProgram("Fisheye", "shaders/filters/fisheye.glsl", "", { { "Factor", 0.35} }), }; @@ -693,7 +683,7 @@ const char* SharpenFilter::method_label[SharpenFilter::SHARPEN_INVALID] = { }; std::vector< FilteringProgram > SharpenFilter::programs_ = { - FilteringProgram("Unsharp Mask", "shaders/filters/sharpen_1.glsl", "shaders/filters/sharpen_2.glsl", { { "Amount", 0.5} }), + FilteringProgram("UnsharpMask", "shaders/filters/sharpen_1.glsl", "shaders/filters/sharpen_2.glsl", { { "Amount", 0.5} }), FilteringProgram("Sharpen", "shaders/filters/sharpen.glsl", "", { { "Amount", 0.5} }), FilteringProgram("Sharp Edge", "shaders/filters/sharpenedge.glsl","", { { "Amount", 0.5} }), FilteringProgram("TopHat", "shaders/filters/erosion.glsl", "shaders/filters/tophat.glsl", { { "Radius", 0.5} }), @@ -726,6 +716,55 @@ void SharpenFilter::accept (Visitor& v) } + +//////////////////////////////////////// +///// // +//// SMOOTHING FILTERS /// +/// //// +//////////////////////////////////////// + +const char* SmoothFilter::method_label[SmoothFilter::SMOOTH_INVALID] = { + "Bilateral", "Kuwahara", "Opening", "Closing", "Erosion", "Dilation", "Remove noise", "Add noise", "Add grain" +}; + +std::vector< FilteringProgram > SmoothFilter::programs_ = { + FilteringProgram("Bilateral","shaders/filters/focus.glsl", "", { { "Factor", 0.5} }), + FilteringProgram("Kuwahara", "shaders/filters/kuwahara.glsl", "", { { "Radius", 1.0} }), + FilteringProgram("Opening", "shaders/filters/erosion.glsl", "shaders/filters/dilation.glsl", { { "Radius", 0.5} }), + FilteringProgram("Closing", "shaders/filters/dilation.glsl", "shaders/filters/erosion.glsl", { { "Radius", 0.5} }), + FilteringProgram("Erosion", "shaders/filters/erosion.glsl", "", { { "Radius", 0.5} }), + FilteringProgram("Dilation", "shaders/filters/dilation.glsl", "", { { "Radius", 0.5} }), + FilteringProgram("Denoise", "shaders/filters/denoise.glsl", "", { { "Threshold", 0.5} }), + FilteringProgram("Noise", "shaders/filters/noise.glsl", "", { { "Amount", 0.25} }), + FilteringProgram("Grain", "shaders/filters/grain.glsl", "", { { "Amount", 0.5} }) +}; + +SmoothFilter::SmoothFilter (): ImageFilter(), method_(SMOOTH_INVALID) +{ +} + +void SmoothFilter::setMethod(int method) +{ + method_ = (SmoothMethod) CLAMP(method, SMOOTH_BILINEAR, SMOOTH_INVALID-1); + setProgram( programs_[ (int) method_] ); +} + +void SmoothFilter::draw (FrameBuffer *input) +{ + // Default + if (method_ == SMOOTH_INVALID) + setMethod( SMOOTH_BILINEAR ); + + ImageFilter::draw( input ); +} + +void SmoothFilter::accept (Visitor& v) +{ + FrameBufferFilter::accept(v); + v.visit(*this); +} + + //////////////////////////////////////// ///// // //// EDGE FILTERS /// diff --git a/ImageFilter.h b/ImageFilter.h index 5569fc8..ca4c9b7 100644 --- a/ImageFilter.h +++ b/ImageFilter.h @@ -198,6 +198,41 @@ private: }; +class SmoothFilter : public ImageFilter +{ +public: + + SmoothFilter(); + + // Algorithms used for smoothing + typedef enum { + SMOOTH_BILINEAR = 0, + SMOOTH_KUWAHARA, + SMOOTH_OPENING, + SMOOTH_CLOSING, + SMOOTH_EROSION, + SMOOTH_DILATION, + SMOOTH_DENOISE, + SMOOTH_ADDNOISE, + SMOOTH_ADDGRAIN, + SMOOTH_INVALID + } SmoothMethod; + static const char* method_label[SMOOTH_INVALID]; + SmoothMethod method () const { return method_; } + void setMethod(int method); + + // implementation of FrameBufferFilter + Type type() const override { return FrameBufferFilter::FILTER_SMOOTH; } + + void draw (FrameBuffer *input) override; + void accept (Visitor& v) override; + +private: + SmoothMethod method_; + static std::vector< FilteringProgram > programs_; +}; + + class EdgeFilter : public ImageFilter { public: diff --git a/SessionCreator.cpp b/SessionCreator.cpp index 600543d..3de0f1d 100644 --- a/SessionCreator.cpp +++ b/SessionCreator.cpp @@ -1275,6 +1275,30 @@ void SessionLoader::visit (SharpenFilter& f) f.setProgramParameters(filter_params); } +void SessionLoader::visit (SmoothFilter& f) +{ + int m = 0; + xmlCurrent_->QueryIntAttribute("method", &m); + f.setMethod(m); + + std::map< std::string, float > filter_params; + XMLElement* parameters = xmlCurrent_->FirstChildElement("parameters"); + if (parameters) { + XMLElement* param = parameters->FirstChildElement("uniform"); + for( ; param ; param = param->NextSiblingElement()) + { + float val = 0.f; + param->QueryFloatAttribute("value", &val); + const char * name; + param->QueryStringAttribute("name", &name); + if (name) + filter_params[name] = val; + } + } + + f.setProgramParameters(filter_params); +} + void SessionLoader::visit (EdgeFilter& f) { int m = 0; diff --git a/SessionCreator.h b/SessionCreator.h index e971bfd..12bc38f 100644 --- a/SessionCreator.h +++ b/SessionCreator.h @@ -68,6 +68,7 @@ public: void visit (BlurFilter&) override; void visit (SharpenFilter&) override; void visit (EdgeFilter&) override; + void visit (SmoothFilter&) override; void visit (AlphaFilter&) override; void visit (ImageFilter&) override; diff --git a/SessionVisitor.cpp b/SessionVisitor.cpp index ebecfe3..336d24d 100644 --- a/SessionVisitor.cpp +++ b/SessionVisitor.cpp @@ -725,6 +725,22 @@ void SessionVisitor::visit (SharpenFilter& f) } } +void SessionVisitor::visit (SmoothFilter& f) +{ + xmlCurrent_->SetAttribute("method", (int) f.method()); + + std::map< std::string, float > filter_params = f.program().parameters(); + XMLElement *parameters = xmlDoc_->NewElement( "parameters" ); + xmlCurrent_->InsertEndChild(parameters); + for (auto p = filter_params.begin(); p != filter_params.end(); ++p) + { + XMLElement *param = xmlDoc_->NewElement( "uniform" ); + param->SetAttribute("name", p->first.c_str() ); + param->SetAttribute("value", p->second ); + parameters->InsertEndChild(param); + } +} + void SessionVisitor::visit (EdgeFilter& f) { xmlCurrent_->SetAttribute("method", (int) f.method()); diff --git a/SessionVisitor.h b/SessionVisitor.h index 3a39c19..fcd05ce 100644 --- a/SessionVisitor.h +++ b/SessionVisitor.h @@ -75,6 +75,7 @@ public: void visit (ResampleFilter&) override; void visit (BlurFilter&) override; void visit (SharpenFilter&) override; + void visit (SmoothFilter&) override; void visit (EdgeFilter&) override; void visit (AlphaFilter&) override; void visit (ImageFilter&) override; diff --git a/Visitor.h b/Visitor.h index d751ebc..da425b7 100644 --- a/Visitor.h +++ b/Visitor.h @@ -47,6 +47,7 @@ class DelayFilter; class ResampleFilter; class BlurFilter; class SharpenFilter; +class SmoothFilter; class EdgeFilter; class AlphaFilter; class ImageFilter; @@ -112,6 +113,7 @@ public: virtual void visit (ResampleFilter&) {} virtual void visit (BlurFilter&) {} virtual void visit (SharpenFilter&) {} + virtual void visit (SmoothFilter&) {} virtual void visit (EdgeFilter&) {} virtual void visit (AlphaFilter&) {} virtual void visit (ImageFilter&) {} diff --git a/rsc/images/icons.dds b/rsc/images/icons.dds index cf10a2aea8260985e04e695e30066ad6d6039e2f..6c8a230dbc056a9717c898d24ae9618a1830fa59 100644 GIT binary patch delta 8081 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 delta 6762 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