Browse Source

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.
kleines Filmröllchen 2 years ago
parent
commit
ec52d16f7a

+ 3 - 0
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(FilterGallery.gml FilterGalleryGML.h filter_gallery_gml)
 compile_gml(ResizeImageDialog.gml ResizeImageDialogGML.h resize_image_dialog_gml)
 compile_gml(ResizeImageDialog.gml ResizeImageDialogGML.h resize_image_dialog_gml)
 compile_gml(LevelsDialog.gml LevelsDialogGML.h levels_dialog_gml)
 compile_gml(LevelsDialog.gml LevelsDialogGML.h levels_dialog_gml)
+compile_gml(Filters/MedianSettings.gml Filters/MedianSettingsGML.h median_settings_gml)
 
 
 set(SOURCES
 set(SOURCES
     CreateNewImageDialog.cpp
     CreateNewImageDialog.cpp
@@ -31,6 +32,8 @@ set(SOURCES
     Filters/Invert.cpp
     Filters/Invert.cpp
     Filters/LaplaceCardinal.cpp
     Filters/LaplaceCardinal.cpp
     Filters/LaplaceDiagonal.cpp
     Filters/LaplaceDiagonal.cpp
+    Filters/Median.cpp
+    Filters/MedianSettingsGML.h
     Filters/Sepia.cpp
     Filters/Sepia.cpp
     Filters/Sharpen.cpp
     Filters/Sharpen.cpp
     HistogramWidget.cpp
     HistogramWidget.cpp

+ 2 - 0
Userland/Applications/PixelPaint/FilterTreeModel.cpp

@@ -17,6 +17,7 @@
 #include "Filters/Invert.h"
 #include "Filters/Invert.h"
 #include "Filters/LaplaceCardinal.h"
 #include "Filters/LaplaceCardinal.h"
 #include "Filters/LaplaceDiagonal.h"
 #include "Filters/LaplaceDiagonal.h"
+#include "Filters/Median.h"
 #include "Filters/Sepia.h"
 #include "Filters/Sepia.h"
 #include "Filters/Sharpen.h"
 #include "Filters/Sharpen.h"
 #include <LibGUI/FileIconProvider.h>
 #include <LibGUI/FileIconProvider.h>
@@ -51,6 +52,7 @@ ErrorOr<NonnullRefPtr<GUI::TreeViewModel>> create_filter_tree_model(ImageEditor*
     add_filter_node.template operator()<Filters::BoxBlur3>(blur_category);
     add_filter_node.template operator()<Filters::BoxBlur3>(blur_category);
     add_filter_node.template operator()<Filters::BoxBlur5>(blur_category);
     add_filter_node.template operator()<Filters::BoxBlur5>(blur_category);
     add_filter_node.template operator()<Filters::Sharpen>(blur_category);
     add_filter_node.template operator()<Filters::Sharpen>(blur_category);
+    add_filter_node.template operator()<Filters::Median>(blur_category);
 
 
     auto color_category = filter_tree_model->add_node("Color", directory_icon);
     auto color_category = filter_tree_model->add_node("Color", directory_icon);
     add_filter_node.template operator()<Filters::Grayscale>(color_category);
     add_filter_node.template operator()<Filters::Grayscale>(color_category);

+ 59 - 0
Userland/Applications/PixelPaint/Filters/Median.cpp

@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "Median.h"
+#include <AK/QuickSort.h>
+#include <Applications/PixelPaint/Filters/MedianSettingsGML.h>
+#include <LibGUI/SpinBox.h>
+
+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<int>(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<int>(m_filter_radius - 1);
+            int top = y - static_cast<int>(m_filter_radius - 1);
+            Vector<Color, 16> values;
+            values.ensure_capacity(static_cast<size_t>(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<GUI::Widget> 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<GUI::SpinBox>("filter_radius")->on_change = [this](auto value) {
+            m_filter_radius = value;
+            update_preview();
+        };
+    }
+
+    return m_settings_widget;
+}
+
+}

+ 33 - 0
Userland/Applications/PixelPaint/Filters/Median.h

@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include "Filter.h"
+#include <LibGUI/Widget.h>
+
+namespace PixelPaint::Filters {
+
+class Median : public Filter {
+public:
+    virtual void apply(Gfx::Bitmap& target_bitmap, Gfx::Bitmap const& source_bitmap) const override;
+
+    virtual RefPtr<GUI::Widget> 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 };
+};
+
+}

+ 16 - 0
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
+    }
+}