Introduction
If you have ever tried to read 16bit images with the System.Drawing.Bitmap
class, you may have been bitterly disappointed. For some obscure reason, it stores 16bit data only with 13bit (from 65535 values to 8192 is quite the difference) and even that doesn't work properly.
But here is the good news: there is a workaround which I will present here!
Background
To get this code working, you need to reference four .NET DLLs:
-
System
-
PresentationCore
-
WindowsBase
-
System.Xaml
The Code
I made a rather simple class to store and handle the data and provide the basic methods. You can add methods you may need as you wish. At the end, I added the whole class in one piece so you can copy/paste it easier.
First, the variables of this class:
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;
Now the constructors, basically we create a BitampSource
class in various ways:
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);
}
}
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");
}
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");
}
}
Now we have the BitampSource
, we can read our information from that and store the image data:
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
{
throw new NotImplementedException();
}
int stride = this.Width * this.BytesPerChannel * this.ChannelCount;
ImageData = new byte[this.Height * stride]; bmpSrc.CopyPixels(ImageData, stride, 0); }
Great, we have our image! You probably want to do stuff with it now. To do that, pin the data to work with it and unpin it when you are finished. I added an example on how to manipulate the data at the end.
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;
}
}
Good good. After you are finished with the image, you might want to save it again. So let's do that:
public void Save(string path)
{
string ext = Path.GetExtension(path).ToLower();
using (FileStream str = new FileStream(path, FileMode.Create)) { this.Save(str, ext); }
}
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); }
Example of Use
Here are examples on how to use the class. Replace the CalcNewRed
, CalcNewGreen
, CalcNewBlue
methods with something useful where you calculate new values.
To ensure the new values are in range, this seems to be the fastest way (where MAXVALUE
is the biggest value it can have, e.g. 255
for byte
or 65535
for ushort
)
Math.Max(0, Math.Min(value, MAXVALUE)
This is how it would work with an 8-bit image.
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)); pix[index + 1] = (byte)Math.Max(0, Math.Min(newG, 255)); pix[index + 2] = (byte)Math.Max(0, Math.Min(newR, 255)); }
else
{
pix[index] = (byte)Math.Max(0, Math.Min(newR, 255)); pix[index + 1] = (byte)Math.Max(0, Math.Min(newG, 255)); pix[index + 2] = (byte)Math.Max(0, Math.Min(newB, 255)); }
if (bmp.HasAlpha) pix[index + 3] = 0; }
}
}
}
finally { bmp.UnlockBits(); }
bmp.Save("ExampleOut.jpg");
And this is how it would work with a 16-bit image.
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)); pix[index + 1] = (ushort)Math.Max(0, Math.Min(newG, 65535)); pix[index + 2] = (ushort)Math.Max(0, Math.Min(newR, 65535)); }
else
{
pix[index] = (ushort)Math.Max(0, Math.Min(newR, 65535)); pix[index + 1] = (ushort)Math.Max(0, Math.Min(newG, 65535)); pix[index + 2] = (ushort)Math.Max(0, Math.Min(newB, 65535)); }
if (bmp.HasAlpha) pix[index + 3] = 0; }
}
}
}
finally { bmp.UnlockBits(); }
bmp.Save("ExampleOut.tif");
With more bits, it would work the same, just use the correct value type (uint
for 32 bit for example) and don't forget to change the max value.
Full Source Code
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;
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);
}
}
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");
}
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
{
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;
}
}
public void Save(string path)
{
string ext = Path.GetExtension(path).ToLower();
using (FileStream str = new FileStream(path, FileMode.Create)) { this.Save(str, ext); }
}
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);
}
}
Points of Interest
You may use this code as you wish. See MIT license for more information.
With a little bit more effort, you could also use this class to handle gray images (i.e. one-channel images) or some of the more special pixel formats.
Any questions left? Leave me a comment, I don't bite (maybe ;))!
History
-
March 2014 - Initial version