lundi 27 août 2018

L'encodage d'objet DICOM - Format XML

La partie PS3.19 (section 10 et Annexe A) du standard DICOM définit un encodage des objets DICOM au format XML. Cette représentation est appelée « Native DICOM Model ». Contrairement à l'encodage classique qui est binaire (non interprétable sous forme de texte), cet encodage est 100% textuel. En définissant un tel système d'encodage, DICOM permet:
  • d'utiliser les (nombreux) outils XML plutôt que de se reposer sur des outils spécifiques DICOM. Il est ainsi plus simple de valider ou d'effectuer des traitements en XML qu'en DICOM ;
  • d'améliorer la lisibilité d'un objet DICOM. Il est en effet plus facile pour un humain de lire et modifier un objet texte plutôt qu'un objet binaire.
Remarque: Ce système d'encodage n'est pas fait pour se substituer à l'encodage classique qui reste le plus économe en taille pour représenter un objet DICOM. Il permet réellement de faciliter les traitements via l'utilisation d'outils XML.

1. Présentation de XML


XML est un langage textuel qui s'écrit à l'aide d'éléments balisés. Chaque élément XML commence par une balise ouvrante et se termine par une balise fermante de même nom.
  • Une balise ouvrante s'écrit : '<' + nom_balise + '>' ;
  • Une balise fermante s'écrit : '</' + nom_balise + '>'.
Un élément XML peut avoir des propriétés, sous la forme d'attributs XML, situées dans la balise ouvrante. Un attribut XML s'écrit sous la forme nom="valeur". Le nom ne peut pas avoir de caractère espace et la valeur est toujours entre guillemets. Chaque élément XML peut contenir une valeur textuelle ou des éléments XML.

2. Encodage DICOM


DICOM définit la représentation XML comme suit:
Native DICOM Model
L'élément racine d'un objet DICOM XML est représenté par une balise NativeDicomModel.

Exemple:
<?xml version="1.0" encoding="UTF-8"?>
<NativeDicomModel>
 ...
</NativeDicomModel>


2.1 La Balise DicomAttribute


Cet élément racine peut contenir des balises DicomAttribute ou chacune d'elle représente un Data Element.

2.1.1. Les attributs de DicomAttribute


Dans l'article « Les données en DICOM », il a été vu qu'un Data Element contient un certain nombre de caractéristiques comme son identifiant (le tag), son type (la VR) ou encore le Keyword qui est un alternatif au tag. Dans l'encodage XML, ces caractéristiques sont représentées par des attributs XML. Les attributs « tag », « vr » et « keyword » sont obligatoires alors que l'attribut « privateCreator » est, quant à lui, optionnel.

2.1.1.1. L'attribut tag


La valeur de cet attribut XML est formée des 8 caractères en capital de l'identifiant hexadécimal du tag DICOM (ggggeeee, ou gggg est le groupe et eeee l'élément dans le groupe).

Exemple: le Data Element (7fe0, 0010) aurait la valeur suivante : tag="7FE00010".

2.1.1.2. L'attribut vr


La valeur de cet attribut XML permet de spécifier le type de l'attribut DICOM. Il est formée des 2 caractères en capital de la VR tel que défini dans le tableau PS3.5 - Table 6.2-1.
Les valeurs autorisées sont : "AE", "AS", "AT", "CS", "DA", "DS", "DT", "FL", "FD", "IS", "LO", "LT", "OB", "OD", "OF", "OL", "OW", "PN", "SH", "SL", "SQ", "SS", "ST", "TM", "UC", "UI", "UL", "UN", "UR", "US", "UT".

Exemple: vr="US"

Remarque: Il existe une petite incohérence dans le standard (version 2018) sur l'optionalité de cet attribut. En effet, la figure A.1.4-1 Native DICOM Model ainsi que le Schéma XML décrit dans la section A.1.6 indiquent que l'attribut « vr » est obligatoire. En revanche le tableau A.1.5-2 indique que l'attribut est optionnel. Puisque 2 sources sur 3 indiquent qu'il est obligatoire, nous admettons que cet attribut est requis.

2.1.1.3. L'attribut keyword


La valeur de cet attribut est formée d'une chaîne de caractères dont le contenu est indiqué dans la colonne Keyword de la partie PS3.6 - section 6 - Tableau 6.1 du standard. Il permet de reconnaître plus facilement un Data Element en lui attribuant un nom unique.

exemple: keyword="SOPClassUID"

2.1.1.4. L'attribut privateCreator


La valeur de cet attribut XML indique le nom ou l'identifiant du créateur du DICOM Element privé. On peut trouver un nom de compagnie ou encore un nom de logiciel. Il est possible de mettre ce que l'on veut comme chaîne de caractères pour indiquer ce qu'est cet élément.

exemple: privateCreator="SIEMENS"

2.1.1.5. Exemple de DicomAttribute


<?xml version="1.0" encoding="UTF-8"?>
<NativeDicomModel>
 <DicomAttribute tag="00080020" vr="DT" keyword="StudyDate">
  ...
 </DicomAttribute>
 <DicomAttribute tag="00190090" vr="DS" keyword="MyPrivateElement" privateCreator="MyCompagny">
  ...
 </DicomAttribute>
  ...
 <DicomAttribute tag="7FE00010" vr="OB" keyword="PixelData">
  ...
 </DicomAttribute>
</NativeDicomModel>

2.1.2 La valeur de DicomAttribute


Chaque balise DicomAttribute représente un Data Element dont la valeur est représentée en XML par une ou plusieurs balises filles qui, en fonction de la VR, peuvent être :
  • des balises Value ;
  • une balise BulkData ou InlineBinary ;
  • une balise PersonName ;
  • des balises Item.

2.1.1 La Balise « Value »


Elle permet de stocker la valeur d'un Data Element pour les VR autres que SQ, PN, OB, OD, OF, OW et UN. La balise « Value » contient un attribut « number » qui spécifie le nombre de valeurs dans le Data Element ainsi que l'ordre des valeurs (ordre d'interprétation) dans la balise DicomAttribute.

Exemple:
<DicomAttribute tag="00080020" vr="DA" keyword="StudyDate">
 <Value number="1">20180609</Value>
</DicomAttribute>
<DicomAttribute tag="00080061" vr="CS" keyword="ModalitiesInStudy">
 <Value number="1">CT</Value>
 <Value number="2">PET</Value>
</DicomAttribute>

2.1.2. La Balise « PersonName »


Cette balise permet de représenter la valeur d'une VR de type PN. Elle est composée de 3 éléments fils optionnels:
  • Alphabetic: pour la représentation des systèmes d'écriture alphabétique;
  • Ideographic: pour la représentation des systèmes d'écriture idéographique; 
  • Phonetic: pour la représentation des systèmes d'écriture phonétique.
Chacun des éléments représentant un système d'écriture est composé de 5 éléments fils optionnels qui, dans le cadre français, ont la signification suivante :
  • FamilyName : Contient le nom d'usage de la personne ;
  • GivenName : Contient le premier prénom ;
  • MiddleName : Contient les prénoms autres que le premier prénom, séparés par un caractère espace ;
  • NamePrefix : Contient la civilité (« M. » ou « Mme ») ;
  • NameSuffix : Non utilisé dans le cadre français.
Remarque: En France, la réponse ministérielle (n°5128 du 3 mars 1983) précise que la civilité Mademoiselle (Mlle) ne devrait pas être utilisée.

Exemple:
<DicomAttribute tag="00100010" vr="PN" keyword="PatientName">
 <PersonName number="1">
  <Alphabetic>
   <FamilyName>Dupond</FamilyName>
   <GivenName>Jean</GivenName>
   <NamePrefix>M.</GivenName>
  </Alphabetic>
 </PersonName>
</DicomAttribute>

2.1.3. La Balise « InlineBinary »


Elle permet d'encoder en base64 la valeur des Data Element dont la VR est OB, OD, OF, OW, ou UN. En effet, ces VR contiennent des données binaires. Or XML étant un langage textuel, il est nécessaire de convertir les données binaires au format texte. DICOM a choisi d'utiliser l'encodage base64 pour effectuer cette transformation.

Exemple:
<DicomAttribute tag="7FE00010" vr="OW" keyword="PixelData">
 <InlineBinary>Qm9uam91ciDDoCB0b2k=</InlineBinary>
</DicomAttribute>

Remarque: L'encodage en base64 présente le gros inconvénient d'augmenter d'au moins 1/3 la taille des données converties. Il est donc préférable, dans la mesure du possible, d'utiliser la balise BulkData qui permet de faire une référence sur les données binaires plutôt que de les inclure.

2.1.4. La Balise « BulkData »


Elle permet d'encoder une référence à un blob (Binary Large OBject) de données. Le but étant d'éviter de stocker de larges données DICOM dans une structure XML. Ce blob est référencé via une URI grâce à un attribut XML « uri ». Ce blob peut être situé localement sur un système de fichier ou encore sur un réseau.

Exemple 1:
<DicomAttribute tag="7FE00010" vr="OW" keyword="PixelData">
 <BulkData uri="file:/C:/monRep/file.dcm?offset=16&length=524"/>
</DicomAttribute>

Exemple 2:
<DicomAttribute tag="7FE00010" vr="OW" keyword="PixelData">
 <BulkData uri="http://server/rs/studies/1.2.3/series/5.6.7/instances/9.8.6"/>
</DicomAttribute>

2.1.5. La Balise « Item »


Elle permet de stocker des éléments XML de type DICOMAttribute. Cela revient à encoder des séquences DICOM. Si une séquence contient plusieurs Item, chacun d'autres eux sera encodé en XML via une balise « Item ».

Exemple:
<DicomAttribute tag="7FE00010" vr="SQ" keyword="ContentSequence">
 <Item number="1">
  <DicomAttribute tag="00080020" vr="DA" keyword="StudyDate">
   ...
  </DicomAttribute>
  <DicomAttribute tag="00080060" vr="CS" keyword="Modality">
   ...
  </DicomAttribute>
 </Item>
 <Item number="2">
  ...
 </Item>
</DicomAttribute>

3. Exemple complet d'un encodage XML


Un exemple de fichier DICOM au format XML est disponible ici.

dimanche 5 août 2018

TP 1 - Construction d’un objet DICOM simple

Le but de cet exercice est de construire un objet DICOM de type « Encapsulated PDF ». Cet dernier permet d'encapsuler un document PDF dans un objet DICOM. Il est utilisé pour transporter un compte-rendu d'imagerie.
Si l'on devait faire une comparaison dans le monde HL7, cela correspondrait à un message HL7 v2 MDM (cf. le livre blanc « Harmonisation des modalités de communication des documents médicaux » publié par Interop'santé) ou HL7 v3 CDA Niveau 1.

DICOM propose d'autres objets plus structurés pour les compte-rendus (Structured Report). Ces objets étant assez complexes, ils feront le sujet d'article(s) dédié(s). Pour l'heure, étudions un objet DICOM simple.

Pour comprendre tout le vocabulaire utilisé dans ce TP, il est préférable d'avoir lu les articles suivants:
Nous allons également utiliser les parties PS3.3, PS3.5, PS3.6 et PS3.16 du standard DICOM.
  • PS3.3 : pour les spécifications de l'objet « Encapsultaed PDF » ;
  • PS3.5 : pour les types de données (type de VR) ;
  • PS3.6 : Pour le dictionnaire des UID (notamment pour la syntaxe de transfert et la SOP class UID) ;
  • PS3.16 : pour les jeux de valeurs.
Dans le cadre de cet exercice seul les éléments obligatoires seront retenus. L'objectif étant de construire un objet DICOM minimal, mais valide.

1. Convention utilisée en DICOM


1.1. Les modules DICOM


Dans un objet DICOM, certains modules (des regroupements d'attributs DICOM) peuvent être obligatoires, alors que d'autres sont optionnels. DICOM utilise la convention suivante pour indiquer quels sont les modules obligatoires et optionnels (voir DICOM PS 3.3 Annexe A1.3).

M
Module obligatoire (M pour Mandatory)
C
Module obligatoire sous condition (C pour Conditional)
U
Module optionel (U pour User Option)

1.2. Les types de données


Dans un module, certains Attributs (Data Element) sont requis et d'autres optionnels. Le tableau ci-dessous récapitule les types de données tels que décrit dans le standard DICOM PS 3.5 (Annexe 7.4).

Type 1
Élément obligatoire
La valeur doit être connue et valide avec une taille > 0. L'absence d'un élément de type 1 ou de sa valeur dans l'objet  constitue une violation du protocole DICOM.
Type 1C
Élément obligatoire sous condition
Les éléments ayant ce type doivent être inclus sous certaines conditions spécifiées. Les éléments de type 1C ont les mêmes obligations,   exigences et conditions que le type 1.
Type 2
Élément obligatoire avec valeur vide autorisée
La valeur peut être inconnue ou sa taille peut être 0. L'absence d'un élément de type 2 (mais pas de sa valeur) dans l'objet  constitue une violation du protocole DICOM.
Type 2C
Élément obligatoire avec valeur vide autorisée sous condition
Les éléments ayant ce type doivent être inclus sous certaines conditions spécifiées. Les éléments de type 2C ont les mêmes obligations, exigences et conditions que le type 2.
Type 3
Élément optionnel
L'absence d'un élément de type 3 ou de sa valeur dans l'objet ne constitue pas une violation du protocole DICOM.

2. Spécifications d’un objet « Encapsulated PDF »


Les spécifications de cet objet sont décrites dans la partie PS3.3 du standard, annexe A.45.1. D'après ces spécifications, les éléments obligatoires sont les suivants:

Nom Attribut
Tag
VR
Description
Module Patient
Patient's Name
(0010, 0010)
PN
Nom du patient.
Chaîne de caractères utilisant une convention suivante: Nom^prénom
Exemple: DUPOND^JEAN
Patient ID
(0010, 0020)
LO
Identifiant du patient
Chaîne de caractères représentant la valeur de l'identifiant du patient.
Exemple : 001
Patient’s Birth Date
(0010, 0030)
DA
Date de naissance du patient
Chaîne de caractères au format YYYYMMDD ou:
·  YYYY est l'année de naissance;
·  MM le mois de naissance;
·  DD le jour de naissance.
Patient’s Sex
(0010, 0040)
CS
Sexe du patient, peut être vide
Valeurs possibles:
·  M = Masculin
·  F = Féminin
·  O = Other
Module General Study
Study Instance UID
(0020, 000D)
UI
Identifiant unique de l'examen
Chaîne de caractères contenant un UID.
Un UID est une série de chiffres séparés par le caractère "." Longueur maximum 64 caractères.
Study Date
(0008, 0020)
DA
Date de début de l'examen
Chaîne de caractères au format YYYYMMDD ou:
·  YYYY est l'année de naissance;
·  MM le mois de naissance;
·  DD le jour de naissance.
Peut être vide
Study Time
(0008, 0030)
TM
Heure de début de l'examen
Chaîne de caractères au format HHMMSS où
·  HH représente les heures ("00" - "23");
·  MM les minutes ("00" - "59");
·  SS les secondes ("00" - "59").
Peut être vide
Referring Physician's Name
(0008,0090)
PN
Nom de la personne (physique ou morale) responsable du PDF. Auteur du Document.
Sous la forme: <nom>^<prénom>
Peut être vide
Study ID
(0020, 0010)
SH
Identifiant d'examen généré par un utilisateur ou un équipement.
Peut être vide
Accession Number
(0008, 0050)
SH
Numéro généré par le RIS qui identifie la demande d'examen radiologique. Ce champ est utilisé si un seul Accession Number est nécessaire.
Si plusieurs Accession Number sont nécessaires, utiliser l'élément Request Attributes Sequence (0040, 0275).
Peut être vide
Module Encapsulated Document Series
Modality
(0008, 0060)
CS
Type de modalité
Valeur "SC" = Secondary Capture
Series Instance UID
(0020, 000E)
UI
Identifiant unique de la série
Chaîne de caractères contenant un UID.
Un UID est une série de chiffres séparés par le caractère "." Longueur maximum 64 caractères.
Serie Number
(0020, 0011)
IS
Un numéro qui identifie la série
Exemple : 1
Module General Equipment
Manufacturer
(0008, 0070)
LO
Fabriquant ou éditeur qui a produit le PDF.
Peut être vide.
Module SC Equipment
Conversion Type
(0008,0064)
CS
Défini le type de converion de l’image ou du document
Valeurs possibles :
   · DV : Digitized Video;
   · DI : Digital Interface;
   · DF : Digitized Film;
   · WSD : Workstation;
   · SD : Scanned Document;
   · SI : Scanned Image;
   · DRW : Drawing;
   · SYN : Synthetic Image.
Exemple: SD
Module Encapsulated Document
Instance Number
(0020, 0013)
IS
Un numéro qui identifie le document
Valeur possible: 1
Content Date
(0008, 0023)
DA
Date de création du document PDF
Chaîne de caractères au format YYYYMMDD ou:
· YYYY est l'année de naissance;
· MM le mois de naissance;
· DD le jour de naissance.
Peut être vide
Content Time
(0008, 0033)
TM
Heure de création du document PDF
Chaîne de caractères au format HHMMSS où
· HH représente les heures ("00" - "23");
· MM les minutes ("00" - "59");
· SS les secondes ("00" - "59").
Peut être vide
Acquisition DateTime
(0008,002A)
DT
Date et heure de génération des données du document.
Peut être vide
Burned In Annotation
(0028,0301)
CS
Indique si le document contient des annotations pouvant  identifier le patient (ex : nom, prénom, identifiant présent dans le PDF).
Un document anonymisé peut utiliser la valeur NO
Valeurs possibles :
· YES
· NO
Peut être vide
Document Title
(0042,0010)
ST
Titre du document PDF.
Peut-être vide.
Concept Name Code Sequence
(0040, A043)
SQ
Code représentant le titre du document. Ce code est sous forme d'une séquence composée d'un seul élément. 
Les valeurs possibles des 3 composantes de cet élément  sont définies dans le standard DICOM, PS3.16, CID 7020.
Peut-être vide.
> Code Value
(0008, 0100)
SH
Voir le tableau du Context ID 7020 du standard DICOM  PS3.16
Exemple: "18748-4"
> Coding Scheme Designator
(0008, 0102)
SH
Voir le tableau du Context ID 7020 du standard DICOM PS3.16
Exemple: "LN"
> Code Meaning
(0008, 0104)
LO
Voir le tableau du Context ID 7020 du standard DICOM PS3.16
Exemple: "Diagnostic Imaging Report"
MIME Type of Encapsulated Document
(0042, 0012)
LO
Type du document encapsulé.
Valeur : application/pdf
Encapsulated Document
(0042, 0011)
OB
Flux d'octets du document PDF.
Module SOP Common
SOP Class UID
(0008, 0016)
UI
Identifiant sous forme d'un OID permettant d'identifier l'objet DICOM.
La valeur doit être: 1.2.840.10008.5.1.4.1.1.104.1
SOP Instance UID
(0008, 0018)
UI
Identifiant unique de l'objet DICOM.
Chaîne de caractères contenant un UID.
Un UID est une série de chiffres séparés par le caractère "." Longueur maximum 64 caractères.
Specific Character Set
(0008, 0005)
CS
Cet attribut doit avoir la valeur "ISO_IR 100".
Note: ISO_IR 100 représente le système de codage  ISO 8859-1 (Latin 1)

3. Code exemple de génération d'un objet « Encapsulated PDF »


Pour générer cet objet, nous allons utiliser l'utilitaire open source dcm4che. Cet utilitaire propose une API pour manipuler les données DICOM et créer des fichiers (il propose bien plus, mais dans ce TP, seule cette partie nous intéresse).

Le projet maven de ce TP est disponible ici.

Le snippet est également présenté ci-dessous :

package com.blog.dicom.tp1;

import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Date;

import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.Sequence;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.UID;
import org.dcm4che3.data.VR;
import org.dcm4che3.io.DicomOutputStream;

public class TP1 {
 public static void main(String[] args) throws FileNotFoundException, IOException {
  // Construction d'un objet DICOM
  final Attributes dataset = new Attributes();

  // Module Patient
  dataset.setString(Tag.PatientName, VR.PN, "DESMAUX^NATHALIE");
  dataset.setString(Tag.PatientID, VR.LO, "001");
  dataset.setString(Tag.PatientBirthDate, VR.DA, "19550920");
  dataset.setString(Tag.PatientSex, VR.CS, "F");

  // Module General Study
  final Date now = new Date();
  dataset.setString(Tag.StudyInstanceUID, VR.UI, "1.2.3.4");
  dataset.setDate(Tag.StudyDate, now);
  dataset.setDate(Tag.StudyTime, now);
  dataset.setString(Tag.ReferringPhysicianName, VR.PN, "");
  dataset.setString(Tag.StudyID, VR.SH, "");
  dataset.setString(Tag.AccessionNumber, VR.SH, "");

  // Module Encapsulated Document Series
  dataset.setString(Tag.Modality, VR.CS, "SC");
  dataset.setString(Tag.SeriesInstanceUID, VR.UI, "1.2.3.5");
  dataset.setString(Tag.SeriesNumber, VR.IS, "1");

  // Module General Equipment
  dataset.setString(Tag.Manufacturer, VR.LO, "IDO-IN");

  // Module SC Equipement
  dataset.setString(Tag.ConversionType, VR.CS, "SD");

  // Module Encapsulated Document
  dataset.setString(Tag.InstanceNumber, VR.IS, "1");
  dataset.setString(Tag.ContentDate, VR.DA, "");
  dataset.setString(Tag.ContentTime, VR.TM, "");
  dataset.setString(Tag.AcquisitionDateTime, VR.DT, "");
  dataset.setString(Tag.BurnedInAnnotation, VR.CS, "YES");
  dataset.setString(Tag.DocumentTitle, VR.ST, "Titre du document");
  dataset.setString(Tag.MIMETypeOfEncapsulatedDocument, VR.LO, "application/pdf");
  final Sequence docTitleCode = dataset.newSequence(Tag.ConceptCodeSequence, 3);
  final Attributes codedTitle = new Attributes();
  docTitleCode.add(codedTitle);

  codedTitle.setString(Tag.CodeValue, VR.SH, "18748-4");
  codedTitle.setString(Tag.CodingSchemeDesignator, VR.SH, "LN");
  codedTitle.setString(Tag.CodeMeaning, VR.LO, "Diagnostic Imaging Report");

  byte[] pdfBytes = Files.readAllBytes(Paths.get("src/main/resources/tp1.pdf"));

  final int pdfLen = pdfBytes.length;

  // S'assure d'avoir un nombre pair d'octets (PS3.5 - OB Type)
  if ((pdfLen & 1) != 0) {
   final byte[] pdfBytesEven = new byte[pdfLen + 1];
   System.arraycopy(pdfBytes, 0, pdfBytesEven, 0, pdfLen);
   pdfBytesEven[pdfLen] = 0;
   pdfBytes = pdfBytesEven;
  }
  dataset.setValue(Tag.EncapsulatedDocument, VR.OB, pdfBytes);

  // Module SOP Common
  dataset.setString(Tag.SOPClassUID, VR.UI, UID.EncapsulatedPDFStorage);
  dataset.setString(Tag.SOPInstanceUID, VR.UI, "1.2.3.4.5");

  dataset.setSpecificCharacterSet("ISO_IR_100");

  // Ecriture de l'objet DICOM sur le disque
  writeDcmFile(dataset, Paths.get("src/main/resources/dcmPdf.dcm"));

 }

 public static void writeDcmFile(final Attributes dataset, final Path completeFilePath) throws IOException {
  // Creation des repertoires parents si necessaire
  if (!Files.exists(completeFilePath.getParent())) {
   Files.createDirectories(completeFilePath.getParent());
  }
  // Ecriture d'un fichier en Explicit VR Little Endian
  try (FileOutputStream fout = new FileOutputStream(completeFilePath.toFile());
   BufferedOutputStream bout = new BufferedOutputStream(fout);
   DicomOutputStream out = new DicomOutputStream(bout, UID.ExplicitVRLittleEndian);) {
   out.writeDataset(dataset.createFileMetaInformation(UID.ExplicitVRLittleEndian), dataset);
   out.flush();
   fout.getFD().sync();
  } catch (final IOException e) {
   e.printStackTrace();
  }
 }
}