Conversão de Sequências DICOM T2 para Formato NIfTI e Ajuste de Janela de Intensidade

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}")

Tags: DICOM NIfTI pydicom SimpleITK dicom2nifti

Publicado em 7-11 06:32