dcmpmap: uma biblioteca para trabalhar com objetos Parametric Map
Este módulo contém as classes para criar, carregar, acessar e armazenar objetos DICOM Parametric Map, que foram originalmente introduzidos no padrão DICOM pelo Supplement 172 em 2014.
No padrão, os dados dentro de cada objeto Parametric Map devem se basear em um destes tipos de dados:
- inteiro sem sinal de 16 bits
- inteiro com sinal de 16 bits
- ponto flutuante de 32 bits
- ponto flutuante de 64 bits
A biblioteca dcmpmap oferece suporte a todos eles.
A classe principal deste módulo é:
Este módulo faz uso intenso do módulo dcmiod para gerenciar os atributos IOD comuns e do módulo dcmfg para o suporte a grupos funcionais (functional group). Consulte as seções "Exemplos" para mais explicações.
Exemplos
Os dois exemplos a seguir mostram:
- Como acessar e extrair informações (incluindo os valores de dados binários) de um objeto Parametric Map
- e como usar a API para criar você mesmo um objeto desse tipo.
Extração de informações de um Parametric Map
A classe Parametric Map usa um template para instanciar internamente o tipo de dado de pixel correto e oferecer uma API dedicada para esse tipo. Os tipos permitidos são Uint16, Sint16, Float32 e Float64.
Como internamente os tipos de dados são tratados em um Variant do C++, o conceito usual para "alternar" entre esses tipos no código é usar um Visitor que sobrecarrega o operador "()" para cada tipo de dado que pode ocorrer no Variant. Esse conceito também é demonstrado a seguir, em que o tipo de dado de pixel é exibido.
O restante do código usa a API dos módulos dcmiod e dcmfg para obter informações básicas sobre Patient, Study, Series e Instance, além das informações de grupo funcional, especialmente o Real World Value Mapping definido no arquivo.
#include "dcmtk/config/osconfig.h" / make sure OS specific configuration is included first /
#include "dcmtk/dcmpmap/dpmparametricmapiod.h"
static void dumpRWVM(const unsigned long frameNumber,
FGInterface& fg)
{
FGRealWorldValueMapping rw = OFstatic_cast(FGRealWorldValueMapping, fg.get(frameNumber, DcmFGTypes::EFG_REALWORLDVALUEMAPPING));
if (rw)
{
size_t numMappings = rw->getRealWorldValueMapping().size();
COUT << " Number of Real World Value Mappings defined: " << numMappings << OFendl;
for (size_t m = 0; m < numMappings; m++)
{
FGRealWorldValueMapping::RWVMItem* item = rw->getRealWorldValueMapping()[m];
OFString label, expl;
item->getLUTLabel(label);
item->getLUTExplanation(expl);
COUT << " RWVM Mapping #" << m << ":" << OFendl;
COUT << " LUT Label: << " << label << OFendl;
COUT << " LUT Explanation: " << expl << OFendl;
COUT << " Measurement Units Code: " << item->getMeasurementUnitsCode().toString() << OFendl;
size_t numQuant = item->getEntireQuantityDefinitionSequence().size();
if (numQuant > 0)
{
COUT << " Number of Quantities defined: " << numQuant << OFendl;
for (size_t q = 0; q < numQuant; q++)
{
ContentItemMacro* macro = item->getEntireQuantityDefinitionSequence()[q];
COUT << " Quantity #" << q << ": " << macro->toString() << OFendl;
}
}
}
}
else
{
CERR << " Error: No Real World Value Mappings defined for frame #" << frameNumber << OFendl;
}
}
class DumpFramesVisitor
{
public:
DumpFramesVisitor(DPMParametricMapIOD* map,
- const unsigned long numPerFrame)
-
m_Map(map)
, m_numPerFrame(numPerFrame)
{
}
template
OFBool operator()(DPMParametricMapIOD::Frames
{
dumpDataType(frames);
for (unsigned long f = 0; f < m_Map->getNumberOfFrames(); f++)
{
COUT << "Dumping info of frame #" << f << ":" << OFendl;
FGInterface& fg = m_Map->getFunctionalGroups();
dumpRWVM(f, fg);
COUT << "Dumping data for frame #" << f << ": " << OFendl;
T* frame = frames.getFrame(f);
for (unsigned long p = 0; p < m_numPerFrame; p++)
{
COUT << frame[p] << " ";
}
COUT << OFendl << OFendl;
}
return 0;
}
OFBool operator()(OFCondition& cond)
{
// Avoid compiler warning
(void)cond;
CERR << "Type of data samples not supported" << OFendl;
return OFFalse;
}
OFBool dumpHeader(DPMParametricMapIOD::Frames
{
// Avoid compiler warning
(void)frames;
COUT << "File has 32 Bit float data" << OFendl;
return OFFalse;
}
OFBool dumpHeader(DPMParametricMapIOD::Frames
{
// Avoid compiler warning
(void)frames;
COUT << "File has 16 Bit unsigned integer data" << OFendl;
return OFFalse;
}
OFBool dumpHeader(DPMParametricMapIOD::Frames
{
// Avoid compiler warning
(void)frames;
COUT << "File has 16 Bit signed integer data" << OFendl;
return OFFalse;
}
OFBool dumpHeader(DPMParametricMapIOD::Frames
{
// Avoid compiler warning
(void)frames;
COUT << "File has 64 Bit float data" << OFendl;
return OFTrue;
}
template
OFBool dumpDataType(DPMParametricMapIOD::Frames
{
// Avoid compiler warning
(void)frames;
CERR << "Type of data samples not supported" << OFendl;
return OFFalse;
}
DPMParametricMapIOD* m_Map;
unsigned long m_numPerFrame;
};
static void dumpGeneral(DPMParametricMapIOD& map)
{
OFString patName, patID, studyUID, studyDate, seriesUID, modality, sopUID;
map.getPatient().getPatientName(patName);
map.getPatient().getPatientID(patID);
map.getStudy().getStudyInstanceUID(studyUID);
map.getStudy().getStudyDate(studyDate);
map.getSeries().getSeriesInstanceUID(seriesUID);
map.getSeries().getModality(modality);
map.getSOPCommon().getSOPInstanceUID(sopUID);
COUT << "Patient Name : " << patName << OFendl;
COUT << "Patient ID : " << patID << OFendl;
COUT << "Study Instance UID : " << studyUID << OFendl;
COUT << "Study Date : " << studyDate << OFendl;
COUT << "Series Instance UID: " << seriesUID << OFendl;
COUT << "SOP Instance UID : " << sopUID << OFendl;
COUT << "---------------------------------------------------------------" << OFendl;
OFBool isPerFrame;
map.getFunctionalGroups().get(0, DcmFGTypes::EFG_REALWORLDVALUEMAPPING, isPerFrame);
if (isPerFrame)
{
COUT << "Real World Value Mapping: Defined per-frame" << OFendl;
}
else
{
COUT << "Real World Value Mapping: Defined shared (i.e. single definition for all frames):" << OFendl;
}
COUT << "---------------------------------------------------------------" << OFendl;
}
int main (int argc, char* argv[])
{
// OFLog::configure(OFLogger::DEBUG_LOG_LEVEL);
OFString inputFile;
if (argc < 2)
{
CERR << "Usage: dump_pmp
return 1;
}
else
{
inputFile = argv[1];
if ((inputFile))
{
CERR << "Input file " << inputFile << " does not exist " << OFendl;
return 1;
}
}
OFvariant
if (OFget
{
DPMParametricMapIOD map = OFget
dumpGeneral(*map);
COUT << "Dumping #" << map->getNumberOfFrames() << " frames of file " << inputFile << OFendl;
Uint16 rows, cols = 0;
map->getRows(rows);
map->getColumns(cols);
unsigned long numPerFrame = rows * cols;
DPMParametricMapIOD::FramesType frames = map->getFrames();
OFvisit
}
else
{
CERR << "Could not load parametric map: " << (*OFget
exit(1);
}
exit(0);
}
Criação de Parametric Maps
A classe Parametric Map usa um template para instanciar internamente o tipo de dado de pixel correto e oferecer uma API dedicada para esse tipo. Os tipos permitidos são Uint16, Sint16, Float32 e Float64. O exemplo abaixo demonstra que o uso da API é, em geral, o mesmo para todos os tipos.
O procedimento do exemplo (a maior parte dele também se aplica ao caso geral) é o seguinte:
- A rotina main() chama test_pmap() quatro vezes, usando a cada vez um Image Pixel Module diferente como parâmetro de template, o que garante que o tipo de dado de pixel correto seja usado no Parametric Map criado.
- test_pmap() demonstra as etapas gerais para criar o mapa:
- Criar um novo Parametric Map chamando DPMParametricMapIOD::create() (via create_pmap()) e, em seguida,
- adicionar os grupos funcionais compartilhados,
- adicionar dimensões,
- e adicionar ao objeto os frames com os respectivos grupos funcionais por frame.
- Por fim, os dados gerais referentes a Patient e Study são definidos.
- Observe que a ordem dessas etapas em test_pmap() não importa.
Por padrão, DPMParametricMapIOD::create() cria uma nova instância DICOM, dentro de uma Series DICOM totalmente nova que pertence a uma Study DICOM também totalmente nova. Todas as informações mínimas de Patient, Study e Series são definidas (por exemplo, Study, Series e SOP Instance UID, além de outras informações passadas para a chamada create(), como Series Number). Patient Name e ID ficam vazios por padrão.
Claro, muitas vezes você pode querer colocar a nova instância em uma Series já existente, ou colocar a Series totalmente nova em uma Study existente, ou pelo menos associá-la a um Patient existente. A forma mais simples de fazer isso é usar a chamada import(), que importa as informações de Patient, ou até de Study, Series e Frame of Reference, a partir de um arquivo existente, ou seja, coloca a instância sob um Patient, Study e/ou Series existentes.
Ao adicionar informações ao Parametric Map usando a API pública, geralmente são realizadas algumas verificações básicas sobre os dados. Por fim, ao chamar saveFile(), verificações adicionais são realizadas, por exemplo, validando a estrutura dos grupos funcionais ou garantindo que todos os valores de elemento exigidos estejam definidos.
#include "dcmtk/config/osconfig.h" / make sure OS specific configuration is included first /
#include "dcmtk/ofstd/oftest.h"
#include "dcmtk/dcmiod/iodutil.h"
#include "dcmtk/dcmpmap/dpmparametricmapiod.h"
#include "dcmtk/dcmfg/fgpixmsr.h"
#include "dcmtk/dcmfg/fgplanpo.h"
#include "dcmtk/dcmfg/fgplanor.h"
#include "dcmtk/dcmfg/fgfracon.h"
#include "dcmtk/dcmfg/fgframeanatomy.h"
#include "dcmtk/dcmfg/fgidentpixeltransform.h"
#include "dcmtk/dcmfg/fgframevoilut.h"
#include "dcmtk/dcmfg/fgrealworldvaluemapping.h"
#include "dcmtk/dcmfg/fgparametricmapframetype.h"
const size_t NUM_FRAMES = 10;
const Uint16 ROWS = 10;
const Uint16 COLS = 10;
const unsigned long NUM_VALUES_PER_FRAME = ROWS * COLS;
// Set Patient and Study example data
static void setGenericData(DPMParametricMapIOD& map)
{
map.getPatient().setPatientName("Onken^Michael");
map.getPatient().setPatientID("007");
map.getStudy().setStudyDate("20160721");
map.getStudy().setStudyTime("111200");
map.getStudy().setStudyID("4711");
}
// Create Parametric Map
template
static OFvariant
{
return DPMParametricMapIOD::create
(
"MR", // Modality
"1", // Series Number
"1", // Instance Number
ROWS,
COLS,
IODEnhGeneralEquipmentModule::EquipmentInfo("Open Connections GmbH", "make_pmp", "SN_0815", "0.1"),
ContentIdentificationMacro("1", "PARAMAP_LABEL", "Example description from test program", "Onken^Michael"),
"VOLUME", // Image Flavor
"MTT", // Derived Pixel Contrast
DPMTypes::CQ_RESEARCH // Content Qualification
);
}
// Add those functional groups that are common for all frames
static OFCondition addSharedFunctionalGroups(DPMParametricMapIOD& map)
{
FGPixelMeasures pixelMeasures;
pixelMeasures.setPixelSpacing("1\\1");
pixelMeasures.setSliceThickness("0.1");
pixelMeasures.setSpacingBetweenSlices("0.1");
FGPlaneOrientationPatient planeOrientPatientFG;
planeOrientPatientFG.setImageOrientationPatient("1", "0", "0", "0", "1", "0");
FGFrameAnatomy frameAnaFG;
frameAnaFG.setLaterality(FGFrameAnatomy::LATERALITY_UNPAIRED);
frameAnaFG.getAnatomy().getAnatomicRegion().set("T-A0100", "SRT", "Brain");
FGIdentityPixelValueTransformation idTransFG;
FGParametricMapFrameType frameTypeFG;
frameTypeFG.setFrameType("DERIVED\\PRIMARY\\VOLUME\\MTT");
// Add groups to Parametric Map
OFCondition result;
if ((result = map.addForAllFrames(pixelMeasures)).good())
if ((result = map.addForAllFrames(planeOrientPatientFG)).good())
if ((result = map.addForAllFrames(frameAnaFG)).good())
if ((result = map.addForAllFrames(idTransFG)).good())
result = map.addForAllFrames(frameTypeFG);
return result;
}
// Add a single dimension for demonstration purposes based on "Image Position Patient"
static OFCondition addDimensions(DPMParametricMapIOD& map)
{
IODMultiframeDimensionModule& mod = map.getIODMultiframeDimensionModule();
OFString dimUID = DcmIODUtil::createUID(0);
OFCondition result = mod.addDimensionIndex(DCM_ImagePositionPatient, dimUID, DCM_RealWorldValueMappingSequence, "Frame position");
return result;
}
// Add one frame to parametric map. Frame number is used to compute some
// varying example data values differing from frame to frame
template
static OFCondition addFrame(DPMParametricMapIOD& map,
const unsigned long frameNo)
{
// Create example data
OFVector
for (size_t n=0; n < data.size(); ++n)
{
data[n] = (n*frameNo+n) + (0.1 * (frameNo % 10));
}
Uint16 rows, cols;
OFCondition cond = map.getImagePixel().getRows(rows);
cond = map.getImagePixel().getColumns(cols);
// Create functional groups
OFVector
OFunique_ptr
OFunique_ptr
OFunique_ptr
FGRealWorldValueMapping::RWVMItem* rvwmItemSimple = new FGRealWorldValueMapping::RWVMItem();
if (!fgPlanePos || !fgFracon || !fgRVWM || !rvwmItemSimple )
return EC_MemoryExhausted;
// Fill in functional group values
// Real World Value Mapping
rvwmItemSimple->setRealWorldValueSlope(10);
rvwmItemSimple->setRealWorldValueIntercept(0);
rvwmItemSimple->setDoubleFloatRealWorldValueFirstValueMapped(0.12345);
rvwmItemSimple->setDoubleFloatRealWorldValueLastValueMapped(98.7654);
rvwmItemSimple->getMeasurementUnitsCode().set("{counts}/s", "UCUM", "Counts per second");
rvwmItemSimple->setLUTExplanation("We are mapping trash to junk.");
rvwmItemSimple->setLUTLabel("Just testing");
CodeSequenceMacro* qCodeName = new CodeSequenceMacro("G-C1C6", "SRT", "Quantity");
CodeSequenceMacro* qSpec = new CodeSequenceMacro("110805", "SRT", "T2 Weighted MR Signal Intensity");
ContentItemMacro* quantity = new ContentItemMacro;
if (!quantity || !qSpec || !quantity)
return EC_MemoryExhausted;
quantity->getEntireConceptNameCodeSequence().push_back(qCodeName);
quantity->getEntireConceptCodeSequence().push_back(qSpec);
rvwmItemSimple->getEntireQuantityDefinitionSequence().push_back(quantity);
quantity->setValueType(ContentItemMacro::VT_CODE);
fgRVWM->getRealWorldValueMapping().push_back(rvwmItemSimple);
// Plane Position
OFStringStream ss;
ss << frameNo;
OFSTRINGSTREAM_GETOFSTRING(ss, framestr) // convert number to string
fgPlanePos->setImagePositionPatient("0", "0", framestr);
// Frame Content
OFCondition result = fgFracon->setDimensionIndexValues(frameNo+1 / value within dimension /, 0 / first dimension /);
// Add frame with related groups
if (result.good())
{
// Add frame
groups.push_back(fgPlanePos.get());
groups.push_back(fgFracon.get());
groups.push_back(fgRVWM.get());
groups.push_back(fgPlanePos.get());
DPMParametricMapIOD::FramesType frames = map.getFrames();
result = OFget
}
return result;
}
// Main routine that creates Parametric Maps
template
static OFCondition test_pmap(const OFString& saveDestination)
{
OFvariant
if (OFCondition* pCondition = OFget
return *pCondition;
DPMParametricMapIOD& map = *OFget
OFCondition result;
if ((result = addSharedFunctionalGroups(map)).good())
if ((result = addDimensions(map)).good())
{
// Add frames (parametric map data), and per-frame functional groups
for (unsigned long f = 0; result.good() && (f < NUM_FRAMES); f++)
result = addFrame
}
// Set some generic data (keep dciodvfy happy on DICOMDIR warnings)
if (result.good())
{
setGenericData(map);
}
// Save
if (result.good())
{
return map.saveFile(saveDestination.c_str());
}
else
{
return result;
}
}
int main (int argc, char* argv[])
{
OFString outputDir;
if (argc < 2)
{
CERR << "Usage: make_pmp
return 1;
}
else
{
outputDir = argv[1];
if ((outputDir))
{
CERR << "Output directory " << outputDir << " does not exist " << OFendl;
return 1;
}
}
//OFLog::configure(OFLogger::DEBUG_LOG_LEVEL);
// Test all possible parametric map types (signed and unsigned integer, floating point
// and double floating point)
test_pmap
test_pmap
test_pmap
test_pmap
return 0;
}