Introduction
Having just used Qt to CRUD DICOM files, I have some experience with handing
metadata. DICOM is a metadata-heavy image format used in medical context. It
contains patient, image, region, and annotation in addition to the pixel data.
DICOM also has structured metadata, meaning it contains sequences, and sequence
items which are used to structure the individual metadata tags. The approach
described herein was used, with great success.
A few notes on DICOM
DICOM is primarily a 16-bit format. It uses numerical tags (called groups and
elements), and stores data in various fundamental types. Many times, itis aided
by an optional dictionary which specifies a ASCII name and data type. There is
an industry-specified dictionary, though vendors do add their own 'private
tags'. Also, there are various serializations - some are 'explicit', as
specifying the data type, others are 'implicit', requiring the use of the
dictionary. In addition to this, there are big endian and little endian formats.
A sample tag looks like:
|GGGG|EEEE|LENG|DATA|
Where each is group, element, data length (always even) and data (padded to
even) resspecively.
A sample seqeunce looks like:
|GGGG|EEEE|LENG| -- Where GGGG|EEEE is a specific sequence tag
BoIT|LEN| x N -- A Begining of item
GGGG|EEEE|LENG|DATA| x M -- any number of tags and
data, (including sequences)
[EoIT] -- Optional End of Item (req if LEN==-1)
This metadata can span 10s of KB.
The DICOM file usually finishes with a special tag for pixel data, which is in
some format as specified by the metadata. This pixel data averages 10s of
megabytes, with actual images approaching 600MB, and over gigabyte in the lab.
Therefore, it is not an acceptable assumption to be able to load the entire
image into memory.
With Qt, I was able to create new DICOM files, read existing files, modify data
(update/delete) and serialize it back out to DICOM, a SQL table or XML.
The Key Observations
Observation the First: Structured data
Structured data can be serialized using 3 operations:
1. Create Node - createNode(QString tag, QVariantMap attrs, QVariant value)
2. Push - push()
3. Pop - pop()
Any format that is not random access can be expressed this way. XML for example
would look like:
<items> createNode("items", {}, QVariant::Qvariant())
push()
<item id="1">data1</item> createNode("item", {"id"="1"}, "data1")
<item id="2">data2</item> createNode("item", {"id"="2"}, "data2")
</items> pop()
Now the only thing needed is a data model that understands these commands. We
need it to hold a variant value, be able to have children, and be randomly
addressable. Enter QRecursiveMap
QRecursiveMap (proposed name) is nothing mode than a QMAP<T, Variant> that has
been extended to have its own value property, and return a sub-recursive map
automatically. createNode() then sets the value. One more small peice is
missing
to handle the pushes/pops. I used another class to take care of maintaining the
current RecursiveMap (the target of createNode()) and provide a stack for
pop()s.
Once these classes are complete, the above XML can be read addressed as
Qvariant data1 = map["items"]["item"]["0"].value(); //T=QString
[Note: Which is a very easy thing to make look like XPATH:
"items/item/0.node()"
(use with .split('/'))]
Observation the Second
Tags are really "tag-found" events, and need to be emitted. This is true of XML
tags, DICOM tags or any data. Therefore, we need to only write a
[Format]TagReader class which 'emits' our 3 commands. For DICOM, I used a
QDataStream. But I also wrote them for XML, RecursiveMap and SQL, pretty
easily.
Once we speak this language of the three magic commands, we're set.
Next, we need to write a Writer for these events. What we end up with are
classes names in the format:
[Media][Format][Operation]
FileDICOMReader
FileDICOMWriter
XMLReader
XMLWriter
MapReader
MapWriter
ScreenWriter - for debug output
To make a program with these classes, we only need select our input class, and
our output class, then QObject::connect() them
FileDICOMReader in("1.dcm");
RecursiveMap map;
MapWriter mw(map);
ScreenWriter dbg;
in.connectTo(map);
in.connectTo(dbg);
in.read(); // this is where all the signal slots happen.
// map now has the tag tree
map["items"]["item"][0].value="new data"; // change a value
MapReader mr(map);
XMLWriter outXml("1.xml");
mr.read(); // the map is iterated over, producing tags and data that are fed to
the XML writer
But wait there's more!
I over-simplified when I said '3' commands. Really, because we work with files,
we need to add begin() and done() so that the writer can initialize, open a
file, close a file, etc.
It is also possible to also set your writer not to accept certain tags, say
pixel data. (For metadata reading this is essential) Also, note for any
potentially large data, the API needs to support a 'chunked' interface, this
way
pixel data can be read and processed in blocks. This keeps memory usage down
and
reduces latency. There is GraphicsMagick (ImageMagick competitor) that supports
a streaming conversion interface. It would be possible to wrap that in a web
service, chunk-read your file and ship your pixel data to the web service, and
read the converted pixel data. If you don't stream it, then you have to load,
send, read. But if you stream it, it will only be milliseconds before you start
getting data back. Which might not mean a whole lot in wall-clock, but the
local
system can service and arbitrary number streams of arbitrary image size,
whereas, if it isn't chunked, the memory limits of the local system will become
a factor.
QDom
With QDom "going away" this technique provides an even easier interface for
documents which need to be modified. Previously QDom was the only way.
The future is limitless!
Exercise to the reader: implement TAR or ZIP serialization using this
technique.
_______________________________________________
Qt-mobility-feedback mailing list
[email protected]
http://lists.trolltech.com/mailman/listinfo/qt-mobility-feedback