From ec52d16f7a28deb576dfa90eb31e200c0f7223eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kleines=20Filmr=C3=B6llchen?= Date: Fri, 12 Aug 2022 22:59:03 +0200 Subject: [PATCH] PixelPaint: Add Median filter The median filter replaces a pixel with the median of all pixels (usually grey value is used) in a square neighborhood. This is a standard image processing filter used for denoising, as despite its simplicity it can e.g. retain edges quite well. The first implementation is quite inefficient mostly to environmental constraints. Due to how images are passed to the processing function, two unnecessary copies happen. And because there's no fast sorting algorithm for small arrays (insertion sort) yet, quick sort needs to be used which is quite slow on this scale. --- .../Applications/PixelPaint/CMakeLists.txt | 3 + .../PixelPaint/FilterTreeModel.cpp | 2 + .../PixelPaint/Filters/Median.cpp | 59 +++++++++++++++++++ .../Applications/PixelPaint/Filters/Median.h | 33 +++++++++++ .../PixelPaint/Filters/MedianSettings.gml | 16 +++++ 5 files changed, 113 insertions(+) create mode 100644 Userland/Applications/PixelPaint/Filters/Median.cpp create mode 100644 Userland/Applications/PixelPaint/Filters/Median.h create mode 100644 Userland/Applications/PixelPaint/Filters/MedianSettings.gml diff --git a/Userland/Applications/PixelPaint/CMakeLists.txt b/Userland/Applications/PixelPaint/CMakeLists.txt index 76b224bbf3..91956db580 100644 --- a/Userland/Applications/PixelPaint/CMakeLists.txt +++ b/Userland/Applications/PixelPaint/CMakeLists.txt @@ -10,6 +10,7 @@ compile_gml(EditGuideDialog.gml EditGuideDialogGML.h edit_guide_dialog_gml) compile_gml(FilterGallery.gml FilterGalleryGML.h filter_gallery_gml) compile_gml(ResizeImageDialog.gml ResizeImageDialogGML.h resize_image_dialog_gml) compile_gml(LevelsDialog.gml LevelsDialogGML.h levels_dialog_gml) +compile_gml(Filters/MedianSettings.gml Filters/MedianSettingsGML.h median_settings_gml) set(SOURCES CreateNewImageDialog.cpp @@ -31,6 +32,8 @@ set(SOURCES Filters/Invert.cpp Filters/LaplaceCardinal.cpp Filters/LaplaceDiagonal.cpp + Filters/Median.cpp + Filters/MedianSettingsGML.h Filters/Sepia.cpp Filters/Sharpen.cpp HistogramWidget.cpp diff --git a/Userland/Applications/PixelPaint/FilterTreeModel.cpp b/Userland/Applications/PixelPaint/FilterTreeModel.cpp index 07133f1e83..fd58238e99 100644 --- a/Userland/Applications/PixelPaint/FilterTreeModel.cpp +++ b/Userland/Applications/PixelPaint/FilterTreeModel.cpp @@ -17,6 +17,7 @@ #include "Filters/Invert.h" #include "Filters/LaplaceCardinal.h" #include "Filters/LaplaceDiagonal.h" +#include "Filters/Median.h" #include "Filters/Sepia.h" #include "Filters/Sharpen.h" #include @@ -51,6 +52,7 @@ ErrorOr> create_filter_tree_model(ImageEditor* add_filter_node.template operator()(blur_category); add_filter_node.template operator()(blur_category); add_filter_node.template operator()(blur_category); + add_filter_node.template operator()(blur_category); auto color_category = filter_tree_model->add_node("Color", directory_icon); add_filter_node.template operator()(color_category); diff --git a/Userland/Applications/PixelPaint/Filters/Median.cpp b/Userland/Applications/PixelPaint/Filters/Median.cpp new file mode 100644 index 0000000000..f051e00469 --- /dev/null +++ b/Userland/Applications/PixelPaint/Filters/Median.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022, kleines Filmröllchen + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Median.h" +#include +#include +#include + +namespace PixelPaint::Filters { + +void Median::apply(Gfx::Bitmap& target_bitmap, Gfx::Bitmap const& source_bitmap) const +{ + // FIXME: Is there a better way to work around aliasing in the source and target? + auto target = MUST(source_bitmap.clone()); + + int filter_size = static_cast(this->filter_size()); + for (int x = 0; x < target_bitmap.width(); ++x) { + for (int y = 0; y < target_bitmap.height(); ++y) { + int left = x - static_cast(m_filter_radius - 1); + int top = y - static_cast(m_filter_radius - 1); + Vector values; + values.ensure_capacity(static_cast(filter_size * filter_size)); + for (int i = left; i < left + filter_size; ++i) { + for (int j = top; j < top + filter_size; ++j) { + if (j < 0 || i < 0 || j >= source_bitmap.height() || i >= source_bitmap.width()) + continue; + values.unchecked_append(source_bitmap.get_pixel(i, j)); + } + } + // FIXME: If there was an insertion sort in AK, we should better use that here. + // Sort the values to be able to extract the median. The median is determined by grey value (luminosity). + quick_sort(values, [](auto& a, auto& b) { return a.luminosity() < b.luminosity(); }); + target->set_pixel(x, y, values[values.size() / 2]); + } + } + + // FIXME: Can we move the `target`s data into the actual target bitmap? Can't be too hard, right? + Gfx::Painter painter(target_bitmap); + painter.blit({}, target, target->rect()); +} + +RefPtr Median::get_settings_widget() +{ + if (!m_settings_widget) { + m_settings_widget = GUI::Widget::construct(); + m_settings_widget->load_from_gml(median_settings_gml); + m_settings_widget->find_descendant_of_type_named("filter_radius")->on_change = [this](auto value) { + m_filter_radius = value; + update_preview(); + }; + } + + return m_settings_widget; +} + +} diff --git a/Userland/Applications/PixelPaint/Filters/Median.h b/Userland/Applications/PixelPaint/Filters/Median.h new file mode 100644 index 0000000000..e41e9e2776 --- /dev/null +++ b/Userland/Applications/PixelPaint/Filters/Median.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022, kleines Filmröllchen + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "Filter.h" +#include + +namespace PixelPaint::Filters { + +class Median : public Filter { +public: + virtual void apply(Gfx::Bitmap& target_bitmap, Gfx::Bitmap const& source_bitmap) const override; + + virtual RefPtr get_settings_widget() override; + + virtual StringView filter_name() override { return "Median Filter"sv; } + + Median(ImageEditor* editor) + : Filter(editor) + { + } + +private: + unsigned filter_size() const { return m_filter_radius * 2 - 1; } + + unsigned m_filter_radius { 2 }; +}; + +} diff --git a/Userland/Applications/PixelPaint/Filters/MedianSettings.gml b/Userland/Applications/PixelPaint/Filters/MedianSettings.gml new file mode 100644 index 0000000000..62295bb191 --- /dev/null +++ b/Userland/Applications/PixelPaint/Filters/MedianSettings.gml @@ -0,0 +1,16 @@ +@GUI::Widget { + fill_with_background_color: true + layout: @GUI::HorizontalBoxLayout {} + + @GUI::Label { + text: "Median filter radius" + text_alignment: "CenterLeft" + width: "shrink" + } + + @GUI::SpinBox { + name: "filter_radius" + min: 1 + max: 5000 + } +}