DICOM (Digital Imaging and Communications in Medicine) es el estándar universal para imágenes médicas digitales: radiografías, TACs, resonancias magnéticas, ecografías y más. Un archivo DICOM combina la imagen con metadatos clínicos del paciente.
¿Qué es DICOM?
Un archivo DICOM (.dcm) contiene:
- Datos de imagen: píxeles de la imagen médica (16 bits por píxel habitual)
- Metadatos del paciente: nombre, fecha de nacimiento, ID
- Información del estudio: modalidad (CT, MRI, XR…), fecha, médico
- Parámetros técnicos: voltaje, corriente, grosor de corte, etc.
Estructura básica DICOM:
├── File Meta Information ← cabecera estándar
├── Data Elements ← etiquetas (Tag + VR + Value)
│ ├── (0010,0010) Patient Name
│ ├── (0010,0020) Patient ID
│ ├── (0008,0060) Modality [CT, MR, XR, US...]
│ ├── (0028,0010) Rows
│ ├── (0028,0011) Columns
│ └── (7FE0,0010) Pixel Data ← datos de imagen
Instalación
pip install pydicom pillow numpy matplotlib
Leer un archivo DICOM
import pydicom
import numpy as np
# Cargar DICOM
ds = pydicom.dcmread('imagen.dcm')
# Ver información del paciente
print(f"Paciente: {ds.PatientName}")
print(f"ID: {ds.PatientID}")
print(f"Fecha nac: {ds.get('PatientBirthDate', 'N/A')}")
print(f"Sexo: {ds.get('PatientSex', 'N/A')}")
# Información del estudio
print(f"\nModalidad: {ds.Modality}") # CT, MR, XR, US...
print(f"Fecha: {ds.StudyDate}")
print(f"Descripción:{ds.get('StudyDescription', 'N/A')}")
# Información de la imagen
print(f"\nDimensiones: {ds.Rows} x {ds.Columns}")
print(f"Bits: {ds.BitsAllocated}")
print(f"Fotométrico: {ds.PhotometricInterpretation}")
# Acceder a los píxeles como numpy array
pixel_array = ds.pixel_array
print(f"\nArray shape: {pixel_array.shape}")
print(f"Dtype: {pixel_array.dtype}")
print(f"Min/Max: {pixel_array.min()} / {pixel_array.max()}")
Ver todos los metadatos
import pydicom
ds = pydicom.dcmread('imagen.dcm')
# Imprimir todos los elementos de datos
for elem in ds:
if elem.tag != (0x7FE0, 0x0010): # excluir pixel data (muy grande)
print(f" {elem.tag} {elem.keyword:40s} = {str(elem.value)[:60]}")
# Buscar por keyword
print(ds.get('Manufacturer', 'N/A'))
print(ds.get('KVP', 'N/A')) # kilovoltios (CT)
print(ds.get('SliceThickness', 'N/A')) # grosor de corte (CT)
Convertir DICOM a PNG/JPG
import pydicom
import numpy as np
from PIL import Image
def dicom_a_png(ruta_dcm, ruta_png, aplicar_windowing=True):
"""
Convierte imagen DICOM a PNG.
aplicar_windowing: normaliza el rango de valores para visualización.
"""
ds = pydicom.dcmread(ruta_dcm)
pixel_array = ds.pixel_array.astype(np.float64)
if aplicar_windowing:
# Aplicar Window Center y Window Width si están disponibles
wc = float(ds.get('WindowCenter', pixel_array.mean()))
ww = float(ds.get('WindowWidth', pixel_array.max() - pixel_array.min()))
if isinstance(wc, pydicom.multival.MultiValue):
wc = float(wc[0])
if isinstance(ww, pydicom.multival.MultiValue):
ww = float(ww[0])
# Aplicar windowing (VOI LUT)
minimo = wc - ww / 2
maximo = wc + ww / 2
pixel_array = np.clip(pixel_array, minimo, maximo)
# Normalizar a 0-255
pixel_min = pixel_array.min()
pixel_max = pixel_array.max()
if pixel_max > pixel_min:
pixel_norm = ((pixel_array - pixel_min) / (pixel_max - pixel_min) * 255).astype(np.uint8)
else:
pixel_norm = np.zeros_like(pixel_array, dtype=np.uint8)
# Guardar como PNG
img = Image.fromarray(pixel_norm)
img.save(ruta_png)
print(f"PNG guardado: {ruta_png} ({ds.Rows}x{ds.Columns})")
dicom_a_png('radiografia.dcm', 'radiografia.png')
Procesar serie DICOM (múltiples cortes)
import pydicom
import numpy as np
from pathlib import Path
from PIL import Image
def procesar_serie_dicom(directorio_dicom, directorio_salida, formato='png'):
"""
Procesa todos los archivos DICOM de un directorio (serie de cortes).
Los ordena por InstanceNumber para mantener el orden anatómico.
"""
origen = Path(directorio_dicom)
destino = Path(directorio_salida)
destino.mkdir(parents=True, exist_ok=True)
# Cargar y ordenar por InstanceNumber
archivos_dicom = []
for dcm_file in origen.glob('*.dcm'):
try:
ds = pydicom.dcmread(dcm_file, stop_before_pixels=True) # rápido
numero = int(ds.get('InstanceNumber', 0))
archivos_dicom.append((numero, dcm_file))
except Exception as e:
print(f" Error leyendo {dcm_file.name}: {e}")
archivos_dicom.sort(key=lambda x: x[0])
print(f"Serie DICOM: {len(archivos_dicom)} cortes encontrados")
for i, (numero, dcm_file) in enumerate(archivos_dicom, 1):
try:
ds = pydicom.dcmread(dcm_file)
pixels = ds.pixel_array.astype(np.float64)
# Normalizar
p_min, p_max = pixels.min(), pixels.max()
if p_max > p_min:
pixels = ((pixels - p_min) / (p_max - p_min) * 255).astype(np.uint8)
else:
pixels = np.zeros_like(pixels, dtype=np.uint8)
img = Image.fromarray(pixels)
nombre = f"corte_{i:04d}.{formato}"
img.save(destino / nombre)
if i % 10 == 0 or i == 1:
print(f" Procesado corte {i}/{len(archivos_dicom)}: {nombre}")
except Exception as e:
print(f" ERROR en {dcm_file.name}: {e}")
print(f"\nSerie exportada a: {destino}")
procesar_serie_dicom('TAC_cabeza/', 'TAC_cabeza_png/')
Anonimización DICOM (eliminar datos del paciente)
import pydicom
import copy
from pathlib import Path
# Tags que contienen datos personales del paciente (según DICOM PS3.15)
TAGS_ANONIMIZAR = [
'PatientName',
'PatientID',
'PatientBirthDate',
'PatientAddress',
'PatientTelephoneNumbers',
'PatientSex',
'OtherPatientIDs',
'PatientAge',
'PatientWeight',
'PatientComments',
'ReferringPhysicianName',
'PerformingPhysicianName',
'InstitutionName',
'InstitutionAddress',
'StudyID',
]
def anonimizar_dicom(ruta_entrada, ruta_salida, id_anonimo='PACIENTE_ANONIMO'):
"""Anonimiza un archivo DICOM eliminando datos del paciente."""
ds = pydicom.dcmread(ruta_entrada)
for tag_keyword in TAGS_ANONIMIZAR:
if hasattr(ds, tag_keyword):
try:
setattr(ds, tag_keyword, '')
except AttributeError:
pass
# Establecer ID anónimo
ds.PatientName = id_anonimo
ds.PatientID = id_anonimo
# Guardar
ds.save_as(ruta_salida)
print(f"Anonimizado: {ruta_entrada} → {ruta_salida}")
def anonimizar_directorio(directorio_entrada, directorio_salida):
origen = Path(directorio_entrada)
destino = Path(directorio_salida)
destino.mkdir(parents=True, exist_ok=True)
for dcm in sorted(origen.glob('*.dcm')):
anonimizar_dicom(dcm, destino / dcm.name)
anonimizar_directorio('dicom_originales/', 'dicom_anonimizados/')
Visualizar con matplotlib
import pydicom
import numpy as np
import matplotlib.pyplot as plt
ds = pydicom.dcmread('ct_scan.dcm')
pixel_array = ds.pixel_array
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# Imagen original (escala de grises)
axes[0].imshow(pixel_array, cmap='gray')
axes[0].set_title(f"Original ({pixel_array.shape[0]}x{pixel_array.shape[1]})")
axes[0].axis('off')
# Con windowing pulmonar (CT)
wc_lung, ww_lung = -600, 1500
lung = np.clip(pixel_array, wc_lung - ww_lung/2, wc_lung + ww_lung/2)
axes[1].imshow(lung, cmap='gray')
axes[1].set_title("Ventana pulmonar")
axes[1].axis('off')
# Con windowing óseo (CT)
wc_bone, ww_bone = 400, 1000
bone = np.clip(pixel_array, wc_bone - ww_bone/2, wc_bone + ww_bone/2)
axes[2].imshow(bone, cmap='gray')
axes[2].set_title("Ventana ósea")
axes[2].axis('off')
plt.suptitle(f"Paciente: {ds.get('PatientName', 'N/A')} | Modalidad: {ds.Modality}")
plt.tight_layout()
plt.savefig('visualizacion_ct.png', dpi=150, bbox_inches='tight')
plt.show()
Conversiones relacionadas
Conversiones frecuentes del catálogo: