1

I have an image sensor board for embedded development for which I need to capture a stream of images and output them in 8-bit monochrome / grayscale format. The imager output is 12-bit monochrome (which takes 2 bytes per pixel).

In the code, I have an IntPtr to a memory buffer that has the 12-bit image data, from which I have to extract and convert that data down to an 8-bit image. This is represented in memory something like this (with a bright light activating the pixels):

enter image description here

As you can see, every second byte contains the LSB that I want to discard, thereby keeping only the odd-numbered bytes (to put it another way). The best solution I can conceptualize is to iterate through the memory, but that's the rub. I can't get that to work. What I need help with is an algorithm in C# to do this.

Here's a sample image that represents a direct creation of a Bitmap object from the IntPtr as follows:

bitmap = new Bitmap(imageWidth, imageHeight, imageWidth, PixelFormat.Format8bppIndexed, pImage);

Click to expand in order to view correct representation

// Failed Attempt #1
unsafe
{
    IntPtr pImage;  // pointer to buffer containing 12-bit image data from imager
    int i = 0, imageSize = (imageWidth * imageHeight * 2);  // two bytes per pixel
    byte[] imageData = new byte[imageSize];
    do
    {
        // Should I bitwise shift?
        imageData[i] = (byte)(pImage + i) << 8;  // Doesn't compile, need help here!
    } while (i++ < imageSize);
}

// Failed Attempt #2
IntPtr pImage;  // pointer to buffer containing 12-bit image data from imager
imageSize = imageWidth * imageHeight;
byte[] imageData = new byte[imageSize];
Marshal.Copy(pImage, imageData, 0, imageSize);
// I tried with and without this loop. Neither gives me images.
for (int i = 0; i < imageData.Length; i++)
{
    if (0 == i % 2) imageData[i / 2] = imageData[i];
}
Bitmap bitmap;
using (var ms = new MemoryStream(imageData))
{
    bitmap = new Bitmap(ms);
}
// This also introduced a memory leak somewhere.

Alternatively, if there's a way to do this with a Bitmap, byte[], MemoryStream, etc. that works, I'm all ears, but everything I've tried has failed.

E_net4
  • 27,810
  • 13
  • 101
  • 139
Chiramisu
  • 4,687
  • 7
  • 47
  • 77
  • To get the low 4 bits you use `myByte & 0x0f`. To get the high 4 bits you use `myByte >> 4`. To get only every 2nd byte you would use `if (i % 2 == 0)` – Charles Jan 21 '20 at 03:33
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/206332/discussion-between-chiramisu-and-herohtar). – Chiramisu Jan 21 '20 at 04:28
  • Can you upload one of your images to convert, else its really hard to Test – Charles Jan 21 '20 at 05:35
  • @Charles Done, as requested. Including the line of code I used to generate it from the `IntPtr`. This image was generated when shining the light on the imager to maximize exposure. – Chiramisu Jan 21 '20 at 05:47
  • 1
    Is there a specific reason that you need the output image to be 8bpp grayscale? Part of your problem with creating the `Bitmap` is that the only 8-bit format available is `Format8bppIndexed`, which is *not* what you want -- that's a *256 **color*** image. You will likely need to do some additional processing of the data to use it with `Bitmap`, but you won't be able to generate an 8bpp grayscale image with it. – Herohtar Jan 21 '20 at 22:49
  • 1
    @Herohtar You are absolutely correct. A coworker and I worked on this for a couple hours and finally found an `unsafe` solution, so I'll post that later when I have time and try to better frame the problem. Hopefully this will be of use to others later. Thanks again for all your help. Very kind!! Let me know if there's anyway I can repay you. ;) – Chiramisu Jan 21 '20 at 23:13
  • @Herohtar Actually, you're wrong. 8-bit grayscale is just a matter of making a 256-colour image and assigning a palette to it that's a fade from black to white in 256 steps. That is indeed how 8bpp grayscale works in the .Net framework. – Nyerguds Jan 27 '20 at 09:54
  • @Chiramisu uh, in a value `F4 0F`, if only 12 bits in that are filled up, then this is a little-endian Int16, meaning the bytes are switched and your value is 0FF4. So the least significant bits to discard are most likely _not_ the `0F` part, but the `4`. Also, the example image you gave is completely uniform, so it gives no indication at all of what a correct decoding would be. – Nyerguds Jan 27 '20 at 09:59
  • @Nyerguds You are correct. I've just put up the solution we used which takes this into account. Also, the image doesn't need to have decodable data in it for this problem, as the issue was merely the extra 4-bits. This image should be sufficient because the result should be a (more or less) solid white image without the black bars you see. – Chiramisu Jan 27 '20 at 16:20
  • @Nyerguds Hm, that appears to be correct; exporting an 8bpp grayscale bitmap from GIMP creates a file that lists what appears to be sets of RGB values from (0,0,0) to (255,255,255), which I assume is the palette, then the pixel values follow as 8-bit values between 0 and 255. – Herohtar Jan 28 '20 at 03:17
  • @Herohtar Yea, some file formats have specific grayscale storage types, but .Net's `System.Drawing` doesn't actually support those. – Nyerguds Jan 28 '20 at 09:48

1 Answers1

1

Here is the algorithm that my coworkers helped formulate. It creates two new (unmanaged) pointers; one 8-bits wide and the other 16-bits.

By stepping through one word at a time and shifting off the last 4 bits of the source, we get a new 8-bit image with only the MSBs. Each buffer has the same number of words, but since the words are different sizes, they progress at different rates as we iterate over them.

unsafe
{
    byte* p_bytebuffer = (byte*)pImage;
    short* p_shortbuffer = (short*)pImage;

    for (int i = 0; i < imageWidth * imageHeight; i++)
    {
        *p_bytebuffer++ = (byte)(*p_shortbuffer++ >> 4);
    }
}

In terms of performance, this appears to be very fast with no perceivable difference in framerate.

Special thanks to @Herohtar for spending a substantial amount of time in chat with me attempting to help me solve this.

TylerH
  • 20,799
  • 66
  • 75
  • 101
Chiramisu
  • 4,687
  • 7
  • 47
  • 77
  • 1
    On a little-endian CPU, an int16 ptr will indeed do that automatically. Just pray you never need to run this on a big-endian machine, though. Also, do note that on .Net, the stride (amount of bytes per line in an image) **_must_ be rounded up to the next multiple of 4 bytes**, so be careful with that. The next step after this code, to use this ptr for making a Bitmap, will _only_ work for images that happen to have a width exactly divisible by 4. – Nyerguds Jan 28 '20 at 09:45
  • 1
    See also, _[Why must “stride” in the System.Drawing.Bitmap constructor be a multiple of 4?](https://stackoverflow.com/q/2185944/395685)_ Note that my answer on that question offers an alternative without the use of unmanaged pointers. – Nyerguds Jan 28 '20 at 09:52