CBT_project/xfr_reprocess_ap.py

130 lines
5.1 KiB
Python
Raw Permalink Normal View History

#!/home/xfr/.conda/envs/cbt/bin/python
"""掃 standardize 輸出目錄中「前後AP方向翻轉」的個案prone 伏位掃描,
前側落在 y 小側colon 0003供重跑修正用
判定對每個 volume L1~L6 輸出遮罩 _binary_sdf _binary_nn
_binary個別跑 orientation.anterior_y_side中線帶椎管兩側質量比較
見該函式 docstring level 票決
flip : y_min > y_max 前後翻轉需重跑
ok : y_max > y_min 方向正常
mixed : 平手需人工確認
unknown : 全部無法判定無明顯前後質量差如鏡稱面異常案例
Usage:
python xfr_reprocess_ap.py <output_dir> # 只回報
python xfr_reprocess_ap.py <output_dir> --fix # 另刪 flip volume 的輸出
# 資料夾 + progress.json
# 條目,之後重跑
# xfr_preprocess.py 即可
"""
import argparse
import json
import os
import shutil
import sys
import time
import SimpleITK as sitk
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from imaging.orientation import anterior_y_side
from imaging.transforms import level_file_path
LEVELS = ('L1', 'L2', 'L3', 'L4', 'L5', 'L6')
MASK_SOURCES = ('binary_sdf', 'binary_nn', 'binary')
def volume_decision(vol_dir):
"""回傳 (decision, per_level dict)。decision ∈ flip/ok/mixed/unknown/nomask。"""
per = {}
for lvl in LEVELS:
# 未旋轉遮罩:新世代在 crop/ 子資料夾、舊世代在頂層
for src in MASK_SOURCES:
p = level_file_path(vol_dir, lvl, src)
if os.path.exists(p):
m = sitk.GetArrayFromImage(sitk.ReadImage(p, sitk.sitkUInt8))
per[lvl] = anterior_y_side(m)
break
if not per:
return 'nomask', per
votes = [v for v in per.values() if v is not None]
if not votes:
return 'unknown', per
n_min = votes.count('y_min')
n_max = votes.count('y_max')
if n_min > n_max:
return 'flip', per
if n_max > n_min:
return 'ok', per
return 'mixed', per
def main():
parser = argparse.ArgumentParser(
description='Find AP-flipped (prone) volumes in a standardized output dir.')
parser.add_argument('output_dir')
parser.add_argument('--fix', action='store_true',
help='Also delete flipped volumes\' output dirs and '
'their progress.json entries')
args = parser.parse_args()
outdir = args.output_dir
if not os.path.isdir(outdir):
print(f'not a directory: {outdir}')
return
vols = sorted(d for d in os.listdir(outdir)
if os.path.isdir(os.path.join(outdir, d)))
flip, mixed, unknown, ok, nomask = [], [], [], [], []
t0 = time.time()
for i, vol in enumerate(vols, 1):
dec, per = volume_decision(os.path.join(outdir, vol))
tag = {'flip': 'FLIP', 'ok': 'ok ', 'mixed': 'MIXED',
'unknown': '?!?', 'nomask': '- '}[dec]
detail = ' '.join(f'{k}={v}' for k, v in per.items())
print(f'[{i}/{len(vols)}] {tag} {vol} {detail}')
{'flip': flip, 'mixed': mixed, 'unknown': unknown,
'ok': ok, 'nomask': nomask}[dec].append(vol)
if (i % 25) == 0:
print(f' ... {i}/{len(vols)} ({(time.time()-t0)/60:.1f} min)', flush=True)
print(f'\n=== Summary: {len(vols)} volumes ===')
print(f' ok (normal) : {len(ok)}')
print(f' FLIP (AP-flipped) : {len(flip)}')
for v in flip:
print(f' - {v}')
print(f' mixed (need check) : {len(mixed)}')
for v in mixed:
print(f' - {v}')
print(f' unknown (no vote) : {len(unknown)}')
for v in unknown:
print(f' - {v}')
print(f' no level mask : {len(nomask)}')
if args.fix and flip:
# 1) progress.json刪掉 flip 的條目(備份)
prog_path = os.path.join(outdir, 'progress.json')
if os.path.exists(prog_path):
with open(prog_path) as f:
prog = json.load(f)
removed = [v for v in flip if prog.pop(v, None) is not None]
bak = f'{prog_path}.apfix-{time.strftime("%Y%m%d_%H%M%S")}'
shutil.copyfile(prog_path, bak)
with open(prog_path, 'w') as f:
json.dump(prog, f, indent=2)
print(f'\nprogress.json: removed {len(removed)} entr(y/ies) '
f'[{", ".join(v.split(".")[-1] or v for v in removed)}]; '
f'backup {bak}')
# 2) 刪輸出資料夾
for v in flip:
shutil.rmtree(os.path.join(outdir, v))
print(f'removed {os.path.join(outdir, v)}')
print('\nNext: rerun `python xfr_preprocess.py` — only the removed '
'volumes will be reprocessed (with the AP flip applied).')
elif not args.fix and flip:
print('\nRerun with --fix to delete the flipped outputs and progress '
'entries, then run `python xfr_preprocess.py`.')
if __name__ == '__main__':
main()