CBT_project/visualization/res_plot_3d.py
Xiao Furen 4f5be3d3e9 refactor(core): remove global state and implement OptimizationContext
Refactor the optimization pipeline to eliminate module-level global variables,
improving thread safety and modularity. Introduced `OptimizationContext`
to explicitly manage shared state during cylinder evaluation.

Key changes:
- core: Replace global variables with `OptimizationContext` dataclass in
  `objective.py`.
- core: Implement `refine_lateral_longer` in `optimizer.py` for deterministic
  local refinement of screw placement.
- core: Update scoring logic in `scoring.py` to use higher penalties for
  out-of-bone voxels.
- imaging: Add advanced symmetry detection including `best_symmetry_plane`
  and `best_symmetry_axis_angle` in `orientation.py`.
- visualization: Enhance 3D plotting in `res_plot_3d.py` with volume
  absorption rendering (Beer-Lambert law) for an X-ray-like appearance.
- xfr_debug: Implement a custom `_Tee` logger to support multi-process
  logging with volume and level-specific tags.
- chore: Update `.gitignore` to include local logs and kilo directories.
2026-08-30 00:54:55 +08:00

534 lines
21 KiB
Python
Raw 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.

import torch
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import to_rgba
from matplotlib.lines import Line2D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import os
from datetime import datetime
import csv
from core.cylinder import generate_cylinder_n_torch, generate_cylinder_o_torch, snap_to_discrete_values
from core.intersection import center_line_intersections_torch
from core.scoring import cl_score_torch, compute_overlap_ratio_from_cylinder_mask, cl_score_torch_xfr
from imaging.orientation import (azimuth_rotation, analyze_vertebral_tilt_contour,
best_symmetry_plane, best_upper_endplate_plane,
segment_spinous_process)
from utils.helpers import save_with_unique_name
# Volume absorption 渲染Beer-Lambert每 voxel 不透明度 = 1 - exp(-mu * voxel_width)
# 骨頭核心厚度達 70-90 voxel沿視線堆疊會使任何 per-voxel alpha 累積成不透明。
# 因此以「抽稀 (SUBSAMPLE) 降低堆疊數量」+「低 mu 控制每點吸收」兩項共同調出淡薄 X-ray 陰影,
# 同時保留皮質 / 鬆質的吸收入射差異mu 比值)。
BONE_MU_CORTICAL = 0.02 # 1/mm → 每 voxel = 1-exp(-0.02*0.5) ~ 0.010
BONE_MU_TRABECULAR = 0.005 # 1/mm → 每 voxel = 1-exp(-0.005*0.5) ~ 0.0025
BONE_MARKER_SIZE = 3.0 # 骨骼散點點面積 (pt^2);略大以補償抽稀後的顆粒感
BONE_SUBSAMPLE = 1 # 每 10 個骨 voxel 畫 1 個降低堆疊不透明度1=全畫)
def set_axes_equal_3d(ax):
"""
Make axes of 3D plot have equal scale so that spheres appear as spheres,
cubes as cubes, etc.
"""
x_limits = ax.get_xlim3d()
y_limits = ax.get_ylim3d()
z_limits = ax.get_zlim3d()
x_range = abs(x_limits[1] - x_limits[0])
x_middle = np.mean(x_limits)
y_range = abs(y_limits[1] - y_limits[0])
y_middle = np.mean(y_limits)
z_range = abs(z_limits[1] - z_limits[0])
z_middle = np.mean(z_limits)
plot_radius = 0.5*max([x_range, y_range, z_range])
ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius])
ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius])
ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius])
try:
ax.set_box_aspect([1, 1, 1])
except AttributeError:
pass
def res_plt_2_torch(
spine_tensor: torch.Tensor,
cortical_tensor: torch.Tensor,
image_shape: tuple[int, int, int],
image2_path: str,
base_folder: str,
label_str: str,
diameter_l: float,
length_l: float,
diameter_r: float,
length_r: float,
best_position_l: list[float],
best_position_r: list[float],
swarm_size: int,
max_iter: int,
total_time: float,
spacing: list[float],
CBT: bool,
device: torch.device,
grid=None
) -> None:
"""
Same plotting function as before, but it uses torch-based generation
and then moves data to CPU for matplotlib 3D scatter.
"""
cyl_l = generate_cylinder_n_torch(
diameter_l,
length_l,
best_position_l[0],
best_position_l[1],
best_position_l[2],
best_position_l[3],
best_position_l[4],
image_shape,
spacing,
device,
grid
)
cyl_lo = generate_cylinder_o_torch(
diameter_l,
length_l,
best_position_l[0],
best_position_l[1],
best_position_l[2],
best_position_l[3],
best_position_l[4],
image_shape,
spacing,
device,
grid
)
cyl_r = generate_cylinder_n_torch(
diameter_r,
length_r,
best_position_r[0],
best_position_r[1],
best_position_r[2],
best_position_r[3],
best_position_r[4],
image_shape,
spacing,
device,
grid
)
cyl_ro = generate_cylinder_o_torch(
diameter_r,
length_r,
best_position_r[0],
best_position_r[1],
best_position_r[2],
best_position_r[3],
best_position_r[4],
image_shape,
spacing,
device,
grid
)
intersections_l, line_mask_l = center_line_intersections_torch(
best_position_l[0],
best_position_l[1],
best_position_l[2],
best_position_l[3],
best_position_l[4],
int(length_l),
spine_tensor,
spacing,
device
)
loss_l = cl_score_torch(cortical_tensor, spine_tensor, cyl_l, cyl_lo, intersections_l)
intersections_r, line_mask_r = center_line_intersections_torch(
best_position_r[0],
best_position_r[1],
best_position_r[2],
best_position_r[3],
best_position_r[4],
int(length_r),
spine_tensor,
spacing,
device
)
# loss_r = cl_score_torch(cortical_tensor, spine_tensor, cyl_r, cyl_ro, intersections_r)
loss_r = cl_score_torch_xfr(cortical_tensor, spine_tensor, cyl_r, cyl_ro, intersections_r)
azi = azimuth_rotation(image2_path)
res = analyze_vertebral_tilt_contour(image2_path, edge_type='superior', show_plot=False, debug=False)
alt = res['superior']['tilt_angle_deg']
# Move data to CPU for plotting
line_mask_l_cpu = line_mask_l.cpu().numpy()
line_mask_r_cpu = line_mask_r.cpu().numpy()
cyl_l_cpu = cyl_l.cpu().numpy()
cyl_lo_cpu = cyl_lo.cpu().numpy()
cyl_r_cpu = cyl_r.cpu().numpy()
cyl_ro_cpu = cyl_ro.cpu().numpy()
spine_cpu = spine_tensor.cpu().numpy()
z_lin1, y_lin1, x_lin1 = np.where(line_mask_l_cpu == 1)
z_lin2, y_lin2, x_lin2 = np.where(line_mask_r_cpu == 1)
z_cyl_l1, y_cyl_l1, x_cyl_l1 = np.where(cyl_l_cpu == 1)
z_cyl_l2, y_cyl_l2, x_cyl_l2 = np.where(cyl_lo_cpu == 1)
z_cyl_r1, y_cyl_r1, x_cyl_r1 = np.where(cyl_r_cpu == 1)
z_cyl_r2, y_cyl_r2, x_cyl_r2 = np.where(cyl_ro_cpu == 1)
# 骨頭 voxel 依「體積吸收」分成兩組:皮質(高不透明度)與鬆質(低不透明度)
cortical_cpu = cortical_tensor.cpu().numpy()
voxel_mm = float(spacing[0])
alpha_cortical = 1.0 - np.exp(-BONE_MU_CORTICAL * voxel_mm)
alpha_trabecular = 1.0 - np.exp(-BONE_MU_TRABECULAR * voxel_mm)
z_corti, y_corti, x_corti = np.where((spine_cpu == 1) & (cortical_cpu == 1))
z_trab, y_trab, x_trab = np.where((spine_cpu == 1) & (cortical_cpu == 0))
# 中矢狀面:骨頭的最佳鏡稱面,一般平面 a·x + b·y + c·z = d法線方向任意
sym = best_symmetry_plane(spine_cpu)
# 棘突:鏡稱面中線帶(|s|<=w且在 AP 谷底之後側的骨 voxel換不同顏色標示
sp_mask, sp_th, sp_info = segment_spinous_process(spine_cpu, sym)
if sp_mask is not None and sp_mask.any():
sp_corti = sp_mask[z_corti, y_corti, x_corti]
sp_trab = sp_mask[z_trab, y_trab, x_trab]
sp_n_bone = max(int(spine_cpu.sum()), 1)
print(f"[SPINOUS] n={sp_info['n_sp']} "
f"({100.0 * sp_info['n_sp'] / sp_n_bone:.1f}% of bone) "
f"band=+/-{sp_info['band_w']:.1f} voxel AP>={sp_info['ap_thresh']:.1f} "
f"mode={sp_info['mode']}")
else:
sp_corti = sp_trab = None
# X-ray 外觀:骨頭合成一個半透明體積吸收點雲(下方);
# 螺絲(中心線 + 圓柱 + 入口軌跡延长)合成一個全不透明點雲,永遠畫在骨頭之上
def _rgba_block(n, color, a):
arr = np.empty((n, 4))
arr[:] = to_rgba(color)
arr[:, 3] = a
return arr
x_bone = np.concatenate([x_corti, x_trab])
y_bone = np.concatenate([y_corti, y_trab])
z_bone = np.concatenate([z_corti, z_trab])
bone_rgba = np.concatenate([
_rgba_block(len(x_corti), 'lightblue', float(alpha_cortical)),
_rgba_block(len(x_trab), 'lightblue', float(alpha_trabecular)),
])
bone_size = np.full(len(x_bone), BONE_MARKER_SIZE)
# 抽稀:降低堆疊不透明度以呈現淡薄 X-ray 陰影
if BONE_SUBSAMPLE > 1:
x_bone = x_bone[::BONE_SUBSAMPLE]
y_bone = y_bone[::BONE_SUBSAMPLE]
z_bone = z_bone[::BONE_SUBSAMPLE]
bone_rgba = bone_rgba[::BONE_SUBSAMPLE]
bone_size = bone_size[::BONE_SUBSAMPLE]
if sp_corti is not None:
sp_flag = np.concatenate([sp_corti, sp_trab])
if BONE_SUBSAMPLE > 1:
sp_flag = sp_flag[::BONE_SUBSAMPLE]
bone_rgba[sp_flag] = to_rgba('purple', 0.95)
x_screw = np.concatenate([x_lin1, x_lin2, x_cyl_l1, x_cyl_l2, x_cyl_r1, x_cyl_r2])
y_screw = np.concatenate([y_lin1, y_lin2, y_cyl_l1, y_cyl_l2, y_cyl_r1, y_cyl_r2])
z_screw = np.concatenate([z_lin1, z_lin2, z_cyl_l1, z_cyl_l2, z_cyl_r1, z_cyl_r2])
_a, _b, _c, _d = sym['plane']
_n = np.array([_a, _b, _c])
_u = np.array(sym['u'])
_v = np.array(sym['v'])
_p0 = _d * _n # 平面上最接近原點的點
xyz_bone = np.stack([x_bone - _p0[0], y_bone - _p0[1], z_bone - _p0[2]], axis=1)
_pu = xyz_bone @ _u
_pv = xyz_bone @ _v
_U_, _V_ = np.meshgrid(np.linspace(_pu.min(), _pu.max(), 8),
np.linspace(_pv.min(), _pv.max(), 8))
_Xp = _p0[0] + _U_ * _u[0] + _V_ * _v[0]
_Yp = _p0[1] + _U_ * _u[1] + _V_ * _v[1]
_Zp = _p0[2] + _U_ * _u[2] + _V_ * _v[2]
# 上終板平面RANSAC 擬合骨頭頂面(前側)的最佳 a·x + b·y + c·z = d
symp = best_upper_endplate_plane(spine_cpu)
_EX = _EY = _EZ = None
if symp is not None:
_ea, _eb, _ec, _ed = symp['plane']
_en = np.array([_ea, _eb, _ec])
_eu = np.array(symp['u'])
_ev = np.array(symp['v'])
_ep0 = _ed * _en
xz_ep = np.stack([x_bone - _ep0[0], y_bone - _ep0[1], z_bone - _ep0[2]], axis=1)
_pu_ep = xz_ep @ _eu
_pv_ep = xz_ep @ _ev
_EU, _EV = np.meshgrid(np.linspace(_pu_ep.min(), _pu_ep.max(), 8),
np.linspace(_pv_ep.min(), _pv_ep.max(), 8))
_EX = _ep0[0] + _EU * _eu[0] + _EV * _ev[0]
_EY = _ep0[1] + _EU * _eu[1] + _EV * _ev[1]
_EZ = _ep0[2] + _EU * _eu[2] + _EV * _ev[2]
screw_rgba = np.concatenate([
_rgba_block(len(x_lin1), 'r', 1.0),
_rgba_block(len(x_lin2), 'r', 1.0),
_rgba_block(len(x_cyl_l1), 'darkcyan', 1.0),
_rgba_block(len(x_cyl_l2), 'pink', 1.0),
_rgba_block(len(x_cyl_r1), 'blue', 1.0),
_rgba_block(len(x_cyl_r2), 'pink', 1.0),
])
screw_size = np.concatenate([
np.full(len(x_lin1), 3), np.full(len(x_lin2), 3),
np.full(len(x_cyl_l1), 36), np.full(len(x_cyl_l2), 36),
np.full(len(x_cyl_r1), 36), np.full(len(x_cyl_r2), 36),
])
fig = plt.figure(figsize=(12, 12))
# 圖例色塊提高到可讀不透明度(實際渲染仍用真實吸收 alpha
_leg_alpha_c = max(float(alpha_cortical), 0.35)
_leg_alpha_t = max(float(alpha_trabecular), 0.2)
legend_handles = [
Line2D([], [], marker='o', ls='', ms=5, color=to_rgba('lightblue', _leg_alpha_c), label='Spine (cortical)'),
Line2D([], [], marker='o', ls='', ms=5, color=to_rgba('lightblue', _leg_alpha_t), label='Spine (trabecular)'),
Line2D([], [], marker='o', ls='', ms=2, color='r', label='Centerline'),
Line2D([], [], marker='o', ls='', ms=6, color='darkcyan', label='Cylinder(L)'),
Line2D([], [], marker='o', ls='', ms=6, color='blue', label='Cylinder(R)'),
Line2D([], [], marker='o', ls='', ms=6, color='pink', label='Entry track (outer)'),
Line2D([], [], color='orange', lw=2, alpha=0.6,
label=f"Mirror plane {sym['plane'][0]:+.2f}x {sym['plane'][1]:+.2f}y {sym['plane'][2]:+.2f}z = {sym['plane'][3]:.1f}"),
]
if sp_corti is not None:
legend_handles.append(Line2D([], [], marker='o', ls='', ms=6, color='purple',
label=f"Spinous process (mirror-plane midline, {sp_info['n_sp']} vox)"))
if symp is not None:
legend_handles.append(Line2D([], [], color='green', lw=2, alpha=0.7,
label=f"Upper endplate plane tilt {symp['tilt_deg']:.1f} deg"))
def _fill_ax(ax):
# X-ray 外觀:關閉 mplot3d 依深度自動排序 zorder否則半透明骨頭會被重繪到
# 螺絲上方);改為固定分層:吸收骨頭 zorder=5全不透明螺絲 zorder=10
ax.computed_zorder = False
sc_bone = ax.scatter(x_bone, y_bone, z_bone, c=bone_rgba, s=bone_size, marker='o')
sc_bone.set_zorder(5)
sc_screw = ax.scatter(x_screw, y_screw, z_screw, c=screw_rgba, s=screw_size, marker='o')
sc_screw.set_zorder(10)
# 中矢狀面(理論左右對稱切分面):半透明橘色平面 x = x_mid
# 平面邊緣畫橘色線,讓 axial / 正視(側看時)也能清楚看到切分線
plane = ax.plot_surface(_Xp, _Yp, _Zp, color='orange', alpha=0.30,
linewidth=1.0, edgecolor='orange', rstride=1, cstride=1)
plane.set_zorder(8)
# 上終板平面:半透明綠色平面(邊緣綠線)
if _EX is not None:
ep = ax.plot_surface(_EX, _EY, _EZ, color='green', alpha=0.35,
linewidth=1.0, edgecolor='green', rstride=1, cstride=1)
ep.set_zorder(7)
ax1 = fig.add_subplot(221, projection='3d')
_fill_ax(ax1)
ax1.set_xlabel('X-axis'); ax1.set_ylabel('Y-axis'); ax1.set_zlabel('Z-axis')
set_axes_equal_3d(ax1)
ax2 = fig.add_subplot(222, projection='3d')
ax2.view_init(elev=90, azim=-90, roll=0)
_fill_ax(ax2)
ax2.set_xlabel('X-axis'); ax2.set_ylabel('Y-axis'); ax2.set_zlabel('Z-axis')
set_axes_equal_3d(ax2)
ax2.legend(handles=legend_handles)
ax3 = fig.add_subplot(223, projection='3d')
ax3.view_init(elev=0, azim=90, roll=0)
_fill_ax(ax3)
ax3.set_xlabel('X-axis'); ax3.set_ylabel('Y-axis'); ax3.set_zlabel('Z-axis')
set_axes_equal_3d(ax3)
ax4 = fig.add_subplot(224, projection='3d')
ax4.view_init(elev=0, azim=0, roll=0)
_fill_ax(ax4)
ax4.set_xlabel('X-axis'); ax4.set_ylabel('Y-axis'); ax4.set_zlabel('Z-axis')
set_axes_equal_3d(ax4)
cyl_points_l = torch.sum(cyl_l).item()
cyl_points_r = torch.sum(cyl_r).item()
overlap_l = ((cortical_tensor == 1) & (cyl_l == 1)).sum().item()
overlap_r = ((cortical_tensor == 1) & (cyl_r == 1)).sum().item()
overlap_b_l = ((spine_tensor == 1) & (cyl_l == 1)).sum().item()
overlap_b_r = ((spine_tensor == 1) & (cyl_r == 1)).sum().item()
overlap_cortical_l = (overlap_l / cyl_points_l) * 100 if cyl_points_l else 0.0
overlap_cortical_r = (overlap_r / cyl_points_r) * 100 if cyl_points_r else 0.0
overlap_vertebral_l = (overlap_b_l / cyl_points_l) * 100 if cyl_points_l else 0.0
overlap_vertebral_r = (overlap_b_r / cyl_points_r) * 100 if cyl_points_r else 0.0
cb_ratio_l = overlap_cortical_l/overlap_vertebral_l if overlap_vertebral_l else 0.0
cb_ratio_r = overlap_cortical_r/overlap_vertebral_r if overlap_vertebral_r else 0.0
user_altitude_l = 90 - best_position_l[4] - alt
user_altitude_r = 90 - best_position_r[4] - alt
user_azimuth_l = 90 - best_position_l[3] - azi
user_azimuth_r = 90 - best_position_r[3] - azi
date_str = datetime.now().strftime("%Y%m%d")
patient_id = os.path.basename(os.path.dirname(image2_path))
output_folder = os.path.join(base_folder, date_str, patient_id)
os.makedirs(output_folder, exist_ok=True)
csv_path = os.path.join(output_folder, 'output.csv')
# 檢查檔案是否存在 (決定是否寫入標題)
file_exists = os.path.isfile(csv_path)
# 欄位標題 (Header)
headers = [
'Label', 'Side', 'Diameter', 'Length', 'Swarm_Size', 'Max_Iter',
'Position_XYZ', 'Raw_Azimuth', 'Azimuth_Diff', 'Raw_Altitude', 'Altitude_Diff',
'Intersections', 'Best_Loss', 'cyl_points', 'Overlap_Cortical', 'Overlap_Bone',
'Cortical_Bone_Ratio', 'User_Azimuth', 'User_Altitude', 'Total_Time'
]
try:
with open(csv_path, 'a', newline='') as csvfile:
writer = csv.writer(csvfile)
# 如果是新檔案,寫入 Header
if not file_exists:
writer.writerow(headers)
# 寫入 Left 數據
writer.writerow([
label_str,
'L',
diameter_l,
length_l,
swarm_size,
max_iter,
# f"({best_position_l[0]:.2f}, {best_position_l[1]:.2f}, {best_position_l[2]:.2f})",
f"({best_position_l[2]:.2f}, {best_position_l[1]:.2f}, {best_position_l[0]:.2f})",
f"{best_position_l[3]:.2f}",
f"{best_position_l[3]-azi:.2f}",
f"{best_position_l[4]:.2f}",
f"{best_position_l[4]-alt:.2f}",
intersections_l,
f"{loss_l:.2f}",
cyl_points_l,
f"{overlap_cortical_l:.2f}",
f"{overlap_vertebral_l:.2f}",
f"{(overlap_cortical_l/overlap_vertebral_l if overlap_vertebral_l!=0 else 0):.2f}",
f"{user_azimuth_l:.2f}",
f"{user_altitude_l:.2f}",
f"{total_time:.2f}"
])
# 寫入 Right 數據
writer.writerow([
label_str,
'R',
diameter_r,
length_r,
swarm_size,
max_iter,
# f"({best_position_r[0]:.2f}, {best_position_r[1]:.2f}, {best_position_r[2]:.2f})",
f"({best_position_r[2]:.2f}, {best_position_r[1]:.2f}, {best_position_r[0]:.2f})",
f"{best_position_r[3]:.2f}",
f"{best_position_r[3]-azi:.2f}",
f"{best_position_r[4]:.2f}",
f"{best_position_r[4]-alt:.2f}",
intersections_r,
f"{loss_r:.2f}",
cyl_points_r,
f"{overlap_cortical_r:.2f}",
f"{overlap_vertebral_r:.2f}",
f"{(overlap_cortical_r/overlap_vertebral_r if overlap_vertebral_r!=0 else 0):.2f}",
f"{user_azimuth_r:.2f}",
f"{user_altitude_r:.2f}",
f"{total_time:.2f}"
])
print(f"[CSV Saved] {csv_path}")
except Exception as e:
print(f"[Error] Failed to write CSV: {e}")
fig.text(0.5, 0.98, f'{label_str} Best Position', ha='center', fontsize=15)
fig.text(
0.5, 0.44,
f'L: Diameter = {diameter_l} mm, {length_l} mm, '
f'R: Diameter = {diameter_r} mm, {length_r} mm, '
f'Swarm size = {swarm_size}, Iteration = {max_iter}, Total time = {total_time:.2f} s',
ha='center', fontsize=12
)
fig.text(
0.5, 0.03,
# f'Left : Position = ({best_position_l[0]:.2f}, {best_position_l[1]:.2f}, {best_position_l[2]:.2f}), '
f'Left : Position = ({best_position_l[2]:.2f}, {best_position_l[1]:.2f}, {best_position_l[0]:.2f}), '
f'Azimuth = {user_azimuth_l:.2f}, Altitude = {user_altitude_l:.2f}, '
f'Intersection = {intersections_l}, Score = {overlap_cortical_l:.2f} / {overlap_vertebral_l:.2f} / {cb_ratio_l:.2f}',
ha='center', fontsize=9
)
fig.text(
0.5, 0.01,
# f'Right : Position = ({best_position_r[0]:.2f}, {best_position_r[1]:.2f}, {best_position_r[2]:.2f}), '
f'Right : Position = ({best_position_r[2]:.2f}, {best_position_r[1]:.2f}, {best_position_r[0]:.2f}), '
f'Azimuth = {user_azimuth_r:.2f}, Altitude = {user_altitude_r:.2f}, '
f'Intersection = {intersections_r}, Score = {overlap_cortical_r:.2f} / {overlap_vertebral_r:.2f} / {cb_ratio_r:.2f}',
ha='center', fontsize=9
)
fig.tight_layout()
date_str = datetime.now().strftime("%Y%m%d")
file_name = os.path.basename(image2_path)
level = file_name.split('_')[0]
output_folder = os.path.join(base_folder, date_str, patient_id)
os.makedirs(output_folder, exist_ok=True)
if CBT == True:
way = 'CBT'
else:
way = 'TPS'
path = save_with_unique_name(output_folder, label_str, way,
diameter_l, length_l, diameter_r, length_r,
swarm_size, max_iter)
fig.savefig(path, dpi=200, bbox_inches="tight")
print("[Saved figure]", path)
plt.close(fig)
def eval_overlap_from_position(
pos,
optimize_size: bool,
spine_tensor: torch.Tensor,
image_shape,
spacing,
device: torch.device,
grid=None,
fixed_diameter: float | None = None,
fixed_length: float | None = None,
):
"""
根據 position 生成 cylinder mask再算 overlap ratio
"""
if optimize_size:
d, L = snap_to_discrete_values(pos[5], pos[6])
params_5 = pos[:5]
else:
if fixed_diameter is None or fixed_length is None:
raise ValueError("fixed_diameter and fixed_length must be provided when optimize_size=False")
d, L = fixed_diameter, fixed_length
params_5 = pos
z, y, x, az, alt = params_5
cyl_mask = generate_cylinder_n_torch(
d, L,
z, y, x,
az, alt,
image_shape, spacing,
device=device,
grid=grid
)
overlap = compute_overlap_ratio_from_cylinder_mask(cyl_mask, spine_tensor)
return overlap, d, L