Découverte de DICOM, le format d'imagerie médicale - PARTIE 1 : la structure
Pour les besoins d'un projet au boulot, j'ai récemment dû me pencher sur le format DICOM. Mon rôle était de comprendre comment se structure un fichier DICOM afin de le vulgariser aux collègues qui bossent sur le projet pour qu'ils puissent générer des fichiers DICOM valides, lisibles par les logiciels de visualisation d'imagerie médicale existants.
Le DICOM, pour Digital Imaging and Communication in Medicine est un standard créé en 1985 conjointement par l'ACR (American College of Radiology) et la NEMA (National Electric Manufacturers Association) afin de standardiser les formats d'imagerie médicale et plus généralement de n'importe quelle donnée médicale.
Le format DICOM est aujourd'hui utilisé pour les radiographies, les scanners, les IRM, les échographies, etc. Si vous avez passé l'un de ces examens et qu'on vous a remis un petit CD-ROM à la sortie, félicitation, vous êtes l'heureux propriétaire de fichiers DICOM ! 😉️
Une chose à savoir avec les fichiers DICOM, c'est qu'ils peuvent contenir une ou plusieurs images dans le même fichier, et que certains examens médicaux comme les scanners et les IRM produisent une série de fichiers indépendants qui, mis bout à bout, permettent de reconstituer une image du corps en trois dimensions.
Dans ce premier article je vais essayer de vous expliquer dans les grandes lignes comment se compose un fichier DICOM, les principales structures et types de données à connaître pour être capable de le manipuler. J'essayerai également d'introduire les termes utiles et de vous pointer les parties intéressantes de la spécification (ce qui ne sera pas du luxe quand on sait qu'elle fait plusieurs milliers de pages ! 😅️).
Vous êtes toujours là ? Alors c'est parti !
- Structure d'un fichier DICOM
- Syntaxe de transfert
- Structure générale d'un fichier DICOM
- Structure de la partie « Meta Information » du fichier (entête)
- Structure de la partie « Data Set » du fichier (corps)
- Structures de donnée « Data Element »
- Longueur des structures de données et padding
- Informations supplémentaires sur les éléments de données et les « tags »
- Les différents types de données (VR)
- On recolle tout ensemble
- Liens et outils essentiels
- Fichiers DICOM d'exemple
- Conclusion
Structure d'un fichier DICOM
Bien que le format en lui-même ne soit pas si compliqué, il existe des tas de variations qui rendent le tout plus complexe que nécessaire. Étant donné que cet article est une introduction au format DICOM, je ne vais pas pouvoir tout aborder et je fais donc le choix de ne parler que de l'une des possibilités lorsque plusieurs alternatives sont possibles.
Syntaxe de transfert
Il y a plusieurs manières d'encoder les structures de données dans la norme DICOM. La norme parle de « Transfer Syntax » pour désigner ces encodages. On dénombre trois syntaxes de transfert principales :
- la « DICOM Implicit VR Little Endian Transfer Syntax » (documentée dans la PS3.5 A.1, c'est-à-dire dans la partie 5, annexe A, section 1 de la norme)
- la « DICOM Explicit VR Little Endian Transfer Syntax » (PS3.5 A.2),
- et la « DICOM Explicit VR Big Endian Transfer Syntax » (PS3.5 A.3).
La première est celle désignée par la norme comme encodage par défaut pour le Data Set (le corps du fichier), la seconde est celle à utiliser obligatoirement pour les structures de la partie Meta Information (l'entête du fichier), mais elle peut aussi être utilisée dans le Data Set, et la dernière a été retirée de la norme en 2016, donc à moins de vouloir ouvrir des vieux fichiers utilisant cette syntaxe, il n'est pas nécessaire de s'en préoccuper.
À tout cela il faut encore rajouter toute une série de syntaxes de transfert à utiliser spécifiquement lorsque l'image contenue dans le fichier est encodée ou compressée. La norme les désigne sous le nom de « Transfer Syntaxes For Encapsulation of Encoded Pixel Data » et elles sont définies dans la section 4 de l'annexe A. Globalement elles reprennent la syntaxe « DICOM Explicit VR Little Endian » (PS3.5 A.2) mais avec des spécificités au niveau de la structure de donnée « Pixel Data » et de son contenu. Je les mentionne uniquement pour que vous sachiez que ça existe, mais on n'en parlera pas davantage ici.
Dans cet article, je vais faire le choix de n'aborder que la syntaxe de transfert nommée « DICOM Explicit VR Little Endian » (PS3.5 A.2), pour trois bonnes raisons :
- c'est celle que je trouve la plus pratique,
- c'est celle qui semble la plus répandue dans le domaine qui m'intéresse,
- et c'est celle qu'il faut de toute façon utiliser dans les entêtes du fichier (donc comme ça j'ai pas besoin de vous parler de deux syntaxes de transfert différentes 😛️).
Mais si vous souhaitez développer une bibliothèque capable de lire n'importe quel DICOM, il vous faudra bien sûr implémenter la syntaxe de transfert « DICOM Implicit VR Little Endian » (PS3.5 A.1) pour être conforme... 😅️
Références utiles :
- Informations générales sur les syntaxes de transfert des fichiers DICOM
- Spécification de la syntaxe de transfert « DICOM Implicit VR Little Endian » (PS3.5 A.1) que vous devriez aussi supporter si vous voulez être conformes à la norme et pouvoir lire un maximum de fichiers
- Spécification de la syntaxe de transfert « DICOM Explicit VR Little Endian » (PS3.5 A.2) que l'on va utiliser par la suite
Maintenant qu'on a défini quelle syntaxe de transfert on allait utiliser, on peut passer à la suite.
Structure générale d'un fichier DICOM
Globalement un fichier DICOM se structure en deux grandes parties :
- Une première nommée Meta Information, qui est en quelque sorte l'entête du fichier. Elle contient un certain nombre d'informations essentielles à l'interprétation du fichier et notamment la syntaxe de transfert utilisée.
- Et une seconde partie nommée Data Set qui va contenir toutes les autres données (nom du patient, image, etc.).
Structure de la partie « Meta Information » du fichier (entête)
La partie Meta Information est composée de la manière suivante :
- Un File Preamble de 128 octets qui peut être utilisé pour les besoins spécifiques de certaines applications mais qui ne sert la plupart du temps à rien. Lorsqu'on n'en a pas besoin, les 128 octets sont remplis avec des zéros (0x00).
- Vient ensuite le DICOM Prefix de 4 octets. Il s'agit de la chaîne de caractère DICM, permettant d'identifier qu'on a bien affaire à un fichier DICOM.
- Et enfin vont s'enchaîner des « éléments de données », une suite de structure de données nommées « Data Element » qui contiennent les informations nécessaires à la lecture du corps du fichier. On reviendra plus en détail sur le contenu dans le second article.
Avis
ATTENTION : Quelle que soit la syntaxe de transfert utilisée, les structures « Data Element » de l'entête d'un fichier DICOM sont toujours encodées en utilisant la « Explicit VR Little Endian Transfer Syntax » (PS3.5 A.2) ; celle que l'on va utiliser partout dans notre cas.
Références utiles :
Structure de la partie « Data Set » du fichier (corps)
La partie Data Set contient toutes les données du fichier DICOM. Elle est composée uniquement de structures « Data Element » collées à la suite les unes des autres.
Références utiles :
Structures de donnée « Data Element »
Comme on a pu le voir, un fichier DICOM est essentiellement composé de structures de données nommées « Data Element ». Le problème c'est qu'il en existe de deux formes différentes avec la syntaxe de transfert que j'ai choisi (Explicit VR Little Endian (PS3.5 A.2)) :
- La première, que je vais nommer par la suite « Data Element 7.1-2 » est utilisée pour contenir les données des types suivants : AE, AS, AT, CS, DA, DS, DT, FL, FD, IS, LO, LT, PN, SH, SL, SS, ST, TM, UI, UL et US.
- Et la seconde, que je vais nommer « Data Element 7.1-1 » s'utilise quant à elle pour les types de données suivants : OB, OD, OF, OL, OV, OW, SQ et UN.
Ne vous inquiétez pas pour l'instant de ce à quoi correspondent ces types, on y revient un peu plus tard ! 😉️
Note
NOTE : Dans la norme DICOM, ces structures de données sont présentées dans l'ordre inverse du mien (d'où la dénomination de 7.1-2 pour la première et 7.1-1 pour la seconde), mais je trouvais plus logique et plus pratique d'en parler dans cet ordre.
Data Element 7.1-2
La forme la plus courante, définie dans le tableau 7.1-2 de la spec, est composée de la manière suivante :
- Un champ Tag, qui permet d'identifier la donnée stockée dans le champ. Ce champ se décompose lui-même en deux sous-champs :
- Group Number [entier 16 bits non signé]
- et Element Number [entier 16 bits non signé].
- Vient ensuite un champ Value Representation (abrégé VR) qui indique le type de la donnée stockée dans le champ [chaîne de caractère composé de 2 caractères ASCII].
- Puis on a le champ Value Length (abrégé VL) qui indique la longueur de la donnée en octet (sans compter les autres champs de la structure mais en incluant les éventuels paddings de la valeur) [entier 16 bits non signé].
- Et enfin on a le champ Value qui contient la donnée en elle-même, avec un padding si la longueur de la valeur est impaire.
Exemple concret avec un Data Element contenant le nom d'un patient :
(7.1-2) | Tag | VR | VL | Value | (pad?) | |
---|---|---|---|---|---|---|
Interprétation | (0010,0010) | PN | 14 | Amanda^Ripley | ||
Donnée binaire | 10 00 | 10 00 | 50 4E | 0E 00 | 41 6D 61 6E 64 61 5E 52 69 70 6C 65 79 | 20 |
Ici on a donc l'élément de donnée (0010,0010) qui correspond au nom du patient. Cet élément est de type PN (Person Name) qui est une chaîne de caractère spécialisée dans le stockage du nom des gens. La valeur fait 14 octets de long et contient le nom de la patiente : Amanda Ripley. Le prénom et le nom ont été séparés par un petit chapeau, conformément à la norme. Et comme la longueur du nom était impaire, un petit padding a été ajouté à la fin.
Les plus attentifs d'entre vous auront sans doute remarqué que j'ai paddé ma valeur avec 0x20 et non avec 0x00 ce qui est un peu inhabituel dans un format binaire... Pourquoi cette excentricité ? Et bah je vais faire durer un peu le suspense, on en reparle un tout petit peu plus loin... 😛️
Références utiles :
Data Element 7.1-1
La seconde forme est presque identique à la première, à deux exceptions près :
- Il y a un padding de 2 octets après le champ VR (les deux octets doivent être définis à 0x00).
- Le champ VL (Value Length) est encodé sur un entier 32 bits non signé (contre 16 bit dans l'autre version).
Et comme pour la structure de donnée 7.1-2, il faut que la longueur de la valeur soit paire, sinon il faut « padder ». 🙃️
Exemple concret avec un Data Element contenant une petite image monochrome de 2×2 px représentant un damier :
(7.1-1) | Tag | VR | pad. | VL | Value | (pad?) | |
---|---|---|---|---|---|---|---|
Interprétation | (7FE0,0010) | OB | 4 | ▞ | |||
Donnée binaire | E0 7F | 10 00 | 4F 42 | 00 00 | 04 00 00 00 | FF 00 00 FF |
Ici on retrouve donc l'élément (7FE0,0010) (Pixel Data) de type OB (Other Byte), soit un tableau d'octets. La valeur fait 4 octets de long et contient les 4 pixels qui composent notre image. Dans le cas présent la longueur de la valeur étant paire, on a pas besoin d'ajouter de padding.
Références utiles :
Longueur des structures de données et padding
Il est important de noter que la longueur totale de la structure de donnée est toujours paire. Lorsque la valeur a une longueur impaire, on lui ajoute un padding pour compenser.
La manière de faire le padding dépend du type de donnée (VR) et est définie type par type dans la norme DICOM. Par exemple pour le nom d'un patient (VR = PN) on utilise des espaces (0x20), pour un tableau d'octet (VR = OB) on utilise des zéros (0x00).
À noter également que pour certains types de données (surtout ceux basés sur des chaînes de caractères), la norme autorise à « padder » avec plus de 1 octet, et de manière indifférenciée avant ou après la valeur (en gros les espaces en début et en fin de chaîne ne sont pas significatifs et doivent être ignorés).
Enfin, certaines implémentations sont buggées et il n'est pas rare de se retrouver avec des chaînes de caractères « paddées » avec des zéros (0x00) en lieu et place des espaces (0x20)... Vous voilà prévenus ! 😅️
Informations supplémentaires sur les éléments de données et les « tags »
Chaque élément de donnée (qui est donc encodée dans une structure de donnée « Data Element ») se voit associer un tag qui sert à l'identifier.
Comme on l'a vu un peu plus tôt, les tags sont composés de deux éléments :
- un numéro de groupe,
- et un numéro d'élément.
Dans la spécification DICOM, vous retrouverez généralement les tags écrits sous la forme d'un tuple de deux nombres hexadécimaux : (GGGG,EEEE), où les G représentent le numéro de groupe et les E le numéro d'élément.
Par exemple le tag (0010,0010) correspond au nom du patient. Pour les plus curieux, sachez qu'il est défini dans la partie PS3.3 annexe C.2.2 de la spécification, intitulée « Patient Identification Module ». Notez au passage que cette section ne définit pas le type de la donnée (VR), il faut pour cela se référer au registre des éléments de données qui se trouve carrément dans une autre partie de la spec (la PS3.6 chapitre 6)... 😅️
Avis
ATTENTION : Dans la norme, les numéros de groupe et d'éléments sont exprimés avec des entiers 16 bits non signé ordonné en big endian, le tout écrit en notation hexadécimale. Or, dans le fichier DICOM ces nombres seront écrits en little endian, il faut donc en tenir compte si vous essayez d'analyser votre fichier avec un éditeur hexadécimal.
Par exemple, le tag (7FE0,0010), qui correspond à un élément « Pixel Data », se retrouvera concrètement écrit comme ceci dans le fichier DICOM : E0 7F 10 00.
Autres informations importantes à savoir sur nos éléments de données :
- Chaque élément de donnée ne peut apparaître qu'une seule fois dans un Data Set (sauf éléments imbriqués).
- On ne peut pas écrire les éléments dans l'ordre qu'on veut dans le fichier : ils doivent être triés par numéro de groupe et d'élément croissant. Par exemple, l'élément (0010,0010) devra se trouver avant l'élément (7FE0,0010) dans le fichier.
Voilà, je crois qu'on a fait le tour ! 😮💨️
Références utiles :
Les différents types de données (VR)
Il existe 34 types de données différents dans la spécification DICOM. Ces types sont désignés par le terme « VR » (Value Representation) dans la norme.
- Certains représentent des nombres, comme SS (Signed Short), UL (Unsigned Long), ou encore UV (Unsigned Very Long),
- D'autres représentent des textes, comme ST (Short Text), SH (Short String) ou encore UC (Unlimited Characters... oui toujours plus !). Ici il faut se méfier des détails : la norme différencie les « String », les « Text » et les « Characters » qui ont des restrictions d'utilisation différentes.
- D'autres encore sont des textes représentant des données spécifiques. Par exemple PN (Person Name) sert à stocker le nom de quelqu'un sous une forme assez particulière (le nom est séparé du prénom par un « caret » (^)). Autres exemples, DA et TM servent à stocker respectivement une date et une heure sous la forme d'une chaîne de caractère.
- On dispose également de tout ce qu'il faut pour stocker des données binaires avec les types OB (Other Bytes, tableau d'octet), OW (Other Word, tableau de mot de 16 bits) ou OV (Other Very Long, tableau de mots de 64 bits). Dans cette catégorie on peut aussi trouver le type UN (Unknown) qui contient des données binaires mais qu'on-sait-pas-du-tout-comment-que-c'est-encodé-là-dedans... 🙃️
Bref les VR sont nombreuses et je vous laisse vous référer à la doc pour en avoir une liste exhaustive avec toutes les informations utiles :
On recolle tout ensemble
Voilà à quoi ressemble notre fichier DICOM au complet lorsque l'on recolle tous ce qu'on a vu précédemment ensemble :
Pour résumer, un fichier DICOM est composé d'un entête (Meta Information) et d'un corps (Data Set) qui contiennent essentiellement des structures de données (Data Element) dont la forme exacte peut varier en fonction de la syntaxe de transfert (Transfer Syntax) utilisée et du type de donnée (VR) qu'elle contient.
C'est pas si compliqué finalement non ? 😋️
Liens et outils essentiels
Je vais à présent vous parler de quelques documents et outils dont je me suis servi et qui me semblent essentiels si vous développez un logiciel capable de lire ou d'écrire des fichiers DICOM.
La norme DICOM
Bon évidemment le tout premier élément que je dois mentionner c'est la norme DICOM elle-même. C'est là que se trouvent toutes les informations nécessaires ; vous avez dû vous en rendre compte avec toutes les références que j'y ai faites dans cet article. 😄️
Elle se compose de 22 parties nommées « PS3.N » où N est le numéro de la partie. Chaque partie est composée de chapitres et d'annexes qui sont eux-mêmes découpés en sections. Je vous mets ci-dessous un lien vers la section PS3.1 6.1 qui liste les différentes parties de la norme avec leur nom ; ça me semble un bon point de départ pour explorer :
Dans cet article on a surtout utilisé la partie 5 (PS3.5) car c'est celle qui définit le format des structures de données, mais dans le prochain article on visitera pas mal les parties 3, 6 et 10.
Pour finir sur la norme, il faut savoir qu'elle est mise à jour plusieurs fois par ans (jusqu'à 5× par ans même) et qu'il faut donc bien faire attention de pas se référer à une version complètement obsolète (on a vite fait de se retrouver sur une révision de 2013 au détour d'une recherche sur les zinterwebz 😉️). Pour cet article je me suis basé sur la version 2024d et tous les liens pointent donc vers cette version. Si vous voulez être sûr de lire la révision la plus récente de la norme, vous pouvez remplacer la version dans l'URL par « current ».
Vous trouverez la liste de toutes les révisions par là :
L'éditeur hexadécimal ImHex
Après la norme DICOM, l'outil qui m'a le plus aidé à comprendre les fichiers DICOM est l'éditeur hexadécimal ImHex. C'est simplement le meilleur que j'ai trouvé pour ce job. Il fournit un puissant éditeur de pattern qui lui permet de mettre en évidence et de décrire chaque partie du fichier. Dans le cas des DICOM c'est supporté de base donc quand vous en ouvrez un, il vous propose directement de l'analyser.
Voici une petite capture d'écran pour que vous puissiez voir à quoi ça ressemble :
En haut à gauche on peut voir les données binaires du fichier DICOM représentées en hexadécimal et en ASCII, le tout bariolé de couleur. Chaque zone de couleur correspond à une partie ou à un champ de l'une des structures de données du DICOM.
La partie en bas à gauche permet de se promener dans le DICOM un peu de la même façon que l'inspecteur HTML intégré aux navigateurs Web. En cliquant sur un élément il est directement mis en évidence dans l'éditeur hexa, et les colonnes « Type » et « Value » nous informent sur le type des données et sur leur interprétation. C'est juste indispensable pour analyser un fichier ! 👌️
En haut au milieu on a un inspecteur de donnée qui permet de nous aider à interpréter ce que l'on pointe. Par exemple si je clique sur un octet dans l'éditeur hexa, il va pouvoir me dire ce que ça donne si on considère cette valeur en tant que nombre 8 / 16 / 24 / 32 bits, signé ou non signé, en big ou en little endian, etc.
Enfin sur la partie supérieure droite, on a l'éditeur de pattern... Pas de panique, vous n'avez pas trop à vous en préoccuper pour le moment : il s'agit du code qui a été automatiquement chargé pour analyser le DICOM. Vous pouvez le modifier pour vos propres besoins, notamment si vous voulez analyser des types de fichiers qui ne sont pas (encore ?) supportés par ImHex, mais ce n'est pas nécessaire pour le DICOM. Je ferais certainement un article à ce sujet car je trouve ça super intéressant ! 😁️
Si vous voulez tester cet outil, sachez qu'il est disponible pour Linux, macOS et Windows, et qu'il est bien sûr open source. Je vous mets les liens ci-dessous :
Le viewer Amide
Amide est un petit viewer de fichier DICOM en GTK 2. Il est surtout conçu pour afficher les séries d'images issues de scanners et d'IRM. C'est assez pratique de l'avoir pour afficher nos DICOM et pour pouvoir s'assurer qu'ils fonctionnent bien.
Amide est disponible pour Linux, macOS et Windows. Il est présent dans les dépôts Debian et Ubuntu, vous pouvez donc l'installer sur ces systèmes avec la commande :
sudo apt install amide
Pour les autres vous pouvez vous référer au site officiel :
Encore plus...
Je vous rajoute en vrac quelques autres outils que j'ai aussi utilisés :
- DICOM Standard Browser, un outil en ligne permettant de naviguer dans le standard DICOM de manière logique et il permet également d'analyser le contenu d'un fichier. C'est un outil absolument essentiel mais je ne m'attarde pas plus dessus aujourd'hui car on en reparlera dans le prochain article... 😉️
- Gimp, qui est capable d'afficher de manière assez limitée l'image d'un fichier DICOM. Si vous n'avez rien d'autre sous la main, ça dépanne.
- pydicom, une excellente bibliothèque Python pour manipuler des fichiers DICOM (aussi bien pour les lire que pour les écrire).
- dicomParser, une bibliothèque JavaScript pour parser les DICOM. Elle ne s'occupe que de parser les fichiers, et donc d'en extraire les différentes informations de manière brute sans aucune interprétation, mais elle le fait bien et c'est déjà ça de moins à faire si vous souhaitez travailler avec des fichiers DICOM en JS. 🙃️
Et un petit dernier que je n'ai pas utilisé personnellement mais qui est une telle référence que je ne pouvais pas ne pas le citer :
- DCMTK, une suite d'outils et de bibliothèques implémentant une large portion de la norme ; une référence en la matière !
Fichiers DICOM d'exemple
Quand on travaille sur le format DICOM, l'une des choses un peu compliquée c'est de mettre la main sur un jeu de donnée sur lequel s'exercer. J'ai eu la chance de ne pas avoir ce souci car notre client nous a partagé quelques séries de scanners anonymisés, mais je ne peux malheureusement pas vous les partager.
J'ai cependant pu mettre la main sur des jeux de données en accès libre aux adresses suivantes :
- https://www.aliza-dicom-viewer.com/download/datasets
- https://medimodel.com/sample-dicom-files/
- https://www.rubomedical.com/dicom_files/
Note
NOTE : pour le premier lien, j'ai constaté que les fichiers de certains jeux de données ne contenaient que la partie Data Set (l'entête est manquant), ce qui empêche la plupart des outils de les ouvrir...
Si vous tombez sur des fichiers qui ne semblent pas fonctionner vous pouvez vérifier avec ImHex si l'entête est présent, et dans le cas contraire passer à une autre série de fichiers. Si vous êtes motivés, vous pouvez aussi compléter les fichiers en écrivant vous-même les entêtes manquants... 😅️
Conclusion
Comme vous avez pu le voir, le format DICOM c'est pas si compliqué mais c'est quand même un sujet assez vaste. Dans le présent article j'ai essayé de vous montrer la structure globale d'un fichier DICOM de manière plutôt théorique ; dans le prochain article on entrera davantage dans la pratique. On verra notamment comment confectionner à la main notre propre fichier DICOM, artisanal, bio et tout et tout... 😛️
Je vous donne donc rendez-vous dans 2 semaines environ pour la suite. À bientôt ! 😉️
L'image de couverture est dérivée d'une image médicale de Nevit Dilmen et est diffusée sous licence CC-BY-SA.