3

I need to get index of every pixel of my Bitmap in color palette (I have an indexed 8bpp image). Now I use the following way:

List<byte> data = new List<byte>(); // indexes
List<Color> pixels = bitmap.Palette.Entries.ToList(); // palette

for (int i = 0; i <bitmap.Height; i++)
    for (int j = 0; j < bitmap.Width; j++)
        data.Add((byte)pixels[k].IndexOf(bitmap.GetPixel(j, i)));

but this way works VERY slowly, because I use several high resolution images.

My questions is:

1) Is there a way to speed up the looping process to get the RGBA values ​​of each pixel?

2) Perhaps there is a more optimal way to get color indexes of images in the palette?

Pankwood
  • 1,799
  • 5
  • 24
  • 43
Vlad i Slav
  • 59
  • 2
  • 12
  • You are adding data to list in each loop. Pre-assign the size in the constructor : List data = new List(1000); where 1000 is the Height * Width of the bitmap. – jdweng Jul 11 '18 at 06:24
  • @jdweng, I don't understand how it may help me. – Vlad i Slav Jul 11 '18 at 07:21
  • The Add to a list method is slow because in each loop you have to call the constructor to the class. It is faster to create the list before you get into the loop. Then use : data[(i * bitmap.Width) + j] = (byte)pixels[k].IndexOf(bitmap.GetPixel(j, i)); – jdweng Jul 11 '18 at 07:29
  • Did you see [this post](https://stackoverflow.com/questions/2593212/editing-8bpp-indexed-bitmaps), Jose's answer may be what you want. – TaW Jul 11 '18 at 07:34
  • @jdweng First of all, it must be byte array instead of List, because List has 0 elements every time and ArgumentOutOfRangeException throwing during `data[(i * bitmap.Width) + j] = (byte)pixels[k].IndexOf(bitmap.GetPixel(j, i));`. Second, this way write to array incorrect binary data, different from those that I receive. And third, I did not notice a significant time difference. – Vlad i Slav Jul 11 '18 at 09:29

2 Answers2

4

Maybe something like this would be faster. The reasoning, Getbits and Setbits are extremely slow, each one calls lockbits internally which pins the internal memory. Best just do it all at once

using LockBits, unsafe, fixed, and a Dictionary

To test the results i used this image

from

I tests the results against your original version and they are the same

Benchmarks

----------------------------------------------------------------------------
Mode             : Release (64Bit)
Test Framework   : .NET Framework 4.7.1 (CLR 4.0.30319.42000)
----------------------------------------------------------------------------
Operating System : Microsoft Windows 10 Pro
Version          : 10.0.17134
----------------------------------------------------------------------------
CPU Name         : Intel(R) Core(TM) i7-2600 CPU @ 3.40GHz
Description      : Intel64 Family 6 Model 42 Stepping 7
Cores (Threads)  : 4 (8)      : Architecture  : x64
Clock Speed      : 3401 MHz   : Bus Speed     : 100 MHz
L2Cache          : 1 MB       : L3Cache       : 8 MB
----------------------------------------------------------------------------

Test 1

--- Random Set ------------------------------------------------------------
| Value    |   Average |   Fastest |    Cycles | Garbage | Test |    Gain |
--- Scale 1 ------------------------------------------------ Time 8.894 ---
| Mine1    |  5.211 ms |  4.913 ms |  17.713 M | 0.000 B | Pass | 93.50 % |
| Original | 80.107 ms | 75.131 ms | 272.423 M | 0.000 B | Base |  0.00 % |
---------------------------------------------------------------------------

The Full code

public unsafe byte[] Convert(string input)
{
   using (var bmp = new Bitmap(input))
   {
      var pixels = bmp.Palette.Entries.Select((color, i) => new {x = color,i})
                                      .ToDictionary(arg => arg.x.ToArgb(), x => x.i);

      // lock the image data for direct access
      var bits = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppPArgb);


      // create array as we know the size
      var data = new byte[bmp.Height * bmp.Width];

      // pin the data array
      fixed (byte* pData = data)
      {
         // just getting a pointer we can increment
         var d = pData;

         // store the max length so we don't have to recalculate it
         var length = (int*)bits.Scan0 + bmp.Height * bmp.Width;

         // Iterate through the scanlines of the image as contiguous memory by pointer 
         for (var p = (int*)bits.Scan0; p < length; p++, d++)

            //the magic, get the pixel, lookup the Dict, assign the values
            *d = (byte)pixels[*p];
      }

      // unlock the bitmap
      bmp.UnlockBits(bits);
      return data;
   }
}

Summary

I am not a image expert by any means, if this doesn't work there maybe something different about your indexed image that i dont understand

Update

To check the pixel format and if it has a palette you can use the following

bmp.PixelFormat
bmp.Palette.Entries.Any()

Update2

The working solution from Vlad i Slav as follows

I needed to replace PixelFormat.Format32bppPArgb to Format32bppArgb and to add this checking

if (pixels.ContainsKey(*p)) 
   *d = (byte)pixels[*p];
else 
   *d = 0;. 

Also need to take a distinct values from palette, because I have been give some errors there.

TheGeneral
  • 79,002
  • 9
  • 103
  • 141
  • Why you are using in var bits = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppPArgb); PixelFormat.Format32bppPArgb??? I need to use Format8bppIndexed. – Vlad i Slav Jul 11 '18 at 07:19
  • 1
    @VladiSlav for the ease of it, still should be 1000 faster , once again this is not tested, however it should work – TheGeneral Jul 11 '18 at 07:26
  • KeyNotFoundException in line `*d = (byte)pixels[*p];`. But I still don't understand how it may works. Your array `data` must have the same values, as array `data` in my sample, I'm right? – Vlad i Slav Jul 11 '18 at 07:36
  • 1
    Vlad i Slav Give my an hour to get home form work and ill write you a working tested version – TheGeneral Jul 11 '18 at 07:37
  • Ok, no problem. Thanks in advance. – Vlad i Slav Jul 11 '18 at 07:42
  • Try to use image with transparent color. Now I have the same error - KeyNotFoundException at `*d = (byte)pixels[*p];` – Vlad i Slav Jul 11 '18 at 09:43
  • I'm tested your code on your image and on my image without transparent colors, and it works fine. I get the correct byte array and have very fast speed. But when I'm tried to use image with transparent color, I have the above-mentioned exception. Any ideas? – Vlad i Slav Jul 11 '18 at 10:10
  • 1
    @VladiSlav you would have to give me a link to a picture that fails so i can look at it, maybe the palette doesnt contain an alpha channel ? – TheGeneral Jul 11 '18 at 10:14
  • 1
    @VladiSlav the palette is empty – TheGeneral Jul 11 '18 at 10:27
  • its a 32 bit image / non indexed image – TheGeneral Jul 11 '18 at 10:27
  • @VladiSlav what do you actually want to do? just get an array of colors? – TheGeneral Jul 11 '18 at 10:28
  • 1
    I'm sorry for incorrect image. Here is png8 image with transparent: [link](https://pngmini.com/vs-webp/pngquant2.png). I'm want to get the indexes of pixels colors in color palette. Nothing more. – Vlad i Slav Jul 11 '18 at 10:42
  • @VladiSlav it is 8 but its not indexed – TheGeneral Jul 11 '18 at 10:44
  • @VladiSlav not index, i added the code to work that out in the update at the bottom of my answer – TheGeneral Jul 11 '18 at 11:04
  • I found a solution. Need to replace `PixelFormat.Format32bppPArgb` on `Format32bppArgb` and to add this checking `if (pixels.ContainsKey(*p)) *d = (byte)pixels[*p]; else *d = 0;`. Also need to take a distinct values from palette, because I have been give some errors there. – Vlad i Slav Jul 11 '18 at 11:22
  • 1
    @VladiSlav if this has solved your solution feel free to answer you own question – TheGeneral Jul 11 '18 at 11:23
  • Ok, but if you can, modify your code with my notes for other users, who will have the same problem. And thank you very much for help!!! – Vlad i Slav Jul 11 '18 at 11:31
  • 1
    @VladiSlav On the reading of transparency in 8-bit png, that is a bug in the .Net framework. [It is indeed 8-bit](https://i.stack.imgur.com/zkbOO.png) ('A' on the palette viewer indicates alpha), but it loads it as 32-bit. I solved that a while ago; see [this question](https://stackoverflow.com/questions/24074641/how-to-read-8-bit-png-image-as-8-bit-png-image-only). – Nyerguds Nov 24 '19 at 16:51
1

If you check the pixelformat in advance and determine that it is PixelFormat.Format8bppIndexed, you can just use bmp.PixelFormat in the LockBits call, and get the real pixel values directly. This also avoids getting the wrong index in case the palette contains duplicate colours.

You do have to be careful with the stride, though; the byte length of each line in an image in .Net is rounded up to the next multiple of 4 bytes. So on an image with, for example, a width of 386 (like your penguin), the actual data will be 388 bytes per line. This is what they call the 'stride'. To avoid getting problems with this, and instead get completely compact data out, copy the input data per line, in chunks that are the length of the actually-used line data, while moving the read pointer forward by the full stride as given by the BitmapData object.

The minimum stride is normally calculated as (bpp * width + 7) / 8. For 8-bit images, of course, this will simply equal the width, since it's 1 byte per pixel, but the function I wrote for it supports any bit depth, even below 8bpp, hence why it makes that calculation.

The full function:

/// <summary>
/// Gets the raw bytes from an image.
/// </summary>
/// <param name="sourceImage">The image to get the bytes from.</param>
/// <param name="stride">Stride of the retrieved image data.</param>
/// <param name="collapseStride">Collapse the stride to the minimum required for the image data.</param>
/// <returns>The raw bytes of the image.</returns>
public static Byte[] GetImageData(Bitmap sourceImage, out Int32 stride, Boolean collapseStride)
{
    if (sourceImage == null)
        throw new ArgumentNullException("sourceImage", "Source image is null!");
    Int32 width = sourceImage.Width;
    Int32 height = sourceImage.Height;
    BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, sourceImage.PixelFormat);
    stride = sourceData.Stride;
    Byte[] data;
    if (collapseStride)
    {
        Int32 actualDataWidth = ((Image.GetPixelFormatSize(sourceImage.PixelFormat) * width) + 7) / 8;
        Int64 sourcePos = sourceData.Scan0.ToInt64();
        Int32 destPos = 0;
        data = new Byte[actualDataWidth * height];
        for (Int32 y = 0; y < height; ++y)
        {
            Marshal.Copy(new IntPtr(sourcePos), data, destPos, actualDataWidth);
            sourcePos += stride;
            destPos += actualDataWidth;
        }
        stride = actualDataWidth;
    }
    else
    {
        data = new Byte[stride * height];
        Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
    }
    sourceImage.UnlockBits(sourceData);
    return data;
}

In your case, this can be called simply like this:

if (image.PixelFormat == PixelFormat.Format8bppIndexed)
{
    Int32 stride;
    Byte[] rawData = GetImageData(image, out stride, true);
    // ... do whatever you want with that image data.
}

Do note, as I said in the comment to the other answer, the .Net framework has a bug that prevents 8-bit PNG images that contain palette transparency information from being loaded correctly as 8-bit. To resolve that issue, look at this answer:

A: How to read 8-bit PNG image as 8-bit PNG image only?

Nyerguds
  • 5,360
  • 1
  • 31
  • 63