#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DICOM to NumPy 変換ユーティリティ
CT/MRIのDICOMシリーズをNumPy配列に変換
"""
import os
import numpy as np
import pickle
from pathlib import Path
try:
import pydicom
PYDICOM_AVAILABLE = True
except ImportError:
PYDICOM_AVAILABLE = False
print("警告: pydicomがインストールされていません")
print("pip install pydicom でインストールしてください")
def load_dicom_series(dicom_dir, output_file=None):
"""
DICOMシリーズを読み込んでNumPy配列に変換
Parameters:
-----------
dicom_dir : str
DICOMファイルが格納されているディレクトリ
output_file : str, optional
出力ファイル名(.npy または .pkl)
Returns:
--------
volume : numpy.ndarray
3Dボリュームデータ
voxel_size : list
ボクセルサイズ [x, y, z] (mm)
metadata : dict
メタデータ情報
"""
if not PYDICOM_AVAILABLE:
raise ImportError("pydicomがインストールされていません")
print(f"DICOMディレクトリを読み込み中: {dicom_dir}")
# DICOMファイルのリストを取得
dicom_files =
for root, dirs, files in os.walk(dicom_dir):
for file in files:
if file.endswith('.dcm') or file.endswith('.DCM'):
dicom_files.append(os.path.join(root, file))
if not dicom_files:
raise FileNotFoundError(f"DICOMファイルが見つかりません: {dicom_dir}")
print(f"DICOMファイル数: {len(dicom_files)}")
# 最初のファイルを読み込んでメタデータを取得
first_ds = pydicom.dcmread(dicom_files[0])
# ソート用の情報を収集
slices =
for dcm_file in dicom_files:
try:
ds = pydicom.dcmread(dcm_file)
# スライス位置を取得
if hasattr(ds, 'ImagePositionPatient'):
slice_location = float(ds.ImagePositionPatient[2])
elif hasattr(ds, 'SliceLocation'):
slice_location = float(ds.SliceLocation)
else:
slice_location = 0
slices.append({
'file': dcm_file,
'location': slice_location,
'dataset': ds
})
except Exception as e:
print(f"警告: {dcm_file} の読み込みをスキップ: {e}")
# スライス位置でソート
slices.sort(key=lambda x: x['location'])
print(f"有効なスライス数: {len(slices)}")
# 画像サイズを取得
img_shape = slices[0]['dataset'].pixel_array.shape
# ボリュームの初期化
volume = np.zeros*1, dtype=np.float32)
# 各スライスを読み込み
print("スライスを読み込み中...")
for i, slice_info in enumerate(slices):
pixel_array = slice_info['dataset'].pixel_array.astype(np.float32)
# HU値への変換(CTの場合)
if hasattr(slice_info['dataset'], 'RescaleIntercept') and \
hasattr(slice_info['dataset'], 'RescaleSlope'):
intercept = float(slice_info['dataset'].RescaleIntercept)
slope = float(slice_info['dataset'].RescaleSlope)
pixel_array = pixel_array * slope + intercept
volume[:, :, i] = pixel_array
if (i + 1) % 10 == 0:
print(f" {i + 1}/{len(slices)} スライス処理完了")
# ボクセルサイズの取得
try:
pixel_spacing = first_ds.PixelSpacing
voxel_x = float(pixel_spacing[0])
voxel_y = float(pixel_spacing[1])
except:
voxel_x = 1.0
voxel_y = 1.0
print("警告: PixelSpacingが見つかりません。デフォルト値(1.0mm)を使用")
try:
voxel_z = float(first_ds.SliceThickness)
except:
# スライス間隔から計算
if len(slices) > 1:
voxel_z = abs(slices[1]['location'] - slices[0]['location'])
else:
voxel_z = 1.0
print(f"警告: SliceThicknessが見つかりません。計算値({voxel_z:.2f}mm)を使用")
voxel_size = [voxel_x, voxel_y, voxel_z]
# メタデータの収集
metadata = {
'PatientName': str(first_ds.PatientName) if hasattr(first_ds, 'PatientName') else 'Unknown',
'PatientID': str(first_ds.PatientID) if hasattr(first_ds, 'PatientID') else 'Unknown',
'StudyDate': str(first_ds.StudyDate) if hasattr(first_ds, 'StudyDate') else 'Unknown',
'Modality': str(first_ds.Modality) if hasattr(first_ds, 'Modality') else 'Unknown',
'StudyDescription': str(first_ds.StudyDescription) if hasattr(first_ds, 'StudyDescription') else 'Unknown',
'SeriesDescription': str(first_ds.SeriesDescription) if hasattr(first_ds, 'SeriesDescription') else 'Unknown',
'ImageShape': volume.shape,
'VoxelSize': voxel_size,
'NumberOfSlices': len(slices),
}
# 結果の表示
print("\n=== 変換完了 ===")
print(f"ボリューム形状: {volume.shape}")
print(f"ボクセルサイズ: {voxel_x:.2f} × {voxel_y:.2f} × {voxel_z:.2f} mm")
print(f"データ範囲: {volume.min():.1f} - {volume.max():.1f}")
print(f"モダリティ: {metadata['Modality']}")
print(f"シリーズ説明: {metadata['SeriesDescription']}")
# 保存
if output_file:
if output_file.endswith('.npy'):
np.save(output_file, volume)
print(f"\nNumPy配列として保存: {output_file}")
elif output_file.endswith('.pkl'):
data = {
'volume': volume,
'voxel_size': voxel_size,
'metadata': metadata
}
with open(output_file, 'wb') as f:
pickle.dump(data, f)
print(f"\nPickle形式で保存: {output_file}")
else:
print("警告: サポートされていない拡張子です(.npy または .pkl を使用)")
return volume, voxel_size, metadata
def convert_nifti_to_numpy(nifti_file, output_file=None):
"""
NIfTIファイルをNumPy配列に変換
Parameters:
-----------
nifti_file : str
NIfTIファイルのパス (.nii または .nii.gz)
output_file : str, optional
出力ファイル名
"""
try:
import nibabel as nib
except ImportError:
raise ImportError("nibabelがインストールされていません: pip install nibabel")
print(f"NIfTIファイルを読み込み中: {nifti_file}")
# NIfTIファイルを読み込み
nii = nib.load(nifti_file)
volume = nii.get_fdata().astype(np.float32)
# ボクセルサイズを取得
voxel_size = nii.header.get_zooms()[:3]
voxel_size = [float(v) for v in voxel_size]
metadata = {
'ImageShape': volume.shape,
'VoxelSize': voxel_size,
'Affine': nii.affine.tolist()
}
print("\n=== 変換完了 ===")
print(f"ボリューム形状: {volume.shape}")
print(f"ボクセルサイズ: {voxel_size[0]:.2f} × {voxel_size[1]:.2f} × {voxel_size[2]:.2f} mm")
print(f"データ範囲: {volume.min():.1f} - {volume.max():.1f}")
if output_file:
if output_file.endswith('.npy'):
np.save(output_file, volume)
print(f"\nNumPy配列として保存: {output_file}")
elif output_file.endswith('.pkl'):
data = {
'volume': volume,
'voxel_size': voxel_size,
'metadata': metadata
}
with open(output_file, 'wb') as f:
pickle.dump(data, f)
print(f"\nPickle形式で保存: {output_file}")
return volume, voxel_size, metadata
def main():
"""メイン関数"""
import argparse
parser = argparse.ArgumentParser(description='医療画像フォーマット変換ツール')
parser.add_argument('input', help='入力ファイルまたはディレクトリ')
parser.add_argument('-o', '--output', help='出力ファイル名(.npy または .pkl)')
parser.add_argument('-t', '--type', choices=['dicom', 'nifti'],
help='入力タイプ(自動判定されない場合に指定)')
args = parser.parse_args()
# 入力タイプの判定
if args.type:
input_type = args.type
elif os.path.isdir(args.input):
input_type = 'dicom'
elif args.input.endswith('.nii') or args.input.endswith('.nii.gz'):
input_type = 'nifti'
else:
print("エラー: 入力タイプを判定できません。--type で指定してください")
return
# 出力ファイル名の生成
if not args.output:
if input_type == 'dicom':
args.output = 'dicom_volume.pkl'
else:
base = os.path.splitext(os.path.basename(args.input))[0]
args.output = f'{base}.pkl'
# 変換実行
try:
if input_type == 'dicom':
volume, voxel_size, metadata = load_dicom_series(args.input, args.output)
else:
volume, voxel_size, metadata = convert_nifti_to_numpy(args.input, args.output)
print("\n変換が正常に完了しました!")
except Exception as e:
print(f"\nエラー: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
# コマンドライン引数がある場合
import sys
if len(sys.argv) > 1:
main()
else:
# 対話モード
print("=== DICOM/NIfTI to NumPy 変換ツール ===\n")
print("1. DICOMシリーズを変換")
print("2. NIfTIファイルを変換")
print("3. 終了")
choice = input("\n選択してください (1-3): ").strip()
if choice == '1':
if not PYDICOM_AVAILABLE:
print("\nエラー: pydicomがインストールされていません")
print("pip install pydicom を実行してください")
sys.exit(1)
dicom_dir = input("DICOMディレクトリのパス: ").strip()
output_file = input("出力ファイル名 (.pkl推奨): ").strip()
if not output_file:
output_file = "dicom_volume.pkl"
try:
load_dicom_series(dicom_dir, output_file)
print("\n変換完了!")
except Exception as e:
print(f"\nエラー: {e}")
elif choice == '2':
nifti_file = input("NIfTIファイルのパス: ").strip()
output_file = input("出力ファイル名 (.pkl推奨): ").strip()
if not output_file:
base = os.path.splitext(os.path.basename(nifti_file))[0]
output_file = f"{base}.pkl"
try:
convert_nifti_to_numpy(nifti_file, output_file)
print("\n変換完了!")
except Exception as e:
print(f"\nエラー: {e}")
elif choice == '3':
print("終了します")
else:
print("無効な選択です")
*1:img_shape[0], img_shape[1], len(slices