Reputation: 559
How to process and extract image data using c# (reading the value of each pixel ) where the pixel is 10 bits depth?
Also the image has 4 bands (R,G,B & NIR).
Thanks in advance.
Upvotes: 1
Views: 2063
Reputation: 5639
I inspired myself by ScummVM's code to write this class to convert colours automatically based on the bit length and bit location of each colour component inside the array:
/// <summary>
/// Class to automate the unpacking (and packing/writing) of RGB(A) colours in colour formats with packed bits.
/// Inspired by https://github.com/scummvm/scummvm/blob/master/graphics/pixelformat.h
/// This class works slightly differently than the ScummVM version, using 4-entry arrays for all data, with each entry
/// representing one of the colour components, so the code can easily loop over them and perform the same action on each one.
/// </summary>
public class PixelFormatter
{
/// <summary>Standard PixelFormatter for .Net's RGBA format.</summary>
public static PixelFormatter Format32BitArgb = new PixelFormatter(4, 8, 16, 8, 8, 8, 0, 8, 24, true);
/// <summary>Number of bytes to read per pixel.</summary>
private Byte bytesPerPixel;
/// <summary>Amount of bits for each component (R,G,B,A)</summary>
private Byte[] bitsAmounts = new Byte[4];
/// <summary>Amount of bits to shift for each component (R,G,B,A)</summary>
private Byte[] shiftAmounts = new Byte[4];
/// <summary>Masks to limit the amount of bits for each component, derived from the bitsAmounts.</summary>
private UInt32[] limitMasks = new UInt32[4];
/// <summary>Multiplier for each component (R,G,B,A). If not explicitly given this can be derived from the number of bits.</summary>
private Double[] multipliers = new Double[4];
/// <summary>Defaults for for each component (R,G,B,A)</summary>
private Byte[] defaults = new Byte[] { 0, 0, 0, 255 };
/// <summary>True to read the input bytes as little-endian.</summary>
private Boolean littleEndian;
/// <summary>The colour components. Though most stuff will just loop an int from 0 to 4, this shows the order.</summary>
private enum ColorComponent
{
Red = 0,
Green = 1,
Blue = 2,
Alpha = 3
}
/// <summary>
/// Creats a new PixelFormatter, with automatic calculation of colour multipliers using the CalculateMultiplier function.
/// </summary>
/// <param name="bytesPerPixel">Amount of bytes to read per pixel</param>
/// <param name="redBits">Amount of bits to read for the red colour component</param>
/// <param name="redShift">Amount of bits to shift the data to get to the red colour component</param>
/// <param name="greenBits">Amount of bits to read for the green colour component</param>
/// <param name="greenShift">Amount of bits to shift the data to get to the green colour component</param>
/// <param name="blueBits">Amount of bits to read for the blue colour component</param>
/// <param name="blueShift">Amount of bits to shift the data to get to the blue colour component</param>
/// <param name="alphaBits">Amount of bits to read for the alpha colour component</param>
/// <param name="alphaShift">Amount of bits to shift the data to get to the alpha colour component</param>
/// <param name="littleEndian">True if the read bytes are interpreted as little-endian.</param>
public PixelFormatter(Byte bytesPerPixel, Byte redBits, Byte redShift, Byte greenBits, Byte greenShift,
Byte blueBits, Byte blueShift, Byte alphaBits, Byte alphaShift, Boolean littleEndian)
: this(bytesPerPixel,
redBits, redShift, CalculateMultiplier(redBits),
greenBits, greenShift, CalculateMultiplier(greenBits),
blueBits, blueShift, CalculateMultiplier(blueBits),
alphaBits, alphaShift, CalculateMultiplier(alphaBits), littleEndian)
{ }
/// <summary>
/// Creates a new PixelFormatter.
/// </summary>
/// <param name="bytesPerPixel">Amount of bytes to read per pixel</param>
/// <param name="redBits">Amount of bits to read for the red colour component</param>
/// <param name="redShift">Amount of bits to shift the data to get to the red colour component</param>
/// <param name="redMultiplier">Multiplier for the red component's value to adjust it to the normal 0-255 range.</param>
/// <param name="greenBits">Amount of bits to read for the green colour component</param>
/// <param name="greenShift">Amount of bits to shift the data to get to the green colour component</param>
/// <param name="greenMultiplier">Multiplier for the green component's value to adjust it to the normal 0-255 range.</param>
/// <param name="blueBits">Amount of bits to read for the blue colour component</param>
/// <param name="blueShift">Amount of bits to shift the data to get to the blue colour component</param>
/// <param name="blueMultiplier">Multiplier for the blue component's value to adjust it to the normal 0-255 range.</param>
/// <param name="alphaBits">Amount of bits to read for the alpha colour component</param>
/// <param name="alphaShift">Amount of bits to shift the data to get to the alpha colour component</param>
/// <param name="alphaMultiplier">Multiplier for the alpha component's value to adjust it to the normal 0-255 range.</param>
/// <param name="littleEndian">True if the read bytes are interpreted as little-endian.</param>
public PixelFormatter(Byte bytesPerPixel, Byte redBits, Byte redShift, Double redMultiplier,
Byte greenBits, Byte greenShift, Double greenMultiplier,
Byte blueBits, Byte blueShift, Double blueMultiplier,
Byte alphaBits, Byte alphaShift, Double alphaMultiplier, Boolean littleEndian)
{
this.bytesPerPixel = bytesPerPixel;
this.littleEndian = littleEndian;
this.bitsAmounts [(Int32)ColorComponent.Red] = redBits;
this.shiftAmounts[(Int32)ColorComponent.Red] = redShift;
this.multipliers [(Int32)ColorComponent.Red] = redMultiplier;
this.limitMasks[(Int32)ColorComponent.Red] = GetLimitMask(redBits, redShift);
this.bitsAmounts [(Int32)ColorComponent.Green] = greenBits;
this.shiftAmounts[(Int32)ColorComponent.Green] = greenShift;
this.multipliers [(Int32)ColorComponent.Green] = greenMultiplier;
this.limitMasks[(Int32)ColorComponent.Green] = GetLimitMask(greenBits, greenShift);
this.bitsAmounts [(Int32)ColorComponent.Blue] = blueBits;
this.shiftAmounts[(Int32)ColorComponent.Blue] = blueShift;
this.multipliers [(Int32)ColorComponent.Blue] = blueMultiplier;
this.limitMasks[(Int32)ColorComponent.Blue] = GetLimitMask(blueBits, blueShift);
this.bitsAmounts [(Int32)ColorComponent.Alpha] = alphaBits;
this.shiftAmounts[(Int32)ColorComponent.Alpha] = alphaShift;
this.multipliers [(Int32)ColorComponent.Alpha] = alphaMultiplier;
this.limitMasks[(Int32)ColorComponent.Alpha] = GetLimitMask(alphaBits, alphaShift);
}
private static UInt32 GetLimitMask(Byte bpp, Byte shift)
{
return (UInt32)(((1 << bpp) - 1) << shift);
}
/// <summary>
/// Using this multiplier instead of a basic int ensures a true uniform distribution of values of this bits length over the 0-255 range.
/// </summary>
/// <param name="colorComponentBitLength">Bits length of the color component</param>
/// <returns>The most correct multiplier to convert colour components of the given bits length to a 0-255 range.</returns>
public static Double CalculateMultiplier(Byte colorComponentBitLength)
{
return 255.0 / ((1 << colorComponentBitLength) - 1);
}
public Color GetColor(Byte[] data, Int32 offset)
{
UInt32 value = ArrayUtils.ReadIntFromByteArray(data, offset, this.bytesPerPixel, this.littleEndian);
return GetColorFromValue(value);
}
public void WriteColor(Byte[] data, Int32 offset, Color color)
{
UInt32 value = GetValueFromColor(color);
ArrayUtils.WriteIntToByteArray(data, offset, this.bytesPerPixel, this.littleEndian, value);
}
public Color GetColorFromValue(UInt32 readValue)
{
Byte[] components = new Byte[4];
for (Int32 i = 0; i < 4; i++)
{
if (bitsAmounts[i] == 0)
components[i] = defaults[i];
else
components[i] = GetChannelFromValue(readValue, (ColorComponent)i);
}
return Color.FromArgb(components[(Int32)ColorComponent.Alpha],
components[(Int32)ColorComponent.Red],
components[(Int32)ColorComponent.Green],
components[(Int32)ColorComponent.Blue]);
}
private Byte GetChannelFromValue(UInt32 readValue, ColorComponent type)
{
UInt32 val = (UInt32)(readValue & limitMasks[(Int32)type]);
val = (UInt32)(val >> this.shiftAmounts[(Int32)type]);
Double valD = (Double)(val * multipliers[(Int32)type]);
return (Byte)Math.Min(255, Math.Round(valD, MidpointRounding.AwayFromZero));
}
public UInt32 GetValueFromColor(Color color)
{
Byte[] components = new Byte[] { color.R, color.G, color.B, color.A};
UInt32 val = 0;
for (Int32 i = 0; i < 4; i++)
{
UInt32 mask = (UInt32)((1 << bitsAmounts[i]) - 1);
Double tempValD = (Double)components[i] / this.multipliers[i];
UInt32 tempVal = (Byte)Math.Min(mask, Math.Round(tempValD, MidpointRounding.AwayFromZero));
tempVal = (UInt32)(tempVal << this.shiftAmounts[i]);
val |= tempVal;
}
return val;
}
}
All you need to know to use it is the order in which the bytes needs to be considered (little-endian or big-endian), the bit offset of each colour component in the final read value (which I limited to 32 bits because that gives a full byte per component anyway), and how many bits each component is.
Of course, you're using NIR instead of alpha, but that doesn't change the method whatsoever; even using my code you can just consider the "alpha" value as NIR.
If the bits are indeed packed together where the next pixel uses the remaining 6 bits from the previous one and so on, things get more complex, and you'll have to process them in blocks of pixels, as Spektre said in his answer. But then you can just read and shift them yourself (using >>
to shift them down) to get the final values, and then call GetColorFromValue
directly to get the colours out.
An example of its usage which I used for reading images extracted from some N64 ROM:
//bytes 84 21 ==> 0x8421 (BE) ==bin==> 1000 0100 0010 0001 ==split==> 10000 10000 10000 1 ==dec==> 16 16 16 1 (RGBA) ==adjust==> 128 128 128 255
private static PixelFormatter SixteenBppFormatter = new PixelFormatter(2, 5, 11, 5, 6, 5, 1, 1, 0, false);
protected static Byte[] Convert16bTo32b(Byte[] imageData, Int32 startOffset, Int32 width, Int32 height, ref Int32 stride)
{
Int32 newImageStride = width * 4; ;
Byte[] newImageData = new Byte[height * newImageStride];
for (Int32 y = 0; y < height; y++)
{
for (Int32 x = 0; x < width; x++)
{
Int32 sourceOffset = y * stride + x * 2;
Int32 targetOffset = y * newImageStride + x * 4;
Color c = SixteenBppFormatter.GetColor(imageData, startOffset + sourceOffset);
PixelFormatter.Format32BitArgb.WriteColor(newImageData, targetOffset, c);
}
}
stride = newImageStride;
return newImageData;
}
The actual method of getting the colour value is a bit more accurate now than the simple *8 I did in the sample decoding comment there, though; the CalculateMultiplier
function takes care of getting the values evenly distributed over the 0-255 range. Though if you want to do it the simple way (might cause less rounding errors in the conversions) the multipliers can all be given manually using the more complex constructor.
The final bytes were then inserted into a newly created 32-bit image with the this method used to access and write into the underlying bytes of the image.
Oh, here are the mentioned ReadIntFromByteArray
and WriteIntToByteArray
from my ArrayUtils
class:
public static UInt32 ReadIntFromByteArray(Byte[] data, Int32 startIndex, Int32 bytes, Boolean littleEndian)
{
Int32 lastByte = bytes - 1;
if (data.Length < startIndex + bytes)
throw new ArgumentOutOfRangeException("startIndex", "Data array is too small to write a " + bytes + "-byte value at offset" + startIndex + ".");
UInt32 value = 0;
for (Int32 index = 0; index < bytes; index++)
{
Int32 offs = startIndex + (littleEndian ? index : lastByte - index);
value += (UInt32)(data[offs] << (8 * index));
}
return value;
}
public static void WriteIntToByteArray(Byte[] data, Int32 startIndex, Int32 bytes, Boolean littleEndian, UInt32 value)
{
Int32 lastByte = bytes - 1;
if (data.Length < startIndex + bytes)
throw new ArgumentOutOfRangeException("startIndex", "Data array is too small to write a " + bytes + "-byte value at offset" + startIndex + ".");
for (Int32 index = 0; index < bytes; index++)
{
Int32 offs = startIndex + (littleEndian ? index : lastByte - index);
data[offs] = (Byte)(value >> (8 * index) & 0xFF);
}
}
Upvotes: 0
Reputation: 51883
I code in C++ not in C# so you need to port my code ...
You should add the pixel composition (how many bits per band and their order).
so I will just create one and you must change it to your case !!!
bit: |9 8 7 6 5 4 3 2 1 0|
band: | R | G | B | NIR |
R - 2 bits
Now how to work with it ...
Now 4*10 = 40 and 40/8=5 which means every 4 pixels are aligned to 5 BYTES (LCM(10,8))
Let assume this holds your image
int xs,ys; // resolution
int siz; // BYTE size of whole image data ... siz = ceil(xs*ys*10/8)
BYTE *dat=new BYTE[siz+5]; // 10bit image data
so now how to read 4 pixels from 5 BYTES and convert to something more BYTE aligned...
data layout is like this:
| 0 | 1 | 2 | 3 | 4 | // BYTE
|rrgggbbn|nn rrgggb|bnnn rrgg|gbbnnn rr|gggbbnnn|
| 0 | 1 | 2 | 3 | // Pixel
first chose new pixel format aligned to BYTES
bit: |15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0|
band: | R | G | B | NIR | // all bands are 4 bits
I would convert pixel format like this:
void convert10to16 (BYTE *dst,BYTE *src)
{
int i=0,o=0;
BYTE in,out;
in=scr[i]; i++; // rrgggbbn
out =(in>>2)&0x30; // 00rr0000
out|=(in>>3)&0x07; // 00rr0ggg
dst[o]=out; o++;
out =(in<<3)&0x30; // 00bb0000
out|=(in<<2)&0x04; // 00bb0n00
in=scr[i]; i++; // nnrrgggb
out|=(in>>6)&0x03; // 00bb0nnn
dst[o]=out; o++;
out =(in )&0x30; // 00rr0000
out|=(in>>1)&0x07; // 00rr0ggg
dst[o]=out; o++;
out =(in<<5)&0x20; // 00b00000
in=scr[i]; i++; // bnnnrrgg
out|=(in>>3)&0x10; // 00bb0000
out|=(in>>4)&0x07; // 00bb0nnn
dst[o]=out; o++;
out =(in<<2)&0x30; // 00rr0000
out|=(in<<1)&0x06; // 00rr0gg0
in=scr[i]; i++; // gbbnnnrr
out|=(in>>7)&0x01; // 00rr0ggg
dst[o]=out; o++;
out =(in>>1)&0x30; // 00bb0000
out|=(in>>2)&0x07; // 00bb0nnn
dst[o]=out; o++;
out =(in<<4)&0x30; // 00rr0000
in=scr[i]; i++; // gggbbnnn
out|=(in>>5)&0x07; // 00rr0ggg
dst[o]=out; o++;
out =(in<<1)&0x30; // 00bb0000
out|=(in )&0x07; // 00bb0nnn
dst[o]=out; o++;
}
hope I did not mistake or typo somewhere but you should get the idea
so now just
BYTE *xxx=new BYTE[xs*ys*2+8] // 16 bit per pixel data (2 BYTE per pixel)
BYTE *src,*dst;
int i;
for (src=dat,dst=xxx,i=0;i<siz;i+=5,src+=5,dst+=8)
convert10to16(dst,src);
also you can rewrite this to access individual pixels without conversion but that is much slower access
Upvotes: 1