CT/MRI画像処理用の3Dボクセル超解像のシステムその5

#!/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