65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 位图,支持完整的 16 位

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (4投票s)

2014年3月18日

MIT

2分钟阅读

viewsIcon

26735

读取和写入每个通道具有完整 16 位(甚至更多)的位图

引言

如果您曾经尝试使用 System.Drawing.Bitmap 类读取 16 位图像,您可能大失所望。出于某种难以理解的原因,它仅以 13 位存储 16 位数据(从 65535 个值到 8192 个,差异很大),而且即使这样也无法正常工作。

但是,好消息是:这里有一个解决方法,我将在这里介绍!

背景

要使这段代码正常工作,您需要引用四个 .NET DLL

  • 系统
  • PresentationCore
  • WindowsBase
  • System.Xaml

代码

我创建了一个相当简单的类来存储和处理数据,并提供基本方法。您可以根据需要添加您可能需要的方法。最后,我将整个类放在一起,以便您可以更轻松地复制/粘贴它。

首先,这个类的变量

public int Width;           //Width of image in pixel
public int Height;          //Height of image in pixel
public int ChannelCount;    //Number of channels
public int BytesPerChannel; //Number of bytes per channel
public bool HasAlpha;       //Image has alpha channel or not
public int BitDepth;        //Bits per channel
public bool IsBGR;          //Byte order BGR or RGB
public bool IsPinned;       //ImageData pinned or not
public IntPtr Scan0;        //Position of first byte (Set when ImageData is pinned)

private PixelFormat InputPixelFormat;  //Pixel format
private GCHandle pinHandle;            //Handler to pin/unpin ImageData
private byte[] ImageData;              //The image data itself  

现在是构造函数,基本上,我们可以通过各种方式创建一个 BitampSource

/// <summary>
/// Loads an image from a path. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
/// </summary>
/// <param name="path">Path to the image</param>
public BitmapEx(string path)
{
    using (Stream str = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
    {
        BitmapDecoder dec = BitmapDecoder.Create(str, 
            BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
        if (dec.Frames.Count > 0) SetFromBitmapSource(dec.Frames[0]);
        else throw new FileLoadException("Couldn't load file " + path);
    }
}

/// <summary>
/// Loads an image from an encoded stream. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
/// </summary>
public BitmapEx(Stream encodedStream)
{
    encodedStream.Position = 0;
    BitmapDecoder dec = BitmapDecoder.Create(encodedStream, 
        BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
    if (dec.Frames.Count > 0) SetFromBitmapSource(dec.Frames[0]);
    else throw new FileLoadException("Couldn't load file");
}

/// <summary>
/// Loads an image from an encoded byte array. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
/// </summary>
public BitmapEx(byte[] encodedData)
{
    using (MemoryStream str = new MemoryStream(encodedData))
    {
        BitmapDecoder dec = BitmapDecoder.Create(str, 
            BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
        if (dec.Frames.Count > 0) SetFromBitmapSource(dec.Frames[0]);
        else throw new FileLoadException("Couldn't load file");
    }
}

现在我们有了 BitampSource,我们可以从中读取信息并存储图像数据

private void SetFromBitmapSource(BitmapSource bmpSrc)
{
    this.Width = bmpSrc.PixelWidth;
    this.Height = bmpSrc.PixelHeight;

    InputPixelFormat = bmpSrc.Format;
    if (bmpSrc.Format == PixelFormats.Bgr24)
    {
        this.ChannelCount = 3;
        this.BytesPerChannel = 1;
        this.HasAlpha = false;
        this.BitDepth = 8;
        this.IsBGR = true;
    }
    else if (bmpSrc.Format == PixelFormats.Bgra32)
    {
        this.ChannelCount = 4;
        this.BytesPerChannel = 1;
        this.HasAlpha = true;
        this.BitDepth = 8;
        this.IsBGR = true;
    }
    else if (bmpSrc.Format == PixelFormats.Rgb24)
    {
        this.ChannelCount = 3;
        this.BytesPerChannel = 1;
        this.HasAlpha = false;
        this.BitDepth = 8;
        this.IsBGR = false;
    }
    else if (bmpSrc.Format == PixelFormats.Rgb48)
    {
        this.ChannelCount = 3;
        this.BytesPerChannel = 2;
        this.HasAlpha = false;
        this.BitDepth = 16;
        this.IsBGR = false;
    }
    else
    {
        //There are some more special cases you might want to handle
        //Also, it's possible that bmpSrc.Format == PixelFormats.Default
        //Then you can only check for BitsPerPixel field 
        //and guess the channel order (I assume it's BGR)
        throw new NotImplementedException();
    }

    int stride = this.Width * this.BytesPerChannel * this.ChannelCount;
    ImageData = new byte[this.Height * stride]; //Init our byte array with the right size
    bmpSrc.CopyPixels(ImageData, stride, 0); //Copy image data to our byte array
} 

太好了,我们得到了我们的图像!您可能现在想对它进行一些操作。为此,请固定数据以进行处理,并在完成后取消固定它。我在最后添加了一个关于如何操作数据的示例。

public void LockBits()
{
    if (!IsPinned)
    {
        pinHandle = GCHandle.Alloc(ImageData, GCHandleType.Pinned); //Pin the image data
        Scan0 = pinHandle.AddrOfPinnedObject();  //Get the pointer to the first byte
        IsPinned = true;
    }
}

public void UnlockBits()
{
    if (IsPinned)
    {
        pinHandle.Free();  //Unpin the image data
        IsPinned = false;
    }
}

很好。完成图像处理后,您可能想再次保存它。所以让我们这样做

/// <summary>
/// Saves the image to a path. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
/// </summary>
/// <param name="path">Path where the image should be saved to</param>
public void Save(string path)
{
    string ext = Path.GetExtension(path).ToLower();
    using (FileStream str = new FileStream(path, FileMode.Create)) { this.Save(str, ext); }
}

/// <summary>
/// Saves the image into a stream.
/// </summary>
/// <param name="ext">Extension of the desired file format. 
/// Allowed values: ".jpg", ".jpeg", ".png", ".tiff", 
/// ".tif", ".bmp", ".gif", ".wdp"</param>
/// <param name="str">The stream where the image will be saved to.</param>
public void Save(Stream str, string ext)
{
    BitmapEncoder enc;  //Find the right encoder
    switch (ext)
    {
        case ".jpg":
        case ".jpeg": enc = new JpegBitmapEncoder(); 
            ((JpegBitmapEncoder)enc).QualityLevel = 100; break;
        case ".tif":
        case ".tiff": enc = new TiffBitmapEncoder(); 
            ((TiffBitmapEncoder)enc).Compression = TiffCompressOption.Lzw; break;
        case ".png": enc = new PngBitmapEncoder(); break;
        case ".bmp": enc = new BmpBitmapEncoder(); break;
        case ".wdp": enc = new WmpBitmapEncoder(); break;

        default:
            throw new ArgumentException("File format not supported *" + ext);
    }
    //Create a BitmapSource from all the data
    BitmapSource src = BitmapSource.Create((int)this.Width, (int)this.Height, 96, 96, 
        InputPixelFormat, null, ImageData, (int)(this.Width * this.BytesPerChannel * this.ChannelCount));
    enc.Frames.Add(BitmapFrame.Create(src));  //Add the data to the first frame
    enc.Save(str);   //Save the data to the stream
}

使用示例

以下是如何使用该类的示例。将 CalcNewRedCalcNewGreenCalcNewBlue 方法替换为有用的方法,您可以在其中计算新值。

为了确保新值在范围内,这似乎是最快的方法(其中 MAXVALUE 是它可以拥有的最大值,例如 byte255 ushort65535

 Math.Max(0, Math.Min(value, MAXVALUE)

这是它如何与 8 位图像一起工作。

BitmapEx bmp = new BitmapEx("Example.jpg");
try
{
    bmp.LockBits();
    unsafe
    {
        int index, x, y;
        int stride = bmp.Width * bmp.ChannelCount;
        byte* pix = (byte*)bmp.Scan0;
        for (y = 0; y < bmp.Height; y++)
        {
            for (x = 0; x < bmp.Width; x++)
            {
                index = y * stride + (x * bmp.ChannelCount);

                float newR = CalcNewRed();
                float newG = CalcNewGreen();
                float newB = CalcNewBlue();

                if (bmp.IsBGR)
                {
                    pix[index] = (byte)Math.Max(0, Math.Min(newB, 255));     //Blue
                    pix[index + 1] = (byte)Math.Max(0, Math.Min(newG, 255)); //Green
                    pix[index + 2] = (byte)Math.Max(0, Math.Min(newR, 255)); //Red
                }
                else
                {
                    pix[index] = (byte)Math.Max(0, Math.Min(newR, 255));     //Red
                    pix[index + 1] = (byte)Math.Max(0, Math.Min(newG, 255)); //Green
                    pix[index + 2] = (byte)Math.Max(0, Math.Min(newB, 255)); //Blue
                }
                if (bmp.HasAlpha) pix[index + 3] = 0;   //Alpha
            }
        }
    }
}
finally { bmp.UnlockBits(); }
bmp.Save("ExampleOut.jpg");

这是它如何与 16 位图像一起工作。

BitmapEx bmp = new BitmapEx("Example.tif");
try
{
    bmp.LockBits();
    unsafe
    {
        int index, x, y;
        int stride = bmp.Width * bmp.ChannelCount;
        ushort* pix = (ushort*)bmp.Scan0;
        for (y = 0; y < bmp.Height; y++)
        {
            for (x = 0; x < bmp.Width; x++)
            {
                index = y * stride + (x * bmp.ChannelCount);

                float newR = CalcNewRed();
                float newG = CalcNewGreen();
                float newB = CalcNewBlue();

                if (bmp.IsBGR)
                {
                    pix[index] = (ushort)Math.Max(0, Math.Min(newB, 65535));     //Blue
                    pix[index + 1] = (ushort)Math.Max(0, Math.Min(newG, 65535)); //Green
                    pix[index + 2] = (ushort)Math.Max(0, Math.Min(newR, 65535)); //Red
                }
                else
                {
                    pix[index] = (ushort)Math.Max(0, Math.Min(newR, 65535));     //Red
                    pix[index + 1] = (ushort)Math.Max(0, Math.Min(newG, 65535)); //Green
                    pix[index + 2] = (ushort)Math.Max(0, Math.Min(newB, 65535)); //Blue
                }
                if (bmp.HasAlpha) pix[index + 3] = 0;   //Alpha
            }
        }
    }
}
finally { bmp.UnlockBits(); }
bmp.Save("ExampleOut.tif");

对于更多的位数,它将以相同的方式工作,只需使用正确的值类型(例如,对于 32 位使用 uint),并且不要忘记更改最大值。

完整源代码

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Media;
using System.Windows.Media.Imaging;

public class BitmapEx
{
    public int Width;
    public int Height;

    public int ChannelCount;
    public int BytesPerChannel;
    public bool HasAlpha;
    public int BitDepth;
    public bool IsBGR;
    public bool IsPinned;
    public IntPtr Scan0;

    private PixelFormat InputPixelFormat;
    private GCHandle pinHandle;
    private byte[] ImageData;

        
    /// <summary>
    /// Loads an image from a path. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
    /// </summary>
    /// <param name="path">Path to the image</param>
    public BitmapEx(string path)
    {
        using (Stream str = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            BitmapDecoder dec = BitmapDecoder.Create(str, 
                BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
            if (dec.Frames.Count > 0) SetFromBitmapSource(dec.Frames[0]);
            else throw new FileLoadException("Couldn't load file " + path);
        }
    }

    /// <summary>
    /// Loads an image from an encoded stream. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
    /// </summary>
    public BitmapEx(Stream encodedStream)
    {
        encodedStream.Position = 0;
        BitmapDecoder dec = BitmapDecoder.Create(encodedStream, 
            BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
        if (dec.Frames.Count > 0) SetFromBitmapSource(dec.Frames[0]);
        else throw new FileLoadException("Couldn't load file");
    }

    /// <summary>
    /// Loads an image from an encoded byte array. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
    /// </summary>
    public BitmapEx(byte[] encodedData)
    {
        using (MemoryStream str = new MemoryStream(encodedData))
        {
            BitmapDecoder dec = BitmapDecoder.Create(str, 
                BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
            if (dec.Frames.Count > 0) SetFromBitmapSource(dec.Frames[0]);
            else throw new FileLoadException("Couldn't load file");
        }
    }
        
    private void SetFromBitmapSource(BitmapSource bmpSrc)
    {
        this.Width = bmpSrc.PixelWidth;
        this.Height = bmpSrc.PixelHeight;

        InputPixelFormat = bmpSrc.Format;
        if (bmpSrc.Format == PixelFormats.Bgr24)
        {
            this.ChannelCount = 3;
            this.BytesPerChannel = 1;
            this.HasAlpha = false;
            this.BitDepth = 8;
            this.IsBGR = true;
        }
        else if (bmpSrc.Format == PixelFormats.Bgra32)
        {
            this.ChannelCount = 4;
            this.BytesPerChannel = 1;
            this.HasAlpha = true;
            this.BitDepth = 8;
            this.IsBGR = true;
        }
        else if (bmpSrc.Format == PixelFormats.Rgb24)
        {
            this.ChannelCount = 3;
            this.BytesPerChannel = 1;
            this.HasAlpha = false;
            this.BitDepth = 8;
            this.IsBGR = false;
        }
        else if (bmpSrc.Format == PixelFormats.Rgb48)
        {
            this.ChannelCount = 3;
            this.BytesPerChannel = 2;
            this.HasAlpha = false;
            this.BitDepth = 16;
            this.IsBGR = false;
        }
        else
        {
            //There are some more special cases you might want to handle
            //Also, it's possible that bmpSrc.Format == PixelFormats.Default
            //Then you can only check for BitsPerPixel field and guess the channel order (I assume it's BGR)
            throw new NotImplementedException();
        }

        int stride = this.Width * this.BytesPerChannel * this.ChannelCount;
        ImageData = new byte[this.Height * stride];
        bmpSrc.CopyPixels(ImageData, stride, 0);
    }

    public void LockBits()
    {
        if (!IsPinned)
        {
            pinHandle = GCHandle.Alloc(ImageData, GCHandleType.Pinned);
            Scan0 = pinHandle.AddrOfPinnedObject();
            IsPinned = true;
        }
    }

    public void UnlockBits()
    {
        if (IsPinned)
        {
            pinHandle.Free();
            IsPinned = false;
        }
    }

    /// <summary>
    /// Saves the image to a path. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
    /// </summary>
    /// <param name="path">Path where the image should be saved to</param>
    public void Save(string path)
    {
        string ext = Path.GetExtension(path).ToLower();
        using (FileStream str = new FileStream(path, FileMode.Create)) { this.Save(str, ext); }
    }

    /// <summary>
    /// Saves the image into a stream.
    /// </summary>
    /// <param name="ext">Extension of the desired file format. 
    /// Allowed values: ".jpg", ".jpeg", ".png", 
    /// ".tiff", ".tif", ".bmp", ".gif", ".wdp"</param>
    /// <param name="str">The stream where the image will be saved to.</param>
    public void Save(Stream str, string ext)
    {
        BitmapEncoder enc;
        switch (ext)
        {
            case ".jpg":
            case ".jpeg": enc = new JpegBitmapEncoder(); 
                ((JpegBitmapEncoder)enc).QualityLevel = 100; break;
            case ".tif":
            case ".tiff": enc = new TiffBitmapEncoder(); 
                ((TiffBitmapEncoder)enc).Compression = TiffCompressOption.Lzw; break;
            case ".png": enc = new PngBitmapEncoder(); break;
            case ".bmp": enc = new BmpBitmapEncoder(); break;
            case ".wdp": enc = new WmpBitmapEncoder(); break;

            default:
                throw new ArgumentException("File format not supported *" + ext);
        }

        BitmapSource src = BitmapSource.Create((int)this.Width, (int)this.Height, 96, 96, 
        InputPixelFormat, null, ImageData, (int)(this.Width * this.BytesPerChannel * this.ChannelCount));
        enc.Frames.Add(BitmapFrame.Create(src));
        enc.Save(str);
    }
}

关注点

您可以根据需要使用此代码。有关更多信息,请参阅 MIT 许可证。

通过付出更多的努力,您还可以使用这个类来处理灰度图像(即单通道图像)或一些更特殊的像素格式。

还有什么问题吗?请给我留言,我不会咬人的(也许;))!

历史

  • 2014 年 3 月 - 初始版本
© . All rights reserved.