Наложение шума на изображение методами ООП
Столкнулся с трудностями, пока пытался создать фильтр для наложения шума на изображение.
namespace PhotoEnhancer
{
public class NoiseFilter : PixelFilter<NoiseParameters>
{
public override string ToString()
{
return "Интенсивность шума";
}
public override Pixel ProcessPixel(Pixel originalPixel,
NoiseParameters parameters)
{
var red = originalPixel.R;
var green = originalPixel.G;
var blue = originalPixel.B;
var rnd = new Random();
for (var i = 0; i < 340; i++)
{
var r = (rnd.NextDouble());
red = (parameters.Intensity * r + (1 - parameters.Intensity) * originalPixel.R);
var r2 = (rnd.NextDouble());
green = (parameters.Intensity * r2 + (1 - parameters.Intensity) * originalPixel.G);
var r3 = (rnd.NextDouble());
blue = (parameters.Intensity * r3 + (1 - parameters.Intensity) * originalPixel.B);
}
return new Pixel(red, green, blue);
}
}
}
Сверху код фильтра, он принимает параметры из соответствующего класса(снизу).
namespace PhotoEnhancer
{
public class NoiseParameters : IParameters
{
public double Intensity { get; set; }
public ParameterInfo[] GetDescription()
{
return new[]
{
new ParameterInfo() {
Name = "Интенсивность",
MinValue = 0,
MaxValue = 1,
DefaultValue = 0,
Increment = 0.01
}
};
}
public void SetValues(double[] values)
{
Intensity = values[0];
}
}
}
Собственно, чтобы наложить шум на изображение "правильно", мне надо получить ширину и высоту изображения, но я не понимаю, как их вытащить из ещё одного модуля, в котором они вытаскиваются через { get;}. Естественно, что мне нужны именно ширина и высота оттуда, но сколько я не ломал голову, не получается придумать способа их оттуда вытащить. В коде создал обычный цикл, чтобы проверить, как работает алгебра, шум накладывается, шум цветной, но моя задача сделать шум на каждый пиксель в изображении, а не просто полоски цветные на картинке.
Код загрузки
private void openToolStripMenuItem_Click(object sender, EventArgs e)
{
if(openFileDialog1.ShowDialog() == DialogResult.OK)
{
buttonApply.Visible = true;
comboBoxFilters.Visible = true;
if (comboBoxFilters.SelectedIndex == -1)
comboBoxFilters.SelectedIndex = 0;
LoadPicture((Bitmap)Image.FromFile(openFileDialog1.FileName));
}
}
Код обработки изображения.
public static Bitmap Photo2Bitmap(Photo photo)
{
var result = new Bitmap(photo.Width, photo.Height);
for(var x = 0; x < photo.Width; x++)
for(var y = 0; y < photo.Height; y++)
{
result.SetPixel(x, y, Color.FromArgb(
(int)(photo[x, y].R * 255),
(int)(photo[x, y].G * 255),
(int)(photo[x, y].B * 255)
));
}
return result;
}
PixelFilter<>
public abstract class PixelFilter<TParameters> : ParametrizedFilter<TParameters>
where TParameters : IParameters, new()
{
public override Photo Process(Photo original, TParameters parameters)
{
var newPhoto = new Photo(original.Width, original.Height);
for (int x = 0; x < original.Width; x++)
for (int y = 0; y < original.Height; y++)
newPhoto[x, y] = ProcessPixel(original[x, y],
parameters);
return newPhoto;
}
public abstract Pixel ProcessPixel(Pixel originalPixel,
TParameters parameters);
}
Класс Photo.
public class Photo
{
public int Width
{
get { return data.GetLength(0); }
}
public int Height
{
get { return data.GetLength(1); }
}
private Pixel[,] data;
public Photo(int width, int height)
{
data = new Pixel[
CheckSize(width, "ширина"),
CheckSize(height, "высота")
];
}
public Pixel this[int x, int y]
{
get { return data[x, y]; }
set { data[x, y] = value; }
}
private int CheckSize(int val, string name)
{
if (val <= 0)
throw new Exception($"Неверная {name} {val}. Размер должен быть положительный");
return val;
}
}
Ответы (1 шт):
Быстрое преобразование Bitmap
Работать с Bitmap через GetPixel/SetPixel - это просто супермедленно. Вот например фильтр - как сделать картинку черно-белой (просто у меня был готовый пример):
public static Bitmap ToGrayScale(Bitmap bmp)
{
Rectangle rect = new Rectangle(Point.Empty, bmp.Size);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
int stride = Math.Abs(bmpData.Stride);
int length = stride * bmp.Height;
byte[] buffer = new byte[length]; // содержимое буфера будет [B G R B G R B G R...]
Marshal.Copy(bmpData.Scan0, buffer, 0, bmpData.Stride * bmp.Height); // stride может быть отрицательным (строчки снизу вверх), именно здесь это важно
bmp.UnlockBits(bmpData);
for (int row = 0; row < bmp.Height; row++)
{
int rowOffset = stride * row;
for (int col = 0; col < bmp.Width; col++)
{
int offset = rowOffset + col * 3; // 24bpp = 3 bytes per pixel
byte brightness = (byte)(buffer[offset] * 0.114f + buffer[offset + 1] * 0.587f + buffer[offset + 2] * 0.299f); // формула получения яркости 0.114 * B + 0.587f * G + 0.299f * R
buffer[offset] = brightness;
buffer[offset + 1] = brightness;
buffer[offset + 2] = brightness;
}
}
Bitmap result = new Bitmap(bmp.Width, bmp.Height, PixelFormat.Format24bppRgb);
BitmapData resultData = result.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
Marshal.Copy(buffer, 0, resultData.Scan0, length);
result.UnlockBits(resultData);
return result;
}
Работа через GetPixel/SetPixel для картинки 2560x1440 заняла бы 2,5 секунды, данный код отработает за ~60мс.
Как прочитать, обработать и записать картинку
Bitmap bmp = new Bitmap("image.jpg");
Bitmap result = ToGrayscale(bmp);
result.Save("out.jpg", ImageFormat.Jpeg);
Генератор шума
Давайте теперь пошумим. Чтобы не прикручивать ваши структуры, напишу очень простой фильтр, добавляющий шум по яркости со смещением [-20..+20]
public static Bitmap SetNoise(Bitmap bmp)
{
Random rnd = new Random();
Rectangle rect = new Rectangle(Point.Empty, bmp.Size);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
int stride = Math.Abs(bmpData.Stride);
int length = stride * bmp.Height;
byte[] buffer = new byte[length]; // содержимое буфера будет [B G R B G R B G R...]
Marshal.Copy(bmpData.Scan0, buffer, 0, bmpData.Stride * bmp.Height); // stride может быть отрицательным (строчки снизу вверх), именно здесь это важно
bmp.UnlockBits(bmpData);
for (int row = 0; row < bmp.Height; row++)
{
int rowOffset = stride * row;
for (int col = 0; col < bmp.Width; col++)
{
int offset = rowOffset + col * 3;
int b = buffer[offset] + rnd.Next(-20, 21); // правая граница не включена в рандом
int g = buffer[offset + 1] + rnd.Next(-20, 21);
int r = buffer[offset + 2] + rnd.Next(-20, 21);
buffer[offset] = ToByteSaturate(b);
buffer[offset + 1] = ToByteSaturate(g);
buffer[offset + 2] = ToByteSaturate(r);
}
}
Bitmap result = new Bitmap(bmp.Width, bmp.Height, PixelFormat.Format24bppRgb);
BitmapData resultData = result.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
Marshal.Copy(buffer, 0, resultData.Scan0, length);
result.UnlockBits(resultData);
return result;
}
private static byte ToByteSaturate(int number)
{
if (number > byte.MaxValue)
return byte.MaxValue;
if (number < byte.MinValue)
return byte.MinValue;
return (byte)number;
}
Было
Стало
В общем, этот метод работы с картинками в 50-100 раз быстрее, чем через GetPixel/SetPixel, пользуйтесь.
ООП
Теперь давайте переложу это все на ООП, чтобы было прикольнее всем этим пользоваться. Сразу оговорюсь, что здесь нет обработки исключений и нет оптимизаций по производительности, но работает весьма быстро.
Класс картинки, отвечает за хранение данных о картинке, а так же чтение ее из Bitmap и преобразование ее обратно.
public class Photo
{
public byte[] Buffer { get; }
public int Stride { get; }
public int Width { get; }
public int Height { get; }
public Photo(Bitmap bmp)
{
Width = bmp.Width;
Height = bmp.Height;
BitmapData bmpData = bmp.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
Stride = Math.Abs(bmpData.Stride);
Buffer = new byte[Stride * bmp.Height];
Marshal.Copy(bmpData.Scan0, Buffer, 0, bmpData.Stride * bmp.Height);
bmp.UnlockBits(bmpData);
}
public Bitmap ToBitmap()
{
Bitmap bmp = new Bitmap(Width, Height, PixelFormat.Format24bppRgb);
BitmapData bmpData = bmp.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
Marshal.Copy(Buffer, 0, bmpData.Scan0, Buffer.Length);
bmp.UnlockBits(bmpData);
return bmp;
}
}
Далее, реализация фильтра
public struct Pixel
{
public Pixel(byte b, byte g, byte r)
{
B = b;
G = g;
R = r;
}
public byte B { get; }
public byte G { get; }
public byte R { get; }
}
public interface IFilter
{
public void Apply(Photo photo);
}
public abstract class Filter : IFilter
{
public virtual void Apply(Photo photo)
{
for (int row = 0; row < photo.Height; row++)
{
int rowOffset = photo.Stride * row;
for (int col = 0; col < photo.Width; col++)
{
int offset = rowOffset + col * 3; // 24bpp = 3 bytes per pixel
Pixel pixel = ConvertPixel(new Pixel(photo.Buffer[offset], photo.Buffer[offset + 1], photo.Buffer[offset + 2]));
photo.Buffer[offset] = pixel.B;
photo.Buffer[offset + 1] = pixel.G;
photo.Buffer[offset + 2] = pixel.R;
}
}
}
protected abstract Pixel ConvertPixel(Pixel pixel);
}
Здесь все просто, фильтр просто применяет цветокоррекцию к каждому пикселю, причем сама цветокоррекция задается в классе, наследующем эту абстракцию.
Давайте обесцветим картинку
public class GrayScaleFilter : Filter
{
protected override Pixel ConvertPixel(Pixel pixel)
{
byte brightness = (byte)(pixel.B * 0.114f + pixel.G * 0.587f + pixel.R * 0.299f);
return new Pixel(brightness, brightness, brightness);
}
}
Теперь воспользуюсь этим фильтром
Bitmap bmp = new Bitmap("image.jpg");
Photo photo = new Photo(bmp);
GrayScaleFilter grayFilter = new GrayScaleFilter();
grayFilter.Apply(photo);
Bitmap res = photo.ToBitmap();
res.Save("_gray.jpg", ImageFormat.Jpeg);
Или вот самый простой фильтр-негатив
public class NegativeFilter : Filter
{
protected override Pixel ConvertPixel(Pixel pixel)
{
return new Pixel((byte)(255 - pixel.B), (byte)(255 - pixel.G), (byte)(255 - pixel.R));
}
}
NegativeFilter negativeFilter = new NegativeFilter();
negativeFilter.Apply(photo);
Теперь параметризованный фильтр, здесь я создам дженерик абстракцию, которая будет отвечать за прием и хранение параметров. То что я увидел у вас в вопросе - это излишества. Старайтесь понимать, за что у вас отвечат тот или иной класс, и старайтесь сделать так, чтобы он отвечал только за что-то одно (принцип SRP из SOLID).
public abstract class ParametrizedFilter<T> : Filter
where T : struct
{
public T Parameters { get; }
public ParametrizedFilter(T parameters)
{
Parameters = parameters;
}
}
А вы ждали что-то покруче? :)
Теперь создам параметры для фильтра шума, и собственно сам фильтр.
public struct NoiseParameters
{
public NoiseParameters(int intensity, byte minValue = byte.MinValue, byte maxValue = byte.MaxValue)
{
Intensity = intensity;
MinValue = minValue;
MaxValue = maxValue;
}
public int Intensity { get; }
public byte MinValue { get; }
public byte MaxValue { get; }
}
public class NoiseFilter : ParametrizedFilter<NoiseParameters>
{
private readonly Random _rnd;
public NoiseFilter(NoiseParameters parameters) : base(parameters)
{
_rnd = new Random();
}
protected override Pixel ConvertPixel(Pixel pixel)
{
int b = pixel.B + _rnd.Next(-Parameters.Intensity, Parameters.Intensity);
int g = pixel.G + _rnd.Next(-Parameters.Intensity, Parameters.Intensity);
int r = pixel.R + _rnd.Next(-Parameters.Intensity, Parameters.Intensity);
return new Pixel(ToByteSaturate(b), ToByteSaturate(g), ToByteSaturate(r));
}
private byte ToByteSaturate(int number)
{
if (number > Parameters.MaxValue)
return Parameters.MaxValue;
if (number < Parameters.MinValue)
return Parameters.MinValue;
return (byte)number;
}
}
Вот и всего делов.
NoiseFilter noiseFilter = new NoiseFilter(new NoiseParameters(50));
noiseFilter.Apply(photo);
Ну и напоследок, почему же все-таки метод Apply имеет модификатор virtual. А чтобы можно было переопределить его для реализации более сложного фильтра, чем попиксельный.
Вот пример сложного фильтра - оператор Собеля.
public class SobelOperator : GrayScaleFilter
{
public override void Apply(Photo photo)
{
base.Apply(photo); // обесцвечивание
const int bpp = 3;
byte[] tempBuffer = new byte[photo.Buffer.Length];
for (int row = 1; row < photo.Height - 1; row++)
{
int rowOffset = photo.Stride * row;
for (int col = 1; col < photo.Width - 1; col++)
{
int offset = rowOffset + col * bpp; // 24bpp = 3 bytes per pixel
int gx = -photo.Buffer[offset - bpp - photo.Stride] + photo.Buffer[offset + bpp - photo.Stride] - 2 * photo.Buffer[offset - bpp] +
2 * photo.Buffer[offset + bpp] - photo.Buffer[offset - bpp + photo.Stride] + photo.Buffer[offset + bpp + photo.Stride];
int gy = photo.Buffer[offset - bpp - photo.Stride] + 2 * photo.Buffer[offset - photo.Stride] + photo.Buffer[offset + bpp - photo.Stride] -
photo.Buffer[offset - bpp + photo.Stride] - 2 * photo.Buffer[offset + photo.Stride] - photo.Buffer[offset + bpp + photo.Stride];
int g = (int)MathF.Sqrt(gx * gx + gy * gy);
if (g > byte.MaxValue)
g = byte.MaxValue;
byte gb = (byte)g;
tempBuffer[offset] = gb;
tempBuffer[offset + 1] = gb;
tempBuffer[offset + 2] = gb;
}
}
Array.Copy(tempBuffer, photo.Buffer, tempBuffer.Length);
}
}
SobelOperator sobelFilter = new SobelOperator();
sobelFilter.Apply(photo);





