Я изучаю C # .NET 5 Intrinsics и интересуюсь передовыми практиками. Сейчас действительно сложно найти достаточно информации о том, как инструкции SIMD (логически / внутренне) работают в .NET. Кроме того, я не знаком с C ++ и языками ассемблера.
Цель решения — подать заявку Оператор Собеля фильтр к загруженному изображению. Для операций с изображениями я использовал System.Drawing.Common
Пакет NuGet. Таким образом, решение предназначено только для Windows.
В SobelOperator
Класс содержит две реализации оператора Собеля:
SobelOperatorScalar
— Скалярное решение, которое можно использовать в качестве запасного варианта, если текущий ЦП несовместим с AVX2.SobelOperatorSimd
— Решение SIMD x86 для аппаратного ускорения. — цель обзора
Код для обзора
public interface ISobelOperator
{
Bitmap Apply(Bitmap bmp);
}
public class SobelOperator : ISobelOperator
{
private static Color[] _grayPallette;
private readonly ISobelOperator _operator;
public bool IsHardwareAccelerated { get; }
public SobelOperator(bool hardwareAccelerated = true)
{
if (_grayPallette == null)
_grayPallette = Enumerable.Range(0, 256).Select(i => Color.FromArgb(i, i, i)).ToArray();
IsHardwareAccelerated = hardwareAccelerated && Avx2.IsSupported;
_operator = IsHardwareAccelerated ? new SobelOperatorSimd() : new SobelOperatorScalar();
}
public Bitmap Apply(Bitmap bmp)
=> _operator.Apply(bmp);
private class SobelOperatorSimd : ISobelOperator
{
private const byte m0 = 0b01001001;
private const byte m1 = 0b10010010;
private const byte m2 = 0b00100100;
//0.299R + 0.587G + 0.114B
private readonly Vector256<float> bWeight = Vector256.Create(0.114f);
private readonly Vector256<float> gWeight = Vector256.Create(0.587f);
private readonly Vector256<float> rWeight = Vector256.Create(0.299f);
private readonly Vector256<int> bMut = Vector256.Create(0, 3, 6, 1, 4, 7, 2, 5);
private readonly Vector256<int> gMut = Vector256.Create(1, 4, 7, 2, 5, 0, 3, 6);
private readonly Vector256<int> rMut = Vector256.Create(2, 5, 0, 3, 6, 1, 4, 7);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe Vector256<int> GetBrightness(byte* ptr)
{
Vector256<int> v0 = Avx2.ConvertToVector256Int32(ptr);
Vector256<int> v1 = Avx2.ConvertToVector256Int32(ptr + 8);
Vector256<int> v2 = Avx2.ConvertToVector256Int32(ptr + 16);
Vector256<int> vb = Avx2.Blend(Avx2.Blend(v0, v1, m1), v2, m2);
vb = Avx2.PermuteVar8x32(vb, bMut);
Vector256<int> vg = Avx2.Blend(Avx2.Blend(v0, v1, m2), v2, m0);
vg = Avx2.PermuteVar8x32(vg, gMut);
Vector256<int> vr = Avx2.Blend(Avx2.Blend(v0, v1, m0), v2, m1);
vr = Avx2.PermuteVar8x32(vr, rMut);
Vector256<float> vfb = Avx.Multiply(Avx.ConvertToVector256Single(vb), bWeight);
Vector256<float> vfg = Avx.Multiply(Avx.ConvertToVector256Single(vg), gWeight);
Vector256<float> vfr = Avx.Multiply(Avx.ConvertToVector256Single(vr), rWeight);
return Avx.ConvertToVector256Int32WithTruncation(Avx.Add(Avx.Add(vfb, vfg), vfr));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe void ToGrayscale(byte* srcPtr, byte* dstPtr, int pixelsCount)
{
byte* tail = srcPtr + (pixelsCount & -16) * 3;
byte* srcEnd = srcPtr + pixelsCount * 3;
byte* dstEnd = dstPtr + pixelsCount;
while (true)
{
while (srcPtr < tail)
{
Vector256<int> vi0 = GetBrightness(srcPtr);
Vector256<int> vi1 = GetBrightness(srcPtr + 24);
Vector128<short> v0 = Sse2.PackSignedSaturate(Avx2.ExtractVector128(vi0, 0), Avx2.ExtractVector128(vi0, 1));
Vector128<short> v1 = Sse2.PackSignedSaturate(Avx2.ExtractVector128(vi1, 0), Avx2.ExtractVector128(vi1, 1));
Sse2.Store(dstPtr, Sse2.PackUnsignedSaturate(v0, v1));
srcPtr += 48;
dstPtr += 16;
}
if (srcPtr == srcEnd)
break;
tail = srcEnd;
srcPtr = srcEnd - 48;
dstPtr = dstEnd - 16;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe Vector128<byte> ApplySobelKernel(byte* srcPtr, int width)
{
Vector256<short> v00 = Avx2.ConvertToVector256Int16(srcPtr);
Vector256<short> v01 = Avx2.ConvertToVector256Int16(srcPtr + 1);
Vector256<short> v02 = Avx2.ConvertToVector256Int16(srcPtr + 2);
Vector256<short> v10 = Avx2.ConvertToVector256Int16(srcPtr + width);
Vector256<short> v12 = Avx2.ConvertToVector256Int16(srcPtr + width + 2);
Vector256<short> v20 = Avx2.ConvertToVector256Int16(srcPtr + width * 2);
Vector256<short> v21 = Avx2.ConvertToVector256Int16(srcPtr + width * 2 + 1);
Vector256<short> v22 = Avx2.ConvertToVector256Int16(srcPtr + width * 2 + 2);
Vector256<short> vgx = Avx2.Subtract(v02, v00);
vgx = Avx2.Subtract(vgx, Avx2.ShiftLeftLogical(v10, 1));
vgx = Avx2.Add(vgx, Avx2.ShiftLeftLogical(v12, 1));
vgx = Avx2.Subtract(vgx, v20);
vgx = Avx2.Add(vgx, v22);
Vector256<short> vgy = Avx2.Add(v00, Avx2.ShiftLeftLogical(v01, 1));
vgy = Avx2.Add(vgy, v02);
vgy = Avx2.Subtract(vgy, v20);
vgy = Avx2.Subtract(vgy, Avx2.ShiftLeftLogical(v21, 1));
vgy = Avx2.Subtract(vgy, v22);
// sqrt(vgx * vgx + vgy * vgy)
Vector256<short> vgp0 = Avx2.UnpackLow(vgx, vgy);
Vector256<short> vgp1 = Avx2.UnpackHigh(vgx, vgy);
Vector256<int> v0 = Avx2.MultiplyAddAdjacent(vgp0, vgp0);
Vector256<int> v1 = Avx2.MultiplyAddAdjacent(vgp1, vgp1);
Vector256<int> gt0 = Avx.ConvertToVector256Int32WithTruncation(Avx.Sqrt(Avx.ConvertToVector256Single(v0)));
Vector256<int> gt1 = Avx.ConvertToVector256Int32WithTruncation(Avx.Sqrt(Avx.ConvertToVector256Single(v1)));
Vector128<short> gts0 = Sse2.PackSignedSaturate(Avx2.ExtractVector128(gt0, 0), Avx2.ExtractVector128(gt1, 0));
Vector128<short> gts1 = Sse2.PackSignedSaturate(Avx2.ExtractVector128(gt0, 1), Avx2.ExtractVector128(gt1, 1));
return Sse2.PackUnsignedSaturate(gts0, gts1);
}
public Bitmap Apply(Bitmap bmp)
{
int width = bmp.Width;
int height = bmp.Height;
int pixelsCount = width * height;
byte[] buffer = new byte[pixelsCount];
Rectangle rect = new Rectangle(Point.Empty, bmp.Size);
Bitmap outBmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
ColorPalette pal = outBmp.Palette;
for (int i = 0; i < 256; i++)
pal.Entries[i] = _grayPallette[i];
outBmp.Palette = pal;
unsafe
{
fixed (byte* bufPtr = buffer)
{
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
ToGrayscale((byte*)bmpData.Scan0.ToPointer(), bufPtr, pixelsCount);
bmp.UnlockBits(bmpData);
BitmapData outBmpData = outBmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
byte* dstPtr = (byte*)outBmpData.Scan0.ToPointer();
int length = pixelsCount - width * 2 - 1;
byte* tail = bufPtr + (length & -16);
byte* srcPos = bufPtr;
byte* srcEnd = bufPtr + length;
byte* dstPos = dstPtr + width + 1;
byte* dstEnd = dstPos + length;
while (true)
{
while (srcPos < tail)
{
Sse2.Store(dstPos, ApplySobelKernel(srcPos, width));
srcPos += 16;
dstPos += 16;
}
if (srcPos == srcEnd)
break;
tail = srcEnd;
srcPos = srcEnd - 16;
dstPos = dstEnd - 16;
}
for (dstPos = dstPtr + width; dstPos <= dstPtr + pixelsCount - width; dstPos += width)
{
*dstPos-- = 0;
*dstPos++ = 0;
}
outBmp.UnlockBits(outBmpData);
}
}
return outBmp;
}
}
private class SobelOperatorScalar : ISobelOperator
{
public Bitmap Apply(Bitmap bmp)
{
BitmapData bmpData = bmp.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
int strideLength = bmpData.Stride * bmpData.Height;
byte[] buffer = new byte[Math.Abs(strideLength)];
Marshal.Copy(bmpData.Scan0, buffer, 0, strideLength);
bmp.UnlockBits(bmpData);
int width = bmp.Width;
int height = bmp.Height;
int pixelsCount = width * height;
byte[] pixelBuffer = new byte[pixelsCount];
byte[] resultBuffer = new byte[pixelsCount];
//0.299R + 0.587G + 0.114B
for (int i = 0; i < pixelsCount; i++)
{
int offset = i * 3;
byte brightness = (byte)(buffer[offset] * 0.114f + buffer[offset + 1] * 0.587f + buffer[offset + 2] * 0.299f);
pixelBuffer[i] = brightness;
}
for (int i = width + 1; i < pixelsCount - width - 1; i++)
{
if (i % width == width - 1)
i += 2;
int gx = -pixelBuffer[i - 1 - width] + pixelBuffer[i + 1 - width] - 2 * pixelBuffer[i - 1] +
2 * pixelBuffer[i + 1] - pixelBuffer[i - 1 + width] + pixelBuffer[i + 1 + width];
int gy = pixelBuffer[i - 1 - width] + 2 * pixelBuffer[i - width] + pixelBuffer[i + 1 - width] -
pixelBuffer[i - 1 + width] - 2 * pixelBuffer[i + width] - pixelBuffer[i + 1 + width];
int gt = (int)MathF.Sqrt(gx * gx + gy * gy);
if (gt > byte.MaxValue) gt = byte.MaxValue;
resultBuffer[i] = (byte)gt;
}
Bitmap outBmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
BitmapData outBmpData = outBmp.LockBits(new Rectangle(Point.Empty, outBmp.Size), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
Marshal.Copy(resultBuffer, 0, outBmpData.Scan0, outBmpData.Stride * outBmpData.Height);
outBmp.UnlockBits(outBmpData);
ColorPalette pal = outBmp.Palette;
for (int i = 0; i < 256; i++)
pal.Entries[i] = _grayPallette[i];
outBmp.Palette = pal;
return outBmp;
}
}
}
Выходной тест
Program.cs
static void Main(string[] args)
{
const string fileName = "image.jpg";
Bitmap bmp = new Bitmap(fileName);
SobelOperator sobelOperator = new SobelOperator();
Console.WriteLine($"SIMD accelerated: {(sobelOperator.IsHardwareAccelerated ? "Yes" : "No")}");
Bitmap result = sobelOperator.Apply(bmp);
result.Save("out.jpg", ImageFormat.Jpeg);
Console.WriteLine("Done.");
Console.ReadKey();
}
Консольный вывод
SIMD accelerated: Yes
Done.
Выходные изображения реализаций Scalar и SIMD бинарно идентичны.
Benchmark.NET
[MemoryDiagnoser]
public class MyBenchmark
{
private readonly ISobelOperator _sobelOperator = new SobelOperator();
private readonly ISobelOperator _sobelOperatorSw = new SobelOperator(false);
private readonly Bitmap bmp = new Bitmap(@"C:Sourceimage.jpg");
[Benchmark(Description = "SIMD Enabled")]
public Bitmap TestSimd()
{
return _sobelOperator.Apply(bmp);
}
[Benchmark(Description = "SIMD Disabled")]
public Bitmap TestScalar()
{
return _sobelOperatorSw.Apply(bmp);
}
}
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<MyBenchmark>();
Console.ReadKey();
}
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-4700HQ CPU 2.40GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=5.0.101
[Host] : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
DefaultJob : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---------------- |----------:|----------:|----------:|---------:|---------:|---------:|----------:|
| 'SIMD Enabled' | 7.285 ms | 0.1165 ms | 0.1089 ms | 992.1875 | 992.1875 | 992.1875 | 3.35 MB |
| 'SIMD Disabled' | 48.412 ms | 0.2312 ms | 0.2162 ms | 454.5455 | 454.5455 | 454.5455 | 16.61 MB |
Решение Intrinsics в ~ 6,6 раза быстрее. И в целом ест меньше памяти, потому что unsafe
и не использует Marshal.Copy
загрузить / сохранить byte[]
буферы.
1 ответ
Баги с изображениями неудобной ширины
Этот код:
Bitmap outBmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
BitmapData outBmpData = outBmp.LockBits(new Rectangle(Point.Empty, outBmp.Size), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
Marshal.Copy(resultBuffer, 0, outBmpData.Scan0, outBmpData.Stride * outBmpData.Height);
Отлично работает, если ширина изображения кратна 4. Но в других случаях это не работает. Проблема в том, что растровые изображения имеют шаг, кратный 4, если их «естественная ширина в байтах» не кратна 4, то в конце каждой строки будет заполнение. Этот код не учитывает это, поэтому для изображений с заполнением он пытается скопировать больше данных из буфера результатов, чем на самом деле в нем, что вызывает исключение. Даже если бы это сработало, это все равно было бы неправильным, в результате каждая строка пикселей «теряла» некоторые пиксели из-за заполнения, срезая изображение.
Эта проблема также влияет на другие места, где растровое изображение заблокировано с размером пикселя, отличным от 4 байтов, но влияет на них по-разному.
Обычно это означает, что нам нужно работать с RGB и 8-битными данными изображения строка за строкой, как бы это ни раздражало. ARGB лучше в этом отношении, но блокировка изображения RGB в формате ARGB имеет большие затраты — с точки зрения производительности это намного хуже, чем принуждение к работе с данными RGB (но код, который мы должны написать для работы с RGB, действительно неприятен ..) В вашем коде есть несколько действительно хороших циклов, жаль, что их приходится заменять на более уродливые, по крайней мере, если вы хотите, чтобы работали изображения разной ширины.
Еще одна маленькая деталь: очень маленькая ширина не работайте с трюком «отступить от конца», чтобы выполнить последнюю итерацию. В этих случаях вы также можете вернуться к скалярной реализации — на SIMD нет надежды, и это все равно маленькие изображения.
Улучшения производительности, которые могут изменить результат
Что касается тестов, я отмечу, что вы используете Intel Haswell, поэтому я также тестировал Intel Haswell. Это может иметь значение: если код A лучше, чем код B на Haswell, ситуация может быть обратной на Skylake или AMD Ryzen.
Квадратный корень в ядре Собеля можно заменить на Avx.Multiply(Avx.ReciprocalSqrt(v0f), v0f)
где v0f
является v0
преобразованы в плавающие. Обратный квадратный корень имеет точность от 11 до 12 бит, и результат в любом случае преобразуется в 8 бит на пиксель. На тестовом изображении это не повлияло на результаты, но немного ускорило ядро. В общем, я не могу гарантировать, что есть никогда разница в результате, особенно при извлечении квадратного корня из идеального квадрата (который находится прямо на острие неправильного усечения, если обратный квадратный корень равен только просто слишком низко).
Преобразование в оттенки серого также можно выполнить немного быстрее. Основная идея там, в некоторой степени основанная на этот код, заключается в использовании Avx2.MultiplyHigh
на векторе ushort
вместо умножения с плавающей запятой. Все остальное происходит в поддержку этого. Однако, как это работает, векторы заканчиваются «потраченными впустую битами», когда результат вычислений отбрасывается. Из-за этого он по-прежнему обрабатывает до 6 инструкций умножения на 16 байтов вывода, хотя на инструкцию выполняется в два раза больше умножений. Таким образом, с точки зрения количества инструкций умножения оно не лучше, чем было раньше, и вдобавок к этому целочисленное умножение Haswell имеет половину пропускной способности по сравнению с умножением с плавающей запятой.
Остальная часть функции также не кажется особенно хорошей, поскольку она чрезвычайно перегружена перемешиванием (Haswell может выполнять только одно перемешивание за цикл, поэтому количество перемешиваний важно). На самом деле я не знаю Почему это быстрее, я могу только сказать вам, что так говорилось в тестах и что разница была значительной, несмотря на относительно высокую временную дисперсию, которую я продолжал наблюдать. Надеюсь, что возможен лучший подход, но я не знаю, что это такое.
const ushort rw = 19595;
const ushort gw = 38470;
const ushort bw = 7471;
Vector256<ushort> rW = Vector256.Create(rw);
Vector256<ushort> gW = Vector256.Create(gw);
Vector256<ushort> bW = Vector256.Create(bw);
const byte _ = 0x80;
Vector256<byte> shuf0 = Vector256.Create(
_, 0, _, 3, _, 6, _, 9, _, 12, _, _, _, _, _, _,
_, 0, _, 3, _, 6, _, 9, _, 12, _, _, _, _, _, _);
Vector256<byte> shuf1 = Vector256.Create(
_, 1, _, 4, _, 7, _, 10, _, 13, _, _, _, _, _, _,
_, 1, _, 4, _, 7, _, 10, _, 13, _, _, _, _, _, _);
Vector256<byte> shuf2 = Vector256.Create(
_, 2, _, 5, _, 8, _, 11, _, 14, _, _, _, _, _, _,
_, 2, _, 5, _, 8, _, 11, _, 14, _, _, _, _, _, _);
Vector256<byte> shuf3 = Vector256.Create(
1, 3, 5, 7, 9, _, _, _, _, _, _, _, _, _, _, _,
_, _, _, _, _, 1, 3, 5, 7, 9, _, _, _, _, _, _);
Vector256<byte> shuf4 = Vector256.Create(
_, _, _, _, _, _, _, _, _, _, 9, _, _, _, _, _,
_, _, _, _, _, _, _, _, _, _, _, 1, 3, 5, 7, 9);
Vector256<ushort> bias = Vector256.Create((ushort)0x02);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe Vector128<byte> GetBrightness16(byte* ptr)
{
var raw0 = Avx2.LoadVector128(ptr);
var raw1 = Avx2.LoadVector128(ptr + 16);
var raw2 = Avx2.LoadVector128(ptr + 32);
var partA = Avx2.InsertVector128(raw0.ToVector256(), Avx2.AlignRight(raw1, raw0, 15), 1);
var partB = Avx2.InsertVector128(Avx2.AlignRight(raw2, raw1, 1).ToVector256(), raw2, 1);
partB = Avx2.AlignRight(partB, partB, 1);
var b0 = Avx2.Shuffle(partA, shuf0).AsUInt16();
var g0 = Avx2.Shuffle(partA, shuf1).AsUInt16();
var r0 = Avx2.Shuffle(partA, shuf2).AsUInt16();
var b1 = Avx2.Shuffle(partB, shuf0).AsUInt16();
var g1 = Avx2.Shuffle(partB, shuf1).AsUInt16();
var r1 = Avx2.Shuffle(partB, shuf2).AsUInt16();
b0 = Avx2.MultiplyHigh(b0, bW);
g0 = Avx2.MultiplyHigh(g0, gW);
r0 = Avx2.MultiplyHigh(r0, rW);
b1 = Avx2.MultiplyHigh(b1, bW);
g1 = Avx2.MultiplyHigh(g1, gW);
r1 = Avx2.MultiplyHigh(r1, rW);
var sum0 = Avx2.AddSaturate(Avx2.AddSaturate(Avx2.Add(b0, g0), r0), bias);
var shufsum0 = Avx2.Shuffle(sum0.AsByte(), shuf3);
var sum1 = Avx2.AddSaturate(Avx2.AddSaturate(Avx2.Add(b1, g1), r1), bias);
var shufsum1 = Avx2.Shuffle(sum1.AsByte(), shuf4);
var shufsum = Avx2.Or(shufsum0, shufsum1);
return Avx2.Or(Vector256.GetLower(shufsum), Vector256.GetUpper(shufsum0));
}
Кстати, я выбрал смещение, чтобы минимизировать разницу с исходным преобразованием оттенков серого. Обычно я бы использовал смещение 0x80 для равномерного округления. Веса цветовых каналов являются приблизительными, в 65536 раз превышающими их вес с плавающей запятой, выбранный таким образом, чтобы в сумме они составляли 65536. MultiplyHigh неявно делит на 65536, поэтому эти шкалы эффективно работают как их аналоги с плавающей запятой. Они не иметь кстати, чтобы добавить ровно 65536: благодаря насыщающим добавкам (которые не требуют дополнительных затрат по сравнению с обычными добавками) ничего плохого не случится, если они добавят немного больше. Вы можете использовать это свойство, чтобы настроить их так, чтобы они более точно соответствовали исходному преобразованию.
В качестве альтернативы можно было бы легко адаптировать скалярную функцию к арифметике с фиксированной точкой, чтобы она давала те же результаты, что и эта версия преобразователя оттенков серого SIMD.
Я попытался использовать невыровненные нагрузки для замены Avx2.AlignRight
(он же vpalignr
), но тест показал, что это было немного медленнее.
Преобразование оттенков серого можно основать на vpmaddubsw
(Avx2.MultiplyAddAdjacent
с векторами байта и sbyte). Для данных ARGB это очень быстро, хотя и заметно менее точно. Я не работал с этим для данных RGB.
Маленькие вещи
ExtractVector128
-снижающая половина вектора
Если вы напишете Avx2.ExtractVector128(gt0, 0)
, вы получаете то, о чем просили, VEXTRACTI128
с нулевым индексом. Однако это не лучшая инструкция по применению, лучшая инструкция по применению — ничего. Никаких инструкций не требуется. Что должно произойти, так это то, что следующая операция будет использовать только 128-битную версию того же векторного регистра. Чтобы указать C # это сделать, Vector256.GetLower
.
Это контрастирует, например, с C ++, где компиляторы обычно делают эту замену самостоятельно.
Разница в производительности от этого, если таковая была, была незначительной. Однако сохранение явной инструкции извлечения может только помочь.
Ненужный intptr.ToPointer()
В разное время код содержит (byte*)intptr.ToPointer()
или какой-то вариант. Достаточно привести к указателю, вызвав ToPointer()
ничего не добавляет к этому коду.
Even if it had worked it still would have been wrong
не могу согласиться, потому чтоPixelFormat
устанавливается явноFormat8bppIndexed
— это означает 1 байт на пиксель. По логике, здесь нет права на ошибку или исключение. Соблюдение выравнивания данных строки пикселей имеет смысл только в том случае, если вы записываете файл формата BMP непосредственно в файл. Это не влияетBmpData.Scan0
$ endgroup $
— эспот
1 час назад
Это не влияет на Scan0, но влияет на конец каждой строки, потому что в этом случае Stride! = Width. Например, изображение шириной 301 будет иметь шаг 304, если оно заблокировано с форматом 8 бит / пиксель.
$ endgroup $
— Гарольд
1 час назад
2)
Generally it means that we need to work with RGB
— Я именно так и делаю, действительно работает быстрее, чем ARGB. Моя первая версияToGrayscale
метод работал с целочисленным умножением и форматом ARGB 32bpp. Тело этого метода действительно работало быстрее, общая производительность байт была хуже примерно на 2 мс для изображения, показанного в сообщении. Я выбрал подход RGB сBlend
тогда. 3)small widths don't work with the "step back from the end"-trick
хорошее наблюдение. Но я не думаю, что изображение размером менее 16 пикселей применимо для фильтрации с помощью оператора Собеля, потому что его ядру для работы требуется как минимум 3 строки.$ endgroup $
— эспот
1 час назад
image with width 301 would have a stride of 304
— Понял, спасибо! Проверим.$ endgroup $
— эспот
1 час назад