Создаем эффект размытия изображения на C#

Размытие

В этой статье мы рассмотрим, как реализовать эффект размытия поля изображения в C#.

Размытие достигается с помощью свертки изображения, концепции, которая применяется к широкому спектру фильтров изображений.

Извилина

Свертка — это линейная операция над сигналом и ядром. В этом случае сигналом является изображение, а ядром является фильтр. Более конкретно, в дискретной обработке изображений свертка является точечным произведением каждой точки в сигнале с ядром. Итак, очень простой пример:

signal: [1, 2, 3, 4]
kernel: [5, 6, 7]
convolution: [1 * 5 + 2 * 6 + 3 * 7, 
              2 * 5 + 3 * 6 + 4 * 7, 
              3 * 5 + 4 * 6 + 0 * 7,
              4 * 5 + 0 * 6 + 0 * 7]  

Это очень простой пример, но он показывает основы свертки. Обратите внимание, что это требует значительного количества операций, в этом случае наш простой 4×1, спутанный с 3×1, взял 12 умножений и 8 сложений. Это будет важно учитывать. Также обратите внимание, что у края у нас заканчиваются сэмплы в сигнале, чтобы сделать свертку правильно, поэтому вычисления выполняются с 0. Нулевая прокладка является лишь одним из многих возможных способов решения этой проблемы.

Коробочное ядро

Чтобы размыть изображение, нам нужно применить 2D-свертку (так как изображения имеют ширину и высоту). Вопрос в том, как будет выглядеть ядро. С правильным ядром мы можем просто связать его с любым изображением, чтобы размыть его.

Чтобы придумать ядро, нам нужно некоторое простое понимание процесса свертки. Как описано выше, каждый пиксель является точечным произведением ядра и области вокруг этого пикселя. Другими словами, ядро будет диктовать, как один пиксель сочетается с пикселями вокруг него. Итак, вот пример ядра:

[0, 0, 0]
[0, 1, 0]
[0, 0, 0]

Предположим, что ядро центрировано на каждом пикселе при его применении к изображению. Что произойдет? Абсолютно ничего. Это ядро приводит к значению, которое полностью состоит из текущего пикселя (значение 1) и ничего из соседних пикселей (значение 0). (Кстати, это ядро называется дельта).

Теперь давайте посмотрим на другое ядро:

[0,  0,   0 ]
[0, 0.5, 0.5]
[0,  0,   0 ]

Потратьте секунду, чтобы подумать об этом. В этом случае результирующий пиксель представляет собой комбинацию половины значения текущего пикселя и половины значения его правого соседа. Это размытие, потому что каждый пиксель разбросан со своим соседом.

Если мы применим аналогичное ядро к каждому пикселю, мы сделаем что-то очень похожее на усреднение значений пикселей в окрестностях. Усреднение снижает резкость общего изображения, таким образом мы получаем размытие.

Таким образом, мы, наконец, получаем ядро размытия изображения:

[1/9, 1/9, 1/9]
[1/9, 1/9, 1/9]
[1/9, 1/9, 1/9]

Это ядро будет равномерно распределять значения каждого пикселя в изображении. Как мы контролируем количество размытий? Размер ядра. Большее ядро будет усреднять большую площадь, тем самым уменьшая резкость больше. Маленькое ядро будет размываться гораздо меньше.

Создайте проект, WinForms разместите на форме 2 pictureBox в них будет загружатся оригинальное изображение, а в другое размытое. 2 кнопки одна для загрузки изображения, другая для вывода размытия. Так же понадобится numericUpDown величина размытия.

Листинг программы ниже:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;

namespace Convolution
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void btnLoad_Click(object sender, EventArgs e)
        {
            using (OpenFileDialog diag = new OpenFileDialog())
            {
                diag.Filter = "Bitmap|*.bmp;*.jpg;*.gif";
                if (diag.ShowDialog() == DialogResult.OK)
                {
                    try
                    {
                        picOriginal.Image = Image.FromFile(diag.FileName);
                    }
                    catch (Exception)
                    {
                        MessageBox.Show("Invalid Image");
                    }
                }
            }
        }

        private void btnApply_Click(object sender, EventArgs e)
        {
            if (picOriginal.Image != null)
            {
                btnApply.Enabled = false;
                this.Cursor = Cursors.WaitCursor;

                picConvolved.Image = FastBoxBlur(picOriginal.Image, (int)nAmount.Value);

                this.Cursor = Cursors.Default;
                btnApply.Enabled = true;
            }
        }

        private Bitmap Convolve(Bitmap input, float[,] filter)
        {
            //Найти центр фильтра
            int xMiddle = (int)Math.Floor(filter.GetLength(0) / 2.0);
            int yMiddle = (int)Math.Floor(filter.GetLength(1) / 2.0);

            //Создать новый образ
            Bitmap output = new Bitmap(input.Width, input.Height);

            FastBitmap reader = new FastBitmap(input);
            FastBitmap writer = new FastBitmap(output);
            reader.LockImage();
            writer.LockImage();

            for (int x = 0; x < input.Width; x++)
            {
                for (int y = 0; y < input.Height; y++)
                {
                    float r = 0;
                    float g = 0;
                    float b = 0;

                    //Применить фильтр
                    for (int xFilter = 0; xFilter < filter.GetLength(0); xFilter++)
                    {
                        for (int yFilter = 0; yFilter < filter.GetLength(1); yFilter++)
                        {
                            int x0 = x - xMiddle + xFilter;
                            int y0 = y - yMiddle + yFilter;

                            //Только если в границах
                            if (x0 >= 0 && x0 < input.Width &&
                                y0 >= 0 && y0 < input.Height)
                            {
                                Color clr = reader.GetPixel(x0, y0);

                                r += clr.R * filter[xFilter, yFilter];
                                g += clr.G * filter[xFilter, yFilter];
                                b += clr.B * filter[xFilter, yFilter];
                            }
                        }
                    }

                    //Нормализовать (основной)
                    if (r > 255)
                        r = 255;
                    if (g > 255)
                        g = 255;
                    if (b > 255)
                        b = 255;

                    if (r < 0)
                        r = 0;
                    if (g < 0)
                        g = 0;
                    if (b < 0)
                        b = 0;

                    //Установка пикселя
                    writer.SetPixel(x, y, Color.FromArgb((int)r, (int)g, (int)b));
                }
            }

            reader.UnlockImage();
            writer.UnlockImage();

            return output;
        }

        /// <summary>
        /// Возвращает ядро 1D с фильтром в формате {1,..,n}
        /// </summary>
        private float[,] GetHorizontalFilter(int size)
        {
            float[,] smallFilter = new float[size, 1];
            float constant = size;

            for (int i = 0; i < size; i++)
            {
                smallFilter[i, 0] = 1.0f / constant;
            }

            return smallFilter;
        }

        /// <summary>
        /// Возвращает ядро 1D с фильтром в формате {1},...,{n}
        /// </summary>
        private float[,] GetVerticalFilter(int size)
        {
            float[,] smallFilter = new float[1, size];
            float constant = size;

            for (int i = 0; i < size; i++)
            {
                smallFilter[0, i] = 1.0f / constant;
            }

            return smallFilter;
        }

        /// <summary>
        /// Возвращает 2D-ядро  с фильтром в формате {1,...,n},...,{1,...,n}
        /// </summary>
        private float[,] GetBoxFilter(int size)
        {
            float[,] filter = new float[size, size];
            float constant = size * size;

            for (int i = 0; i < filter.GetLength(0); i++)
            {
                for (int j = 0; j < filter.GetLength(1); j++)
                {
                    filter[i, j] = 1.0f / constant;
                }
            }

            return filter;
        }

        private Bitmap BoxBlur(Image img, int size)
        {
            //Применение фильтра путем объединения изображения с помощью 2D-ядра
            return Convolve(new Bitmap(img), GetBoxFilter(size));
        }

        private Bitmap FastBoxBlur(Image img, int size)
        {
            //Применение фильтра путем объединения изображения с помощью двух отдельных 1D-ядер (быстрее)
            return Convolve(Convolve(new Bitmap(img), GetHorizontalFilter(size)), GetVerticalFilter(size));
        }
    }
}

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;

namespace Convolution
{
    unsafe public class FastBitmap
    {
        private struct PixelData
        {
            public byte blue;
            public byte green;
            public byte red;
            public byte alpha;

            public override string ToString()
            {
                return "(" + alpha.ToString() + ", " + red.ToString() + ", " + green.ToString() + ", " + blue.ToString() + ")";
            }
        }

        private Bitmap workingBitmap = null;
        private int width = 0;
        private BitmapData bitmapData = null;
        private Byte* pBase = null;

        public FastBitmap(Bitmap inputBitmap)
        {
            workingBitmap = inputBitmap;
        }

        public void LockImage()
        {
            Rectangle bounds = new Rectangle(Point.Empty, workingBitmap.Size);

            width = (int)(bounds.Width * sizeof(PixelData));
            if (width % 4 != 0) width = 4 * (width / 4 + 1);

            //Блокировка изображения
            bitmapData = workingBitmap.LockBits(bounds, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
            pBase = (Byte*)bitmapData.Scan0.ToPointer();
        }

        private PixelData* pixelData = null;

        public Color GetPixel(int x, int y)
        {
            pixelData = (PixelData*)(pBase + y * width + x * sizeof(PixelData));
            return Color.FromArgb(pixelData->alpha, pixelData->red, pixelData->green, pixelData->blue);
        }

        public Color GetPixelNext()
        {
            pixelData++;
            return Color.FromArgb(pixelData->alpha, pixelData->red, pixelData->green, pixelData->blue);
        }

        public void SetPixel(int x, int y, Color color)
        {
            PixelData* data = (PixelData*)(pBase + y * width + x * sizeof(PixelData));
            data->alpha = color.A;
            data->red = color.R;
            data->green = color.G;
            data->blue = color.B;
        }

        public void UnlockImage()
        {
            workingBitmap.UnlockBits(bitmapData);
            bitmapData = null;
            pBase = null;
        }
    }
}

 

Обновлено: 08.01.2022 — 17:04

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.