Set pixel color by coordinates (x, y) in 1D byte array

79 views Asked by At

does anybody know what am i doing wrong. I'm trying to change pixel color by (x, y) coordinates on 1D byte array, but i cannot succeed, pixels are scattered everywhere around.

This is my file header:

file.Write(
    new byte[62]
    {
        0x42, 0x4d,
        0x3e, 0xf4, 0x1, 0x0,
        0x0, 0x0, 0x0, 0x0,
        0x3e, 0x0, 0x0, 0x0,
        0x28, 0x0, 0x0, 0x0,
        0xe8, 0x3, 0x0, 0x0,
        0xe8, 0x3, 0x0, 0x0,
        0x1, 0x0,
        0x1, 0x0,
        0x0, 0x0, 0x0, 0x0,
        0x0, 0xf4, 0x0, 0x0,
        0x0, 0x0, 0x0, 0x0,
        0x0, 0x0, 0x0, 0x0,
        0x0, 0x0, 0x0, 0x0,
        0x0, 0x0, 0x0, 0x0,
        0xff, 0x0, 0x0, 0x0,
        0x0, 0x0, 0xff, 0x0});

This is my byte array:

int width = 1000;
int height = 128;

var t = new byte[width * height];

This is the method to set pixel (I'm pretty sure the calculation is wrong, I might need to include more of my map information, but I'm not sure what):

static void SetPixel(byte[] img, int x, int y, int width, int height)
{
    if (x >= 0 && x < width && y >= 0 && y < height)
    {
        Console.WriteLine($"{x} {y}");

        int index = (y * width + x);
        img[index] = 0x1; 
    }
}

Lastly, here's how I call the method:

 SetPixel(t, 1, 0, width, height);

I've tried many solutions I've found online, none of them helps. Tried playing around with width, height.

3

There are 3 answers

0
Olivier Jacot-Descombes On

If I interpret the data correctly, you are creating a bitmap file. You have set the value Bits per Pixel to 1; however, you are adding one byte per pixel. Therefore, you should set this value to 8 (one byte = 8 bits). However, this requires a color palette to be stored.

If you really want 1-bit colors (monochrome), then you must divide the byte size by 8 and set pixels by setting single bits inside the bytes.

The row size must be a multiple of 4 bytes. This function calculates it:

static int RowSize(int width)
{
    return (width / 8 + 3) / 4 * 4;
}

We create the pixel array with:

const int HeaderSize = 62;
const int width = 1000;
const int height = 128;

int rowSize = RowSize(width);

byte[] t = new byte[rowSize * height];

We could calculate the file size manually, but we can get the corresponding bytes like this...

int fileSize = t.Length + HeaderSize;
byte[] fs = BitConverter.GetBytes(fileSize);

(You could also use the modern BinaryPrimitives Class instead of BitConverter.)

... and set them in the file header like this:

file.Write(
    new byte[HeaderSize] {
        0x42, 0x4d,             // Signature
        fs[0], fs[1], fs[2], fs[3], // File Size
        0x00, 0x00, 0x00, 0x00, // Reserved 1 + 2
        0x3e, 0x00, 0x00, 0x00, // File Offset to PixelArray (62)

        0x28, 0x00, 0x00, 0x00, // DIB Header Size
        0xe8, 0x03, 0x00, 0x00, // Image Width
        0x80, 0x00, 0x00, 0x00, // Image Height
        0x01, 0x00,             // Planes
        0x01, 0x00,             // Bits per Pixel
        0x00, 0x00, 0x00, 0x00, // Compression
        0x00, 0xf4, 0x00, 0x00, // Image Size
        0x00, 0x00, 0x00, 0x00, // X Pixels per Meter
        0x00, 0x00, 0x00, 0x00, // Y Pixels per Meter
        0x00, 0x00, 0x00, 0x00, // Colors in Color Table
        0x00, 0x00, 0x00, 0x00, // Important Color Count
        0xff, 0x00, 0x00, 0x00, // Palette Color 1 (Blue)
        0x00, 0x00, 0xff, 0x00  // Palette Color 2 (Red)
    });
file.Write(t);

We set a pixel with:

static void SetPixel(byte[] img, int x, int y, int width, int height)
{
    if (x >= 0 && x < width && y >= 0 && y < height) {
        int rowSize = RowSize(width);
        int byteIndex = y * rowSize + x / 8;
        int bitMask = 0b1000_0000 >> (x % 8);
        img[byteIndex] = (byte)(img[byteIndex] | bitMask);
    }
}

Also, you have set Image Height in the DIB header to 1000 = 0xe8, 0x03 instead of 128 = 0x80.

I just made tests and this seems to work so far.

See also:

0
Olivier Jacot-Descombes On

The solution implementing the header as byte array is difficult to understand. It is easy to make things wrong. It is also not easy to use. Therefore, I present a second solution here, where I wrapped up everything inside a class MonochromeBitmap. I am using C# 12 features like primary constructors.

Usage example:

const int width = 1000;
const int height = 128;

var bitmap = new MonochromeBitmap(width, height, Color.White, Color.Black);
for (int i = 0; i < 10; i++) {
    bitmap.SetPixel(x: i, y: i, width, height);
}
for (int i = 10; i < 110; i++) {
    bitmap.SetPixel(x: 10, y: i, width, height);
}
for (int i = 10; i < 110; i++) {
    bitmap.SetPixel(x: i, y: 10, width, height);
}
bitmap.Save(@"C:\Users\Tadas\Desktop\test.bmp");

The constructor expects the width and height of the bitmap as well as the two desired colors.

MonochromeBitmap implements the headers as structs. This is much more readable and easier to configure as a simple byte array. To be able to use the same notation as in Graphics_File_Formats_Second_Edition_1996, page 600 ff, I declare a few type name aliases along with other usings:

using System.Drawing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using DWORD = uint;
using LONG_ = int;
using WORD_ = ushort;

Implementation:

public class MonochromeBitmap(int width, int height, Color color0, Color color1)
{
    private readonly int _rowSize = RowSize(width);
    private readonly byte[] _pixels = new byte[RowSize(width) * height];

    public void SetPixel(int x, int y, int width, int height)
    {
        if (x >= 0 && x < width && y >= 0 && y < height) {
            int byteIndex = y * _rowSize + x / 8;
            int bitMask = 0b1000_0000 >> (x % 8);
            _pixels[byteIndex] = (byte)(_pixels[byteIndex] | bitMask);
        }
    }

    public void Save(string path)
    {
        using var file = new FileStream(path, FileMode.Create);
        int offset;
        unsafe {
            offset = sizeof(FileHeader) + sizeof(BitmapHeader) + 2 * sizeof(PaletteElement);
        }
        file.Write(GetBytes(new FileHeader(offset)));
        file.Write(GetBytes(new BitmapHeader(width, height, 1)));
        file.Write(GetBytes(new PaletteElement(color0)));
        file.Write(GetBytes(new PaletteElement(color1)));
        file.Write(_pixels);
    }

    // BMP Version 2 (Microsoft Windows 2.x)
    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    private readonly struct FileHeader(int bitmapOffset)
    {
        readonly WORD_ FileType = 0x4d42; // "BM" in ASCII.
        readonly DWORD FileSize = 0;      // Can be 0 in uncompressed BMP files.
        readonly WORD_ Reserved1 = 0;     // Always 0.
        readonly WORD_ Reserved2 = 0;     // Always 0.
        readonly DWORD BitmapOffset = (DWORD)bitmapOffset;
    }

    // BMP Version 3 (Microsoft Windows 3.x)
    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    private readonly struct BitmapHeader(int width, int height, int bitsPerPixel)
    {
        readonly DWORD Size = 40;       // Size of this header in bytes.
        readonly LONG_ Width = width;   // Image width in pixels. pos. = bottom-up, neg. = top-down,
                                        // does not include any scan-line boundary padding.
        readonly LONG_ Height = height; // Image height in pixels.
        readonly WORD_ Planes = 1;      // Number of color planes (always 1).
        readonly WORD_ BitsPerPixel = (WORD_)bitsPerPixel;

        // Fields added for Windows 3.x follow this line
        readonly DWORD Compression = 0;  // Compression methods used. 0 = uncompressed
        readonly DWORD SizeOfBitmap = 0; // Size of bitmap in bytes. Can be 0 in uncompressed BMP files.
        readonly LONG_ HorzResolution;   // Horizontal resolution in pixels per meter.
        readonly LONG_ VertResolution;   // Vertical resolution in pixels per meter.
        readonly DWORD ColorsUsed = (DWORD)(1 << bitsPerPixel); // Number of colors in the image.
        readonly DWORD ColorsImportant;  // Minimum number of important colors. 0 if all colors are important.
    }

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    private readonly struct PaletteElement(Color color)
    {
        readonly byte Blue = color.B;
        readonly byte Green = color.G;
        readonly byte Red = color.R;
        readonly byte Reserved = 0; // Padding (always 0) 
    }

    private static int RowSize(int width)
    {
        return (width / 8 + 3) / 4 * 4;
    }

    private static unsafe byte[] GetBytes<T>(T data) where T : struct
    {
        byte[] bytes = new byte[sizeof(T)];
        fixed (byte* ptr = bytes) {
            Unsafe.Copy(ptr, ref data);
        }
        return bytes;
    }
}

Since I am using some usafe blocks, you must add <AllowUnsafeBlocks>true</AllowUnsafeBlocks> to a property group in your .csproj.

0
Olivier Jacot-Descombes On

The solution implementing the header as byte array is difficult to understand. It is easy to make things wrong. It is also not easy to use. Therefore, I present a second solution here, where I wrapped up everything inside a class MonochromeBitmap. I am using C# 12 features like primary constructors.

Usage example:

const int width = 1000;
const int height = 128;

var bitmap = new MonochromeBitmap(width, height, Color.White, Color.Black);
for (int i = 0; i < 10; i++) {
    bitmap.SetPixel(x: i, y: i);
}
for (int i = 10; i < 110; i++) {
    bitmap.SetPixel(x: 10, y: i);
}
for (int i = 10; i < 110; i++) {
    bitmap.SetPixel(x: i, y: 10);
}
bitmap.Save(@"C:\Users\Oli\Desktop\test.bmp");

The constructor expects the width and height of the bitmap as well as the two desired colors.

MonochromeBitmap implements the headers as structs. This is much more readable as a simple byte array. To be able to use the same notation as in Graphics_File_Formats_Second_Edition_1996, page 600 ff, I declare a few type name aliases along with other usings:

using System.Drawing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using DWORD = uint;
using LONG_ = int;
using WORD_ = ushort;

Implementation:

public class MonochromeBitmap(int width, int height, Color color0, Color color1)
{
    private readonly int _rowSize = RowSize(width);
    private readonly byte[] _pixels = new byte[RowSize(width) * height];

    public void SetPixel(int x, int y)
    {
        if (x >= 0 && x < width && y >= 0 && y < height) {
            int byteIndex = y * _rowSize + x / 8;
            int bitMask = 0b1000_0000 >> (x % 8);
            _pixels[byteIndex] = (byte)(_pixels[byteIndex] | bitMask);
        }
    }

    public void Save(string path)
    {
        using var file = new FileStream(path, FileMode.Create);
        int offset;
        unsafe {
            offset = sizeof(FileHeader) + sizeof(BitmapHeader) + 2 * sizeof(PaletteElement);
        }
        file.Write(GetBytes(new FileHeader(offset)));
        file.Write(GetBytes(new BitmapHeader(width, height, 1)));
        file.Write(GetBytes(new PaletteElement(color0)));
        file.Write(GetBytes(new PaletteElement(color1)));
        file.Write(_pixels);
    }

    // BMP Version 2 (Microsoft Windows 2.x)
    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    private readonly struct FileHeader(int bitmapOffset)
    {
        readonly WORD_ FileType = 0x4d42; // "BM" in ASCII.
        readonly DWORD FileSize = 0;      // Can be 0 in uncompressed BMP files.
        readonly WORD_ Reserved1 = 0;     // Always 0.
        readonly WORD_ Reserved2 = 0;     // Always 0.
        readonly DWORD BitmapOffset = (DWORD)bitmapOffset;
    }

    // BMP Version 3 (Microsoft Windows 3.x)
    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    private readonly struct BitmapHeader(int width, int height, int bitsPerPixel)
    {
        readonly DWORD Size = 40;       // Size of this header in bytes.
        readonly LONG_ Width = width;   // Image width in pixels. pos. = bottom-up, neg. = top-down,
                                        // does not include any scan-line boundary padding.
        readonly LONG_ Height = height; // Image height in pixels.
        readonly WORD_ Planes = 1;      // Number of color planes (always 1).
        readonly WORD_ BitsPerPixel = (WORD_)bitsPerPixel;

        // Fields added for Windows 3.x follow this line
        readonly DWORD Compression = 0;  // Compression methods used. 0 = uncompressed
        readonly DWORD SizeOfBitmap = 0; // Size of bitmap in bytes. Can be 0 in uncompressed BMP files.
        readonly LONG_ HorzResolution;   // Horizontal resolution in pixels per meter.
        readonly LONG_ VertResolution;   // Vertical resolution in pixels per meter.
        readonly DWORD ColorsUsed = (DWORD)(1 << bitsPerPixel); // Number of colors in the image.
        readonly DWORD ColorsImportant;  // Minimum number of important colors. 0 if all colors are important.
    }

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    private readonly struct PaletteElement(Color color)
    {
        readonly byte Blue = color.B;
        readonly byte Green = color.G;
        readonly byte Red = color.R;
        readonly byte Reserved = 0; // Padding (always 0) 
    }

    private static int RowSize(int width)
    {
        return (width / 8 + 3) / 4 * 4;
    }

    private static unsafe byte[] GetBytes<T>(T data) where T : struct
    {
        byte[] bytes = new byte[sizeof(T)];
        fixed (byte* ptr = bytes) {
            Unsafe.Copy(ptr, ref data);
        }
        return bytes;
    }
}

Since I am using some usafe blocks, you must add <AllowUnsafeBlocks>true</AllowUnsafeBlocks> to a property group in your .csproj.