dcmpmap: une bibliothèque pour manipuler les objets Parametric Map
Ce module contient les classes permettant de créer, charger, consulter et enregistrer des objets DICOM Parametric Map, qui ont été introduits à l'origine dans la norme DICOM par le Supplement 172 en 2014.
Dans la norme, les données contenues dans chaque objet Parametric Map doivent reposer sur l'un des types de données suivants :
- entier non signé 16 bits
- entier signé 16 bits
- virgule flottante 32 bits
- virgule flottante 64 bits
Tous sont pris en charge par la bibliothèque dcmpmap.
La classe principale de ce module est :
Ce module fait largement appel au module dcmiod pour la gestion des attributs IOD communs, et au module dcmfg pour la prise en charge des groupes fonctionnels (functional group). Consultez les sections "Exemples" pour plus de détails.
Exemples
Les deux exemples suivants montrent :
- comment accéder aux informations (y compris les valeurs de données binaires) d'un objet Parametric Map et les extraire
- et comment utiliser l'API pour créer soi-même un tel objet.
Extraction d'informations depuis un Parametric Map
La classe Parametric Map utilise un template pour instancier en interne le type de données de pixel correct et proposer une API dédiée à ce type. Les types autorisés sont Uint16, Sint16, Float32 et Float64.
En interne, les types de données sont gérés via un Variant C++, si bien que la technique habituelle pour "basculer" entre ces types dans le code consiste à utiliser un Visitor qui surcharge l'opérateur "()" pour chaque type de données pouvant apparaître dans le Variant. Ce principe est également illustré ci-dessous, où le type de données de pixel est affiché.
Le reste du code utilise l'API des modules dcmiod et dcmfg pour obtenir les informations de base sur Patient, Study, Series et Instance, ainsi que les informations de groupe fonctionnel, en particulier le Real World Value Mapping défini dans le fichier.
#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 d'échantillons de données non pris en charge" << 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 d'échantillons de données non pris en charge" << 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);
}
Création de Parametric Map
La classe Parametric Map utilise un template pour instancier en interne le type de données de pixel correct et proposer une API dédiée à ce type. Les types autorisés sont Uint16, Sint16, Float32 et Float64. L'exemple ci-dessous montre que l'utilisation de l'API est globalement identique pour tous les types.
La procédure suivie dans l'exemple (dont la majeure partie s'applique également au cas général) est la suivante :
- La routine main() appelle test_pmap() quatre fois, en utilisant à chaque fois un Image Pixel Module différent comme paramètre de template, ce qui garantit que le bon type de données de pixel est utilisé dans le Parametric Map créé.
- test_pmap() illustre l'ensemble des étapes de création de la carte :
- Créer un nouveau Parametric Map en appelant DPMParametricMapIOD::create() (via create_pmap()), puis
- ajouter les groupes fonctionnels partagés,
- ajouter des dimensions,
- et ajouter à l'objet les frames avec les groupes fonctionnels par frame associés.
- Enfin, les données générales relatives à Patient et Study sont définies.
- Notez que l'ordre de ces étapes dans test_pmap() n'a pas d'importance.
Par défaut, DPMParametricMapIOD::create() crée une nouvelle instance DICOM, au sein d'une toute nouvelle Series DICOM appartenant elle-même à une toute nouvelle Study DICOM. Toutes les informations minimales pour Patient, Study et Series sont définies (par exemple Study, Series et SOP Instance UID, ainsi que d'autres informations transmises à l'appel de create(), comme Series Number). Patient Name et ID restent vides par défaut.
Bien sûr, il arrive souvent que vous vouliez plutôt placer la nouvelle instance dans une Series existante, ou placer la toute nouvelle Series dans une Study existante, ou au moins l'affecter à un Patient existant. Le moyen le plus simple d'y parvenir est d'utiliser l'appel import(), qui importe les informations de Patient, voire de Study, Series et Frame of Reference, à partir d'un fichier existant, c'est-à-dire qu'il place l'instance sous un Patient, une Study et/ou une Series existants.
Lorsque des informations sont ajoutées au Parametric Map via l'API publique, quelques vérifications de base sont généralement effectuées sur les données. Enfin, lors de l'appel à saveFile(), des vérifications supplémentaires ont lieu, par exemple la validation de la structure des groupes fonctionnels ou la confirmation que toutes les valeurs d'éléments requises sont définies.
#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;
}