Python tool that:
- Separates and organizes mixed DICOM studies (from a CD or DVD) by Exam (Series), putting the Patient name and the Protocol in the name of the output folder.
- Copies and sequentially renames the images (0001.ext, 0002.ext, ...) preserving the extension, which is what ImageJ expects.
- Exports metadata CSV files (global, per patient, and per patient and protocol) and generates basic QA reports.
Important: the file type is never changed. Only the base name of the organized copies is rewritten, and the originals are never modified.
- Python 3.9+
- Packages:
pydicom,pandas,pyyaml(installed bypip install -e .)
Run these steps once to install the organize-dicom command inside a virtual environment. Run them from the root folder of the repository, the one holding pyproject.toml.
Linux (bash):
git clone https://github.com/jofavalle/dicom-directory-separator-and-metadata-extractor.git
cd dicom-directory-separator-and-metadata-extractor
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip setuptools wheel
python -m pip install -e .
# Check the installation
organize-dicom --helpWindows (PowerShell):
git clone https://github.com/jofavalle/dicom-directory-separator-and-metadata-extractor.git
cd dicom-directory-separator-and-metadata-extractor
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -U pip setuptools wheel
python -m pip install -e .
# Check the installation
organize-dicom --helpNotes:
python -m pip install -e .has to run in the repository folder, not inside thedicom/data folder.- Once it is installed you can call
organize-dicomfrom any folder; just point--inputand--outputat the paths you want.
From the directory holding dicomdir and/or the dicom/ folder:
# A) Run the script directly
python3 script/organize-dicom.py \
--input . \
--output organized_by_test \
--config config.yaml
# B) Install the command and use it
pip install -e .
organize-dicom --input . --output organized_by_test --config config.yamlIn PowerShell, from the directory holding dicomdir and/or the dicom/ folder:
# A) Install the command (once per environment)
python -m pip install -e .
# B) Run the command
organize-dicom --input . --output organized_by_test --config config.yaml
# C) Or run the script directly
python script/organize-dicom.py --input . --output organized_by_test --config config.yamlNotes for Windows:
- Keep
--link-mode copy(the default) unless you needhardlink(same NTFS volume) orsymlink(which needs Developer Mode or an elevated console). - If you hit long path errors, enable long paths in Windows or use shorter output paths.
- The filesystem is case insensitive; use
--on-collision renameif you suspect collisions.
# Export metadata only, without copying images
organize-dicom --input . --output out --no-organize --export-metadata --qa
# Export every tag (wide CSV) without QA
organize-dicom --input . --output out_all --all-tags --no-organize --no-qa- The repository does not need to sit in the same folder as the images.
- There are two ways to use it:
- Installed as a command (
pip install -e .): callorganize-dicomfrom any folder and point--inputat the directory holding the DICOM files. - Running the script directly (without installing): run
python script/organize-dicom.pyfrom inside the repository, but--inputcan be any path (there is no need to move the repository next to the data).
- Installed as a command (
--inputhas to point at the root holdingdicomdir, or at a folder with adicom/subfolder.--outputis created if it does not exist. An absolute path is used as given, but a relative path is resolved against--input, not against the directory you are standing in. So--input /media/CD --output organizedwrites to/media/CD/organized, that is, inside the data. Pass an absolute path whenever the results have to land somewhere else.
Examples:
# Running from your home directory against a CD mounted at /media/CD
organize-dicom --input /media/CD --output ~/out --no-organize --export-metadata --qa
# From inside the repository, processing a folder outside it. The output path
# is absolute on purpose: ./organized would land in /path/to/studies/organized.
python3 script/organize-dicom.py --input /path/to/studies --output ~/organizedExamples on Windows (PowerShell):
# CD or DVD mounted as E:\
organize-dicom --input E:\ --output "$env:USERPROFILE\Documents\out" --no-organize --export-metadata --qa
# Folder with spaces (use quotes)
organize-dicom --input "E:\DICOM studies" --output "C:\Temp\organized"
# Run the script directly
python script/organize-dicom.py --input "D:\data\dicom" --output "C:\out\organized"Tips:
- On Windows, use double quotes "..." when the path contains spaces.
- Both
--inputand--outputaccept absolute and relative paths, keeping in mind that a relative--outputhangs off--input.
--input: root holdingdicomdir, or a folder with adicom/subfolder (default:.)--dicom-folder: subfolder to walk when there is no DICOMDIR (default:dicom)--dry-run: write nothing, only print the summary of what the run would do--organize/--no-organize: copy and rename per exam (default: yes)--export-metadata/--no-export-metadata: global and per patient CSV files (default: yes)--qa/--no-qa: QA reports (default: yes)--config: YAML used to normalize protocol names (see the example inconfig.example.yaml)--all-tags: export every tag at instance level (much wider CSV)--workers: threads used to read metadata (default: 8)
- One output folder per Exam (Series):
Exam_<PatientName>__<ProtocolNorm>__Series_<SeriesNumber>_<SeriesInstanceUID>/ - Inside it: the sequential copies
0001.ext,0002.ext, ... keeping the original extension, plus amanifest.csvholding the mapping. - Advanced options:
--link-mode {copy,hardlink,symlink}(default:copy)--on-collision {skip,overwrite,rename}(default:skip)--pad-width N(fixed padding; automatic when 0)--copy-workers N(copy concurrency)
Breaking change: these folders used to be named Prueba_<...>. Any Prueba_* glob in a script or an ImageJ macro stops matching, and re-running over an --output that still holds an old tree builds a parallel tree beside it instead of taking the --on-collision skip branch, because the names no longer line up. Rename the old folders if you need both runs to meet:
for d in Prueba_*; do mv "$d" "Exam_${d#Prueba_}"; done--modality CT(repeatable)--date-range YYYYMMDD:YYYYMMDD--patient-id <ID>(repeatable)--protocol-include "regex"/--protocol-exclude "regex"
global_index_instances.csv: one row per file with the main metadata (or every tag with--all-tags).global_index_series.csv: summary per series (counts, InstanceNumber ranges).csv/patients/...: CSV files per patient (instances and series).csv/patient_protocols/...: CSV files per patient and protocol.csv/patient_protocols/summary_ct_params_by_patient_protocol.csv: one row per patient and protocol with the CT acquisition parameters, in the columnsPatientID,ProtocolNorm,n_series,n_instances,kernel,kvp,exposure_time_ms,xray_tube_current_mA,exposure_mAsandpitch. Every distinct value found in the group is listed, separated by|, so a cell holding more than one value means the parameter changed within that protocol.qa/*.csv: hierarchy, InstanceNumber gaps, duplicate SOPs and critical tags (whichever apply). A check that found nothing writes no file.
Beyond the identifying tags, global_index_instances.csv carries the CT exposure and reconstruction parameters when the files provide them: ConvolutionKernel, KVP, ExposureTime, ExposureTimeInms, XRayTubeCurrent, Exposure, SpiralPitchFactor and PitchFactor. Those are the tags the CT summary aggregates: the exposure time is taken from ExposureTimeInms and falls back to ExposureTime, and the pitch from SpiralPitchFactor and falls back to PitchFactor.
Example in config.example.yaml:
protocol_map:
"Senos Paranasales": "CT_Senos_Paranasales"
"Cerebro mas de 10 anios": "CT_Cerebro_>10a"
protocol_regex:
- pattern: "^cerebro.*anios$"
replace: "CT_Cerebro"Save it as config.yaml in the root, or pass it with --config path.
The keys are the protocol names as the scanner writes them, so they are matched byte for byte and are left in the original language on purpose.
There is a smoke test, and it needs no real study: it builds a synthetic DICOM tree in a temporary directory, runs the tool over it and checks the result, so it can run on a fresh clone.
python3 -m venv .venv && .venv/bin/pip install -e .
./tests/smoke.shMIT. See LICENSE.
- Email: av18012@ues.edu.sv