【Python】DICOMのソート(Sort)処理とNIfTI変換


1. 目的
2. 準備
2.1. pydicomのインストール
2.2. dcm2nii / dcm2niixのPATH設定
3. ソースコード
3.1. dcm2niiバージョン
3.2. dcm2niixバージョン
4. 使い方


1. 目的

  • 様々なSeriesDescriptionで撮像されたDICOMデータを、SeriesDescriptionごとにフォルダ分け
  • DICOMをNIfTI変換
  • Pythonで実行

2. 準備

2.1. pydicomのインストール

PythonでDICOMを扱うために、pydicomを用いる。

pip3 install pydicom

2.2. dcm2nii / dcm2niixのPATH設定

dcm2niiあるいはdcm2niixのPATHを通す(言い換えれば、dcm2niidcm2niixはここにありますよ!ってPCに教えてあげるイメージ。もともと、dcm2niixというコマンドはないけど、dcm2niixにPATHが通してあることで、PC側でああ、あのdcm2niixを実行しろということね?とdcm2niixを実行してくれる)。

PATHの通し方は、以下の記事を参考にするとよい。

3. ソースコード

実際に、DICOMをNIfTIに変換するソフトは、Chris Rorden教授が開発したdcm2niiを用いる。

近年、dcm2niiの開発は終了し、代わりにdcm2niixの開発に移行したので最新版を使用したければdcm2niixを用いるとよい。

3.1. dcm2niiバージョン

dcm2niiを用いたソースコードは、次の通り。

以下のファイルを「dcm_sconv.py」として保存。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys, os, time, re, shutil, argparse, subprocess
import pydicom
#from memory_profiler import profile

__version__ = '1.2 (2020/01/27)'

__desc__ = '''
sort dicom files, convert to nifti.
'''
__epilog__ = '''
examples:
  dcm_sconv DICOM_DIR
  
  dcm_sconv DICOM_DIR -o SORTED_DICOM_DIR -n NIFTI_DIR

  dcm_sconv DICOM_DIR -o SORTED_DICOM_DIR -n NIFTI_DIR -r InstanceCreatorUID

  # --rule
  # SeriesDescription  ... default
  # InstanceCreatorUID
  # SeriesTime
'''

def generate_dest_dir_name(dicom_dataset, rule):
    datetime = name = "%s-%s" % (dicom_dataset.SeriesDate, dicom_dataset.SeriesTime)
    if rule == 'SeriesDescription':
        rule_text = dicom_dataset.SeriesDescription
    elif rule == 'InstanceCreatorUID':
        rule_text = dicom_dataset.InstanceCreatorUID
    elif rule == 'SeriesTime':
        rule_text = dicom_dataset.SerieseTime
    return re.sub(r'[\\|/|:|?|"|<|>|\|]|\*', '', name + '_' + rule_text)

#@profile
def copy_dicom_files(src_dir, out_dir, rule, nifdir):
    dir_names = []

    # copy files
    for root, dirs, files in os.walk(src_dir):
        for file in files:
            try:
                src_file = os.path.join(root, file)
                dataset = pydicom.dcmread(src_file)
                dest_dir_name = generate_dest_dir_name(dataset, rule)
                dest_dir = os.path.join(out_dir, dest_dir_name)
                dir_names.append(dest_dir_name)
                os.makedirs(dest_dir, exist_ok=True)
                shutil.copy2(src_file, dest_dir)
                print("copy %s -> %s" % (src_file, dest_dir))
            except:
                pass

    # convert files
    if nifdir:
        for dir_name in sorted(set(dir_names)):
            dest_dir = os.path.join(nifdir, dir_name)
            src_dir = os.path.join(out_dir, dir_name)
            os.makedirs(dest_dir, exist_ok=True)
            command = 'dcm2nii -o "%s" "%s"' % (os.path.abspath(dest_dir), os.path.abspath(src_dir))
            print(command)
            subprocess.run(command, shell=True)

if __name__ == '__main__':
    start_time = time.time()
    parser = argparse.ArgumentParser(description=__desc__, epilog=__epilog__,
        formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument('dirs', metavar='DICOM_DIR', help='DIROM directory.', nargs=1)
    parser.add_argument('-r', '--rule', metavar='RULE', default='SeriesDescription', help='classification rule. deafult SeriesDescription')
    parser.add_argument('-o', '--out', metavar='OUT_DIR', default='.', help='output directory. default: CWD')
    parser.add_argument('-n', '--nifdir', metavar='NIFTI_DIR', help='convert nifti to NIFTI_DIR.')

    err = 0
    try:
        args = parser.parse_args()
        copy_dicom_files(args.dirs[0], args.out, args.rule, args.nifdir)
        print("execution time: %.2f second." % (time.time() - start_time))
    except Exception as e:
        print("%s: error: %s" % (__file__, str(e)))
        err = 1

    sys.exit(err)

3.2. dcm2niixバージョン

dcm2niixを用いたソースコードは、次の通り。

以下のファイルを「dcm_sconv.py」として保存。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys, os, time, re, shutil, argparse, subprocess
import pydicom

__version__ = '1.0 (2021/03/03)'

__desc__ = '''
sort dicom files, convert to nifti. dcm2niix version.
'''
__epilog__ = '''
examples:
  dcm_sconv DICOM_DIR
 
  dcm_sconv DICOM_DIR -o SORTED_DICOM_DIR -n NIFTI_DIR

  dcm_sconv DICOM_DIR -o SORTED_DICOM_DIR -n NIFTI_DIR -r InstanceCreatorUID

'''

def generate_dest_dir_name(dicom_dataset):
    name = "%d_%s-%s_%s" % (dicom_dataset.SeriesNumber, dicom_dataset.SeriesDate, dicom_dataset.SeriesTime, dicom_dataset.SeriesDescription)
    
    name = re.sub(r'/', '-', name)
    name = re.sub(r' ', '_', name)
    name = re.sub(r'\*', 'x', name)
    return re.sub(r'[\\|/|:|?|"|<|>|\|]', '', name)

def copy_dicom_files(src_dir, out_dir, nifdir):
    dir_names = []

    # copy files
    for root, dirs, files in os.walk(src_dir):
        for file in files:
            try:
                src_file = os.path.join(root, file)
                dest_dir_name = generate_dest_dir_name(pydicom.dcmread(src_file))
                print(src_file, dest_dir_name)
                dest_dir = os.path.join(out_dir, dest_dir_name)
                dir_names.append(dest_dir_name)
                os.makedirs(dest_dir, exist_ok=True)
                shutil.copy2(src_file, dest_dir)
                print("copy %s -> %s" % (src_file, dest_dir))
            except:
                pass

    # convert files
    if nifdir:
        for dir_name in sorted(set(dir_names)):
            dest_dir = os.path.join(nifdir, dir_name)
            src_dir = os.path.join(out_dir, dir_name)
            os.makedirs(dest_dir, exist_ok=True)
            command = 'dcm2niix -z y -o "%s" "%s"' % (os.path.abspath(dest_dir), os.path.abspath(src_dir))
            print(command)
            subprocess.run(command, shell=True)

if __name__ == '__main__':
    start_time = time.time()
    parser = argparse.ArgumentParser(description=__desc__, epilog=__epilog__,
        formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument('dirs', metavar='DICOM_DIR', help='DIROM directory.', nargs=1)
    parser.add_argument('-o', '--out', metavar='OUT_DIR', default='.', help='output directory. default: CWD')
    parser.add_argument('-n', '--nifdir', metavar='NIFTI_DIR', help='convert nifti to NIFTI_DIR.', default=None)

    err = 0
    try:
        args = parser.parse_args()
        print(args.dirs[0])
        print(args.out)
        copy_dicom_files(args.dirs[0], args.out, args.nifdir)
        print("execution time: %.2f second." % (time.time() - start_time))
    except Exception as e:
        print("%s: error: %s" % (__file__, str(e)))
        err = 1

    sys.exit(err)

4. 使い方

dcm_sconv.pyの基本的な使い方は、以下の通り。

デフォルトでは、「SeriesDescription」でDICOMをソートする。

  • -r : ソートために参照するタグ
  • -o : ソートされたDICOMの出力先
  • -n : NIfTIの出力先
usage: dcm_sconv.py [-h] [-r RULE] [-o OUT_DIR] [-n NIFTI_DIR] DICOM_DIR

sort dicom files, convert to nifti.

positional arguments:
  DICOM_DIR             DIROM directory.

optional arguments:
  -h, --help            show this help message and exit
  -r RULE, --rule RULE  classification rule. deafult SeriesDescription
  -o OUT_DIR, --out OUT_DIR
                        output directory. default: CWD
  -n NIFTI_DIR, --nifdir NIFTI_DIR
                        convert nifti to NIFTI_DIR.

examples:
  dcm_sconv DICOM_DIR
  
  dcm_sconv DICOM_DIR -o SORTED_DICOM_DIR -n NIFTI_DIR

  dcm_sconv DICOM_DIR -o SORTED_DICOM_DIR -n NIFTI_DIR -r InstanceCreatorUID

  # --rule
  # SeriesDescription  ... default
  # InstanceCreatorUID
  # SeriesTime

例えば、ソート前の「DICOM」フォルダがあり、ソートしたDICOMを「DICOM_sort」フォルダに、NIfTI変換したものを「NIfTI」フォルダに出力する場合、次のようなコマンドを実行する。

dcm_sconv.py DICOM -o DICOM_sort -n NIfTI

実行すると、次のようにソートしたDICOMフォルダ(DICOM_sort)とDICOMをNIfTIに変換したNIfTIフォルダ(NIfTI)が出力される。

.
├── DICOM
│   └── 20210215
│       └── 09430000
│           ├── 43170000
│           ├── 43170001
│           ├── 43170002
│           ├── 43170003
│           ├── 43170004
│           ├── 43170005
│           ├── 43170006
│           ├── 43170007
│           └── 43170008
├── DICOM_sort
│   ├── 20210215-084713.807000_AAHead_Scout
│   ├── 20210215-084714.204000_AAHead_Scout_MPR_sag
│   ├── 20210215-084714.279000_AAHead_Scout_MPR_cor
│   ├── 20210215-084714.304000_AAHead_Scout_MPR_tra
│   ├── 20210215-084917.121000_diffusion_ep2d_tra_TRACEW_DFC
│   ├── 20210215-084917.352000_diffusion_ep2d_tra_ADC_DFC
│   ├── 20210215-085009.550000_flair_tse_tra
│   ├── 20210215-085420.073000_Head_MRA_ToF3d_p3
│   ├── 20210215-085421.657000_Head_MRA_ToF3d_p3_MIP_SAG
│   ├── 20210215-085421.691000_Head_MRA_ToF3d_p3_MIP_COR
│   ├── 20210215-085421.729000_Head_MRA_ToF3d_p3_MIP_TRA
│   ├── 20210215-085434.632000_AC-PC_tra_setup
│   ├── 20210215-085542.913000_MIP Range
│   ├── 20210215-085549.528000_MIP Range[1]
│   ├── 20210215-090029.565000_T1_MPR
│   ├── 20210215-090041.792000_T1_MPR_MPR_Cor
│   ├── 20210215-090042.377000_T1_MPR_MPR_Tra
│   ├── 20210215-090601.946000_T2_SPC
│   ├── 20210215-090621.390000_T2_SPC_MPR_Cor
│   ├── 20210215-090622.471000_T2_SPC_MPR_Tra
│   ├── 20210215-090738.025000_DWI_AP_dir68_SBRef
│   ├── 20210215-090738.052000_DWI_AP_dir68
│   ├── 20210215-091149.566000_DWI_PA_dir69_SBRef
│   ├── 20210215-091149.596000_DWI_PA_dir69
│   ├── 20210215-091447.563000_SEField1_AP
│   ├── 20210215-091618.024000_BOLD_REST1_AP_SBRef
│   ├── 20210215-091618.051000_BOLD_REST1_AP
│   ├── 20210215-092008.622000_SEField1_PA
│   ├── 20210215-092141.948000_BOLD_REST1_PA_SBRef
│   ├── 20210215-092141.981000_BOLD_REST1_PA
│   ├── 20210215-092810.969000_ASL_ADNI
│   ├── 20210215-092811.016000_Perfusion_Weighted
│   ├── 20210215-093329.565000_QSM_3D
│   ├── 20210215-093329.770000_QSM_3D
│   ├── 20210215-093405.589000_PhoenixZIPReport
│   ├── 20210215-093425.884000_T2Star_Images
│   └── 20210215-093747.855000_tse3_t1_Conc1_p1_Neuromelanin
└── NIfTI
    ├── 20210215-084713.807000_AAHead_Scout
    ├── 20210215-084714.204000_AAHead_Scout_MPR_sag
    ├── 20210215-084714.279000_AAHead_Scout_MPR_cor
    ├── 20210215-084714.304000_AAHead_Scout_MPR_tra
    ├── 20210215-084917.121000_diffusion_ep2d_tra_TRACEW_DFC
    ├── 20210215-084917.352000_diffusion_ep2d_tra_ADC_DFC
    ├── 20210215-085009.550000_flair_tse_tra
    ├── 20210215-085420.073000_Head_MRA_ToF3d_p3
    ├── 20210215-085421.657000_Head_MRA_ToF3d_p3_MIP_SAG
    ├── 20210215-085421.691000_Head_MRA_ToF3d_p3_MIP_COR
    ├── 20210215-085421.729000_Head_MRA_ToF3d_p3_MIP_TRA
    ├── 20210215-085434.632000_AC-PC_tra_setup
    ├── 20210215-085542.913000_MIP Range
    ├── 20210215-085549.528000_MIP Range[1]
    ├── 20210215-090029.565000_T1_MPR
    ├── 20210215-090041.792000_T1_MPR_MPR_Cor
    ├── 20210215-090042.377000_T1_MPR_MPR_Tra
    ├── 20210215-090601.946000_T2_SPC
    ├── 20210215-090621.390000_T2_SPC_MPR_Cor
    ├── 20210215-090622.471000_T2_SPC_MPR_Tra
    ├── 20210215-090738.025000_DWI_AP_dir68_SBRef
    ├── 20210215-090738.052000_DWI_AP_dir68
    ├── 20210215-091149.566000_DWI_PA_dir69_SBRef
    ├── 20210215-091149.596000_DWI_PA_dir69
    ├── 20210215-091447.563000_SEField1_AP
    ├── 20210215-091618.024000_BOLD_REST1_AP_SBRef
    ├── 20210215-091618.051000_BOLD_REST1_AP
    ├── 20210215-092008.622000_SEField1_PA
    ├── 20210215-092141.948000_BOLD_REST1_PA_SBRef
    ├── 20210215-092141.981000_BOLD_REST1_PA
    ├── 20210215-092810.969000_ASL_ADNI
    ├── 20210215-092811.016000_Perfusion_Weighted
    ├── 20210215-093329.565000_QSM_3D
    ├── 20210215-093329.770000_QSM_3D
    ├── 20210215-093405.589000_PhoenixZIPReport
    ├── 20210215-093425.884000_T2Star_Images
    └── 20210215-093747.855000_tse3_t1_Conc1_p1_Neuromelanin

Print Friendly, PDF & Email

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください