#!/home/xfr/.conda/envs/cbt/bin/python """一次性的 layout 遷移:把已產出 volume 的「未旋轉 檔」移入 crop/ 子資料夾。 新 layout(2026-09-08 起 xfr_preprocess 直接產出,無須再跑本腳本): /crop/_binary|_smd|_smd_resampled|_binary_sdf|_binary_nn|_roi.nii.gz /crop/_planes.png (原 /lumbar/) /rotated/_*.nii.gz (不變) /transform.json、_labels.txt(頂層,不變) 本腳本供改動前已產出、尚未重新處理的 volume 用(例:standardized-xfr-3): 1) / 頂層的 _*.nii.gz(L = LABEL_MAP level 名)移到 /crop/ 2) /lumbar/*.png 移到 /crop/,lumbar/ 清空後刪除 Idempotent:已遷就的 volume 重跑無作用;crop/ 已有同名檔時不覆蓋、跳過該檔 (頂層檔保留,人工核對)。rotated/ 與其他頂層檔不動。 讀端(xfr_orig_labels / xfr_inverse_transform / xfr_cbt_native 等)以 imaging.transforms.level_file_path 同時認 crop/ 與頂層,舊世代 (standardized-xfr-2 等)不遷移也能讀;如需一致化可對該 root 跑本腳本。 Usage: python xfr_migrate_crop.py [ROOT] [--dry-run] """ import argparse import os import shutil import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from config.constant import LABEL_MAP DEFAULT_ROOT = '/mnt/1248/open2/cyrou/CBT/Seg/Resample/standardized-xfr-3' LEVEL_SET = set(LABEL_MAP.values()) def migrate_volume(vol_dir, dry_run=False): """回傳 (moved, kept) 兩個清單(人讀的說明文字)。""" crop_dir = os.path.join(vol_dir, 'crop') moved, kept = [], [] for f in sorted(os.listdir(vol_dir)): if not f.endswith('.nii.gz'): continue if f.split('_', 1)[0] not in LEVEL_SET: kept.append(f) continue dst = os.path.join(crop_dir, f) if os.path.exists(dst): kept.append(f'{f}(crop/ 已有同名檔,未動)') continue src = os.path.join(vol_dir, f) if dry_run: moved.append(f'{f} (dry-run)') else: os.makedirs(crop_dir, exist_ok=True) shutil.move(src, dst) moved.append(f) lumbar_dir = os.path.join(vol_dir, 'lumbar') if os.path.isdir(lumbar_dir): for f in sorted(os.listdir(lumbar_dir)): dst = os.path.join(crop_dir, f) if os.path.exists(dst): kept.append(f'lumbar/{f}(crop/ 已有同名檔,未動)') continue if dry_run: moved.append(f'lumbar/{f} (dry-run)') else: os.makedirs(crop_dir, exist_ok=True) shutil.move(os.path.join(lumbar_dir, f), dst) moved.append(f'lumbar/{f}') if not dry_run: try: os.rmdir(lumbar_dir) except OSError: kept.append('lumbar/(仍有其他檔,未刪)') return moved, kept def main(): parser = argparse.ArgumentParser( description='Migrate per-level pre-rotated files from / top level ' 'to /crop/ (plus lumbar/*.png -> crop/).') parser.add_argument('root', nargs='?', default=DEFAULT_ROOT, help=f'standardized generation dir (default: {DEFAULT_ROOT})') parser.add_argument('--dry-run', action='store_true', help='Only report what would be moved.') args = parser.parse_args() if not os.path.isdir(args.root): sys.exit(f'not a directory: {args.root}') vols = sorted(d for d in os.listdir(args.root) if os.path.isdir(os.path.join(args.root, d))) tag = '[dry-run] ' if args.dry_run else '' n_done = n_empty = 0 for i, vol in enumerate(vols, 1): vol_dir = os.path.join(args.root, vol) moved, kept = migrate_volume(vol_dir, dry_run=args.dry_run) if not moved: n_empty += 1 print(f'{tag}[{i}/{len(vols)}] - {vol} (no file to move)') continue n_done += 1 print(f'{tag}[{i}/{len(vols)}] ok {vol} moved {len(moved)} file(s)') for m in moved: print(f' -> {m}') for k in kept: print(f' !! kept: {k}') print(f'\n{tag}done: {n_done} volume(s) migrated, {n_empty} untouched ' f'out of {len(vols)}') if __name__ == '__main__': main()