Introduces a new scoring component for vertebral body (VBODY) rewards to improve optimization accuracy. The update includes: - Added `vbody_tensor` to `OptimizationContext` and scoring functions to reward screw placement within the vertebral body. - Enhanced `segment_spinous_process` with diagnostic capabilities to detect spinous process absence (e.g., post-laminectomy). - Improved `resample_img` to prevent physical boundary clipping and handle interpolation more robustly for CT and label data. - Implemented a metadata cache using TinyDB in the preprocessing pipeline to skip low-resolution or insufficient scans efficiently. - Added robust error handling for NFS-based file operations and directory creation. - Added new visualization tools for bone figures and level plotting. refactor(imaging): improve segmentation and resampling precision - Refactored `seg_bone` to support original resolution binary masks and Signed Maurer Distance Maps (SMD) for more accurate boundary handling. - Updated `resample_img` to use `ceil` for output size calculation to ensure full physical coverage. - Optimized `process_single_image` to utilize metadata for skipping processing of invalid or low-quality scans.
97 lines
No EOL
3.8 KiB
Python
97 lines
No EOL
3.8 KiB
Python
#!/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 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_sdf(SDF 平滑遮罩)
|
||
sdf_path = os.path.join(vol_dir, f'{lvl}_binary_sdf.nii.gz')
|
||
binary_path = sdf_path if os.path.exists(sdf_path) \
|
||
else os.path.join(vol_dir, f'{lvl}_binary.nii.gz')
|
||
cortical_path = os.path.join(vol_dir, f'{lvl}_cortical.nii.gz')
|
||
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() |