Por que “stride” no construtor System.Drawing.Bitmap deve ser um múltiplo de 4?

Eu estou escrevendo um aplicativo que requer que eu tome um formato de bitmap proprietário (um MVTec Halcon HImage) e convertê-lo em um System.Drawing.Bitmap em c #.

As únicas funções proprietárias que me foram dadas para me ajudar a fazer isso envolvem escrever para o arquivo, exceto pelo uso de uma function “obter ponteiro”.

Essa function é ótima, me fornece um ponteiro para os dados de pixel, a largura, a altura e o tipo da imagem.

Meu problema é que quando eu crio meu System.Drawing.Bitmap usando o construtor:

new System.Drawing.Bitmap(width, height, stride, format, scan) 

Eu preciso especificar um “stride” que é um múltiplo de 4. Isso pode ser um problema, pois não tenho certeza de qual bitmap de tamanho minha function será atingida. Supondo que eu acabe com um bitmap que é 111×111 pixels, não tenho como executar essa function além de adicionar uma coluna falsa à minha imagem ou subtrair 3 colunas.

Existe uma maneira que eu possa fugir dessa limitação?

Isso remonta aos primeiros projetos de CPU. A maneira mais rápida de analisar os bits do bitmap é lendo-os 32 bits por vez, começando no início de uma linha de varredura. Isso funciona melhor quando o primeiro byte da linha de varredura é alinhado em um limite de endereço de 32 bits. Em outras palavras, um endereço que é múltiplo de 4. Nas CPUs iniciais, ter esse primeiro byte desalinhado custaria ciclos extras da CPU para ler duas palavras de 32 bits da RAM e embaralhar os bytes para criar o valor de 32 bits. Garantir que cada linha de varredura inicie em um endereço alinhado (automático se a passada for um múltiplo de 4) evita isso.

Isso não é mais uma preocupação real nas CPUs modernas, agora o alinhamento ao limite da linha de cache é muito mais importante. No entanto, o múltiplo de 4 requisitos para stride ficou preso por razões de appcompat.

Btw, você pode facilmente calcular o stride do formato e largura com isso:

  int bitsPerPixel = ((int)format & 0xff00) >> 8; int bytesPerPixel = (bitsPerPixel + 7) / 8; int stride = 4 * ((width * bytesPerPixel + 3) / 4); 

Como já foi dito por Jake, você calcula o passo encontrando os bytes por pixel (2 para 16 bits, 4 para 32 bits) e depois multiplicando-o pela largura. Então, se você tem uma largura de 111 e uma imagem de 32 bits, você teria 444, que é um múltiplo de 4.

No entanto, digamos por um minuto que você tenha uma imagem de 24 bits. 24 bit é igual a 3 bytes, então com uma largura de 111 pixels você teria 333 como seu passo. Isto é, obviamente, não um múltiplo de 4. Então você gostaria de arredondar para 336 (o próximo múltiplo mais alto de 4). Mesmo que você tenha um pouco de extra, esse espaço não utilizado não é significativo o suficiente para realmente fazer muita diferença na maioria dos aplicativos.

Infelizmente, não há maneira de contornar essa restrição (a menos que você sempre use imagens de 32 ou 64 bits, que são sempre múltiplos de 4.

Lembre-se que o stride é diferente da width . Você pode ter uma imagem que tenha 111 (8 bits) pixels por linha, mas cada linha é armazenada na memory de 112 bytes.

Isso é feito para fazer uso eficiente da memory e, como disse @Ian, está armazenando os dados no int32 .

Porque está usando o int32 para armazenar cada pixel.

 Sizeof(int32) = 4 

Mas não se preocupe, quando a imagem é salva da memory para o arquivo, ela utilizará o uso de memory mais eficiente possível. Internamente usa 24 bits por pixel (8 bits vermelho, 8 verde e 8 azul) e deixa os últimos 8 bits redundantes.

Uma maneira muito mais fácil é apenas criar a imagem com o construtor (width, height, pixelformat) . Então cuida do passo em si.

Então, você pode simplesmente usar o LockBits para copiar seus dados de imagem, linha por linha, sem se preocupar com as coisas do Stride; você pode literalmente solicitar isso do object BitmapData . Para a operação de cópia real, para cada linha de varredura, você apenas aumenta o ponteiro de destino pela passada e o ponteiro de origem pela largura de dados da linha.

Aqui está um exemplo em que obtive os dados da imagem em uma matriz de bytes. Se forem dados totalmente compactos, a sua passada de input normalmente é apenas a largura da imagem multiplicada pela quantidade de bytes por pixel. Se são dados de paleta de 8 bits, é simplesmente exatamente a largura.

Se os dados da imagem foram extraídos de um object de imagem, você deve ter armazenado o stride original desse processo de extração exatamente da mesma maneira, obtendo-o do object BitmapData .

 ///  /// Creates a bitmap based on data, width, height, stride and pixel format. ///  /// Byte array of raw source data /// Width of the image /// Height of the image /// Scanline length inside the data /// Pixel format /// Color palette /// Default color to fill in on the palette if the given colors don't fully fill it. /// The new image 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; } 

Código correto:

 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); }