Размытие
В этой статье мы рассмотрим, как реализовать эффект размытия поля изображения в 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; } } }