Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors

How to Convert a NIfTI File into a DICOM Series Using Python

Convert NIfTI files to DICOM series using Python: step-by-step guide with nibabel and pydicom. Convert NIfTI files to DICOM series using Python: step-by-step guide with nibabel and pydicom."

NIfTI to a DICOM series in Python is a reverse write: nibabel.load, get_fdata, stamp each slice with convertNsave, loop with nifti2dicom_1file. This page is that how-to. It is not DICOM → NIfTI, and it is not JPEG/PNG → DICOM.

If you meant DICOM → NIfTI → how to convert DICOM to NIfTI. If you meant JPG/PNG → DICOM → JPEG to DICOM conversion with Python. If you meant write extra header fields → store metadata in NIfTI and NRRD.

Why a template DICOM

A DICOM file is pixels plus a header. The cheap way to get a legal header is to start from a template .dcm and overwrite the fields that must match the new slice. The GitHub repo ships a sample template if you do not have one.

Pixels-only is not enough. Each instance needs its own SOPInstanceUID and InstanceNumber, a shared SeriesInstanceUID, and geometry (ImagePositionPatient, PixelSpacing, ImageOrientationPatient) taken from the NIfTI affine. Skip those and a viewer stacks the slices in the wrong place — or refuses the series.

Field Where it comes from Why
InstanceNumber slice index + 1 Order inside the series
SOPInstanceUID generate_uid() per file One UID per instance; never reuse the template’s
SeriesInstanceUID generate_uid() once per NIfTI Keeps the slices in one series
ImagePositionPatient affine @ [0, 0, k, 1] World origin of that slice
PixelSpacing / thickness column lengths of the affine mm between voxels
ImageOrientationPatient unit vectors of affine columns 0 and 1 Row / column direction cosines

Environment

python -m venv nifti2dicom
source nifti2dicom/bin/activate   # Windows: nifti2dicomScriptsactivate
pip install nibabel pydicom numpy tqdm

Sanity check: import nibabel, pydicom, numpy; from pydicom.uid import generate_uid. Docs: nibabel, pydicom. Full script: amine0110/nifti2dicom.

Load the volume

import nibabel

nifti_file = nibabel.load(nifti_dir)
nifti_array = nifti_file.get_fdata()
affine = nifti_file.affine

nibabel.load opens the file. get_fdata() is the 3D array. Slices in this script are nifti_array[:, :, k] — axis 2. If your volume is stored on another axis, transpose first; do not pass a 3D block to PixelData.

convertNsave: one slice, including geometry

import os

import numpy as np
import pydicom
from pydicom.uid import generate_uid


def voxel_sizes(affine):
    return np.sqrt((affine[:3, :3] ** 2).sum(axis=0))


def convertNsave(arr, file_dir, index, affine, series_uid, template_path="images/dcmimage.dcm"):
    dicom_file = pydicom.dcmread(template_path)
    arr = np.clip(np.asarray(arr), 0, None).astype(np.uint16)

    dicom_file.Rows = arr.shape[0]
    dicom_file.Columns = arr.shape[1]
    dicom_file.PhotometricInterpretation = "MONOCHROME2"
    dicom_file.SamplesPerPixel = 1
    dicom_file.BitsStored = 16
    dicom_file.BitsAllocated = 16
    dicom_file.HighBit = 15
    dicom_file.PixelRepresentation = 0
    dicom_file.PixelData = arr.tobytes()

    dicom_file.InstanceNumber = index + 1
    dicom_file.SOPInstanceUID = generate_uid()
    dicom_file.SeriesInstanceUID = series_uid
    if getattr(dicom_file, "file_meta", None) is not None:
        dicom_file.file_meta.MediaStorageSOPInstanceUID = dicom_file.SOPInstanceUID

    xyz = affine @ np.array([0.0, 0.0, float(index), 1.0])
    dicom_file.ImagePositionPatient = [float(xyz[0]), float(xyz[1]), float(xyz[2])]

    spacing = voxel_sizes(affine)
    dicom_file.PixelSpacing = [float(spacing[1]), float(spacing[0])]  # row, col
    dicom_file.SliceThickness = float(spacing[2])
    dicom_file.SpacingBetweenSlices = float(spacing[2])

    col_dir = affine[:3, 0] / (spacing[0] or 1.0)
    row_dir = affine[:3, 1] / (spacing[1] or 1.0)
    dicom_file.ImageOrientationPatient = [
        float(col_dir[0]), float(col_dir[1]), float(col_dir[2]),
        float(row_dir[0]), float(row_dir[1]), float(row_dir[2]),
    ]

    os.makedirs(file_dir, exist_ok=True)
    dicom_file.save_as(os.path.join(file_dir, f"slice{index}.dcm"))

dcmread loads the template. Intensities become uint16 after a clip of negatives — rescale or window first if your NIfTI is float in a range that is not already storage units. PixelData = arr.tobytes() is the slice.

InstanceNumber is 1-based so viewers sort the series. SOPInstanceUID is new on every file; copying the template’s UID makes every slice look like the same instance. SeriesInstanceUID is minted once in nifti2dicom_1file and passed in, so the folder is one series.

ImagePositionPatient is the world coordinate of voxel (0, 0, k): multiply the affine by [0, 0, k, 1]. PixelSpacing is row then column (NIfTI j, then i). Thickness is the length of affine column 2. Orientation is the unit vectors of columns 0 and 1. That is the geometry a PACS uses to stack the series. A template that still has the donor’s ImagePositionPatient will place every slice on the same plane.

nifti2dicom_1file: one volume

from tqdm import tqdm


def nifti2dicom_1file(nifti_dir, out_dir):
    nifti_file = nibabel.load(nifti_dir)
    nifti_array = nifti_file.get_fdata()
    affine = nifti_file.affine
    series_uid = generate_uid()
    number_slices = nifti_array.shape[2]
    os.makedirs(out_dir, exist_ok=True)

    for slice_ in tqdm(range(number_slices)):
        convertNsave(nifti_array[:, :, slice_], out_dir, slice_, affine, series_uid)

The loop is axis 2. convertNsave writes slice{k}.dcm. One SeriesInstanceUID for the whole file.

nifti2dicom_mfiles: a folder of volumes

def nifti2dicom_mfiles(nifti_dir, out_dir=""):
    files = os.listdir(nifti_dir)
    for file in files:
        in_path = os.path.join(nifti_dir, file)
        if not os.path.isfile(in_path):
            continue
        out_path = os.path.join(out_dir, file)
        os.makedirs(out_path, exist_ok=True)
        nifti2dicom_1file(in_path, out_path)

Each NIfTI becomes its own output folder. Skip directories so a stray subfolder does not get passed to nibabel.load. Catch FileNotFoundError / nibabel read errors per file if you do not want one bad volume to stop the batch.

Repo with the template DICOM and the original functions: amine0110/nifti2dicom.

PYCAD builds the imaging side of products that have to write these series for real. Case studies.

We build custom medical imaging platforms — advanced DICOM viewers, AI segmentation, and the clinical systems around them.

Get in Touch

Copyright © 2026 PYCAD. All Rights Reserved.