Почему "шаг" в конструкторе System.Drawing.Bitmap должен быть кратным 4?
Я пишу приложение, которое требует от меня взять собственный формат точечного рисунка (MVTec Halcon HImage) и преобразовать его в System.Drawing.Bitmap в C#.
Единственные проприетарные функции, предоставленные мне, чтобы помочь мне сделать это, включают в себя запись в файл, за исключением использования функции "получить указатель".
Эта функция великолепна, она дает мне указатель на данные пикселей, ширину, высоту и тип изображения.
Моя проблема заключается в том, что когда я создаю свой System.Drawing.Bitmap с помощью конструктора:
new System.Drawing.Bitmap(width, height, stride, format, scan)
Мне нужно указать "шаг", кратный 4. Это может быть проблемой, так как я не уверен, какой размер растрового изображения повлияет на мою функцию. Предположим, что я получаю растровое изображение размером 111x111 пикселей, у меня нет способа запустить эту функцию, кроме добавления поддельного столбца к моему изображению или вычитания 3 столбцов.
Есть ли способ, которым я могу обойти это ограничение?
6 ответов
Это восходит к ранним разработкам ЦП. Самый быстрый способ прорезать биты растрового изображения - это читать их по 32 бита за раз, начиная с начала строки сканирования. Это работает лучше всего, когда первый байт строки сканирования выровнен по границе 32-битного адреса. Другими словами, адрес, кратный 4. В ранних процессорах наличие первого байта, выровненного неправильно, стоило бы дополнительных циклов ЦП, чтобы прочитать два 32-разрядных слова из ОЗУ и перемешать байты для создания 32-разрядного значения. Обеспечение того, чтобы каждая строка сканирования начиналась с выровненного адреса (автоматически, если шаг кратен 4), позволяет избежать этого.
На современных процессорах это больше не беспокоит, теперь выравнивание по границе строки кэша гораздо важнее. Тем не менее, по соображениям appppat требование к шагу, кратное 4, сохраняется.
Кстати, вы можете легко рассчитать шаг по формату и ширине с помощью этого:
int bitsPerPixel = ((int)format & 0xff00) >> 8;
int bytesPerPixel = (bitsPerPixel + 7) / 8;
int stride = 4 * ((width * bytesPerPixel + 3) / 4);
Намного проще сделать изображение с помощью (width, height, pixelformat)
конструктор. Тогда он позаботится о самом шаге.
Тогда вы можете просто использовать LockBits
копировать данные изображения в него построчно, не беспокоясь о Stride; Вы можете буквально просто запросить это от BitmapData
объект. Для фактической операции копирования для каждой строки развертки вы просто увеличиваете целевой указатель на шаг, а исходный указатель на ширину данных вашей строки.
Вот пример, где я получил данные изображения в байтовом массиве. Если это полностью компактные данные, ваш входной шаг обычно равен ширине изображения, умноженной на количество байтов на пиксель. Если это 8-битные данные с палитрой, это просто ширина.
Если данные изображения были извлечены из объекта изображения, вы должны были сохранить исходный шаг из этого процесса извлечения точно таким же образом, вытащив его из BitmapData
объект.
/// <summary>
/// Creates a bitmap based on data, width, height, stride and pixel format.
/// </summary>
/// <param name="sourceData">Byte array of raw source data</param>
/// <param name="width">Width of the image</param>
/// <param name="height">Height of the image</param>
/// <param name="stride">Scanline length inside the data</param>
/// <param name="pixelFormat">Pixel format</param>
/// <param name="palette">Color palette</param>
/// <param name="defaultColor">Default color to fill in on the palette if the given colors don't fully fill it.</param>
/// <returns>The new image</returns>
public static Bitmap BuildImage(Byte[] sourceData, Int32 width, Int32 height, Int32 stride, PixelFormat pixelFormat, Color[] palette, Color? defaultColor)
{
Bitmap newImage = new Bitmap(width, height, pixelFormat);
BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
Int32 newDataWidth = ((Image.GetPixelFormatSize(pixelFormat) * width) + 7) / 8;
// Compensate for possible negative stride on BMP format.
Boolean isFlipped = stride < 0;
stride = Math.Abs(stride);
// Cache these to avoid unnecessary getter calls.
Int32 targetStride = targetData.Stride;
Int64 scan0 = targetData.Scan0.ToInt64();
for (Int32 y = 0; y < height; y++)
Marshal.Copy(sourceData, y * stride, new IntPtr(scan0 + y * targetStride), newDataWidth);
newImage.UnlockBits(targetData);
// Fix negative stride on BMP format.
if (isFlipped)
newImage.RotateFlip(RotateFlipType.Rotate180FlipX);
// For indexed images, set the palette.
if ((pixelFormat & PixelFormat.Indexed) != 0 && palette != null)
{
ColorPalette pal = newImage.Palette;
for (Int32 i = 0; i < pal.Entries.Length; i++)
{
if (i < palette.Length)
pal.Entries[i] = palette[i];
else if (defaultColor.HasValue)
pal.Entries[i] = defaultColor.Value;
else
break;
}
newImage.Palette = pal;
}
return newImage;
}
Как было сказано ранее Джейком, вы вычисляете шаг, находя байты на пиксель (2 для 16 бит, 4 для 32 бит), а затем умножая его на ширину. Таким образом, если у вас ширина 111 и 32-битное изображение, у вас будет 444, что кратно 4.
Однако, скажем на минуту, что у вас есть 24-битное изображение. 24 бита равны 3 байтам, поэтому при ширине в 111 пикселей у вас будет 333 шага. Это, очевидно, не кратное 4. Таким образом, вы хотите округлить до 336 (следующий по величине кратный 4). Даже при том, что у вас есть немного лишнего, это неиспользуемое пространство не является достаточно значительным, чтобы действительно иметь значение в большинстве приложений.
К сожалению, нет никакого способа обойти это ограничение (если только вы не всегда используете 32-битные или 64-битные изображения, которые всегда кратны 4.
Помните stride
отличается от width
, У вас может быть изображение, которое имеет 111 (8-битных) пикселей на строку, но каждая строка хранится в памяти 112 байтов.
Это сделано для эффективного использования памяти и, как сказал @Ian, хранит данные в int32
,
Потому что он использует int32 для хранения каждого пикселя.
Sizeof(int32) = 4
Но не волнуйтесь, когда изображение сохраняется из памяти в файл, оно будет использовать наиболее эффективное использование памяти. Внутренне он использует 24 бита на пиксель (8 бит красного, 8 зеленого и 8 синего) и оставляет последние 8 бит избыточными.
Правильный код:
public static void GetStride(int width, PixelFormat format, ref int stride, ref int bytesPerPixel)
{
//int bitsPerPixel = ((int)format & 0xff00) >> 8;
int bitsPerPixel = System.Drawing.Image.GetPixelFormatSize(format);
bytesPerPixel = (bitsPerPixel + 7) / 8;
stride = 4 * ((width * bytesPerPixel + 3) / 4);
}