Работа с цифровым изображением глубиной 10 бит

Как обрабатывать и извлекать данные изображения, используя C# (считывая значение каждого пикселя), где пиксель имеет глубину 10 бит?

Также изображение имеет 4 полосы (R,G,B & NIR).

Заранее спасибо.

2 ответа

Я пишу код на C++, а не на C#, поэтому вам нужно перенести мой код...

Вы должны добавить пиксельную композицию (сколько бит на полосу и их порядок).

  • Вы написали 10 бит на пиксель и R,G,B,NIR (я предполагаю, около ИК) полосы в нем.
  • Вы не указали формат пикселя
  • так что я просто создам один, и вы должны изменить его в своем случае!!!

     bit: |9 8 7 6 5 4 3 2 1 0|
    band: | R |  G  | B | NIR |
    
  • R - 2 бита

  • G - 3 бита
  • B - 2 бита
  • NIR - 3 бита

Теперь как с этим работать...

  • Я бы преобразовал его в более управляемый размер (например, 4 бита на полосу)
  • так что я могу использовать стандартные типы данных...
  • и после того, как вы закончили с вашим процессом, просто конвертируйте его обратно в формат 10 бит пикселей

Теперь 4*10 = 40 и 40/8=5, что означает, что каждые 4 пикселя выровнены по 5 БАЙТАМ (LCM(10,8))

  • Давайте предположим, что это держит ваш образ

    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
    
  • так что теперь, как прочитать 4 пикселя из 5 байтов и преобразовать их во что-нибудь более выровненное по байту...

  • макет данных выглядит так:

    |    0    |    1    |    2     |    3     |    4   | // BYTE
    |rrgggbbn|nn rrgggb|bnnn rrgg|gbbnnn rr|gggbbnnn|
    |      0     |       1    |      2     |     3     | // Pixel
    
  • сначала выбрал новый формат пикселя, выровненный по байтам

     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
    
  • Я бы конвертировал формат пикселей так:

    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++;
     }
    
  • надеюсь, что я не ошибся или опечатка где-то, но вы должны понять

  • преобразование обратно в 10-битный формат пикселей может быть сделано таким же образом
  • так что теперь просто

    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);
    
  • также вы можете переписать это, чтобы получить доступ к отдельным пикселям без преобразования, но это гораздо медленнее, доступ

Я вдохновил себя кодом ScummVM на написание этого класса для автоматического преобразования цветов на основе длины в битах и ​​расположения битов каждого цветового компонента в массиве:

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

Все, что вам нужно знать, чтобы использовать его, это порядок, в котором необходимо учитывать байты (с прямым порядком байтов или с прямым порядком байтов), битовое смещение каждого компонента цвета в конечном значении чтения (которое я ограничил 32 битами, потому что это в любом случае дает полный байт на компонент) и сколько битов в каждом компоненте.

Конечно, вы используете NIR вместо альфа, но это не меняет метод; даже используя мой код, вы можете просто считать значение "альфа" как NIR.

Если биты действительно упакованы вместе, где следующий пиксель использует оставшиеся 6 битов из предыдущего и так далее, все становится более сложным, и вам придется обрабатывать их в блоках пикселей, как сказал Спектре в своем ответе. Но тогда вы можете просто прочитать и сдвинуть их самостоятельно (используя >> сдвинуть их вниз), чтобы получить окончательные значения, а затем вызвать GetColorFromValue непосредственно, чтобы получить цвета.

Пример его использования, который я использовал для чтения изображений, извлеченных из некоторого ПЗУ N64:

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

Фактический метод получения значения цвета теперь немного более точен, чем простой *8, который я сделал в примере с примером декодирования там; CalculateMultiplier Функция заботится о равномерном распределении значений в диапазоне 0-255. Хотя, если вы хотите сделать это простым способом (может привести к меньшим ошибкам округления в преобразованиях), все множители можно задать вручную, используя более сложный конструктор.

Затем последние байты были вставлены во вновь созданное 32-битное изображение с помощью метода this, который использовался для доступа и записи в нижележащие байты изображения.

О, вот упомянутые ReadIntFromByteArray а также WriteIntToByteArray от моего ArrayUtils учебный класс:

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);
    }
}
Другие вопросы по тегам