Wednesday, July 25, 2007

So you want to save and load 16 bit images in C#.

Say, for instance, that you're working for a medical device company, and you need to write in C#.

How do you load in the images from the device?

For those of you who don't know, images from medical devices are usually 16 bit grayscale images, maybe sometimes 15. That's ushort, or UInt16, and making it a short or SInt16 can sometimes have dire consequences. Dire, as in, people die, because this is a medical device we're making. That's the first thing-- our images are 16 bit graqyscale, from now until 18 bit or 20 bit imagers are made. For those of you who are curious, I'll end this post with a small discussion about what bit depth really means, but for now, let's just say you want to display the damn thing.

The C# (or .NET) environment has a 16 bit grayscale bitmap class. If it worked, it would be ideal for us, because we could display it right after loading it. Alas, it is not to be; the 16 bit class doesn't work. Microsoft's documentation on the subject of PixelFormats is woefully inadequate; even after that, though, just try using a 16 bit grayscale. Watch it fail with no real error code.

Microsoft, as a side note, really has no idea what goes into this kind of application, as they demonstrate here. I'm not sure they really should, either; it's not their purview to mess with medical devices, but it is annoying that they seem to think they know anything about the subject.

So, we need an outside library to load our images.

If you're in the medical imaging field, then you are using DICOM. DICOM is a standard format for storing and retrieving medical image data, either locally or from a central server, that should be entirely independent of vendor. Really, this is the agreed-upon format for medical images, and if you're making a serious medical imaging app, you need this format, period.

To use it, I use dcmtk. I don't claim to be an expert; luckily, one of my coworkers knows a lot about DICOM, and wrote me a little reader code snippet for our appplication. I could show it to you, but the problem is, each application is different, and uses their own DICOM tags. Plus, there may be IP issues; if I get to post my code in its entirety, then I can show you the DICOM load function.

For me, I really don't care what format the data comes in, just so long as I can get my hands on numbers. I just want the data, the width, and the height. Depth is good for 3D images as well. Everything else can be figured out. So, my internal representation of an image is quite simple, and you can see it here:



namespace
YourNamespaceHere {

public class
ImageContainer {
private
bool mNulled;
public
bool Nulled {

get { return mNulled; }
}

private
ushort[] mData;

public
ushort[] Data {//note that this returns a reference, not a copy
get { return mData; }
}


private
int mXSize, mYSize;
public
int XSize {

get { return mXSize; }
}

public
int YSize {

get { return mYSize; }
}

private
int mAssessmentID;

public
int AssessmentID {
get { return mAssessmentID; }
}


private
String mName;
public
String Name {
get {

if
(mAssessmentID < 0) {
if
(mkVp < 0 && mmAs < 0) {

return
mName;
}
else {
return
mName + " " + mkVp.ToString() + " " + mmAs.ToString();
}
}
else {

return
"Assessment Image " + mAssessmentID;
}
}
}

private
int mkVp;

public
int kVp {
get { return mkVp; }

set { mkVp = value; }
}

private
int mmAs;

public
int mAs{
get { return mmAs; }

set { mmAs = value; }
}


//private DicomHeader
//private Annotations
//private string openedas, so that save can use the default type

public
ImageContainer(String inNullString) {

System.Console.WriteLine("Null image constructor used.");
mData = null;

mXSize = -1;
mYSize = -1;
mName = "Null Image";

mkVp = -1;
mmAs = -1;
mAssessmentID = -1;

mNulled = true;
}


//generic constructor
//should get an 'opened as' tag as well, plus perhaps dicom header info if present.
public ImageContainer(ushort[] inData, int inXSize, int inYSize, String inName, int inAssessmentID) {

mData = inData;//note that this line means that this structure is responsible
//for the memory that's passed to it!
mXSize = inXSize;
mYSize = inYSize;

mName = inName;
mkVp = -1;
mmAs = -1;

mAssessmentID = inAssessmentID;
mNulled = false;
}


public
int GetPixelAt(int inX, int inY) {

return
mData[inY * mXSize + inX];
}


public
Rectangle ImageRect {
get { return new Rectangle(0, 0, XSize, YSize); }
}

}
}




I'm sorry the formatting blows, but I guess that's just Blogger for you. If anyone knows other tools, please let me know.

Note a few things:
1) I'm not allowing for _any_ data processing methods. Those go elsewhere.
2) I'm not allowing for _any_ display methods. Those also go elsewhere.
3) Essentially, this is just a name, width, height, and data. That's it, that's all I need.
4) I'm using get/set methods here for data access. That's the way it's done in C#. I'm not entirely sure why, but I'm told that it makes the compiler happier.
5) I will only use the getPixel(x, y) method very infrequently, and NEVER to march through the image. It's just way too slow; the overhead for calling a function in C# is ridiculous.

So, we have to fill that with data. For that, I will use the Free Image API, with their handily provided C# wrapper. You may have some issues compiling the C# wrapper; if so, you may need to change some classes to structs to make the compiler work. Don't worry, we're not even going to touch the methods that it complains about.

To Load, we will need to make sure:
1) We call the methods from the FreeImageAPI properly, ie:

UInt32 theBitmap = FreeImage.Load(FreeImage.GetFileType(theCompleteName, 0), theCompleteName, 0);


2) We go into unsafe code in order to read from the pointers. Email me if you want this code; blogger's formatting for code makes it entirely unreadable. There's sample code in the FreeImageAPI about transferring data from their bitmap into our image. It should be fairly straightforward, I'd think, but the casting may cause you some headaches.
3) Make SURE you unload whatever you load, ie:

FreeImage.Unload(theBitmap);

Otherwise, you will leak a block of memory the size of the bitmap (which can be quite substantial).
4) Call GC.Collect() and GC.WaitForPendingNotifiers() in order to clean up any blocks lying around. If you're loading significantly large images and then making copies or running FFT's on them or anything else that could be memory intensive, waiting for the GC to collect memory can sometimes be a bad idea, because it will cause you to thrash. So, clean up after yourself, and call the GC. (It's a pet peeve of mine that C# seems to encourage laziness with the way the GC works, but then doesn't always clean up allocated memory).

How about saving?

Pretty much the same thing, create a bitmap, move your data into it, and then make sure you unload the bitmap. The incantation for saving is:
FreeImageAPI.FreeImage.Save(FreeImageAPI.FREE_IMAGE_FORMAT.FIF_TIFF, theBitmap, inDirectory + "\\" + inName, 0);

This code is if you're saving to TIFF format, and theBitmap is the FreeImage image you've made and put your data into.

Next time: Display!


Bit depth, you say? What about it? Here's the deal: Medical devices, whether they have truly 16 bits or not, often claim that they do, and use ushorts as their type. Sometimes, you'll get cross mixing with 15bit data, and have to make sure that you're displaying the image properly. As to whether or not your device is using all 16 bits of dynamic range, I think you'll have to answer that yourself (and it's not an easy thing to answer).

No comments: