Affichage des articles dont le libellé est Encodage. Afficher tous les articles
Affichage des articles dont le libellé est Encodage. Afficher tous les articles

lundi 8 octobre 2018

Les images en DICOM - Partie 2

Cet article constitue la seconde partie consacrée aux images dans DICOM.
Dans la première partie, nous avons parlé des images dites « natives », c'est-à-dire qui n'utilisent pas de technique de compression.
Ce billet est consacré aux images dites « encapsulées », c'est-à-dire qu'elles utilisent des techniques de compression pour encoder les pixels des images.
Le but des techniques de compression est d'économiser de l'espace si l'objet DICOM est stocké sur un support physique, ou de gagner du temps lors du transfert d'un objet DICOM via un réseau.

Le thème des techniques de compression dans DICOM est assez vaste. C'est pourquoi ce sujet est décomposé en deux sous-parties :
  • La compression et l'encodage en DICOM ;
  • Les syntaxes de transfert & les différents schémas de compression adoptés par DICOM.
Ce billet parle de la première sous-partie.

1. La Compression


Compresser une image, ou de manière plus générale des données, consiste à appliquer une transformation sur ces données pour en réduire la taille. Une fois cette opération effectuée, il n'est alors en général plus possible de les lire (un peu comme si elles étaient cryptées, sauf qu'ici, le but premier n'est pas de rendre illisible les données, mais d'en réduire la taille). Pour pouvoir interpréter de nouveau les données, il faut appliquer la transformation inverse.

La transformation pour réduire la taille des données est appelée « compression ». À l'inverse, la transformation pour retrouver les données lisibles s'appelle la « décompression ».
Pour compresser des données, on utilise un compresseur et pour les décompresser, on utilise un décompresseur.

Il faut donc un binôme Compresseur/ Decompresseur (appelé CoDec) pour réduire la taille des données puis les lire de nouveau.

Avec le développement des différentes techniques de compression, deux catégories se sont imposées.
  • La compression réversible (ou compression sans perte) ;
  • La compression irréversible (ou compression avec perte).
Pour illustrer le principe général de ces deux familles, reprenons le très bon exemple issu du livre « DICOM, A Practical Introduction and Survival Guide » d'Oleg S. Pianykh.

1.1 Compression sans perte


La compression sans perte permet de ne pas changer des données. Ainsi, une fois l'opération de compression et de décompression effectuée, les données sont les mêmes que si aucune transformation n'avait été appliquée.

Pour avoir une idée générale de comment il est possible d'effectuer une compression sans perte, prenons l'exemple suivant:

Supposons que l'on dispose de la suite de valeur de pixels suivante:

1000, 1001, 1002, 1002, 1000, 1000, 1001, 1057, ...

Un algorithme de compression sans perte pourrait être d'exploiter la répétitivité de la valeur la plus fréquente en la remplaçant par quelque chose de plus court. Par exemple, remplacer la chaîne « 1000 » par le caractère « a ». On obtiendrait donc :

a, 1001, 1002, 1002, a,  a, 1001, 1057, ...

La chaîne « a » étant plus courte que la chaîne « 1000 », la longueur globale est donc plus petite.
Il est toujours possible de décompresser le résultat et retrouver la suite originale en remplaçant les « a » par « 1000 ».

Remarque: Le codage de Huffman ou le Run Length Encoding (RLE) sont des exemples d'algorithmes de compression sans perte.

1.2 Compression avec perte


La compression avec perte va sacrifier des données pour obtenir un meilleur taux de compression. Ainsi, une fois l'opération de compression et de décompression effectuée, les données retrouvées ne sont pas les mêmes que les données originales.

Prenons deux couleurs très similaires dont les valeurs RGB sont:
Ces deux couleurs peuvent sembler identiques visuellement car le changement est tellement petit que l’œil humain n'arrive pas à les distinguer.  Une technique de compression avec perte est de jouer sur ces petits changements imperceptibles.

Reprenons la suite de valeurs de pixels ci-dessus:

1000, 1001, 1002, 1002, 1000, 1000, 1001, 1057, ...

« 1000 » et « 1001 » sont très proches. Tellement proches que la différence est négligeable. Nous allons donc remplacer la chaîne « 1001 » par « 1000 ».
On obtient donc:

1000, 1000, 1002, 1002, 1000, 1000, 1000, 1057, ...

En appliquant le même algorithme de compression sans perte que ci-dessus (remplacer « 1000 » par « a »), on obtient la suite :

a, a, 1002, 1002, a, a, a, 1057, ...

Le résultat est donc encore plus court que précédemment (meilleur taux de compression). En revanche en décompressant cette suite, nous sommes incapable de dire quel « a » représentait un « 1000 » et quel autre « a » représentait un « 1001 ». Une fois décompressé, la suite est différente de l'originale (même si visuellement aucune différence n'est notable). En finalité, nous avons donc perdu (sacrifié) des données pour une meilleure compression.

Remarque: La transformée en cosinus discrète (TCD) est un exemple d'algorithme de codage avec perte. Il est utilisé par JPEG (et par DICOM) pour sacrifier des données et obtenir un meilleur taux de compression (cf. PS3.5 Annexe F.1).

Conseil: Le format d'image BMP par exemple est non destructeur alors que le format JPEG est quant à lui destructeur (sacrifice de données). C'est pourquoi, il est préférable de travailler sur une image BMP et de l'enregistrer en JPEG à la fin, plutôt que de travailler sur une image en JPEG et qu'à chaque enregistrement (pour sauvegarder les changements de manière périodique) on dégrade un peu plus l'image.

3. Encodage d'une image en DICOM


Les tags évoqués dans la première partie sont toujours valables. La seule contrainte est que ces derniers soient cohérent par rapport à l'image (voir remarque ci-dessous). Seul l'utilisation du tag (7FE0, 0010) Pixel Data change. La description détaillé de cet élément DICOM est décrite dans la partie PS3.5 Annexe A.4 du standard.

Remarque: Les valeurs mentionnées dans les éléments de données spécifiant l'encodage d'une image (Photometric Interpretation, Samples per Pixel, Planar Configuration, Bits Allocated, Bits Stored, High Bit, Pixel Representation, Rows, Colums, etc.) doivent être cohérents avec ceux qui apparaissent dans le flux d'octets de l'image compressée. Toutefois, en cas d'inconsistance, il est recommandé que le processus de décodage utilise les paramètres mentionnés dans le flux d'octets représentant les données compressées.

3.1. Généralité


Lorsque l'on utilise une technique de compression (avec ou sans perte) pour stocker les pixels d'une image, le standard indique que l'encodage DICOM doit toujours se faire en Little Endian et en Explicit VR (cf. PS3.5 - Annexe A.4).

3.2. Pixel Data


Comme pour les images natives, le tag (7FE0, 0010) Pixel Data permet de stocker les pixels d'une ou plusieurs images. Il existe néanmoins des contraintes sur cet élément pour les images encapsulées:
  • La VR doit être « OB » (Other Bytes). Il n'est pas possible d'utiliser une VR de type « OW » (Other Words) ;
  • Pixel Data doit contenir le flux d'octet exprimant l'image compressée (entête comprise) ;
  • Contrairement aux images natives qui stockent directement les pixels dans l'élément (7FE0, 0010) Pixel Data, il faut impérativement utiliser une séquence DICOM pour stocker les pixels compressés (d'où le nom « Encapsulated Image »). De plus, la séquence doit respecter les contraintes suivantes:
    • L'encodage de la séquence doit se faire en « Undefined Length » ;
    • Le premier élément de la séquence doit être une « Basic Offset Table » ;
    • Les autres éléments de la séquence sont les pixels de l'image (ou des images dans le cas d'une Multi-frame).
Autre point important, une image peut être fragmentée en plusieurs parties. Chacune de ces dernières faisant l'objet d'un item de la séquence.

Quelques mots sur la fragmentation d'image : Une image peut être entièrement contenue dans un item, ou être divisée dans plusieurs parties. Cette fragmentation permet de supporter la bufferisation pendant la compression d'une image ou d'éviter de dépasser la taille maximum d'un fragment. Un logiciel voulant lire une image DICOM peut détecter la fragmentation d'une image en comparant le nombre de fragments (nombre d'items -1 pour la Basic Offset Table) avec le nombre d'images (tag (0028,0008) Number of Frames).

Remarque: Les tags (7FE0,0008) Float Pixel Data et (7FE0,0009) Double Float Pixel Data ne peuvent pas être utilisés pour stocker des images compressées. Ces deux Data Element ne peuvent stocker que des données au format Natif (cf. PS3.5 - section 8.2).

3.2.1 L'item Basic Offset Table (premier item de la séquence)


Cet item est obligatoire, mais sa valeur est optionnelle. Si la valeur est présente, elle contient, pour chaque image, l'offset du premier octet du premier fragment (item) de chacune des images. L'offset est calculé à partir du premier octet du premier item après la Basic Offset Table. Chaque offset est codé sur 32 bits (4 octets).

Remarque: Lorsque la valeur n'est pas présente, la longueur de l'item doit être 0000H.

Des exemples sont disponibles dans la section 3.2.3. ci-dessous.

3.2.2 Les autres items (suivant le premier item)


L'encodage de chaque item se fait en « Explicit Length » et de la manière suivante:
  • Le champ « Tag » doit avoir la valeur (FFFE, E000) ;
  • Le champ « Length » de 4 octets doit contenir le nombre d'octets contenu dans le champ « Value » ;
  • Le champ « Value » contient le flux d'octets (ou une partie du flux d'octets) représentant l'image compressée ;
  • Le champ « Value » doit toujours avoir une longueur paire. La valeur peut donc être complétée par le caractère NULL (cf. L'encodage d'objet DICOM - Format Classique (Partie 1)).

3.2.3 Exemples d'encodage d'une séquence


3.2.3.1. Exemple d'encodage d'une image (single frame) définie avec une séquence de trois fragments sans Basic Offset Table

Tag VR Longueur Valeur
(7FE0, 0010)
avec une VR
de OB
OB 0000H
Réservé
FFFF FFFFH
longueur indéfinie
Basic Offset Table SANS Valeur 1er Fragment (Single Frame) des pixels
Tag Longueur Tag Longueur Valeur
(FFFE, E000) 0000 0000H (FFFE, E000) 0000 04C6H Fragment Compressé
4 octets 2 octets 2 octets 4 octets 4 octets 4 octets 4 octets 4 octets 04C6H octets

Suite de la séquence (Valeur)
2d Fragment (Single Frame) de pixels 3ème Fragment (Single Frame) de pixels Sequence Delimiter Item
Tag Longueur Valeur Tag Longueur Valeur Sequence Delim. Tag Longueur
(FFFE, E000) 0000 024AH Fragment Compressé (FFFE, E000) 0000 0628H Fragment Compressé (FFFE,  E0DD) 0000 0000H
4 octets 4 octets 024AH octets 4 octets 4 octets 0628H octets 4 octets 4 octets

C.2.3.2. Exemple d'encodage d'une Multi-frame de 2 images définie avec une séquence de trois fragments avec Basic Offset Table

Tag VR Longueur Valeur
(7FE0, 0010)
avec une VR
de OB
OB 0000H
Réservé
FFFF FFFFH
longueur indéfinie
Basic Offset Table AVEC Valeur 1er Fragment (Frame 1) de pixels
Tag Longueur Valeur Tag Longueur Valeur
(FFFE, E000) 0000 0008H 0000 0000H
0000 0646H
(FFFE, E000) 0000 02C8H Fragment
Compressé
4 octets 2 octets 2 octets 4 octets 4 octets 4 octets 0008H octets 4 octets 4 octets 02C8H octets

Suite de la séquence (Valeur)
2d Fragment (Frame 1) de pixels 3ème Fragment (Frame 2) de pixels Sequence Delimiter Item
Tag Longueur Valeur Tag Longueur Valeur Sequence Delimiter Tag Longueur
(FFFE, E000) 0000 036EH Fragment
Compressé
(FFFE, E000) 0000 0BC8H Fragment
Compressé
(FFFE, E0DD) 0000 0000H
4 octets 4 octets 036EH octets 4 octets 4 octets 0BC8H octets 4 octets 4 octets

Remarque: DICOM autorise l'encodage (la compression) des pixels d'une image dans certains formats. Par exemple, il n'est pas possible d'utiliser les formats PNG ou ZIP, mais il est possible d'utiliser les formats JPEG et RLE. Ces formats sont décrits dans la seconde sous-partie consacrée à la compression des images en DICOM.

4. Exemples de fichiers

5. Quelques remarques sur l'imagerie médicale et la compression

  • Le standard DICOM ne donne aucune indication sur comment utiliser les différentes compressions disponibles. C'est en effet en dehors du périmètre du standard. De plus, beaucoup trop de paramètres entrent en jeux pour indiquer quelle compression utiliser sur telle ou telle image (type d'image, qualité de l'image, etc.). Malgré cette absence d'indication dans le standard, une règle de bonne pratique est que quelque soit le schéma de compression, il est déconseillé d'utiliser un codec propriétaire pour l'archivage. Cela pourrait mener à des problèmes d'interopérabilités et de migrations.
  • Il y a souvent une incompréhension sur la taille des fichiers DICOM lorsque ces derniers sont encapsulés. En effet, ce n'est pas parce qu'un fichier fait 30 Mo que cela indique qu'une fois chargée en mémoire, l'image fera effectivement 30 Mo. Si le ratio de compression et de 10:1. Alors une fois décompressée, l'image fera 300 Mo. Charger une série ou un examen DICOM en entier peut vite mettre à genoux un ordinateur si celui est sous dimensionné.
  • Autre point important, ce n'est pas parce que l'on compresse une image pour en réduire la taille que le transfert réseau se fera plus rapidement. En effet, compresser et décompresser une image consomme du processeur et du temps. Si le temps de compression + le temps de transfert + le temps de décompression est supérieur ou égal au temps de transfert de l'image sans technique de compression, alors le gain est nul, voir négatif.

lundi 24 septembre 2018

Les images en DICOM - Partie 1

Dans les articles précédents nous avons vu comment DICOM gère, organise et encode les informations médicales. Voyons maintenant comment le standard représente et stocke les images. Il existe, en DICOM, deux grands types de formats pour stocker les images (cf. PS3.5 - section 8.2):
  • Les formats natifs : Ce sont les premiers formats apparus. Ils permettent de stocker les images au format Bitmaps, c'est-à-dire sans compression.  ;
  • Les formats encapsulés: Ces formats permettent de stocker les images en utilisant des techniques de compressions. Ces dernières peuvent être avec ou sans perte de données.
Afin de traiter ces deux approches, plusieurs articles sont nécessaires pour décomposer les idées et ne pas faire un « gros pavé ».

Dans ce billet nous nous intéresserons uniquement à la première approche, c'est-à-dire les formats natifs. Deux autres billets seront consacrés aux formats encapsulés et aux transferts de syntaxes.

1. Les Images


Dans le monde numérique, une image est une matrice rectangulaire de pixels1 (des petits points de différentes couleurs).

Si nous voulions stocker une image, de quelles informations aurions-nous besoin ?
Tout d'abord, Il nous faudrait savoir les dimensions de l'image (sa hauteur et sa largeur).
Ensuite, il faudrait connaitre la valeur des pixels de l'image. Cependant, un pixel peut prendre différentes couleurs (voir la figure ci-dessus). Il faudrait donc pouvoir spécifier la couleur de chaque pixel. Pour cela, il faut pouvoir dire quel « espace de couleur » (représentation des couleurs dans un système de gestion des couleurs en informatique) est utilisé (comme par exemple RGB, HSV ou encore HSL) car oui, il existe différentes manières de coder une couleur.
Il existe ainsi toute une panoplie d'informations nécessaires pour représenter une image.

DICOM a listé tous ces éléments et les a regroupés dans un module « image ». Dans le dictionnaire de données (PS3.6), tous les « Data Element » concernant une image ont l'identifiant de groupe 0028.

2. Encodage d'une image en DICOM


Parmi les tags DICOM les plus utilisés pour caractériser une image, on trouve:

2.1. Rows & Columns


Les tags (0028, 0010) Rows et (0028, 0011) Columns permettent de définir la dimension de l'image. Le tag « Rows » représente la hauteur de l'image et le tag « Columns » spécifie sa largeur.

2.2. Samples Per Pixel


Le tag (0028, 0002) Samples per Pixel spécifie le nombre de canaux de couleur pour un pixel. Par exemple, pour une image en niveaux de gris, un seul canal suffit. Pour une image en couleur utilisant le système de couleur RGB, 3 canaux sont nécessaires (un pour le rouge, un autre pour le vert et un dernier pour le bleu).

2.3. Photometric Interpretation


Le tag (0028,0004) Photometric Interpretation indique l'espace de couleur utilisé. Suivant le type d'image et le format utilisé, cet attribut DICOM peut prendre la valeur :
  • MONOCHROME1 ou MONOCHROME2 pour les images en niveaux de gris.
    • MONOCHROME1 indique que la valeur la moins élevée d'un pixel sera interprétée par du blanc et la valeur la plus élevée par du noir ;
    • MONOCHROME2 indique que la valeur la moins élevée d'un pixel sera interprétée par du noir et la valeur la plus élevée par du blanc.
  • PALETTE COLOR, RGB ou les différentes variantes de YBR pour les images en couleurs (voir PS3.3 section 7.6.3.1.2 pour les valeurs possibles de YBR et leur signification).

2.4. Planar configuration


Le tag (0028,0006) Planar configuration indique l'arrangement des canaux dans la représentation des pixels. Les valeurs possibles de ce champ DICOM sont « 0 » ou « 1 ».
  • La valeur « 0 » signifie que les canaux sont entrelacés pour représenter un pixel. Cette manière de faire est la façon la plus intuitive et celle utilisée par défaut par DICOM. Pour une image RGB, cela signifie que les pixels seront encodés de la manière suivante : R1, G1, B1, R2, G2, B2, ..., etc.
  • La valeur « 1 » indique que chaque canal est encodé de manière continue. On encode déjà tout un canal puis ensuite un autre, et ainsi de suite. Pour une image RGB, cela implique que les pixels seront encodés de la manière suivante R1, R2, R3, ..., G1, G2, G3, ..., B1, B2, B3, etc.
Remarque 1 : Le tag (0028,0006) Planar configuration n'a de sens que si plusieurs canaux interviennent dans la représentation d'un pixel (tag (0028, 0002) Samples Per Pixel supérieur à 1).

Remarque 2 : Le tag (0028,0006) Planar configuration n'a pas non plus de sens si une technique de compression implique une réorganisation des canaux dans le flux compressé. Dans de tels cas, si l'attribut est requis, la valeur à mettre dans ce champ est spécifiée dans la partie PS 3.5 section 8.2 en fonction de la technique de compression utilisée.

2.5. Bits Allocated, Bits Stored & High Bit


Le tag (0028,0100) Bits Allocated spécifie l'espace alloué pour chaque canal d'un pixel exprimé en bit. Les valeurs de ce champ sont en général 8 ou 16 pour une image en niveaux de gris et 24 pour une image couleur.

Le tag (0028,0101) Bits Stored indique le nombre de bits utilisé parmi ceux alloués pour chaque canal. Il est ainsi possible d'allouer 2 octets (Bits Allocated = 16) mais de n'en utiliser que 12. Par exemple, supposons que l'on ait une image en niveaux de gris pour laquelle chaque pixel utilise 8 bits. Chacun de ces pixels peut prendre une valeur entre 0 et 255 (28 valeurs). Le nombre de niveau de gris est donc de 256. Si maintenant on utilise 12 bits au lieu de 8, chaque pixel peut prendre 212 = 4096 valeurs, soit 4096 niveaux de gris. En résumé, les bits stored permettent de représenter la richesse d'une image en terme de nombre de valeurs.

Remarque: Il est possible que tous les bits alloués ne soient pas exploités. Par exemple, si seulement 12 bits sur 16 sont utilisés, les 4 restants seront « masqués » lors de l'interprétation par une application DICOM. Cet espace non utilisé a, dans les débuts du standard, été exploité pour stocker d'autres informations. Cependant, cette technique est aujourd'hui complètement obsolète.

Le tag (0028,0102) High Bit définit où se trouvent les bits utilisés, à l'intérieur de l'espace alloué. Les bits utilisés sont représentés par un segment continu (les bits utilisés sont adjacents). La valeur correspond à l'index du dernier bit utilisé.

Exemple d'utilisation de ces 3 éléments DICOM - Image Monochrome


Supposons que l'on dispose d'une image en niveaux de gris (Monochrome) de 512x512 pixels et que chaque pixel utilise 12 bits.
  • Bits Allocated : Pour stocker 12 bits il nous faut 2 octets (16 bits). (0028,0100) Bits Allocated aura donc pour valeur 16.
  • Bits Stored : Chaque pixel utilise 12 bits. (0028,0101) Bits Stored aura pour valeur 12.
  • High Bit : Sur les 16 bits (numérotés de 0 à 15), seuls 12 sont nécessaires. Nous souhaitons que ce soit les bits 2 à 13. (0028,0102) High Bit aura donc la valeur 13.


Remarque: En pratique, on commence toujours par stocker les bits à partir de l'offset 0. (0028,0102) High Bit est toujours égal à la valeur de Bits Stored - 1.

La taille de l'image stockée est donc 512 x 512 x 2 = 524288 octets = 512 ko.

Exemple d'utilisation de ces 3 éléments DICOM - Image Couleur


Voyons un autre exemple avec une image de couleur de 512 x 512 pixels dans un espace de couleur de type RGB.
  • Photometric Interpretation : Puisque c'est une image couleur RGB, chaque pixel aura 3 canaux. Photometric Interpretation aura pour valeur 3.
  • Bits Allocated : Pour stocker 12 bits il nous faut 2 octets (16 bits). (0028,0100) Bits Allocated aura donc pour valeur 16.
  • Bits Stored : Chaque composante de chaque pixel utilise 12 bits. (0028,0101) Bits Stored aura pour valeur 12.
  • High Bit : Sur les 16 bits (numérotés de 0 à 15), seuls 12 sont nécessaires. Nous souhaitons que ce soit les bits 0 à 11. (0028,0102) High Bit aura donc la valeur 11.


La taille de l'image stockée est donc 512 x 512 x 2 x 3 = 524288 octets = 1.5 Mo.

2.6. Pixel Representation


Le tag (0028, 0103) Pixel Representation indique si les pixels sont encodés avec des octets signés (Pixel Representation a la valeur 1) ou non signés (Pixel Representation a la valeur 0). L'octet non signé est très souvent utilisé par défaut.

Octet « signé » et « non signé » : L'informatique utilise un système binaire à base de « 0 » et « 1 » mais pas de « + » et « - ». Par conséquent, le seul moyen est de convenir que si un nombre est susceptible d'être négatif, on lui réserve un bit pour indiquer le signe. Ce bit de signe est celui le plus à gauche de l'octet.

2.7. Pixel Data


Le tag (7FE0, 0010) Pixel Data permet de stocker les pixels. L'ordre des pixels encodés est de gauche à droite et de haut en bas. Le coin supérieur gauche (de coordonnées 1,1) est encodé en premier, suivi du reste de la première ligne. Vient ensuite le premier pixel de la seconde ligne (de coordonnées 2,1), puis le reste de la seconde ligne, et ainsi de suite.

Exemple d'encodage d'une image en niveau de gris:

Remarque: L'élément (7FE0, 0010) Pixel Data ne fait étonnement pas partie du groupe 0028 (le groupe « Image »). Le standard ne spécifie pas les raisons, mais en analysant la partie PS3.6 - section 6 du standard, on peut remarquer deux choses:
  • Le groupe 7FE0 ne dispose que de trois élements : (7FE0, 0008) Float Pixel Data, (7FE0, 0009) Double Float Pixel Data et (7FE0, 0010) Pixel Data. ;
  • Le groupe 7FE0 se trouve à la fin du dictionnaire des éléments DICOM.
On peut en déduire que l'identifiant 7FE0 représente le groupe « Pixel » et que DICOM a voulu séparer le stockage des pixels du reste des caractéristiques d'une image.
Pourquoi cette séparation ?
On peut supposer que les concepteurs du standard préféraient stocker les pixels en fin de fichier DICOM, afin de pouvoir extraire facilement les méta données DICOM sans les pixels.

Remarque: La VR de Pixel Data peut être OB ou OW suivant le nombre d'octets nécessaire pour stocker un pixel. Pour rappel, si la VR est OW, il faut effectuer un byte swapping pour interpréter les octets.

Remarque: La VR de Pixel Data doit être OW si l'encodage est en Implicit VR Little Endian.

3. Exemple d'encodage d'une image en DICOM


Supposons que l'on veuille encoder l'image suivante :

Cette image a une dimension de 6 x 6 pixels et utilise un espace de couleur RGB. Pour encoder cette image, nous allons utiliser les valeurs par défaut pour les tags Planar Configuration et Pixel Representation. Puisque l'on veut encoder une image en couleur, 3 canaux sont nécessaires pour chaque pixel. On utilisera un octet par canal (un par composante). Cela représente une taille de 3 x 36 = 108 octets pour encoder les 36 pixels de l'image.

L'encodage DICOM donne le résultat suivant:
(0028,0002) US #2 [3] SamplesPerPixel
(0028,0004) CS #4 [RGB] PhotometricInterpretation
(0028,0006) US #2 [0] PlanarConfiguration
(0028,0010) US #2 [6] Rows
(0028,0011) US #2 [6] Columns
(0028,0100) US #2 [8] BitsAllocated
(0028,0101) US #2 [8] BitsStored
(0028,0102) US #2 [7] HighBit
(0028,0103) US #2 [0] PixelRepresentation
(7FE0,0010) OW #108 [CC CC CC FF EE A9 FF EE A9 FF EE A9 FF EE A9 CC CC CC
                     FF EE A9 FF EE A9 FF EE A9 FF EE A9 FF EE A9 FF EE A9
                     FF EE A9 00 00 00 FF EE A9 FF EE A9 00 00 00 FF EE A9
                     FF EE A9 FF EE A9 FF EE A9 FF EE A9 FF EE A9 FF EE A9
                     FF EE A9 FF EE A9 00 00 00 00 00 00 FF EE A9 FF EE A9
                     CC CC CC FF EE A9 FF EE A9 FF EE A9 FF EE A9 CC CC CC] PixelData


4. Les Multi-frames


Il est possible de stocker plus d'une image dans un objet DICOM. On appelle cela des Multi-frames. Le nombre d'images contenu dans un objet DICOM est indiqué par le tag (0028,0008) Number of Frames. Cet élément contient un entier positif indiquant combien d'images sont présentes dans le tag (7FE0, 0010) Pixel Data.
L'élément (7FE0, 0010) Pixel Data contient les différentes images enchaînées les unes dernières les autres.
Pour rendre un peu plus concrètes les explications, reprenons l'exemple ci-dessus (smiley de dimension 6 x 6). Si nous voulions créer un objet avec deux images de smiley, l'objet DICOM ressemblerait à :

(0028,0002) US #2 [3] SamplesPerPixel
(0028,0004) CS #4 [RGB] PhotometricInterpretation
(0028,0006) US #2 [0] PlanarConfiguration
(0028,0008) IS #2 [2] NumberOfFrames
(0028,0010) US #2 [6] Rows
(0028,0011) US #2 [6] Columns
(0028,0100) US #2 [8] BitsAllocated
(0028,0101) US #2 [8] BitsStored
(0028,0102) US #2 [7] HighBit
(0028,0103) US #2 [0] PixelRepresentation
(7FE0,0010) OW #216 [...] PixelData

On remarque que par rapport à l'objet DICOM précédent, un nouveau tag a fait son apparition et un autre a été modifié. Le tag Pixel Data contient maintenant 216 octets (au lieu de 108). La délimitation entre les images se fait par calcul grâce aux différents tags DICOM indiquant les caractéristiques d'une image.
Une image a une taille de Rows x Colums x SamplesPerPixel x (BitsAllocated / 8) octets. Dans notre cas, chaque image occupe 6 x 6 x 3 x (8 / 8) = 108 octets. Pour aller d'une image à l'autre il faut donc sauter de 108 octets.

5. Les palettes de couleurs


Dans les exemples précédents, nous avons encodé une petite image de 6 x 6 pixels. Supposons maintenant que nous ayons une image couleur de 512 x 512 pixels, qui dispose de 256 couleurs différentes. Utiliser 1 octet par composante RGB est suffisant pour encoder ces 256 couleurs. La taille sera donc de 512 x 512 x 3 = 768 ko.

Les images basées sur un encodage RGB peuvent rapidement occuper un espace de stockage considérable. La question que l'on peut se poser est : N'y aurait-il pas un moyen de réduire cet espace occupé ? Une réponse possible est l'utilisation d'une palette de couleurs.

Les annexes PS3.3 - C.7.6.3.1.5, PS3.3 - C.7.6.3.1.6 et PS3.5 - A.4 décrivent l'utilisation des palettes de couleurs pour encoder une image en couleur.
L'idée est d'utiliser une table de correspondance (appelée « Lookup Table »). À chaque valeur de cette table est associée une clé (appelée « index »). On accède à chaque valeur de la table par sa clé. Côté pixels, on ne stocke plus la couleur (les 3 composantes RGB par exemple) mais la clé permettant d'accéder à la couleur dans la table de correspondance.
Le pixel i fait référence à l'index 23 de la palette de couleur. Il est de couleur verte.
Un tel système d'encodage permet de gagner de la place pour stocker une image.

Reprenons l'exemple ci-dessus de l'image couleur de 512 x 512 pixels avec 256 couleurs. Avec un encodage RGB, l'espace occupé par les pixels est de 768 ko.
En utilisant un encodage par palette de couleur, l'espace occupé serait de
  • pour l'image : 512 x 512 x 1 =  262 144 octets = 256 ko
  • pour la palette de couleur: 256 * 3 = 768 octets
La taille totale de l'image serait donc 262 912 octets ≃ 256 ko, soit 1/3 de la taille avec un encodage RGB.

5.1 Encodage DICOM


DICOM utilise 3 types de Data Element pour décrire l'encodage des palettes de couleurs :
  • (0028, 1101-1103) Palette Color Lookup Table Descriptor: Pour la description des données dans les éléments Palette Color Lookup Table Data ;
  • (0028, 1201-1203) Palette Color Lookup Table Data: Pour stocker les données de la palette de couleurs ;
  • (7FE0, 0010) Pixel Data: Pour stocker l'index de la table de correspondance de chaque couleur de pixel.

5.1.1. Palette Color Lookup Table Descriptor


Ce type de Data Element permet de décrire le format des données de la palette. Les palettes de couleurs utilisent le modèle RGB, il faut donc 1 Data Element pour décrire chacune des 3 couleurs primaires.
  • (0028, 1101) Red Palette Color Lookup Table Descriptor pour la composante rouge ;
  • (0028, 1102) Green Palette Color Lookup Table Descriptor pour la composante verte ;
  • (0028, 1103) Blue Palette Color Lookup Table Descriptor pour la composante bleue.
Chacun de ces éléments est composé de 3 valeurs:
  • La première valeur « Number of entries » permet de spécifier le nombre d'entrées dans la table de correspondance (Lookup Table). Il est possible d'aller jusqu'à 216 entrées. De manière générale, on trouve souvent 256 comme nombre d'entrées (et donc de couleurs).
  • La seconde valeur « First value mapped » indique la première entrée (le premier index) de la table de correspondance. En général, c'est toujours la valeur 0. Si d'aventure un pixel fait référence à un index inférieur à « First value mapped », alors c'est la « First value mapped » qui sera utilisée. De manière similaire, si un pixel fait référence à un index supérieur à la taille du tableau (Number of entries), alors c'est la dernière entrée de la table qui sera utilisée.
  • La dernière valeur « Number of bits » caractérise le nombre de bits utilisés pour chaque  donnée dans Lookup Table Data. Elle peut prendre les valeurs 8 ou 16. 

Remarque: Le tag (0028, 1104) Alpha Palette Color Lookup Table Descriptor existe dans le dictionnaire de données (PS3.6) et dans la description de la section PS3.3 - C.7.6.3.1.5 du standard. Mais une note de la section PS3.3 - C.7.6.3.1.1 explique que l'utilisation du canal Alpha a été retirée du standard (valeur « 4 » interdite dans le tag (0028, 0002) Samples Per Pixel, et valeur « ARGB » interdite dans (0028, 0004) Photometric Interpretation). Le tag concernant le canal Alpha est conservé uniquement à des fins de compatibilité.

5.1.2. Palette Color Lookup Table Data


Ce type de Data Element permet de stocker les données de la palette. Les palettes de couleurs utilisant le modèle RGB, il faut donc 1 Data Element pour chacune des 3 couleurs primaires.
  • (0028, 1201) Red Palette Color Lookup Table Data : Pour stocker les n composantes de rouge des n entrées possibles de la table de correspondance ;
  • (0028, 1202) Green Palette Color Lookup Table Data : Pour stocker les n composantes de vert des n entrées possibles de la table de correspondance ;
  • (0028, 1203) Blue Palette Color Lookup Table Data : Pour stocker les n composantes de bleu des n entrées possibles de la table de correspondance.
Il y a autant de valeurs dans chacun de ces éléments que de valeurs spécifiées par « Number of entries » dans les descriptions des Lookup Table.

Remarque : Les Data Element (0028, 1201-1204) Palette Color Lookup Table Data ont une VR SS ou US. Ces deux VR  utilisent donc 2 octets pour représenter une valeur. Dans le cas ou la valeur « Number of bits » des Data Element (0028, 1101-1104) Palette Color Lookup Table Descriptor est 8 bits (1 octet), il est possible de représenter deux entrées du tableau avec une seule valeur US ou SS.

5.1.3 Pixel Data


Dans le cas d'utilisation d'une palette de couleurs, le tag (7FE0, 0010) Pixel Data stocke l'index d'accès à une couleur dans la table de correspondance (contrairement à l'encodage monochrome ou RGB qui stocke les valeurs de chaque pixel). 

5.1.4 Exemple concret


Reprenons l'image du smiley de 6 x 6 pixels et encodons la avec une palette de couleur
La section PS3.3 - C.7.6.3.1.2 indique que si la valeur PALETTE COLOR est utilisée, alors (0028, 0002) Sample Per Pixels doit avoir la valeur « 1 ». L'encodage DICOM donne le résultat suivant:
(0028,0002) US #2 [1] SamplesPerPixel
(0028,0004) CS #4 [PALETTE COLOR] PhotometricInterpretation
(0028,0006) US #2 [0] PlanarConfiguration
(0028,0010) US #2 [6] Rows
(0028,0011) US #2 [6] Columns
(0028,0100) US #2 [8] BitsAllocated
(0028,0101) US #2 [8] BitsStored
(0028,0102) US #2 [7] HighBit
(0028,0103) US #2 [0] PixelRepresentation
(0028,1101) US #6 [4\0\8] RedPaletteColorLookupTableDescriptor
(0028,1102) US #6 [4\0\8] GreenPaletteColorLookupTableDescriptor
(0028,1103) US #6 [4\0\8] BluePaletteColorLookupTableDescriptor
(0028,1201) US #4 [255 127 0 0] RedPaletteColorLookupTableData
(0028,1201) US #4 [242 127 0 0] GreenPaletteColorLookupTableData
(0028,1201) US #4 [0 127 0 0] BluePaletteColorLookupTableData
(7FE0,0010) OW #36 [01 00 00 00 00 01 00 00 00 00 00 00 00 02 00 00 02 00
                    00 00 00 00 00 00 00 00 02 02 00 00 01 00 00 00 00 01] PixelData


Remarque: L'utilitaire dcmquant du toolkit dcmtk a été utilisé pour générer l'exemple ci-dessus à partir de l'image du smiley encodé en RGB.

Remarque: DICOM impose d'avoir un nombre pair pour la taille de chaque DICOM Element. C'est pourquoi l'outil dcmquant duplique la dernière valeur. Cela ne change rien à l'interprétation des palettes de couleurs car tout index supérieur à la taille du tableau sera interprété comme le dernier index de la table de correspondance.

(1) Pixel : Provient de la locution anglaise picture element, qui signifie « élément d'image ».

lundi 10 septembre 2018

L'encodage d'objet DICOM - Format JSON

La partie PS3.18 (Annexe F) du standard définit un encodage des objets DICOM au format JSON. Ce modèle de représentation est appelé « DICOM JSON Model ».
Tout comme le format XML, JSON est 100% textuel, indépendant d'un langage (on respecte la notion d'universalité - cf. Introduction) et utilisé pour transférer des structures de données légères.

« DICOM JSON Model » complémente le format XML « Native DICOM Model ». Tout comme le format XML, le format JSON pourrait être utilisé pour échanger des objets DICOM, mais le standard recommande de ne l'employer que dans des cas bien précis, tels que des applications mobiles qui utiliseraient les services Web DICOM (QIDO-RS par exemple).

Ces applications n'ont pas besoin de savoir lire des objets DICOM standard, et donc d'embarquer des bibliothèques particulières permettant de décoder du DICOM.

Remarque : Que ce soit l'encodage XML ou JSON, ces deux alternatives sont très proches l'une de l'autre car elles permettent d'avoir une représentation 100% textuelle de l'objet DICOM et de ne se reposer que sur des outils standards de traitement XML ou JSON. Le choix entre JSON et XML est donc assez subjectif.
Actuellement, le standard recommande le format XML pour des traitements sur un serveur (validation ou coercition d'attributs par exemple) et le format JSON pour les transferts réseaux sur les applications mobiles en DICOM web (métadonnées dicom). JSON étant moins verbeux que XML, ce choix de format pourrait être amené à disparaître avec le développement des outils JSON qui permettraient d'être iso-fonctionnel par rapport aux outils XML.

Il est vivement conseillé d'avoir lu les parties Encodage Classique - partie 1 et Encodage Classique - partie 2 pour comprendre ce billet.

1. Présentation de JSON


Le format JSON utilise une syntaxe très simple mais qui permet de représenter n'importe quelle donnée. Cette syntaxe est composée de 4 types primitifs et 2 types structurés.

1.1 Les types primitifs

  • Strings :  une séquence de 0 ou n caractères Unicode. Elles sont obligatoirement entourées de guillemets (non interchangeables avec des apostrophes) ;
  • Numbers : un nombre décimal signé qui peut contenir une part fractionnable ou élevée à la puissance. JSON ne permet pas les nombres inexistants (NaN), et ne fait aucune différence entre un entier et un flottant ;
  • booleans: Permet de représenter un booléen. Deux valeurs sont autorisées « true » ou « false » ;
  • null: indique une valeur vide, utilisant le mot clé « null ».

1.2 Les types structurés

  • Les Objets : Un objet (object) est un ensemble de paires « clé:valeur ».  La « clé » est de type « string », suivie du caractère « : » puis de la « valeur ». Chaque paire composant l'objet est séparée de la précédente par le caractère « , ». Un objet commence par le caractère « { » et termine par le caractère « } » ;

  • Les tableaux : Un tableau (array) est une collection de plusieurs valeurs. Il commence par le caractère « [ » et termine par le caractère « ] ». Chaque valeur est séparée de la précédente par le caractère « , ».
Dans une paire « clé:valeur », la valeur peut être d'un des 6 types présentés ci-dessus.


Exemple d'objet JSON simple:
{
 "nom":"Dupond",
 "prenoms":["Jean", "Paul", "Jacques"],
 "age":37,
 "representantLegal":null,
 "fumeur":true
}

Exemple d'objet JSON complexe (structures JSON imbriquées)
{
 "nom":"Dupond",
 "prenoms":["Marie"],
 "age":12,
 "representantLegal":{
   "nom":"Dupond",
   "prenoms":["Jean", "Paul", "Jacques"],
   "age":37,
   "representantLegal":null,
   "fumeur":true
 },
 "fumeur":false
}

2. Encodage DICOM


En DICOM, la brique de base d'un objet est le Data Element. Cet élément est représenté en JSON par la syntaxe suivante :

"ggggeeee": {
  "vr": ... ,
  "Value" | "InlineBinary" | "BulkDataURI": ...
}

Un Data Element est représenté par une paire « clé:valeur » dont la « clé » 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). La valeur est un objet JSON composé de deux sous paires. La première (« vr ») permet de représenter le type du Data Element. La seconde paire (que l'on appellera « content ») indique la valeur du Data Element. En fonction de la VR, la « clé » de content peut prendre une des valeurs suivantes: Value, InlineBinary ou BulkDataURI.

Le tableau suivant résume le mapping entre chaque VR DICOM, le type JSON associé, et la clé de la représentation JSON de la valeur d'un Data Element. Le mapping entre les VR DICOM et les types JSON est disponible dans la partie PS3.18 - Annexe F2.3.


VR Clé de content Type JSON de content
AE
Value
String
AS
Value
String
AT
Value
String
CS
Value
String
DA
Value
String
DS
Value
Number
DT
Value
String
FL
Value
Number
FD
Value
Number
IS
Value
Number
LO
Value
String
LT
Value
String
OB
InlineBinary ou BulkDataURI
si InlineBinary : Flux d'octet encodé en Base64
si BulkDataURI: String
OD
InlineBinary ou BulkDataURI
si InlineBinary : Flux d'octet encodé en Base64
si BulkDataURI: String
OF
InlineBinary ou BulkDataURI
si InlineBinary : Flux d'octet encodé en Base64
si BulkDataURI: String
OL
InlineBinary ou BulkDataURI
si InlineBinary : Flux d'octet encodé en Base64
si BulkDataURI: String
OW
InlineBinary ou BulkDataURI
si InlineBinary : Flux d'octet encodé en Base64
si BulkDataURI: String
PN
Value
Tableau de systèmes d'écritures
SH
Value
String
SL
Value
Number
SQ
Value
Tableau de DICOM JSON Objects
SS
Value
Number
ST
Value
String
TM
Value
String
UC
Value
String
UI
Value
String
UL
Value
Number
UN
InlineBinary ou BulkDataURI
Flux d'octet encodé en Base64
UR
Value
String
US
Value
Number
UT
Value
String

Remarque: Au format JSON, un objet DICOM est appelé « DICOM JSON Object ».

2.1. L'élément « Value »


2.1.1. Cas général


Pour les Data Element ayant une VR autre que PN, SQ, OB, OD, OF, OL, OW ou UN, l'élément « Value » contient un tableau de valeurs. En effet, un Data Element pouvant avoir plusieurs valeurs, cette caractéristique est représentée en JSON par un tableau.

Exemple 1 - Tableau d'un élément:
"00100020": {
 "vr": "LO",
 "value": ["12345"]
}

Exemple 2 - Tableau de plusieurs éléments:
"00080061": {
  "vr": "CS",
  "value": ["CT", "PET"]
}

2.1.2. Cas du type PN


Pour les Data Element avec une VR PN, « Value » contient un tableau d'objets JSON. Chaque objet de ce tableau peut contenir trois éléments : Alphabetic, Ideographic et Phonetic.
Chacun de ces 3 éléments a pour valeur une chaîne de caractères qui respecte le formalisme suivant :

FamilyName^GivenName^MiddleName^NamePrefix^NameSuffix.

Tout comme pour les autres systèmes d'encodages DICOM (classique ou XML), chacun des composants  FamilyName, GivenName, MiddleName, NamePrefix et NameSuffix est optionnel.

Exemple:
"00100010": {
  "vr":"PN",
   "value":[{
      "Alphabetic":"Yamada^Tarou",
      "Ideographic":"山田^太郎",
      "Phonetic":"やまだ^たろう"
   }]
}

2.1.3. Cas du type SQ


Pour les Data Element avec une VR SQ, « Value » contient un tableau d'objets JSON. Chacun de ces objets représente un Item de la séquence. Chaque Item étant à son tour un « JSON DICOM Object ».

Exemple:
"00081110":{
 "vr":"SQ",
 "value":[{
    "00081150":{
    "vr":"UI",
    "value":["1.2.840.10008.3.1.2.3.1"]
   },
   "00081155":{
    "vr":"UI",
    "value":["1.2.3.4.5.6"]
   }
  }]
}

2.2. L'élément « InlineBinary »


Cet élément permet d'encoder en base64 la valeur des Data Element dont la VR est OB, OD, OF, OW, ou UN. En effet, les VR précédemment citées contiennent des données binaires. Or JSON é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:
"7FE00010":{
  "vr":"OW",
  "InlineBinary":"Qm9uam91ciDDoCB0b2k="
}

Remarque : Tout comme en XML, l'encodage de données binaires en base64 alourdit la taille de l'objet JSON. L'encodage JSON n'est donc pas optimal pour le transfert d'objet DICOM.

2.3. L'élément « BulkDataURI »


Cet élément permet d'encoder une référence à un blob (Binary Large OBject). Le but étant d'éviter de stocker de larges données DICOM dans une structure JSON. Ce blob peut être situé localement sur un système de fichiers ou encore sur un réseau.

Exemple 1 - blob situé localement sur un système de fichiers:
"7FE00010":{
  "vr":"OW",
  "BulkDataURI":"file:/C:/monRep/file.dcm?offset=16&length=524"
}

Exemple 2 - blob situé sur un réseau:
"7FE00010":{
  "vr":"OW",
  "BulkDataURI":"http://server/rs/studies/1.2.3/series/5.6.7"
}


3. Exemple d'un objet DICOM encodé en JSON


Un exemple est disponible dans le standard dans l'annexe PS3.18 Annexe F.4.

Un exemple de fichier DICOM complet au format JSON est également disponible ici.