CBT_project/xfr_plot_level.py
Xiao Furen d167c1f7c7 feat(imaging): implement coordinate transformation pipeline and directory restructuring
Introduce a robust coordinate transformation system to manage the relationship
between original CT space and rotated/standardized segmentation spaces.
This includes a new directory hierarchy to separate unrotated crops from
rotated outputs and utility functions for geometric mapping.

Key changes:
- Implement `imaging/transforms.py` to handle bounding box metadata,
  affine standardization, and coordinate mapping between spaces.
- Restructure dataset output: unrotated segmentation files (binary, SDF,
  ROI, etc.) are now stored in a `<vol>/crop/` subdirectory to distinguish
  them from `<vol>/rotated/` aligned versions.
- Add `level_file_path` utility to abstract file discovery across legacy
  (top-level) and new (crop-based) directory structures.
- Enhance `seg_bone` to capture and export bounding box metadata
  (`bbox2`, `nn_bbox`, `bbox_orig`) into `transform.json`.
- Implement `xfr_cbt_native.py` for mapping screw positions back to
  original CT space.
- Update preprocessing and visualization scripts to support the new
  directory layout and transformation metadata.
- Improve TinyDB metadata migration logic to prevent accidental corruption
  of existing database structures.
2026-09-09 13:39:47 +08:00

99 lines
No EOL
3.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/home/xfr/.conda/envs/cbt/bin/python
"""
為每個 (volume, level) 的骨頭遮罩({level}_binary_sdf.nii.gz缺則 _binary繪製
X-ray 四視角圖(不畫螺絲),存到 Output/{date}/{volume}/。
輸出檔名:{volume} {level}_CBT.png
1.3.6.1.4.1.9328.50.4.0005 L1_CBT.png
Usage:
python xfr_plot_level.py # 全部 volume 的 L1~L5
python xfr_plot_level.py 0005 # 該 volume 的 L1~L5
python xfr_plot_level.py 0005 L1 # 單一 (volume, level)
python xfr_plot_level.py --dir <standardized_dir> --output <output_base>
"""
import argparse
import os
import sys
from imaging.transforms import level_file_path
from visualization.res_bone_figure import render_bone_figure
standardized_dir = '/mnt/1248/open2/cyrou/CBT/Seg/Resample/standardized-xfr/'
output_base = '/mnt/1248/open2/cyrou/Output'
LEVELS = ('L1', 'L2', 'L3', 'L4', 'L5')
USAGE = 'Usage: python xfr_plot_level.py [volume_id] [level] [--dir D] [--output O]'
def parse_args(argv):
parser = argparse.ArgumentParser(description='繪製各 lumbar level 的骨頭 X-ray 圖(不畫螺絲)')
parser.add_argument('volume_id', nargs='?', default=None,
help='volume ID完整 UID 或末段,如 0005省略全部')
parser.add_argument('level', nargs='?', default=None,
help=f'level{" / ".join(LEVELS)});省略=全部')
parser.add_argument('--dir', default=standardized_dir,
help=f'standardized 資料夾(預設 {standardized_dir}')
parser.add_argument('--output', default=output_base,
help=f'輸出根目錄(預設 {output_base}')
return parser.parse_args(argv)
def main():
args = parse_args(sys.argv[1:])
if args.level is not None:
level_key = args.level.upper()
if level_key not in LEVELS:
print(f'Invalid level: {args.level} (choose from {"/".join(LEVELS)})')
sys.exit(1)
if args.level is not None and args.volume_id is None:
print(f'{USAGE}\nlevel requires volume_id')
sys.exit(1)
volumes = [d for d in sorted(os.listdir(args.dir))
if os.path.isdir(os.path.join(args.dir, d))]
if args.volume_id is not None:
key = args.volume_id.lower()
vols = [v for v in volumes
if v.lower() == key or v.rsplit('.', 1)[-1] == key]
if not vols:
print(f'Volume not found: {args.volume_id}')
sys.exit(1)
volumes = vols
levels = (args.level.upper(),) if args.level else LEVELS
tasks = [(vid, lvl) for vid in volumes for lvl in levels]
print(f'{len(volumes)} volume(s) x {len(levels)} level(s) = {len(tasks)} figure(s)',
flush=True)
ok, skip = [], []
for i, (vid, lvl) in enumerate(tasks, 1):
vol_dir = os.path.join(args.dir, vid)
# _binary.nii.gz 現為原解析度0.5mm 用 _binary_sdfSDF 平滑遮罩)
# 未旋轉檔:新世代在 crop/ 子資料夾、舊世代在頂層
sdf_path = level_file_path(vol_dir, lvl, 'binary_sdf')
binary_path = sdf_path if os.path.exists(sdf_path) \
else level_file_path(vol_dir, lvl, 'binary')
cortical_path = level_file_path(vol_dir, lvl, 'cortical')
path = render_bone_figure(vid, lvl, binary_path, cortical_path,
base_folder=args.output)
if path is None:
skip.append((vid, lvl))
print(f'[{i}/{len(tasks)}] {vid} {lvl}: skipped', flush=True)
else:
ok.append((vid, lvl))
print(f'[{i}/{len(tasks)}] {vid} {lvl}: saved {path}', flush=True)
print('=' * 60)
print(f'Done. saved={len(ok)} skipped={len(skip)}')
if skip:
print('Skipped (missing/empty mask):')
for vid, lvl in skip:
print(f' {vid} {lvl}')
if __name__ == '__main__':
main()