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