Estruturação de Diretórios
Inicialmente, é necessário criar uma estrutura de pastas espelhada à estrutura original dos exames DICOM:
from pathlib import Path
origem = Path("./CT")
destino = Path("./nii")
destino.mkdir(parents=True, exist_ok=True)
for pasta in origem.iterdir():
if pasta.is_dir():
(destino / pasta.name).mkdir(parents=True, exist_ok=True)
Filtragem de Sequências T2
Para manter apenas as imagens pertencentes à sequência T2, percorremos todos os arquivos DICOM e verificamos o campo SeriesDescription:
from pathlib import Path
import pydicom
def remover_nao_t2(caminho_raiz):
for arquivo in Path(caminho_raiz).rglob("*.dcm"):
try:
ds = pydicom.dcmread(str(arquivo))
descricao = getattr(ds, "SeriesDescription", "")
if "t2" not in descricao.lower():
arquivo.unlink()
print(f"Removido: {arquivo}")
except Exception as erro:
print(f"Falha ao processar {arquivo}: {erro}")
remover_nao_t2("./dcm_bak")
Inventário de Séries DICOM
O código abaixo lista todas as séries únicas encontradas, exibindo descrição, UID e contagem de imagens:
from pathlib import Path
import pydicom
from collections import defaultdict
def coletar_info_series(caminho):
series = defaultdict(lambda: {"uid": "", "quantidade": 0})
for arquivo in Path(caminho).rglob("*.dcm"):
try:
ds = pydicom.dcmread(str(arquivo))
desc = getattr(ds, "SeriesDescription", "N/A")
uid = getattr(ds, "SeriesInstanceUID", "N/A")
series[desc]["uid"] = uid
series[desc]["quantidade"] += 1
except Exception:
continue
return dict(series)
resultado = coletar_info_series("./images")
for desc, dados in resultado.items():
print(f"Descrição: {desc} | UID: {dados['uid']} | Total: {dados['quantidade']}")
Conversão DICOM para NIfTI
Este script valida a integridade de cada série, verifica a consistência dos metadados e realiza a conversão utilizando a bilbioteca dicom2nifti:
from pathlib import Path
import pydicom
import dicom2nifti
import tempfile
import shutil
def converter_series_dicom(pasta_origem, pasta_destino):
grupos = {}
relatorio = {}
for arquivo in Path(pasta_origem).rglob("*"):
if arquivo.is_file():
try:
ds = pydicom.dcmread(str(arquivo), force=True)
uid = getattr(ds, "SeriesInstanceUID", None)
if uid:
grupos.setdefault(uid, []).append(ds)
except Exception:
continue
for uid, lista_ds in grupos.items():
if len(lista_ds) <= 10:
relatorio[uid] = "Insuficiente"
continue
lista_ordenada = sorted(lista_ds, key=lambda d: int(d.InstanceNumber))
numeros = [int(d.InstanceNumber) for d in lista_ordenada]
if numeros != list(range(1, max(numeros) + 1)):
relatorio[uid] = "Sequencia interrompida"
continue
ref = lista_ordenada[0]
linhas = ref.Rows
colunas = ref.Columns
espacamento = getattr(ref, "PixelSpacing", None)
espessura = getattr(ref, "SliceThickness", None)
consistente = True
for d in lista_ordenada:
if d.Rows != linhas or d.Columns != colunas:
consistente = False
break
if espacamento and d.PixelSpacing != espacamento:
consistente = False
break
if espessura and getattr(d, "SliceThickness", None) != espessura:
consistente = False
break
if not consistente:
relatorio[uid] = "Atributos divergentes"
continue
with tempfile.TemporaryDirectory() as tmp:
for d in lista_ordenada:
if hasattr(d, "PixelData"):
shutil.copy2(d.filename, tmp)
else:
relatorio[uid] = "Sem PixelData"
break
else:
saida_tmp = Path(tmp) / "result"
saida_tmp.mkdir()
dicom2nifti.convert_directory(tmp, str(saida_tmp))
for arq in saida_tmp.glob("*.nii.gz"):
destino_final = Path(pasta_destino) / f"{uid}.nii.gz"
shutil.move(str(arq), str(destino_final))
relatorio[uid] = "OK"
return relatorio
pasta_dicom = Path("./dcm_bak")
pasta_nii = Path("./nii")
for subpasta in pasta_dicom.iterdir():
if subpasta.is_dir():
saida = pasta_nii / subpasta.name
saida.mkdir(parents=True, exist_ok=True)
status = converter_series_dicom(subpasta, saida)
for uid, resultado in status.items():
print(f"UID: {uid} -> {resultado}")
Criação de Máscaras Vazias
Geração de arquivos de máscara binária preenchidos com zeros, preservando as dimensões e a matriz afim da imagem original:
from pathlib import Path
import nibabel as nib
import numpy as np
pasta_raiz = Path("./nii")
for arquivo in pasta_raiz.rglob("*.nii.gz"):
if "_mask" in arquivo.name:
continue
imagem = nib.load(str(arquivo))
dados = imagem.get_fdata()
matriz = np.zeros(dados.shape, dtype=np.uint8)
nome_mascara = arquivo.name.replace(".nii.gz", "_mask.nii.gz")
caminho_mascara = arquivo.parent / nome_mascara
nib.save(nib.Nifti1Image(matriz, imagem.affine), str(caminho_mascara))
Verificação de Arquivos Convertidos
from pathlib import Path
pasta_principal = Path("./nii")
contador = sum(1 for _ in pasta_principal.rglob("*.nii.gz"))
print(f"Total de arquivos NIfTI: {contador}")
Conversão de Arquivo DICOM Individual
Para converter uma única imagem DICOM com rotação e geração de máscara:
from pathlib import Path
import pydicom
import numpy as np
import nibabel as nib
entrada = Path("./single_dcm")
saida = Path("./single_nii")
saida.mkdir(parents=True, exist_ok=True)
for dcm in entrada.glob("*.dcm"):
dataset = pydicom.dcmread(str(dcm))
matriz = dataset.pixel_array
# Aplicar rotação anti-horária e espelhamento horizontal
transformada = np.fliplr(np.rot90(matriz))
base = dcm.stem
nib.save(
nib.Nifti1Image(transformada, affine=np.eye(4)),
str(saida / f"{base}.nii.gz")
)
mascara = np.zeros_like(transformada)
nib.save(
nib.Nifti1Image(mascara, affine=np.eye(4)),
str(saida / f"{base}_mask.nii.gz")
)
Conversão em Lote com SimpleITK
Abordagem alternativa utilizando SimpleITK para conversão direta de pastas DICOM:
from pathlib import Path
import SimpleITK as sitk
import pydicom
def converter_pasta_dicom(pasta_dicom, pasta_saida, nome_saida):
leitor = sitk.ImageSeriesReader()
arquivos = leitor.GetGDCMSeriesFileNames(str(pasta_dicom))
leitor.SetFileNames(arquivos)
imagem = leitor.Execute()
caminho = pasta_saida / f"{nome_saida}.nii.gz"
sitk.WriteImage(imagem, str(caminho))
def processar_lote(raiz, destino):
pastas_processadas = set()
for arquivo in Path(raiz).rglob("*.dcm"):
pasta = arquivo.parent
if pasta in pastas_processadas:
continue
pastas_processadas.add(pasta)
try:
ds = pydicom.dcmread(str(arquivo))
nome = getattr(ds, "SeriesInstanceUID", "sem_uid")
except Exception:
nome = "sem_uid"
subpasta = pasta.relative_to(raiz)
saida = Path(destino) / subpasta
saida.mkdir(parents=True, exist_ok=True)
try:
converter_pasta_dicom(pasta, saida, nome)
print(f"Convertido: {pasta}")
except Exception as e:
print(f"Erro ao converter {pasta}: {e}")
processar_lote("./dicom", "./nii")
Ajuste de Janela de Intensidade
Aplicação de windowing para visualização abdominal (centro=40, largura=400), normalizando os valores de pixel:
from pathlib import Path
import nibabel as nib
import numpy as np
def aplicar_janela(caminho_arquivo, centro, largura):
img = nib.load(str(caminho_arquivo))
dados = img.get_fdata()
limite_inferior = centro - largura / 2
limite_superior = centro + largura / 2
dados_recortados = np.clip(dados, limite_inferior, limite_superior)
nova_img = nib.Nifti1Image(dados_recortados, img.affine, img.header)
nib.save(nova_img, str(caminho_arquivo))
pasta_nii = Path("./nii")
for arquivo in pasta_nii.rglob("*.nii.gz"):
aplicar_janela(arquivo, centro=40, largura=400)
print(f"Janela aplicada: {arquivo.name}")