Skip to content
Commits on Source (19)
......@@ -22,6 +22,7 @@ set(DICOM_BUILD_SHARED_LIBS "@BUILD_SHARED_LIBS@")
set(DICOM_USE_ITK_GDCM "@USE_ITK_GDCM@")
set(DICOM_USE_GDCM "@USE_GDCM@")
set(DICOM_USE_DCMTK "@USE_DCMTK@")
set(DICOM_USE_SQLITE "@USE_SQLITE@")
# Auto-configured settings
set(DICOM_USE_VTKZLIB "@DICOM_USE_VTKZLIB@")
......
......@@ -20,6 +20,7 @@
#cmakedefine DICOM_BUILD_TESTING
#cmakedefine DICOM_USE_GDCM
#cmakedefine DICOM_USE_DCMTK
#cmakedefine DICOM_USE_SQLITE
#cmakedefine DICOM_USE_VTKZLIB
/* Version number. */
......@@ -32,13 +33,4 @@
/* Legacy (for backwards compatibility) */
#define DICOM_BUILD_VERSION DICOM_PATCH_VERSION
/* For compatibility with VTK 7.1 */
#ifndef VTK_DELETE_FUNCTION
#if (__cplusplus >= 201103L) || (defined(_MSC_VER) && _MSC_VER >= 1800)
#define VTK_DELETE_FUNCTION =delete
#else
#define VTK_DELETE_FUNCTION
#endif
#endif
#endif
......@@ -3,7 +3,7 @@
#ifndef vtkDICOMModule_h
#define vtkDICOMModule_h
#include <vtkABI.h>
#include "vtkABI.h"
#include "vtkDICOMConfig.h"
#if defined(DICOM_BUILD_SHARED_LIBS)
......
......@@ -134,7 +134,10 @@ $<$<BOOL:$<TARGET_PROPERTY:${module_name},INCLUDE_DIRECTORIES>>:
# which aren't library dependencies, merely dependencies for generators and
# such. The dependecies specified under "DEPENDS" in the vtk_module(..) macro
# call are located under _LINK_DEPENDS.
foreach(dep ${${module_name}_LINK_DEPENDS})
if(NOT ${module_name}_WRAP_DEPENDS)
set(${module_name}_WRAP_DEPENDS ${${module_name}_LINK_DEPENDS})
endif()
foreach(dep ${${module_name}_WRAP_DEPENDS})
if(NOT "${module_name}" STREQUAL "${dep}")
if(NOT ${dep}_EXCLUDE_FROM_WRAPPING)
list(APPEND OTHER_HIERARCHY_FILES "${${dep}_WRAP_HIERARCHY_FILE}")
......
......@@ -18,8 +18,8 @@ include(CTest)
# Project version
set(DICOM_MAJOR_VERSION 0)
set(DICOM_MINOR_VERSION 7)
set(DICOM_PATCH_VERSION 10)
set(DICOM_MINOR_VERSION 8)
set(DICOM_PATCH_VERSION 7)
set(DICOM_SHORT_VERSION "${DICOM_MAJOR_VERSION}.${DICOM_MINOR_VERSION}")
set(DICOM_VERSION "${DICOM_SHORT_VERSION}.${DICOM_PATCH_VERSION}")
......@@ -164,6 +164,17 @@ if(USE_DCMTK)
include_directories(${DCMTK_INCLUDE_DIRS})
endif()
# Check for sqlite3 for reading OsiriX databases
set(USE_SQLITE_DEFAULT OFF)
set(SQLITE_LIBS)
if(APPLE)
set(USE_SQLITE_DEFAULT ON)
endif()
option(USE_SQLITE "Use SQLite for OsiriX databases" ${USE_SQLITE_DEFAULT})
if(USE_SQLITE)
set(SQLITE_LIBS sqlite3)
endif()
# Store the git hash of the current head
if(EXISTS "${DICOM_SOURCE_DIR}/.git/HEAD")
file(READ "${DICOM_SOURCE_DIR}/.git/HEAD" DICOM_SOURCE_VERSION)
......@@ -244,17 +255,13 @@ if(NOT Module_vtkDICOM)
# The library dependencies
if("${VTK_MAJOR_VERSION}" GREATER 5)
set(VTK_LIBS vtkCommonCore vtkCommonDataModel vtkImagingCore vtkIOImage vtkIOSQL)
set(VTK_LIBS vtkCommonCore vtkCommonDataModel vtkImagingCore vtkIOCore
vtkIOImage)
# If vtkIOMPIImage is present, it has factories for vtkIOImage
list(FIND VTK_LIBRARIES vtkIOMPIImage TMP_INDEX)
if(TMP_INDEX GREATER -1)
set(VTK_LIBS ${VTK_LIBS} vtkIOMPIImage)
endif()
# If vtkIOMySQL is present, it has factories for vtkIOSQL
list(FIND VTK_LIBRARIES vtkIOMySQL TMP_INDEX)
if(TMP_INDEX GREATER -1)
set(VTK_LIBS ${VTK_LIBS} vtkIOMySQL)
endif()
else()
set(VTK_LIBS vtkCommon vtkFiltering vtkImaging vtkIO vtkInfovis) # VTK 5.x
endif()
......@@ -264,7 +271,10 @@ if(NOT Module_vtkDICOM)
${VTK_WRAP_INCLUDE_DIRS})
# For the python wrappers
if(BUILD_PYTHON_WRAPPERS)
if(BUILD_PYTHON_WRAPPERS AND
TARGET vtkCommonCorePythonD OR
TARGET vtkCommonKitPythonD OR
TARGET vtkCommonPythonD)
set(DICOM_PYTHON_LIBRARIES vtkDICOMPythonD)
endif()
......
Copyright (c) 2012-2016 David Gobbi
Copyright (c) 2012-2017 David Gobbi
All rights reserved.
Redistribution and use in source and binary forms, with or without
......
......@@ -34,6 +34,10 @@
#ifndef mainmacro_h
#define mainmacro_h
#ifdef _WIN32
#include <stddef.h>
#endif
//! Expand command line arguments for Windows command line.
/*!
* This will expand any wildcards in the arguments, and is only applied
......
......@@ -14,7 +14,7 @@
#include "progress.h"
#include <vtkDICOMUtilities.h>
#include "vtkDICOMUtilities.h"
void ProgressObserver::Execute(vtkObject *, unsigned long e, void *vp)
{
......
......@@ -14,7 +14,7 @@
#ifndef progress_h
#define progress_h
#include <vtkCommand.h>
#include "vtkCommand.h"
// Capture progress events
class ProgressObserver : public vtkCommand
......@@ -22,8 +22,12 @@ class ProgressObserver : public vtkCommand
public:
static ProgressObserver *New() { return new ProgressObserver; }
vtkTypeMacro(ProgressObserver,vtkCommand);
virtual void Execute(
vtkObject *caller, unsigned long eventId, void *callData);
#ifdef VTK_OVERRIDE
void Execute(
vtkObject *caller, unsigned long eventId, void *callData) VTK_OVERRIDE;
#else
void Execute(vtkObject *caller, unsigned long eventId, void *callData);
#endif
void SetText(const char *text) { this->Text = text; }
protected:
ProgressObserver() : Stage(0), Anim(0), LastTime(0), Text("") {}
......
......@@ -2,7 +2,7 @@
Program: DICOM for VTK
Copyright (c) 2012-2015 David Gobbi
Copyright (c) 2012-2017 David Gobbi
All rights reserved.
See Copyright.txt or http://dgobbi.github.io/bsd3.txt for details.
......@@ -14,6 +14,7 @@
#include "readquery.h"
#include "vtkDICOMFile.h"
#include "vtkDICOMSequence.h"
#include "vtkDICOMDictionary.h"
......@@ -22,7 +23,6 @@
#include <stdlib.h>
#include <algorithm>
#include <iostream>
#ifdef _WIN32
#include <windows.h>
......@@ -30,50 +30,126 @@
typedef vtkDICOMVR VR;
// Prototype for function that reads one query key
bool dicomcli_readkey_query(
const char *cp, vtkDICOMItem *query, QueryTagList *ql, bool qfile);
namespace {
// Build a tagpath
vtkDICOMTagPath path_append(const vtkDICOMTagPath& tpath, vtkDICOMTag tag)
// A class for reading a text file line-by-line.
// It uses its own buffer, since the file is unbuffered.
class LineReader
{
vtkDICOMTagPath result(tag);
public:
LineReader(vtkDICOMFile *file);
~LineReader();
size_t ReadLine(std::string *s);
if (tpath.GetHead() != vtkDICOMTag())
private:
vtkDICOMFile *File;
size_t BufferSize;
unsigned char *Buffer;
const unsigned char *Pointer;
const unsigned char *EndPointer;
};
LineReader::LineReader(vtkDICOMFile *file) :
File(file), BufferSize(4096)
{
if (tpath.HasTail())
this->Buffer = new unsigned char [this->BufferSize];
this->Pointer = &this->Buffer[0];
this->EndPointer = this->Pointer;
}
LineReader::~LineReader()
{
result = vtkDICOMTagPath(
tpath.GetHead(), tpath.GetIndex(),
tpath.GetTail().GetHead(), 0, tag);
delete [] this->Buffer;
this->File->Close();
}
else
size_t LineReader::ReadLine(std::string *s)
{
s->clear();
size_t total = 0;
while (!this->File->GetError() && !this->File->EndOfFile())
{
if (this->Pointer == this->EndPointer)
{
this->Pointer = this->Buffer;
this->EndPointer = this->Buffer;
this->EndPointer += this->File->Read(this->Buffer, this->BufferSize);
if (this->Pointer == this->EndPointer)
{
break;
}
}
const unsigned char *ucp = this->Pointer;
const char *cp = reinterpret_cast<const char *>(ucp);
size_t l = 0;
while (ucp != this->EndPointer && *ucp != '\r' && *ucp != '\n')
{
ucp++;
l++;
}
this->Pointer = ucp;
if (ucp == this->EndPointer)
{
// append and continue
s->append(cp, l);
total += l;
}
else if (*ucp == '\n')
{
// newline means end of line
this->Pointer++;
l++;
s->append(cp, l);
total += l;
break;
}
else if (*ucp == '\r')
{
// carriage return is end of line, also eat following newline
this->Pointer++;
l++;
if (this->Pointer == this->EndPointer)
{
s->append(cp, l);
total += l;
l = 0;
this->Pointer = this->Buffer;
this->EndPointer = this->Buffer;
this->EndPointer += this->File->Read(this->Buffer, this->BufferSize);
cp = reinterpret_cast<const char *>(this->Pointer);
}
if (this->Pointer != this->EndPointer && *this->Pointer == '\n')
{
result = vtkDICOMTagPath(
tpath.GetHead(), 0, tag);
this->Pointer++;
l++;
}
if (l != 0)
{
s->append(cp, l);
total += l;
}
break;
}
}
return total;
}
return result;
}
// Prototype for function that reads one query key
bool dicomcli_readkey_query(
const char *cp, vtkDICOMItem *query, QueryTagList *ql,
bool ql_unique, bool qfile);
// Read a query file
bool dicomcli_readquery(
const char *fname, vtkDICOMItem *query, QueryTagList *ql)
{
#ifdef _WIN32
int cn = MultiByteToWideChar(CP_UTF8, 0, fname, -1, NULL, 0);
wchar_t *wfname = new wchar_t[cn];
MultiByteToWideChar(CP_UTF8, 0, fname, -1, wfname, cn);
ifstream f(wfname);
delete [] wfname;
#else
ifstream f(fname);
#endif
if (!f.good())
const char *fname, vtkDICOMItem *query, QueryTagList *ql, bool ql_unique)
{
return false;
}
vtkDICOMFile f(fname, vtkDICOMFile::In);
// Each query line is either:
// # a comment
......@@ -84,17 +160,17 @@ bool dicomcli_readquery(
// GGGG,EEEE\GGGG,EEEE # a tag nested within a sequence
int lineNumber = 0;
while (f.good())
{
std::string line;
std::getline(f, line);
LineReader lr(&f);
while (lr.ReadLine(&line))
{
const char *cp = line.c_str();
size_t n = line.size();
lineNumber++;
// strip leading whitespace
size_t s = 0;
while (s < n && isspace(cp[s]))
while (s < n && (cp[s] & 0x80) == 0 && isspace(cp[s]))
{
s++;
}
......@@ -105,36 +181,40 @@ bool dicomcli_readquery(
continue;
}
if (!dicomcli_readkey_query(cp, query, ql, true))
if (!dicomcli_readkey_query(cp, query, ql, ql_unique, true))
{
fprintf(stderr, "Error %s line %d:\n", fname, lineNumber);
return false;
}
}
return true;
return (f.GetError() == 0);
}
// If qfile is true, then key is being read from a query file
bool dicomcli_readkey_query(
const char *cp, vtkDICOMItem *query, QueryTagList *ql, bool qfile)
const char *cp, vtkDICOMItem *query, QueryTagList *ql,
bool ql_unique, bool qfile)
{
// read the tag path
vtkDICOMTagPath tagPath;
std::string creator;
size_t tagDepth = 0;
size_t s = 0;
size_t lineStart = s;
size_t n = strlen(cp);
bool tagError = false;
while (!tagError && tagDepth < 3)
const int maxerrlen = 80;
// set the default character set to utf-8
vtkDICOMCharacterSet cs(vtkDICOMCharacterSet::ISO_IR_192);
while (!tagError)
{
// check for private creator in square brackets
size_t creatorStart = s;
size_t creatorEnd = s;
if (cp[s] == '[')
{
s++;
creatorStart = s;
size_t t = ++s;
while (s < n && cp[s] != ']')
{
s++;
......@@ -145,26 +225,24 @@ bool dicomcli_readkey_query(
tagError = true;
continue;
}
creatorEnd = s;
s++;
creator.assign(&cp[t], s++ - t);
}
std::string creator(&cp[creatorStart], creatorEnd - creatorStart);
// read the DICOM tag
vtkDICOMTag tag(0x0000,0x0000);
size_t tagStart = s;
bool isHex = true;
bool hasComma = false;
size_t commaPos = 0;
while (s < n && (isalnum(cp[s]) || (cp[s] == ',' && !hasComma)))
while (s < n && (cp[s] & 0x80) == 0 &&
(isalnum(cp[s]) || (cp[s] == ',' && !hasComma)))
{
if (cp[s] == ',')
{
hasComma = true;
commaPos = s - tagStart;
}
else if (!isxdigit(cp[s]))
else if ((cp[s] & 0x80) != 0 || !isxdigit(cp[s]))
{
isHex = false;
}
......@@ -201,7 +279,7 @@ bool dicomcli_readkey_query(
}
// if creator, then resolve the tag now
if (creator.length() > 0)
if (creator.length() > 0 && (tag.GetGroup() & 0x0001) != 0)
{
if (tagDepth == 0)
{
......@@ -209,24 +287,40 @@ bool dicomcli_readkey_query(
}
else
{
vtkDICOMSequence seq = query->GetAttributeValue(tagPath);
// Since "item" is copy-on-write, we want to avoid creating
// an unnecessary copy when the the creator element is added.
// So we get the item, add the creator element, then get
// the tag and value for that element, and let the item go
// out of scope so that its reference count is decremented
// (hopefully decremented to 1, so that the "copy" is not
// performed). Then we add the creator element to the
// query data set (which may have become a copy due to
// copy-on-write, if the reference count was greater than one).
vtkDICOMTagPath ctagPath;
vtkDICOMValue cval;
{
// this must be in its own scope in order to work...
vtkDICOMSequence seq = query->Get(tagPath);
vtkDICOMItem item = seq.GetItem(0);
tag = item.ResolvePrivateTagForWriting(tag, creator);
vtkDICOMTag ctag(tag.GetGroup(), tag.GetElement() >> 8);
vtkDICOMTagPath ctagPath = path_append(tagPath, ctag);
query->SetAttributeValue(ctagPath, creator);
ctagPath = vtkDICOMTagPath(tagPath, 0, ctag);
cval = item.Get(ctag);
}
// add the creator element
query->Set(ctagPath, cval);
}
}
// build the tag path
tagPath = path_append(tagPath, tag);
tagPath = vtkDICOMTagPath(tagPath, 0, tag);
if (s < n && (cp[s] == '/' || cp[s] == '\\'))
{
// create an item for the next level of depth
if (!query->GetAttributeValue(tagPath).IsValid())
if (!query->Get(tagPath).IsValid())
{
query->SetAttributeValue(tagPath, vtkDICOMSequence(1));
query->Set(tagPath, vtkDICOMSequence(1));
}
s++;
tagDepth++;
......@@ -238,7 +332,7 @@ bool dicomcli_readkey_query(
}
// if an error occurred while reading tag, skip to next line
if (tagError || tagDepth > 2)
if (tagError)
{
return false;
}
......@@ -261,7 +355,7 @@ bool dicomcli_readkey_query(
if (!vr.IsValid() || vr == VR::OX || vr == VR::XS || vr == VR::UN)
{
int m = static_cast<int>(vrEnd - lineStart);
m = (m > 40 ? 40 : m);
m = (m > maxerrlen ? maxerrlen : m);
fprintf(stderr, "Error: Unrecognized DICOM VR \"%*.*s\"\n",
m, m, &cp[lineStart]);
return false;
......@@ -275,7 +369,7 @@ bool dicomcli_readkey_query(
vtkDICOMTagPath tmpPath = tagPath;
while (tmpPath.HasTail())
{
pitem = pitem->GetAttributeValue(tag).GetSequenceData();
pitem = pitem->Get(tag).GetSequenceData();
tmpPath = tmpPath.GetTail();
tag = tmpPath.GetHead();
}
......@@ -303,7 +397,7 @@ bool dicomcli_readkey_query(
((dictvr == VR::XS && (vr == VR::SS || vr == VR::US)))))
{
int m = static_cast<int>(vrEnd - lineStart);
m = (m > 40 ? 40 : m);
m = (m > maxerrlen ? maxerrlen : m);
fprintf(stderr, "Error: VR of \"%*.*s\" doesn't match dict VR of %s\n",
m, m, &cp[lineStart], dictvr.GetText());
}
......@@ -312,7 +406,7 @@ bool dicomcli_readkey_query(
if (!vr.IsValid() || vr == VR::UN)
{
int m = static_cast<int>(s - lineStart);
m = (m > 40 ? 40 : m);
m = (m > maxerrlen ? maxerrlen : m);
fprintf(stderr, "Error: Unrecognized DICOM tag \"%*.*s\"\n",
m, m, &cp[lineStart]);
return false;
......@@ -358,16 +452,16 @@ bool dicomcli_readkey_query(
}
else
{
while (s < n && (!qfile || !isspace(cp[s])))
while (s < n && (!qfile || (cp[s] & 0x80) != 0 || !isspace(cp[s])))
{
s++;
}
valueEnd = s;
}
}
else if (s < n && (!qfile || !isspace(cp[s])))
else if (s < n && (!qfile || (cp[s] & 0x80) != 0 || !isspace(cp[s])))
{
if (isgraph(cp[s]))
if ((cp[s] & 0x80) != 0 || isgraph(cp[s]))
{
fprintf(stderr, "Error: Illegal character \"%c\" after tag.\n", cp[s]);
}
......@@ -382,10 +476,10 @@ bool dicomcli_readkey_query(
if (valueStart == valueEnd)
{
// only overwrite previous value if '=' was explicitly used
if (keyHasAssignment || !query->GetAttributeValue(tagPath).IsValid())
if (keyHasAssignment || !query->Get(tagPath).IsValid())
{
// empty value (always matches, always retrieved)
query->SetAttributeValue(tagPath, vtkDICOMValue(vr));
query->Set(tagPath, vtkDICOMValue(vr));
}
}
else if (valueContainsQuotes)
......@@ -400,18 +494,33 @@ bool dicomcli_readkey_query(
t++;
}
}
query->SetAttributeValue(tagPath, vtkDICOMValue(vr, sval));
if (vr.HasSpecificCharacterSet())
{
query->Set(tagPath, vtkDICOMValue(vr, cs, sval));
}
else
{
query->Set(tagPath, vtkDICOMValue(vr, sval));
}
}
else
{
if (vr.HasSpecificCharacterSet())
{
query->Set(tagPath,
vtkDICOMValue(vr, cs, &cp[valueStart], valueEnd - valueStart));
}
else
{
query->SetAttributeValue(tagPath,
query->Set(tagPath,
vtkDICOMValue(vr, &cp[valueStart], valueEnd - valueStart));
}
}
// add the tag path to the list, if it isn't already there
if (ql)
{
if (std::find(ql->begin(), ql->end(), tagPath) == ql->end())
if (!ql_unique || std::find(ql->begin(), ql->end(), tagPath) == ql->end())
{
ql->push_back(tagPath);
}
......@@ -421,9 +530,9 @@ bool dicomcli_readkey_query(
}
bool dicomcli_readkey(
const char *cp, vtkDICOMItem *query, QueryTagList *ql)
const char *cp, vtkDICOMItem *query, QueryTagList *ql, bool ql_unique)
{
return dicomcli_readkey_query(cp, query, ql, false);
return dicomcli_readkey_query(cp, query, ql, ql_unique, false);
}
bool dicomcli_looks_like_key(const char *cp)
......@@ -464,7 +573,7 @@ bool dicomcli_looks_like_key(const char *cp)
}
digitrun = 0;
}
else if (isxdigit(cp[l]))
else if ((cp[l] & 0x80) == 0 && isxdigit(cp[l]))
{
digitrun++;
}
......@@ -504,19 +613,7 @@ bool dicomcli_looks_like_key(const char *cp)
bool dicomcli_readuids(
const char *fname, vtkDICOMItem *query, QueryTagList *ql)
{
#ifdef _WIN32
int cn = MultiByteToWideChar(CP_UTF8, 0, fname, -1, NULL, 0);
wchar_t *wfname = new wchar_t[cn];
MultiByteToWideChar(CP_UTF8, 0, fname, -1, wfname, cn);
ifstream f(wfname);
delete [] wfname;
#else
ifstream f(fname);
#endif
if (!f.good())
{
return false;
}
vtkDICOMFile f(fname, vtkDICOMFile::In);
// Basic file structure:
// # one or more comments
......@@ -526,23 +623,23 @@ bool dicomcli_readuids(
QueryTagList ql2;
std::string val;
int lineNumber = 0;
while (f.good())
{
std::string line;
std::getline(f, line);
LineReader lr(&f);
while (lr.ReadLine(&line))
{
const char *cp = line.c_str();
size_t n = line.size();
lineNumber++;
// strip leading whitespace
size_t s = 0;
while (s < n && isspace(cp[s]))
while (s < n && (cp[s] & 0x80) == 0 && isspace(cp[s]))
{
s++;
}
// skip trailing whitespace
if (n > s && isspace(cp[n-1]))
if (n > s && (cp[n-1] & 0x80) == 0 && isspace(cp[n-1]))
{
--n;
}
......@@ -556,7 +653,7 @@ bool dicomcli_readuids(
if (ql2.size() == 0)
{
// get the tag line, if not gotten yet
if (!dicomcli_readkey_query(cp, query, &ql2, true))
if (!dicomcli_readkey_query(cp, query, &ql2, true, true))
{
fprintf(stderr, "Error %s line %d: ", fname, lineNumber);
fprintf(stderr, "Need Valid DICOM tag at top of file.\n");
......@@ -584,7 +681,7 @@ bool dicomcli_readuids(
{
// add the key and value to the query
vtkDICOMTagPath tagPath = ql2[0];
query->SetAttributeValue(tagPath, val);
query->Set(tagPath, val);
if (ql && std::find(ql->begin(), ql->end(), tagPath) == ql->end())
{
......@@ -592,5 +689,26 @@ bool dicomcli_readuids(
}
}
return true;
return (f.GetError() == 0);
}
void dicomcli_error_helper(vtkDICOMMetaData *meta, int i)
{
if (meta)
{
// print some useful identifying information about a DICOM file
fprintf(stderr, "Cannot read the DICOM file for the following entry:\n");
fprintf(stderr, "StudyInstanceUID=\"%s\",\n",
meta->Get(DC::StudyInstanceUID).AsString().c_str());
fprintf(stderr, "SeriesInstanceUID=\"%s\",\n",
meta->Get(DC::SeriesInstanceUID).AsString().c_str());
fprintf(stderr, "PatientID=\"%s\", StudyDate=\"%s\", StudyTime=\"%s\",\n",
meta->Get(DC::PatientID).AsString().c_str(),
meta->Get(DC::StudyDate).AsString().c_str(),
meta->Get(DC::StudyTime).AsString().c_str());
fprintf(stderr, "StudyID=\"%s\", SeriesNumber=\"%s\", InstanceNumber=\"%s\"\n",
meta->Get(DC::StudyID).AsString().c_str(),
meta->Get(DC::SeriesNumber).AsString().c_str(),
meta->Get(i, DC::InstanceNumber).AsString().c_str());
}
}
......@@ -2,7 +2,7 @@
Program: DICOM for VTK
Copyright (c) 2012-2015 David Gobbi
Copyright (c) 2012-2017 David Gobbi
All rights reserved.
See Copyright.txt or http://dgobbi.github.io/bsd3.txt for details.
......@@ -15,6 +15,7 @@
#define readquery_h
#include "vtkDICOMItem.h"
#include "vtkDICOMMetaData.h"
#include "vtkDICOMTagPath.h"
#include <vector>
......@@ -27,14 +28,20 @@ typedef std::vector<vtkDICOMTagPath> QueryTagList;
* attribute values. If the ordering within the file is important,
* then a QueryTagList can also be provided. The tags will be pushed
* onto the QueryTagList in the same order as they are encountered in
* the file.
* the file. If ql_unique is true (the default) then repeated tags
* will not be pushed onto the QueryTagList.
*/
bool dicomcli_readquery(
const char *fname, vtkDICOMItem *query, QueryTagList *ql=0);
const char *fname, vtkDICOMItem *query, QueryTagList *ql=0,
bool ql_unique=true);
//! Read a single query key, return 'true' on success.
/*!
* See dicomcli_readquery() for more information.
*/
bool dicomcli_readkey(
const char *key, vtkDICOMItem *query, QueryTagList *ql=0);
const char *key, vtkDICOMItem *query, QueryTagList *ql=0,
bool ql_unique=true);
//! Check if text looks like a query key (for error checking).
bool dicomcli_looks_like_key(const char *key);
......@@ -48,4 +55,14 @@ bool dicomcli_looks_like_key(const char *key);
bool dicomcli_readuids(
const char *fname, vtkDICOMItem *query, QueryTagList *ql=0);
//! Print brief info about a file for error messages.
/*!
* Sometimes a file that is found in an index doesn't actually exist,
* or cannot be read. This method will print out the PatientID,
* StudyDate, StudyTime, StudyId, SeriesNumber, and InstanceNumber
* so that the user will have an idea of what file is missing, since
* the DICOM filenames themselves are usually not informative.
*/
void dicomcli_error_helper(vtkDICOMMetaData *meta, int i);
#endif /* readquery_h */
......@@ -13,11 +13,9 @@
=========================================================================*/
#include "vtkConsoleOutputWindow.h"
#include "vtkObjectFactory.h"
vtkConsoleOutputWindow *vtkConsoleOutputWindow::New()
{
return new vtkConsoleOutputWindow;
}
vtkStandardNewMacro(vtkConsoleOutputWindow);
vtkConsoleOutputWindow::vtkConsoleOutputWindow()
{
......
......@@ -28,8 +28,13 @@ class vtkConsoleOutputWindow : public vtkOutputWindow
public:
vtkTypeMacro(vtkConsoleOutputWindow, vtkOutputWindow);
static vtkConsoleOutputWindow* New();
virtual void PrintSelf(ostream& os, vtkIndent indent);
virtual void DisplayText(const char*);
#ifdef VTK_OVERRIDE
void PrintSelf(ostream& os, vtkIndent indent) VTK_OVERRIDE;
void DisplayText(const char*) VTK_OVERRIDE;
#else
void PrintSelf(ostream& os, vtkIndent indent);
void DisplayText(const char*);
#endif
static void Install();
protected:
......@@ -38,8 +43,13 @@ protected:
void Initialize();
private:
vtkConsoleOutputWindow(const vtkConsoleOutputWindow&); // Not implemented.
void operator=(const vtkConsoleOutputWindow&); // Not implemented.
#ifdef VTK_DELETE_FUNCTION
vtkConsoleOutputWindow(const vtkConsoleOutputWindow&) VTK_DELETE_FUNCTION;
void operator=(const vtkConsoleOutputWindow&) VTK_DELETE_FUNCTION;
#else
vtkConsoleOutputWindow(const vtkConsoleOutputWindow&);
void operator=(const vtkConsoleOutputWindow&);
#endif
};
#endif
## Attributes {#attributes}
@brief Managing DICOM meta data.
## Overview
The meta data can be loaded from a DICOM file in two ways: either via
the vtkDICOMReader class (which also reads the image data), or via the
vtkDICOMParser class (which reads only the meta data). These classes
store the meta data in a vtkDICOMMetaData object, which provides storage
for all of the files in the DICOM series. The Get() method for this class,
which returns the value of a DICOM attribute, accepts a file index as a
parameter. This allows one to choose the file in the series for which
to get the attribute.
~~~~~~~~{.cpp}
// Get meta data from a vtkDICOMReader
reader->UpdateInformation();
vtkDICOMMetaData *meta = reader->GetMetaData();
// Get the number of files for which "meta" holds meta-data
int n = meta->GetNumberOfInstances();
// Get EchoTime attribute for first file in series.
if (meta->Has(DC::EchoTime))
{
int fileIndex = 0;
double t = meta->Get(fileIndex, DC::EchoTime).AsDouble();
}
// If fileIndex is not given, attribute is retrieved from first file.
std::string str = meta->Get(DC::SeriesDescription).AsString();
~~~~~~~~
Attributes are returned as a vtkDICOMValue object, which has methods
such as AsDouble(), AsInt() or AsString() that allow conversion of the
value to various C++ types. If the DICOM file does not have the
requested attribute, then the returned value will be empty and
"value.IsValid()" will return "false". Calling AsString() on an invalid
value will return the empty string, and likewise calling AsInt() or
AsDouble() on an invalid value will return 0.
## Per-slice meta data
As discussed in the Overview, a "file index" can be used when retrieving
an attribute from vtkDICOMMetaData. It is important to note that this
is a file index and not a slice index. The slices are sorted by spatial
location, which might be different from the file order. Furthermore, a
single DICOM file might actually contain multiple slices, with each
slice stored in a different frame within the file.
The reader provides an array, the FileIndexArray, that can be used to
convert a "slice index" to a file index. It also provides a
FrameIndexArray that can be used to convert a slice index to a frame
number. Together, these can be used regardless of whether the there was
one slice per file, or one slice per frame in a multi-frame file: the
vtkDICOMMetaData object provides a Get() method that takes
both a file index and a frame index, along with the tag of the attribute
to be inspected.
~~~~~~~~{.cpp}
// Read the files and get the meta data.
reader->SetFileNames(fileNameArray);
reader->Update();
vtkDICOMMetaData *meta = reader->GetMetaData();
// Get the arrays that map slice to file and frame.
vtkIntArray *fileMap = reader->GetFileIndexArray();
vtkIntArray *frameMap = reader->GetFrameIndexArray();
// Get the file and frame for a particular slice.
int sliceIndex = 6;
int fileIndex = fileMap->GetComponent(sliceIndex, 0);
int frameIndex = frameMap->GetComponent(sliceIndex, 0);
// Get the position for that slice.
vtkDICOMValue pv = meta->Get(fileIdx, frameIdx, DC::ImagePositionPatient);
double position[3] = { 0.0, 0.0, 0.0 };
if (pv.IsValid() && pv.GetNumberOfValues() == 3)
{
pv.GetValues(position, 3);
}
~~~~~~~~
As a caveat, for multi-frame files, the example given above assumes
that the meta data contains a per-frame ImagePositionPatient attribute.
This is the case for enhanced multi-frame CT and MRI files, but not
for multi-frame nuclear medicine files. Whenever retrieving meta data
from a DICOM image, it is wise to consult the DICOM standard to see
how the attributes are defined for the various modality-specific IODs
(information object descriptions).
## Per-component meta data
In the example in the previous section, the fileMap->GetComponent()
method was called with two arguments, but the second argument was set
to zero.
If the vtkDICOMReader assigned a vector dimension to the data, then
the the vtkImageData will have multiple scalar values in each voxel.
For instance, the first component in each voxel may have come from a
file that provided the real component of a complex-valued image, while
the second component from a file that provided the imaginary
component. In this case, one would do the following to retrieve the
meta data from the "imaginary" file:
~~~~~~~~{.cpp}
// Read the files and get the meta data.
reader->SetFileNames(fileNameArray);
reader->Update();
vtkDICOMMetaData *metaData = reader->GetMetaData();
// Get the arrays that map slice to file and frame.
vtkIntArray *fileMap = reader->GetFileIndexArray();
vtkIntArray *frameMap = reader->GetFrameIndexArray();
// Get the file and frame for a particular slice and component.
int sliceIndex = 6;
int vectorIndex = 1; // 2nd component is the imaginary component
int fileIndex = fileMap->GetComponent(sliceIndex, vectorIndex);
int frameIndex = frameMap->GetComponent(sliceIndex, vectorIndex);
// Get an attribute from the meta data.
vtkDICOMValue v = metaData->Get(fileIdx, frameIdx, tag);
~~~~~~~~
If the data has a time dimension and the reader's TimeAsVectorOn() method
was called, then the components of each voxel can correspond both to a
specific time slot, and to a specific vector component. To make the
situation even more complicated, each pixel in the DICOM files might
be an RGB pixel and therefore have three components as given
by the SamplesPerPixel attribute in the meta data.
The number of components in the FileIndexArray and FrameIndexArray is
equal to the vector dimension, and if TimeAsVectorOn() was called, then
the vector dimension will include the time dimension. The FileIndexArray
and FrameIndexArray do not have components that correspond to the individual
R,G,B components in RGB images, since the R, G, and B components will always
have the same meta data because they always come from the same file and
frame.
The following strategy is recommended for accessing per-component meta
data in multi-dimensional images:
~~~~~~~~{.cpp}
// Get the arrays that map slice to file and frame.
vtkIntArray *fileMap = reader->GetFileIndexArray();
vtkIntArray *frameMap = reader->GetFrameIndexArray();
// Get the image data and meta data.
vtkImageData *image = reader->GetOutput();
vtkDICOMMetaData *meta = reader->GetMetaData();
// Get the number of components in the data.
int numComponents = image->GetNumberOfScalarComponents();
// Get the full vector dimension for the DICOM data.
int vectorDimension = fileMap->GetNumberOfComponents();
// Compute the samples per pixel in original files.
int samplesPerPixel = numComponents/vectorDimension;
// Check for time dimension
int timeDimension = reader->GetTimeDimension();
if (timeDimension == 0)
{
timeDimension = 1;
}
// Get all attributes for a specific time.
int vectorIndex = timeIndex*vectorDimension/timeDimension;
int vectorEndIndex = (timeIndex + 1)*vectorDimension/timeDimension;
for (int i = vectorIndex; i < vectorEndIndex; i++)
{
int fileIndex = fileMap->GetComponent(sliceIndex, i);
int frameIndex = frameMap->GetComponent(sliceIndex, i);
vtkDICOMValue v = meta->Get(fileIdx, frameIdx, tag);
// print or display the value
}
// Extract an image at the desired time slot (e.g. for display).
int componentIndex = timeIndex*vectorDimension/timeDimension*samplesPerPixel;
vtkNew<vtkImageExtractComponents> extractor;
extractor->SetInputConnection(reader->GetOutputPort());
if (samplesPerPixel == 1)
{
extractor->SetComponents(componentIndex);
}
else if (samplesPerPixel == 2) // rare/nonexistent in DICOM images
{
extractor->SetComponents(componentIndex, componentIndex + 1);
}
else
{
extractor->SetComponents(componentIndex, componentIndex + 1, componentIndex + 2);
}
extractor->Update();
~~~~~~~~
## Nested data, tag paths, and multi-frame files
DICOM meta data can be nested. For example, nesting is used to store
per-frame meta data for enhanced multi-frame DICOM files. In order to
make it easy to access nested attributes, the vtkDICOMTagPath
class describes the full path to a nested attribute.
~~~~~~~~{.cpp}
// Get an attribute for frame 3 of a multi-frame file.
int frameIdx = 3;
double echoTime = meta->Get(
vtkDICOMTagPath(DC::PerFrameFunctionalGroupSequence, frameIdx,
DC::CardiacSynchronizationSequence, 0,
DC::NominalCardiacTriggerDelayTime)).AsDouble();
~~~~~~~~
This is rather verbose, so a more convenient method for accessing per-frame
data is provided for enhanced multi-frame files. You can give the frame
index after the file index, in which case the Get() method
will perform a search for the attribute without requiring a full path.
~~~~~~~~{.cpp}
// Get an attribute for frame 3 of an enhanced multi-frame file.
int fileIdx = 0;
int frameIdx = 3;
vtkDICOMValue vw = meta->Get(fileIdx, frameIdx, DC::WindowWidth);
vtkDICOMValue vc = meta->Get(fileIdx, frameIdx, DC::WindowCenter);
if (vw.IsValid() && vc.IsValid())
{
// set the window for the image
}
~~~~~~~~
The vtkDICOMMetaDataAdapter class can also be used to access enhanced
multi-frame files as if each frame was a separate file.
## Iterating over data elements
The vtkDICOMMetaData object also provides iterator-style access to the
data elements. This is useful, for instance, when you want to iterate
through all of the elements in the meta data in sequential order. It is
also useful if you want to check which attributes vary between files in
the series.
~~~~~~~~{.cpp}
// Iterate through all data elements in the meta data.
for (vtkDICOMDataElementIterator iter = meta->Begin(); iter != meta->End(); ++iter)
{
vtkDICOMTag tag = iter->GetTag();
std::cout << "tag: " << tag << std::endl;
// Crucial step: check for values that vary across the series.
if (iter->IsPerInstance())
{
int n = iter->GetNumberOfInstances();
for (int i = 0; i < n; i++)
{
std::cout << "instance " << i << ": " << iter->GetValue(i) << std::endl;
}
}
else
{
// Not PerInstance: value is the same for all files in series.
std::cout << "all instances: " << iter->GetValue() << std::endl;
}
}
// Get the iterator to a specific element (hash table lookup).
vtkDICOMDataElementIterator iter = meta->Find(DC::ImageOrientationPatient);
if (iter != meta->End())
{
// do something
}
~~~~~~~~
You might be surprised by the PerInstance check, but it is necessary due
to the fact that vtkDICOMMetaData holds the meta data for an entire series
of DICOM files. Most attributes are the same across the series, but a
few attributes vary from one file to the next. These per-file attributes
are identified when the file is read by vtkDICOMReader.
## Dictionaries
When iterating through the data elements in the meta data, as described
in the previous section, it can be useful to get information about
the meaning of the data elements that are encountered. Complete information
can only be provided by the DICOM standards documents themselves, but the
vtkDICOMDictionary can at least provide a summary of what kind of data to
expect for a given attribute.
~~~~~~~~{.cpp}
// do a dictionary lookup on a tag
vtkDICOMDictEntry entry;
entry = vtkDICOMDictionary::FindDictEntry(vtkDICOMTag(0x0008,0x0020));
// check if entry was found in dictionary
if (entry.IsValid())
{
std::cout << entry.GetName() << std::endl; // prints "StudyDate"
std::cout << entry.GetVR() << std::endl; // prints "DA"
std::cout << entry.GetVM() << std::endl; // prints "1"
}
// do a dictionary lookup by name
entry = vtkDICOMDictionary::FindDictEntry("StudyDate");
if (entry.IsValid())
{
std::cout << entry.GetTag() << std::endl; // prints "0008,0020"
}
~~~~~~~~
The vtkDICOMDictionary class provides information for attributes that are
described in the DICOM standard, as well as information for private
attributes defined by medical device manufacturers.
Every DICOM file is likely to have a mix of standard attributes and private
attributes. Fortunately, it is easy to tell the difference between the
two: private attributes always use a tag with with an odd group number,
while the DICOM standard only uses even group numbers. The lookup of
private tags requires the name of the private dictionary.
~~~~~~~~{.cpp}
// do a private dictionary lookup in GE dictionary GEMS_ACQU_01
vtkDICOMDictEntry entry;
entry = vtkDICOMDictionary::FindDictEntry("CellSpacing", "GEMS_ACQU_01");
if (entry.IsValid())
{
std::cout << entry.GetTag() << std::endl; // prints "0019,0004"
std::cout << entry.GetName() << std::endl; // prints "CellSpacing"
std::cout << entry.GetVR() << std::endl; // prints "DS"
std::cout << entry.GetVM() << std::endl; // prints "1"
}
~~~~~~~~
## Private meta data
Because private tags are not registered with any central authority, there
is no guarantee that they are unique. Instead, each private group within
a DICOM file contains 240 blocks (each with 256 elements) that can be
be individually reserved for elements belonging to a specific private
dictionary. The details of how this is done are described in Part 5,
Section 7.8 of the DICOM standard.
The result of this is that private tags are of the form (gggg,xxee)
where "xx" is a hexadecimal value between `10` and `ff`
that identifies the block that was used to store the attribute. The
tricky thing is that this value can vary from one DICOM file to the next,
though it is usually consistent within a single series.
Some people are surprised by this, because the first block (i.e. `10`)
is the only block that is used in most files.
To ensure that you are looking for private attributes in the correct
location (i.e. within the correct block), you must resolve each
private tag before using it.
~~~~~~~~{.cpp}
// start with the tag in its "dictionary" form, with xx=00
vtkDICOMTag ptag = vtkDICOMTag(0019,0004);
// resolve the private tag (find out what block was reserved)
ptag = meta->ResolvePrivateTag(ptag, "GEMS_ACQU_01");
if (ptag == vtkDICOMTag(0xffff,0xffff))
{
// the special tag value ffff,ffff indicates that the tag could not be
// resolved: no private block was reserved for dictionary GEMS_ACQU_01
}
else
{
// ptag will now be 0019,xx04 where "xx" is usually 10 (first block)
double spacing = meta->Get(ptag).AsDouble();
}
~~~~~~~~
## Directories {#directory}
@brief Reading a directory that contains DICOM files.
## DICOM file directory
DICOM files rarely have descriptive names, so it is usually necessary
to identify them by their contents. A typical DICOM folder listing looks
something like this:
~~~~~~~~{.cpp}
IM-0001-0001.dcm IM-0001-0005.dcm IM-0001-0009.dcm IM-0001-0013.dcm
IM-0001-0002.dcm IM-0001-0006.dcm IM-0001-0010.dcm IM-0001-0014.dcm
IM-0001-0003.dcm IM-0001-0007.dcm IM-0001-0011.dcm IM-0001-0015.dcm
IM-0001-0004.dcm IM-0001-0008.dcm IM-0001-0012.dcm IM-0001-0016.dcm
~~~~~~~~
These files might be 16 slices of a 3D image, or the first three files might
be localizer images while the remaining 13 files are slices of a 3D volume.
Or they might be something else entirely. So the first thing to do with a
batch of DICOM files is to find out how they fit together. There are two
classes that can be used for this: the old vtkDICOMFileSorter class, and
the newer vtkDICOMDirectory class.
## Scanning a directory
The vtkDICOMDirectory class solves this problem by scanning the directory
and reporting information on all of the DICOM files that it
finds. In addition, if the directory contains a **DICOMDIR** file (as is
the case for a DICOM CD), then the DICOMDIR file is used as an index.
In the simplest case, the directory will contain a single series as in the
example below. Since the example is looking for images (and not for other
DICOM files such as structured reports), the RequirePixelData flag is set:
~~~~~~~~{.cpp}
vtkNew<vtkDICOMDirectory> dicomdir;
dicomdir->SetDirectoryName("E:");
dicomdir->RequirePixelDataOn();
dicomdir->Update();
int n = dicomdir->GetNumberOfSeries();
vtkNew<vtkDICOMReader> reader;
if (n > 0)
{
// read the first series found
reader->SetFileNames(dicomdir->GetFileNamesForSeries(0));
reader->Update();
}
else
{
std::cerr << "No DICOM images in directory!" << std::endl;
}
~~~~~~~~
Since vtkDICOMDirectory will scan subdirectories recursively, it can be
used to catalogue a large collection of DICOM files:
~~~~~~~~{.cpp}
// Iterate through all of the studies that are present.
int n = dicomdir->GetNumberOfStudies();
for (int i = 0; i < n; i++)
{
// Get information related to the patient study
vtkDICOMItem patient = dicomdir->GetPatientRecordForStudy(i);
vtkDICOMItem study = dicomdir->GetStudyRecord(i);
std::cout << patient.Get(DC::PatientName) << " ";
std::cout << patient.Get(DC::PatientID) << " ";
std::cout << study.Get(DC::StudyDate) << " ";
std::cout << study.Get(DC::StudyTime) << std::endl;
// Iterate through all of the series in this study.
int j1 = dicomdir->GetFirstSeriesForStudy(i);
int j2 = dicomdir->GetLastSeriesForStudy(i);
for (int j = j1; j <= j2; j++)
{
// get some of the series attributes as a vtkDICOMItem
vtkDICOMItem series = dicomdir->GetSeriesRecord(j);
// get all the files in the series
vtkStringArray *sortedFiles = dicomdir->GetFileNamesForSeries(j);
std::cout << sortedFiles.GetNumberOfValues() << " files: ";
std::cout << series.Get(DC::SeriesInstanceUID) << std::endl;
}
}
~~~~~~~~
The PatientRecord, StudyRecord, and SeriesRecord in the above example
contain attributes for the images that were stored in the DICOMDIR
index file. At the bare minimum, these will provide the PatientName,
PatientID, StudyDate, StudyTime, StudyInstanceUID, and SeriesInstanceUID.
Usually the StudyDescription, SeriesDescription and SeriesNumber are also
available. Furthermore, the method GetMetaDataForSeries() returns an
amalgamation of the Patient, Study, Series, and Image information as
a vtkDICOMMetaData object (though the per-image information is sometimes
limited to just the InstanceNumber and the SOPInstanceUID).
### Searching for files that match a query
The vtkDICOMDirectory class has another trick up its sleeve. It can
search for files that have certain attributes, for example it can
search a filesystem for all scans of a specific patient. This is
similar in purpose to querying a PACS system, except that you are
instead providing one or more disk directories where the files might
exist. These directories are searched recursively for all files that
match the query.
~~~~~~~~{.cpp}
// Make a list of the directories to search.
vtkNew<vtkStringArray> dicompath;
dicompath->InsertNextValue("/Volumes/Images1");
dicompath->InsertNextValue("/Volumes/Images2");
// Make a list of attributes to match, using the utf-8 character set.
vtkDICOMItem query;
query.Set(DC::SpecificCharacterSet, "ISO_IR 192");
query.Set(DC::PatientName, "Doe^John");
query.Set(DC::StudyDate, "2012-2015");
query.Set(DC::Modality, "MR");
query.Set(DC::ImageType, "PRIMARY");
query.Set(DC::SeriesDescription, "*T1w*");
vtkNew<vtkDICOMDirectory> dicomdir;
// The SetInputFileNames method takes directories, too!
dicomdir->SetInputFileNames(dicompath.GetPointer());
// Optionally restrict the search to files ending with ".dcm"
dicomdir->SetFilePattern("*.dcm");
dicomdir->SetFindQuery(query);
dicomdir->Update();
~~~~~~~~
All text attributes except for dates accept the wildcards '\*' and '?'. For
dates, you can use a hyphen to specify a range of dates. Matching of all
text is done by first converting the text to utf-8 for compatibility, and for
patient names the matching is case insensitive. The query **PRIMARY**
will match the multi-valued attribute value
**ORIGINAL\\PRIMARY**, since only one value has to match.
If the query contains any non-ASCII text, you must set the
SpecificCharacterSet attribute to whichever character set your program
uses internally. This does not have to be the same as the character set
used in the files you are searching for. It is recommended to use utf-8
as the character set for the query, regardless of the character set used
in the DICOM files.
## Sorting a list of files
The vtkDICOMFileSorter class is obsolete, since its capabilities are
eclipsed by the vtkDICOMDirectory class, but its use is documented here
for historical reasons. It is also worth stating that the "sorting"
provided by this class is, in fact, unnecessary: the vtkDICOMReader
itself is capable of sorting its input files into the correct order.
~~~~~~~~{.cpp}
// Instantiate a DICOM sorter.
vtkNew<vtkDICOMFileSorter> sorter;
// Provide an array containing a list of filenames.
sorter->SetInputFileNames(filenames);
// Update the sorter (i.e. perform the sort).
sorter->Update();
// Get the first series.
int i = sorter->GetNumberOfSeries();
if (i > 0)
{
vtkStringArray *sortedFiles = sorter->GetFileNamesForSeries(0);
// do something with the files
}
~~~~~~~~
In addition, the sorter can discover which series belong to the same
study. That is, it can tell us which series were collected during the
same imaging session. One thing the sorter does *not* do is sort
the images in the series according to slice location. It only sorts the
images according to the Instance Number embedded in each image, where the
Instance Number gives the logical viewing order prescribed by the medical
device that generated the images. It is up to the vtkDICOMReader to
check the slice positions for the files and sort them by location before
generating an image volume or time series.
~~~~~~~~{.cpp}
// Sort the input filenames by series and study.
sorter->SetInputFileNames(filenames);
sorter->Update();
// Iterate through all of the studies that are present.
int n = sorter->GetNumberOfStudies();
for (int i = 0; i < n; i++)
{
// Iterate through all of the series in this study.
int j1 = sorter->GetFirstSeriesForStudy(i);
int j2 = sorter->GetLastSeriesForStudy(i);
for (int j = j1; j <= j2; j++)
{
vtkStringArray *sortedFiles = sorter->GetFileNamesForSeries(j);
// do something with the files
}
}
~~~~~~~~
## Image Display {#imageDisplay}
@brief Correct display of pixel values.
## Overview
In general, the pixel values that are stored in a DICOM file cannot be
directly displayed to the screen. Instead, the pixel values go through
a *display pipeline* that changes the values to something suitable for
viewing. Unless the pixels are stored in the file as RGB or as 8-bit
grayscale, the display pipeline will generally do one of two things:
it will map the values through a palette to generate RGB pixels, or it
will rescale the values (sometimes in multiple steps) for monochrome display.
The vtkDICOMReader will, by default, produce the *Modality Values* of
the pixels, which are the values associated with the modality that
produced the images (e.g. Hounsfield values for CT). If desired, the
AutoRescaleOff() method can be called on the reader to force it to
produce *Stored Values* instead, which are the pixel values stored
in the file. Note that *Modality Values* are equal to *Stored Values*
for many modalities, including MRI and Ultrasound.
## Photometric Interpretation
The
[Image Pixel Module](http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.3.html)
of the DICOM standard defines the Photometric Interpretation attribute
as a hint for how the stored pixel values should be interpreted by a
DICOM image display pipeline. Typical values are:
* MONOCHROME1 - negative image, where higher values are darker (e.g. radiographic film )
* MONOCHROME2 - positive image, where higher values are brighter (e.g. CT, MR)
* PALETTE COLOR - indexed color with palette
* RGB - full-color image with separate RGB components
In addition to the above, the YBR family of photometric interpretations
indicate that the image was stored with one luma channel and two color
channels, which is commonly done when a color image is compressed.
The vtkDICOMReader automatically converts YBR images to RGB, so display
pipelines should interpret YBR the same as RGB.
* YBR_FULL - used by ultrasound, usually with RLE compression
* YBR_FULL_422 - used by JPEG lossy compressed images
* YBR_PARTIAL_420 - used by MPEG lossy compressed video
* YBR_ICT - used by JPEG2000 lossy compressed images
* YBR_RCT - used by JPEG2000 lossless compressed images
Here is a short example of how to interpret the output of
vtkDICOMReader for display:
~~~~~~~~{.cpp}
reader->Update();
vtkDICOMMetaData *meta = reader->GetMetaData();
vtkDICOMValue photometric = meta->Get(DC::PhotometricInterpretation)
if (photometric.Matches("MONOCHROME1"))
{
// display with a lookup table that goes from white to black
}
else if (photometric.Matches("MONOCHROME2"))
{
// display with a lookup table that goes from black to white,
// or display with a suitable pseudocolor lookup table
}
else if (photometric.Matches("PALETTE*"))
{
// display with palette lookup table (see vtkDICOMLookupTable),
// or convert to RGB with vtkDICOMApplyPalette
}
else if (photometric.Matches("RGB*") || photometric.Matches("YBR*"))
{
// display RGB data directly
}
~~~~~~~~
## Grayscale image display
DICOM defines a grayscale display pipeline that converts the
*Stored Values* in the file to *Presentation Values* that are
suitable for display. The details of how this is done are described
in the
[Lookup Tables and Presentation
States](http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.11.html)
chapter of the DICOM standard. A brief summary of the steps in the
grayscale display pipeline is as follows:
1. *Stored Values* are rescaled to generate *Modality Values*
2. *Modality Values* are windowed to generate *Windowed Values* (or *Values of Interest*)
3. *Windowed Values* are mapped to *Presentation Values*
The *Modality Values* are real values in units that are appropriate to
the modality, for example, Hounsfield units for CT or counts for PET.
The **RescaleIntercept** and **RescaleSlope** define a linear mapping
from *Stored Values* to *Modality Values*:
ModalityValue = RescaleSlope * StoredValue + RescaleIntercept
For modalities such as ultrasound and MRI that do not have any units,
the RescaleSlope and RescaleIntercept are absent and the *Modality Values*
are equal to the *Stored Values*.
The *Windowed Values* (or more generically, *Values of Interest* or
*VOIs*) represent the range of values to be displayed. Decreasing the
**WindowWidth** increases the perceived contrast of the image, and
decreasing the **WindowCenter** increases the perceived brightness of
the image. Note that, as shown in the pseudocode below, the DICOM
standard uses a definition of "window" and "level" that might be
different from that to which you are accustomed. The DICOM WindowWidth
must be greater than or equal to 1.
n = number of output gray levels (usually 256 or 1024)
w = WindowWidth - 1.0
c = WindowCenter - 0.5
if (ModalityValue <= c - 0.5*w), then WindowedValue = 0
else if (ModalityValue > c + 0.5*w), then WindowedValue = n - 1
else WindowedValue = ((ModalityValue - c)/w + 0.5) * (n - 1)
For ultrasound (and for 8-bit images in general) the WindowWidth and
WindowCenter may be absent from the file. If absent, they can be
assumed to be 256 and 128 respectively, which provides an 8-bit identity
mapping. More information can be found in the
[VOI LUT Module](http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.11.2.html)
of the DICOM standard. Note that a DICOM display application should
allow the user to adjust the window according to his or her needs.
The *Presentation Values* are the values that are ultimately displayed
to the user. If the PhotometricInterpretation is MONOCHROME2, then the
*Windowed Values* should be displayed directly. If the
the PhotometricInterpetation is MONOCHROME1, then the grayscale
lookup table should be inverted such that a value of 0 is displayed as
white and the maximum value is displayed as black.
## Color image display
There are several categories of color display for DICOM, the most important
of which are:
* RGB (or YBR) values stored directly in the file
* PALETTE COLOR where a palette LUT is applied to the stored values
* A supplemental palette LUT applied to some or all stored values
* A pseudo-color palette LUT applied to the windowed values
For convenience, the vtkDICOMApplyPalette filter can convert a PALETTE
COLOR image to RGB, or can convert an image with a supplemental palette
to RGB, but will pass through all other images. For more information
about supplemental palettes, see the
[Supplemental Palette Lookup Table Module](http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.19.html).
As an alternative to vtkDICOMApplyPalette, the palette lookup table can
be retrieved from a DICOM data set with the vtkDICOMLookupTable class.
The final category of color display, pseudo-color, is displayed as per
the grayscale pipeline described above, except that a pseudocolor LUT
is used in place of the black-to-white (MONOCHROME2) or
white-to-black (MONOCHROME1) LUT.
## Image Orientation {#imageOrientation}
@brief Medical image orientation in VTK.
## Overview
When people use the term "image orientation" with respect to medical images,
they usually mean one or more of the items listed below:
1. The order in which the pixels, rows, and slices are stored in the computer's
memory.
2. The orientation of the image slices in a real-world coordinate system, for example the patient coordinate system as defined by the medical imaging equipment that generated the images.
3. The orientation of the subject (patient) with respect to the viewer when the images are viewed on a workstation.
The first of these, the way the data is stored in memory, should merely
be an implementation detail, but unfortunately the vtkImageViewer class
insists that the pixels must be arranged in memory such that the pixel
at the bottom-left corner of the image is the pixel at the lowest address
in memory. This is in conflict with DICOM, which stores the top-left pixel
as the first pixel in the file. To provide compatibility with the
vtkImageViewer, the default behavior of the vtkDICOMReader is to flip
the image in memory while it is loading it from the file. This behavior
can be turned off by calling reader->SetMemoryRowOrderToFileNative().
The second and third items in the list can be referred to as the real-world
orientation, and the display orientation, respectively. Neither of these can
be considered an implementation detail, as both of them are crucial to the
user experience. Also, it is important not to confuse one with the other.
An application that incorrectly manages the real-world orientation is
seriously flawed, even if it manages to display the images to the user
in the correct orientation.
The real-world orientation is provided by the GetPatientMatrix() method
of the vtkDICOMReader. This method returns a vtkMatrix4x4 object that
describes the coordinate transformation from the data coordinates of the
vtkImageData that stores the image, to the real-world Patient Coordinate
System defined in the DICOM standard. The matrix is
used to correctly place the image in the VTK world coordinate system.
The PatientMatrix is constructed from the ImagePositionPatient and
ImageOrientationPatient attributes in the series of DICOM files that are
provided to the reader. Note that unless SetMemoryRowOrderToFileNative()
has been called on the reader, the orientation of the matrix will be
flipped with respect to ImageOrientationPatient in order to account for
the fact that the image rows were flipped in memory.
~~~~~~~~{.cpp}
reader->SetMemoryRowOrderToFileNative(); // keep native row order
reader->Update(); // update the reader
vtkMatrix4x4 *matrix = reader->GetPatientMatrix();
// create an image actor and specify the orientation.
vtkNew<vtkImageActor> actor;
actor->GetMapper()->SetInputConnection(reader->GetOutputPort());
actor->SetUserMatrix(matrix);
~~~~~~~~
Setting the actor's UserMatrix will ensure that
the real-world orientation of the image is correctly handled, as far as
the VTK display pipeline is concerned. It does not, however, set the
display orientation, which is the responsibility of the application.
The display orientation can be set via manipulation of the VTK camera.
## Image Reader {#imageReader}
@brief Nitty-gritty details for loading DICOM images.
## Overview
The vtkDICOMReader converts a series of images (usually collated by
vtkDICOMDirectory) into three outputs:
1. a vtkImageData object to hold the pixel data for the volume
2. a vtkMatrix4x4 to hold the position and orientation of the volume
3. a vtkDICOMMetaData object to hold the meta-data
Each slice of the output image data is generated from one of the input
files (or, in the case of multi-frame images, from one frame). The
slices are sorted into the correct order by the reader, so it is not
necessary to sort the files beforehand. The reader also computes a 4x4
matrix, the "PatientMatrix", that can be used to convert from image data
coordinates (measured in millimeters from the center of the first
voxel), to DICOM patient coordinates (as described in the
[DICOM Image Plane Module](http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.2.html)).
In order to achieve this, the reader checks the Image Position and Image
Orientation that are recorded in the meta data for each slice. Then, if
and only if the slices have the same orientation and the slice positions
fit a line, the reader sorts the slices according to location. The
vtkImageData *z* spacing is then set to the average center-to-center
distance between adjacent slices.
In the absence of Image Position information in the meta data, or if the
slices do not form a rectilinear or parallelepiped volume, then the
slices are sorted only according to the Instance Number in the meta
data. There is also a reader method called SortingOff() than can be
called to disable sorting entirely, so that the order of the slices in
the vtkImageData will reflect the order of the list of files provided to
the reader.
## Row order
DICOM images are rasterized from top to bottom, meaning that the first
pixel in the file is meant to be displayed at the top left corner of the
viewport. The convention for the VTK image reader classes, however, is
to store the pixels in memory such that the first pixel is at the bottom
left corner. To support his convention, the default behavior of the
vtkDICOMReader is to flip the images while reading them so that last
image row in the file becomes the first row of the vtkImageData. In
this way, if the image is displayed by the vtkImageViewer, it will be
displayed the right way up.
However, flipping the image has consequences. When the reader flips the
image image, it likewise flips the PatientMatrix with respect to the
ImageOrientationPatient stored in the file. Also, since a flip along
one axis requires a flip along another axis in order for the coordinate
system to obey the right-hand-rule, the reversal of the ordering of the
rows is accompanied by a reversal of the ordering of the slices. This
is done because the alternative, introducing a PatientMatrix with a
negative determinant, will cause difficulties with 3D processing and
display of the images.
Hence, unless the intent is to use vtkDICOMReader in a simple 2D display
pipeline with vtkImageViewer, it is recommended to call the
SetMemoryRowOrderToFileNative() method when reading the images. This
will keep the keep the image in its original top-to-bottom ordering.
~~~~~~~~{.cpp}
vtkNew<vtkDICOMReader> reader;
reader->SetFileNames(fileNameArray);
reader->SetMemoryRowOrderToFileNative();
reader->Update();
// get the matrix to use when displaying the data
// (this matrix provides position and orientation)
vtkMatrix4x4 *matrix = reader->GetPatientMatrix();
~~~~~~~~
## CT gantry tilt
Certain CT scan protocols present a challenge for 3D image analysists
because the slice orientation is tilted with respect to the scan
direction, resulting in an acquisition volume that is a parallelepiped.
This occurs because of the geometry of the CT scanner itself:
traditionally, CT scanners were designed to acquire one slice at a
time with the bed moving a few millimeters between slices. The CT gantry,
and hence the slices, could be tilted to better align with anatomy,
but tilting the bed of the scanner to match was undesirable for obvious
reasons. Hence the direction in which the bed moved was not at a right
angle to the slices.
Volume rendering techniques and many 3D filtering techniques assume
a uniform, orthonormal sampling of the data. It is therefore necessary
to "un-tilt" or rectify these gantry-tilted CT images. This can be done
with the vtkDICOMCTRectifier class, which rectifies tilted CT volumes
and acts as a simple pass-through for volumes that are already rectilinear.
The rectification is achieved through a simple in-plane translation and
sinc interpolation of the slices. The resulting slices have the same
orientation as the original slices.
~~~~~~~~{.cpp}
vtkNew<vtkDICOMCTRectifier> rectify;
rectify->SetVolumeMatrix(reader->GetPatientMatrix());
rectify->SetInputConnection(reader->GetOutputPort());
rectify->Update();
// get the new PatientMatrix for the rectified volume
vtkMatrix4x4 *matrix = rectify->GetRectifiedMatrix();
~~~~~~~~
## CT and PET rescaled values
Another challenge with CT images is that the pixel intensity scaling,
as recorded in the RescaleSlope and RescaleIntercept attributes of
the file, can vary from slice to slice. This is done to make the best
use of the limited dynamic range of the analog-to-digital converter in
the scanner. By default, the vtkDICOMReader will automatically detect
the changes in RescaleSlope and RescaleIntercept, and will then adjust
the slices so that they are all the same. This is controlled with the
AutoRescaleOn()/Off() method of the reader.
In general, this automatic adjustment is safe for CT images, because
usually it is only RescaleIntercept that varies, and it usually varies
by a whole number. There is no loss of fidelity when the integer pixel
value is adjusted by a whole number, as long as it remains within the
range that can be represented by the datatype. For PET images, however,
the RescaleSlope also varies and it is necessary to use floating-point
values.
If accurate pixel values are required (which is the case in most medical
applications), it is recommended that AutoRescaleOff() is used. The
rescaling of the pixel values to "real" values should be done with the
vtkDICOMApplyRescale filter, which will produce pixel values as "double"
(or, optionally, as "float") after applying the RescaleSlope and the
RescaleIntercept for each slice.
~~~~~~~~{.cpp}
vtkNew<vtkDICOMCTReader> reader;
reader->SetFileNames(fileNameArray);
reader->SetMemoryRowOrderToFileNative();
reader->AutoRescaleOff();
vtkNew<vtkDICOMApplyRescale> rescale;
rescale->SetInputConnection(reader->GetOutputPort());
rescale->SetOutputScalarTypeToFloat();
rescale->Update();
~~~~~~~~
The vtkDICOMReader provides the rescaling information, along with all the
other meta data, via the VTK data pipeline (i.e. via GetOutputPort()).
If you use this filter together with vtkDICOMCTRectifier in the same
pipeline, it is recommened that this filter comes before the rectifier.
## Multi-dimensional images
In addition to sorting slices by location, the reader attempts to detect
multi-dimensional data sets. It recognizes up to 5 dimensions: *x*, *y*,
*z*, *t*, and a vector dimension. This is best illustrated by example.
If an MR raw-data DICOM series provides real and imaginary pixel data
at each slice location, then the vtkImageData produced by the reader
will have two components (real and imaginary). We interpret this
as an image with a vector dimension of 2.
When a time dimension is present, things become interesting. The default
behavior of the reader is to store adjacent time points in adjacent
vtkImageData slices. This works well when the images are to be displayed
slice-by-slice. It is, however, inappropriate if the vtkImageData is to
be displayed as a multi-planar reformat or as a volume. For this reason,
the vtkDICOMReader has a method called
TimeAsVectorOn() that will cause the reader to treat each voxel as a
time vector. In other words, if the DICOM data has 10 individual time
slots, then the vtkImageData will have 10 components per voxel (or 30
components in the case of RGB data). By selecting a specific component
or range of components when displaying the data, one can display a
specific point in time.
Five dimensions come into play when the DICOM series has frames that
are at the same location and within the same time slot. Going back to
the (real,imaginary) example, if such a series of images is read after
TimeAsVectorOn() is called, then the vtkImageData will have 20 components
per voxel if there are 10 time slots. The 20 components can be thought of
as 10 component blocks with 2 components per block. A filter like
vtkImageExtractComponents can be used to extract a block of components
that corresponds to a particular time slot.
If the behavior described in the preceding paragraphs is not desirable,
then one can use the SetDesiredTimeIndex(int) method to read just one
time slot, and use a set of *N* readers to read the *N* time
slots as *N* separate VTK data sets.
~~~~~~~~{.cpp}
vtkNew<vtkDICOMReader> reader;
reader->SetFileNames(filenames);
// read just the meta data, to get the time dimension
reader->UpdateInformation();
int numberOfTimeSlots = reader->GetTimeDimension();
if (numberOfTimeSlots > 1)
{
// example: read only the final time slot
reader->SetDesiredTimeIndex(numberOfTimeSlots-1);
}
// update the reader
reader->Update();
~~~~~~~~
## Enhanced multi-frame and multi-stack files
The DICOM standard allows for multiple slices (frames) per file, or even
multiple stacks of slices per file. In the case of multi-frame files,
each frame is assigned a position and a time slot and the frames are sorted
according to the slice sorting method described in the previous section.
In multi-stack files there are, as one might expect, more than one
rectilinear (or perhaps non-rectilinear) volume. If sorting has been
turned off with the SortingOff() method, then all the frames in the file
are read sequentially into vtkImageData slices. If sorting is on, however,
then the reader is only able to read one stack at a time. The method
SetDesiredStackID() allows one of the stacks to be chosen by name.
~~~~~~~~{.cpp}
// for this example, 'filename' is multi-frame, multi-stack file
vtkNew<vtkDICOMReader> reader;
reader->SetFileName(filename);
// read the meta data, get a list of stacks
reader->UpdateInformation();
vtkStringArray *stackNames = reader->GetStackIDs();
// specify a stack, here we assume we know the name:
reader->SetDesiredStackID("1");
~~~~~~~~
## Stacks in legacy files
Even though the DICOM standard only describes the use of stacks in
reference to enhanced multi-frame files, the concept is also useful
when working with "legacy" files that only have one slice per file
(such legacy files are, in fact, still the norm while the enhanced
files described in the previous section are relatively uncommon).
The reader defines a stack as a set of slices that have the same
orientation, and whose corners are placed along straight lines in
space. This allows for rectilinear and parallelepiped volumes,
with the latter being necessary for the tilted-gantry CT scans
discussed previously. If multiple stacks are required to capture
all of the slices in series, the reader will automatically sort the
slices into stacks and reader->GetStackIDs() will return an array
with multiple values. In this case, the reader will name the stacks
"0", "1", "2", etcetera.
Generally, such multi-stack series only occur for locators or for
scout series. For example, a MR scout series will often have a few
images for each orientation, and will therefore result in three
stacks. A CT locator will be identified as a single-slice stack
that occurs before the main stack.
## Using DICOM with MINC or NIfTI
For DICOM images of the head, chest, or abdomen the *x* coordinate
increases from right to left, the *y* coordinate increases from anterior
to posterior, and the *z* coordinate increases from inferior to
superior. This is often referred to as the LPS coordinate system, and
refers to the coordinates that are achieved after the image is been
transformed via the PatientMatrix. The NIfTI and MINC file formats use
a coordinate system where *x* increases to the right and *y* increases
to the front (anterior). Hence, the coordinate system is rotated by 180
degrees as compared to DICOM.
The vtkDICOMToRAS filter can adjust a DICOM image so that it shares the
same coordinate system as MINC and NIfTI. It can also be used to do
the reverse, and convert a NIfTI or MINC image to DICOM coordinates.
~~~~~~~~{.cpp}
vtkNew<vtkDICOMToRAS> converter;
converter->SetInputConnection(reader->GetOutputPort());
converter->SetPatientMatrix(reader->GetPatientMatrix());
converter->SetAllowRowReordering(true);
converter->SetAllowColumnReordering(false);
converter->UpdateMatrix();
vtkMatrix4x4 *matrix = converter->GetRASMatrix();
converter->Update();
vtkImageData *image = converter->GetOutput();
~~~~~~~~
## Image Writer {#imageWriter}
@brief Details on how to write DICOM images.
## Overview
The vtkDICOMWriter takes a vtkImageData object as input, and writes a
series of DICOM image files to disk. Since the required meta data for
an image varies from one modality to another, the writer delegates the
creation of the meta data to another class called vtkDICOMGenerator.
A short example of how this is done is as follows:
~~~~~~~~{.cpp}
// Create a generator for MR images.
vtkNew<vtkDICOMMRGenerator> generator;
// Create a meta data object with some desired attributes.
vtkNew<vtkDICOMMetaData> meta;
meta->Set(DC::PatientName, "Doe^John");
meta->Set(DC::ScanningSequence, "GR"); // Gradient Recalled
meta->Set(DC::SequenceVatiant, "SP"); // Spoiled
meta->Set(DC::ScanOptions, "");
meta->Set(DC::MRAcquisitionType, "2D");
// Plug the generator and meta data into the writer.
vtkNew<vtkDICOMWriter> writer;
writer->SetInputConnection(lastFilter->GetOutputPort());
writer->SetMetaData(meta.GetPointer());
writer->SetGenerator(generator.GetPointer());
// Set the output filename format as a printf-style string.
writer->SetFilePattern("%s/IM-0001-%04.4d.dcm");
// Set the directory to write the files into.
writer->SetFilePrefix("/the/output/directory");
// Write the file.
writer->Write();
~~~~~~~~
The vtkDICOMMRGenerator assists with conformance by generating all
the data set attributes that are required by the MR IOD. It will also
scan through the vtkMetaData object that is provided to the writer,
and use any of its attributes as long as 1) they are defined in the MR IOD,
and 2) they are deemed to be valid for the image that is being written.
A partial list of attributes that are never taken from the input meta data
is as follows:
1. `SOPInstanceUID` (this is always re-generated to ensure its uniqueness)
2. `SeriesInstanceUID` (ditto)
3. `ImageType` (this is set to `DERIVED\SECONDARY\OTHER` by default)
4. `PixelSpacing` (this is set from the VTK image information)
5. `Rows` and `Columns` (ditto)
6. `ImagePositionPatient` and `ImageOrientationPatient` (these are set from the PatientMatrix)
The generator always creates a new `SOPInstanceUID` for each file and a
new `SeriesInstanceUID` for each series. There is no way to set these
UIDs manually. The `ImageType` is set to `DERIVED` by default,
because an image cannot be considered to be `ORIGINAL` if it was modified
in any way after its original acquisition. Finally, all information
related to the pixel values or the slice geometry is generated from
the vtkImageData information and from the PatientMatrix.
The vtkDICOMWriter allows several parameters, including `ImageType`,
to be set when writing the file. These are demonstrated in the following
example.
~~~~~~~~{.cpp}
// Plug the generator and meta data into the writer.
vtkNew<vtkDICOMWriter> writer;
writer->SetInputConnection(lastFilter->GetOutputPort());
writer->SetMetaData(meta.GetPointer());
writer->SetGenerator(generator.GetPointer());
// Set the output filename format as a printf-style string.
writer->SetFilePattern("%s/IM-0001-%04.4d.dcm");
// Set the directory to write the files into
writer->SetFilePrefix("/the/output/directory");
// Set the image type to Multi-planar Reformat.
// (forward slashes will be converted to backward slashes)
writer->SetImageType("DERIVED/SECONDARY/MPR");
writer->SetSeriesDescription("Sagittal Multi-planar Reformat");
// Set the 4x4 matrix that gives the position and orientation.
writer->SetPatientMatrix(patientMatrix);
~~~~~~~~
## Customizing the generators
At the present time, the vtkDICOMWriter has only three generators
available: the vtkDICOMMRGenerator (for MR), the vtkDICOMCTGenerator (for CT),
and the vtkDICOMSCGenerator (for Secondary Capture, e.g. screenshots).
Writing a new generator class is the recommended method for adding support
for a new modality to the vtkDICOM library, though that is beyond the scope
of this document.
## Writing a raw pixel buffer to a DICOM file
In addition to the vtkDICOMWriter, there is a class called the
vtkDICOMCompiler that can write meta data and image data directly
to a file without it being processed by a vtkDICOMGenerator. It
can be used to efficiently perform such actions as changing the
transfer syntax of the data or tweaking the meta data. By design,
the vtkDICOMCompiler will take, as input, a meta data object that
describes a series of images, and it will then write the files in
the series one-by-one.
~~~~~~~~{.cpp}
vtkNew<vtkDICOMCompiler> compiler;
compiler->SetMetaData(meta);
int n = meta->GetNumberOfInstances();
for (int i = 0; i < n; i++)
{
char outputFile[256];
sprintf(outputFile, "IM-0001-%04.4d.dcm", i+1);
compiler->SetFileName(outputFile);
compiler->SetIndex(i);
compiler->WriteHeader();
compiler->WritePixelData(rawPixelBufferForFile);
}
~~~~~~~~
## Installation {#installation}
@brief The different ways to install the package.
## Building as part of the VTK build
If are building VTK 7 yourself, then vtk-dicom can be enabled as a VTK
remote module. This will cause cmake to automatically download the
vtk-dicom source code from Github and build it as part of the VTK build.
To enable vtk-dicom, select the following when you configure VTK with cmake:
Module_vtkDICOM:BOOL=ON
After running cmake once, the following new options will become available:
BUILD_DICOM_PROGRAMS:BOOL=ON
USE_GDCM:BOOL=OFF
USE_ITK_GDCM:BOOL=OFF
USE_DCMTK:BOOL=OFF
It is highly recommended that you turn the USE_GDCM option ON, otherwise
vtk-dicom will not be able to read compressed dicom files. This requires
[downloading GDCM from its project website](https://sourceforge.net/projects/gdcm/).
The USE_ITK_GDCM option can be used as an alternative to USE_GDCM if you
are building a program that uses ITK. With this option, it is not necessary
to build GDCM separately.
USE_DCMTK can be enabled if you cannot use GDCM, but still require the
ability to read compressed files. Note that the default DCMTK build does
not provide JPEG2000 support, though it can be purchased as an add-on.
## Building outside of VTK
The following procedure can be used to build the vtkDICOM
library, assuming that you have already built VTK with CMake.
This example is for building the package on Linux or OS X in
the bash shell.
$ # specify the directory where you built VTK, this is an example
$ export VTK_DIR=/Volumes/HD2/vtk-release-build/
$ unzip vtk-dicom-master.zip
$ mkdir vtk-dicom-master-build
$ cd vtk-dicom-master-build
$ cmake -D CMAKE_BUILD_TYPE:STRING=Release ../vtk-dicom-master
$ make
Optionally, the following cmake variables can
be set with ccmake or cmake-gui:
BUILD_EXAMPLES ON
BUILD_PROGRAMS ON
BUILD_SHARED_LIBS OFF
BUILD_TESTING ON
USE_DCMTK OFF
USE_GDCM OFF
USE_ITK_GDCM OFF
VTK_DIR /Volumes/HD2/vtk-release-build/
The USE_DCMTK and USE_GDCM variables
allow you to add the image decompression capabilities of either of these
packages (do not specify both!) to the vtkDICOMReader. If you do not
specify one of these packages, then the vtkDICOMReader will only be
able to read uncompressed files. See the section above for additional
details on these variables.
## Configuring your own cmake project
The easiest way to use the vtk-dicom library in your own project is
to add the following command block to the main CMakeLists.txt file
in your project:
find_package(DICOM QUIET)
if(DICOM_FOUND)
include(${DICOM_USE_FILE})
endif()
set(VTK_DICOM_LIBRARIES vtkDICOM)
It is not recommended to try to use vtk-dicom (or VTK itself)
within projects that are not built with cmake.
## Wrapper languages
All of the vtk-dicom classes can be used with Python, as long as VTK was
built with python wrapping enabled. A few of the classes, including the
reader and writer, can be used though Java and Tcl.
If you are already using the vtk .deb packages that came with your
debian or ubuntu linux distribution, then you have the option of using
the vtk-debian packages that were created by Mathieu Malaterre and that
are maintained by the [neuro-debian team](http://neuro.debian.net/):
sudo apt-get install python-vtk-dicom
sudo apt-get install libvtkdicom-java