Guida tecnica alla generazione dei DICOM UID

Nel panorama dell’informatica medica, il DICOM (Digital Imaging and Communications in Medicine) rappresenta lo standard sovrano. Al centro del sistema vi è la necessità di identificare in modo univoco ogni entità: un paziente, uno studio radiologico, una serie di immagini o una singola istanza (SOP Instance). Per farlo vengono utilizzati i DICOM UID (Unique Identifiers).

Background teorico: standard e regolamentazione

Un DICOM UID è un identificatore basato sullo standard ISO 8824 (Object Identifiers – OID). Un DICOM UID deve essere globalmente univoco. Questo significa che due immagini generate in due ospedali differenti non avranno mai lo stesso UID.

Struttura anatomica

Un UID è composto da due parti principali separate da un punto:

				
					UID = [Root] . [Suffix]
				
			
  • Root: la parte che identifica l’organizzazione o il produttore che ha generato l’identificatore (es. 1.2.840.10008 è la root base del DICOM).
  • Suffix: la parte variabile che identifica l’oggetto specifico (studio, serie, immagine) all’interno di quella root.

Vincoli tecnici (DICOM PS3.5)

Affinché una stringa sia un UID valido, deve rispettare regole rigorose:

  • Caratteri: solo numeri (0-9) e punti (.).
  • Lunghezza: massimo 64 caratteri.
  • Leading Zeros: ogni componente (il numero compreso tra i punti) non può iniziare con uno zero, a meno che il numero non sia esattamente 0:
    • 1.2.840.0.123
    • 1.2.840.05.123 (lo 0 prima del 5 è vietato)
  • Immutabilità: una volta generato e assegnato a un’immagine, il UID non deve mai cambiare. Se cambia, per il mondo DICOM quella è un’immagine differente.

Metodi di generazione

Esistono due approcci principali per generare la porzione <Root> e la porzione <Suffix>.

A. Root organizzativa (ISO/ANSI)

Un’organizzazione può richiedere una root ufficiale presso un’autorità nazionale. La root viene gestita da un’autorità centrale, che agisce come elemento di disambiguazione per garantire che i UID rimangano univoci a livello globale.

Una volta ottenuta, l’azienda è responsabile della generazione dei suffissi. Un suffisso tipico è strutturato nel seguente modo:

				
					[Root] . [Product_ID] . [SW_Version] . [Level] . [Payload]
				
			

Componenti nel dettaglio:

  • Product/Device ID: un numero che identifica il software o il macchinario specifico (es. 101 per il PACS, 102 per il Viewer).
  • Software Version: utile per il debugging; spesso la versione viene convertita in un intero, ad esempio 2.4.1 diventa 241.
  • Level: fondamentale per separare gli oggetti gerarchici:
    • 1 = Study Instance UID
    • 2 = Series Instance UID
    • 3 = SOP Instance UID
  • Payload: tipicamente utilizza una combinazione di data/ora e un contatore:
    • Strategia timestamp compatto: utilizza un singolo numero YYYYMMDDHHMMSSms per evitare leading zeros illegali
    • Strategia contatore: aggiunge un numero incrementale finale per gestire generazioni nello stesso millisecondo

Se la Root è troppo lunga, è consigliabile utilizzare hash brevi invece di timestamp completi per non superare il limite di 64 caratteri.

B. Root 2.25

Se non si possiede una root registrata, lo standard consente l’utilizzo del prefisso fisso 2.25.. L’uso della root 2.25 non è una scelta arbitraria dello standard DICOM, ma deriva da un accordo internazionale tra ISO e ITU-T.
In questo caso, il suffisso consiste generalmente nella conversione decimale di un UUID.

Conversione tecnica:

  1. Generazione casuale di 16 byte (come un UUID v4).
  2. Conversione della stringa in un singolo intero decimale.
    • Il numero esadecimale viene trattato come un unico intero a 128 bit e convertito in base 10.
    • Il valore massimo di un intero a 128 bit è $2^{128} – 1$. Questo significa che la parte decimale avrà al massimo 39 cifre.
  1. Formattazione finale: prepend del prefisso 2.25: 2.25.[DecimalNumber].

Esempio: 2.25.123456789012345678901234567890123456789.

Implementazione tecnica in TypeScript

				
					import * as crypto from "crypto";

/**
 * DICOM hierarchy levels
 */
export enum DicomLevel {
  STUDY = 1,
  SERIES = 2,
  INSTANCE = 3
}

export interface GeneratorConfig {
  root?: string;
  deviceId?: string;
  softwareVersion?: string;
}

export class DicomUidGenerator {
  private readonly root: string;
  private readonly deviceId: string;
  private readonly swVersion: string;
  private lastTimestamp: string = "";
  private counter: number = 0;

  constructor(config: GeneratorConfig = {}) {
    this.root = config.root || "2.25";
    this.deviceId = (config.deviceId || "1").replace(/\D/g, "");
    this.swVersion = (config.softwareVersion || "1").replace(/\D/g, "");
  }

  /**
   * Method 2.25: Generation via UUID
   */
  public generateFromRandom(): string {
    const buffer = crypto.randomBytes(16);
    const decimalValue = BigInt("0x" + buffer.toString("hex"));
    return this.finalize(`2.25.${decimalValue.toString()}`);
  }

  /**
   * Structured Method: For those who own a registered Root
   */
  public generateStructured(level: DicomLevel): string {
    if (this.root === "2.25") {
      throw new Error("ISO/ANSI Root required for structured generation.");
    }

    const timestamp = this.getDicomTimestamp();
    if (timestamp === this.lastTimestamp) {
      this.counter++;
    } else {
      this.lastTimestamp = timestamp;
      this.counter = 0;
    }

    const components = [
      this.root,
      this.deviceId,
      this.swVersion,
      level,
      timestamp,
      this.counter
    ];
    return this.finalize(components.join("."));
  }

  private getDicomTimestamp(): string {
    const now = new Date();
    return [
      now.getFullYear(),
      (now.getMonth() + 1).toString().padStart(2, "0"),
      now.getDate().toString().padStart(2, "0"),
      now.getHours().toString().padStart(2, "0"),
      now.getMinutes().toString().padStart(2, "0"),
      now.getSeconds().toString().padStart(2, "0"),
      now.getMilliseconds().toString().padStart(3, "0")
    ].join("");
  }

  private finalize(uid: string): string {
    const sanitized = uid
      .split(".")
      .map(part =>
        part.length > 1 && part.startsWith("0") ? part.replace(/^0+/, "") : part
      )
      .join(".");

    if (sanitized.length > 64) {
      throw new Error(`UID Overflow (${sanitized.length} characters).`);
    }

    const dicomRegex = /^(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*$/;
    if (!dicomRegex.test(sanitized)) {
      throw new Error("Invalid DICOM syntax.");
    }

    return sanitized;
  }
}

				
			

 

Conclusioni

Generare correttamente i DICOM UID garantisce che i dati medici viaggino in sicurezza senza rischi di sovrascrittura o ambiguità. Un UID errato non provoca soltanto un bug software; può causare la perdita di un esame nell’archivio ospedaliero o, peggio ancora, la sovrascrittura di dati clinici.

Quale approccio adottare?

Non esiste una soluzione universale, ma una scelta dettata dal contesto:

  • il metodo 2.25 (randomico) è più robusto contro le collisioni ed è il più semplice da scalare orizzontalmente
  • il metodo strutturato (root proprietaria) permette di soddisfare requisiti di certificazione più stringenti e di tracciare i dati, offrendo vantaggi nelle fasi di debugging e auditing.

Indipendentemente dal metodo utilizzato, le regole rimangono le stesse: assicurarsi di usare solo numeri e punti, restare entro il limite dei 64 caratteri e rimuovere eventuali leading zeros dai segmenti. Implementare correttamente queste logiche significa garantire che ogni esame, serie o immagine rimanga univoco e tracciabile in tutto il mondo.

Ultimi articoli

Programma per visualizzare immagini DICOM

DICOM Standard: cos’è, come funziona e perché è fondamentale nell’imaging medico

intelligenza artificiale nella diagnostica per immagini

Intelligenza artificiale nella diagnostica per immagini: come cambia la radiologia

Caddy come Load Balancer e strategie di fallback

area contatti

Per informazioni, progetti, idee, scrivici