Skip to content

Commit 75c3a11

Browse files
committed
Add plot robust dvh to portpy visualization.py. Add dose_1d arguments to visualization functions like plot_2d_slice and utils save_nrrd.py
1 parent 0168914 commit 75c3a11

File tree

2 files changed

+194
-5
lines changed

2 files changed

+194
-5
lines changed

portpy/photon/utils/save_nrrd.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
from portpy.photon.structures import Structures
1010

1111

12-
def save_nrrd(my_plan: Plan = None, sol: dict = None, data_dir: str = None, ct: CT = None,
12+
def save_nrrd(my_plan: Plan = None, sol: dict = None, dose_1d: np.ndarray = None, data_dir: str = None, ct: CT = None,
1313
structs: Structures = None, ct_filename: str = 'ct', dose_filename: str = 'dose',
1414
rt_struct_filename: str = 'rtss') -> None:
1515
"""
1616
save nrrd in the path directory else save in patient data directory
1717
1818
:param my_plan: object of class Plan
1919
:param sol: optimal solution dict
20+
:param dose_1d: dose as 1d array
2021
:param data_dir: save nrrd images of ct, dose_1d and struct_name set in path directory
2122
:param ct: object of class CT
2223
:param structs: object of class structs
@@ -46,6 +47,8 @@ def save_nrrd(my_plan: Plan = None, sol: dict = None, data_dir: str = None, ct:
4647
if sol is not None:
4748
dose_1d = sol['inf_matrix'].A @ (sol['optimal_intensity']*my_plan.get_num_of_fractions())
4849
dose_arr = sol['inf_matrix'].dose_1d_to_3d(dose_1d=dose_1d)
50+
else:
51+
dose_arr = my_plan.inf_matrix.dose_1d_to_3d(dose_1d=dose_1d)
4952
dose = sitk.GetImageFromArray(dose_arr)
5053
dose.SetOrigin(ct.ct_dict['origin_xyz_mm'])
5154
dose.SetSpacing(ct.ct_dict['resolution_xyz_mm'])

portpy/photon/visualization.py

Lines changed: 190 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,188 @@ def plot_dvh(my_plan: Plan, sol: dict = None, dose_1d: np.ndarray = None, struct
171171
plt.savefig(filename, bbox_inches="tight", dpi=300)
172172
return ax
173173

174+
@staticmethod
175+
def plot_robust_dvh(my_plan: Plan, sol: dict = None, dose_1d_list: list = None, struct_names: List[str] = None,
176+
dose_scale: dose_type = "Absolute(Gy)",
177+
volume_scale: volume_type = "Relative(%)", plot_scenario=None, **options):
178+
"""
179+
Create dvh plot for the selected structures
180+
181+
:param my_plan: object of class Plan
182+
:param sol: optimal sol dictionary
183+
:param dose_1d: dose_1d in 1d voxels
184+
:param struct_names: structures to be included in dvh plot
185+
:param volume_scale: volume scale on y-axis. Default= Absolute(cc). e.g. volume_scale = "Absolute(cc)" or volume_scale = "Relative(%)"
186+
:param dose_scale: dose_1d scale on x axis. Default= Absolute(Gy). e.g. dose_scale = "Absolute(Gy)" or dose_scale = "Relative(%)"
187+
:keyword style (str): line style for dvh curve. default "solid". can be "dotted", "dash-dotted".
188+
:keyword width (int): width of line. Default 2
189+
:keyword colors(list): list of colors
190+
:keyword legend_font_size: Set legend_font_size. default 10
191+
:keyword figsize: Set figure size for the plot. Default figure size (12,8)
192+
:keyword create_fig: Create a new figure. Default True. If False, append to the previous figure
193+
:keyword title: Title for the figure
194+
:keyword filename: Name of the file to save the figure in current directory
195+
:keyword show: Show the figure. Default is True. If false, next plot can be append to it
196+
:keyword norm_flag: Use to normalize the plan. Default is False.
197+
:keyword norm_volume: Use to set normalization volume. default is 90 percentile.
198+
:return: dvh plot for the selected structures
199+
200+
:Example:
201+
>>> Visualization.plot_dvh(my_plan, sol=sol, struct_names=['PTV', 'ESOPHAGUS'], dose_scale='Absolute(Gy)',volume_scale="Relative(%)", show=False, create_fig=True )
202+
"""
203+
204+
if not isinstance(dose_1d_list, list):
205+
dose_1d_list = [dose_1d_list]
206+
if len(dose_1d_list) == 0:
207+
raise ValueError("dose_list is empty")
208+
if sol is None:
209+
sol = dict()
210+
sol['inf_matrix'] = my_plan.inf_matrix # create temporary solution
211+
212+
if dose_1d_list is None:
213+
dose_1d_list = []
214+
if isinstance(sol, list):
215+
for s in sol:
216+
if 'inf_matrix' not in s:
217+
s['inf_matrix'] = my_plan.inf_matrix
218+
dose_1d_list += [s['inf_matrix'].A @ (s['optimal_intensity'] * my_plan.get_num_of_fractions())]
219+
220+
# getting options_fig:
221+
style = options['style'] if 'style' in options else 'solid'
222+
width = options['width'] if 'width' in options else None
223+
colors = options['colors'] if 'colors' in options else None
224+
legend_font_size = options['legend_font_size'] if 'legend_font_size' in options else 15
225+
figsize = options['figsize'] if 'figsize' in options else (12, 8)
226+
title = options['title'] if 'title' in options else None
227+
filename = options['filename'] if 'filename' in options else None
228+
show = options['show'] if 'show' in options else False
229+
# create_fig = options['create_fig'] if 'create_fig' in options else False
230+
show_criteria = options['show_criteria'] if 'show_criteria' in options else None
231+
ax = options['ax'] if 'ax' in options else None
232+
fontsize = options['fontsize'] if 'fontsize' in options else 12
233+
legend_loc = options["legend_loc"] if "legend_loc" in options else "upper right"
234+
# getting norm options
235+
norm_flag = options['norm_flag'] if 'norm_flag' in options else False
236+
norm_volume = options['norm_volume'] if 'norm_volume' in options else 90
237+
norm_struct = options['norm_struct'] if 'norm_struct' in options else 'PTV'
238+
239+
# plt.rcParams['font.size'] = font_size
240+
# plt.rc('font', family='serif')
241+
if width is None:
242+
width = 3
243+
if colors is None:
244+
colors = Visualization.get_colors()
245+
if struct_names is None:
246+
# orgs = []
247+
struct_names = my_plan.structures.structures_dict['name']
248+
max_dose = 0.0
249+
max_vol = 0.0
250+
all_orgs = my_plan.structures.structures_dict['name']
251+
# orgs = [struct.upper for struct in orgs]
252+
pres = my_plan.get_prescription()
253+
legend = []
254+
255+
if ax is None:
256+
fig, ax = plt.subplots(figsize=figsize)
257+
# if norm_flag:
258+
# norm_factor = Evaluation.get_dose(sol, dose_1d=dose_1d, struct=norm_struct, volume_per=norm_volume) / pres
259+
# dose_1d = dose_1d / norm_factor
260+
count = 0
261+
for i in range(np.size(all_orgs)):
262+
if all_orgs[i] not in struct_names:
263+
continue
264+
if my_plan.structures.get_fraction_of_vol_in_calc_box(all_orgs[i]) == 0: # check if the structure is within calc box
265+
print('Skipping Structure {} as it is not within calculation box.'.format(all_orgs[i]))
266+
continue
267+
dose_sort_list = []
268+
for dose_1d in dose_1d_list:
269+
x, y = Evaluation.get_dvh(sol, struct=all_orgs[i], dose_1d=dose_1d)
270+
dose_sort_list.append(x)
271+
d_sort_mat = np.column_stack(dose_sort_list)
272+
# Compute min/max DVH curve taken across scenarios.
273+
d_min_mat = np.min(d_sort_mat, axis=1)
274+
d_max_mat = np.max(d_sort_mat, axis=1)
275+
276+
if dose_scale == 'Absolute(Gy)':
277+
max_dose = np.maximum(max_dose, d_max_mat[-1])
278+
ax.set_xlabel('Dose (Gy)', fontsize=fontsize)
279+
elif dose_scale == 'Relative(%)':
280+
max_dose = np.maximum(max_dose, d_max_mat[-1])
281+
max_dose = max_dose/pres*100
282+
ax.set_xlabel('Dose ($\%$)', fontsize=fontsize)
283+
284+
if volume_scale == 'Absolute(cc)':
285+
y = y * my_plan.structures.get_volume_cc(all_orgs[i]) / 100
286+
max_vol = np.maximum(max_vol, y[1] * 100)
287+
ax.set_ylabel('Volume (cc)', fontsize=fontsize)
288+
elif volume_scale == 'Relative(%)':
289+
max_vol = np.maximum(max_vol, y[0] * 100)
290+
ax.set_ylabel('Volume Fraction ($\%$)', fontsize=fontsize)
291+
# ax.plot(x, 100 * y, linestyle=style, linewidth=width, color=colors[count])
292+
293+
# ax.plot(d_min_mat, 100 * y, linestyle='dotted', linewidth=width*0.5, color=colors[count])
294+
# ax.plot(d_max_mat, 100 * y, linestyle='dotted', linewidth=width*0.5, color=colors[count])
295+
ax.fill_betweenx(100 * y, d_min_mat, d_max_mat, alpha=0.25, color=colors[count])
296+
297+
# Plot user-specified scenarios.
298+
if plot_scenario is not None:
299+
if plot_scenario == 'mean':
300+
dose_mean = np.mean(d_sort_mat, axis=1)
301+
ax.plot(dose_mean, 100 * y, linestyle=style, color=colors[count], linewidth=width, label=all_orgs[i])
302+
elif not isinstance(plot_scenario, list):
303+
plot_scenario = [plot_scenario]
304+
305+
for n in range(len(plot_scenario)):
306+
scene_num = plot_scenario[n]
307+
if norm_flag:
308+
norm_factor = Evaluation.get_dose(sol, dose_1d=dose_1d_list[scene_num], struct=norm_struct, volume_per=norm_volume) / pres
309+
dose_sort_list[scene_num] = dose_sort_list[scene_num] / norm_factor
310+
d_min_mat = d_min_mat / norm_factor
311+
d_max_mat = d_max_mat / norm_factor
312+
ax.plot(dose_sort_list[scene_num], 100 * y, linestyle=style, color=colors[count], linewidth=width)
313+
count = count + 1
314+
# legend.append(all_orgs[i])
315+
316+
if show_criteria is not None:
317+
for s in range(len(show_criteria)):
318+
if 'dose_volume' in show_criteria[s]['type']:
319+
x = show_criteria[s]['parameters']['dose_gy']
320+
y = show_criteria[s]['constraints']['limit_volume_perc']
321+
ax.plot(x, y, marker='x', color='red', markersize=20)
322+
# plt.xlabel('Dose (Gy)')
323+
# plt.ylabel('Volume Fraction (%)')
324+
current_xlim = ax.get_xlim()
325+
final_xmax = max(current_xlim[1], max_dose * 1.1)
326+
ax.set_xlim(0, final_xmax)
327+
ax.set_ylim(0, max_vol)
328+
handles, labels = ax.get_legend_handles_labels()
329+
unique = [(h, l) for i, (h, l) in enumerate(zip(handles, labels)) if l not in labels[:i]]
330+
ax.legend(*zip(*unique), prop={'size': legend_font_size}, loc=legend_loc)
331+
# ax.legend(legend, prop={'size': legend_font_size}, loc=legend_loc)
332+
ax.grid(visible=True, which='major', color='#666666', linestyle='-')
333+
334+
# Show the minor grid lines with very faint and almost transparent grey lines
335+
# plt.minorticks_on()
336+
ax.minorticks_on()
337+
plt.grid(visible=True, which='minor', color='#999999', linestyle='--', alpha=0.2)
338+
y = np.arange(0, 101)
339+
# if norm_flag:
340+
# x = pres * np.ones_like(y)
341+
# else:
342+
if dose_scale == "Absolute(Gy)":
343+
x = pres * np.ones_like(y)
344+
else:
345+
x = 100 * np.ones_like(y)
346+
347+
ax.plot(x, y, color='black')
348+
if title:
349+
ax.set_title(title)
350+
if show:
351+
plt.show()
352+
if filename is not None:
353+
plt.savefig(filename, bbox_inches="tight", dpi=300)
354+
return ax
355+
174356
@staticmethod
175357
def plot_fluence_2d(beam_id: int, sol: dict = None, optimal_fluence_2d: List[np.ndarray] = None,
176358
inf_matrix: InfluenceMatrix = None, **options):
@@ -263,16 +445,17 @@ def plot_fluence_3d(beam_id: int, sol: dict = None, optimal_fluence_2d: List[np.
263445
return ax
264446

265447
@staticmethod
266-
def plot_2d_slice(my_plan: Plan = None, sol: dict = None, ct: CT = None, structs: Structures = None,
448+
def plot_2d_slice(my_plan: Plan = None, sol: dict = None, dose_1d: np.ndarray = None, ct: CT = None, structs: Structures = None,
267449
slice_num: int = 40, struct_names: List[str] = None, show_dose: bool = False,
268450
show_struct: bool = True, show_isodose: bool = False,
269451
**options) -> None:
270452
"""
271453
272454
Plot 2d view of ct, dose_1d, isodose and struct_name contours
273455
274-
:param sol: solution to optimization
275456
:param my_plan: object of class Plan
457+
:param sol: Optional solution to optimization
458+
:param dose_1d: Optional dose as 1d array
276459
:param ct: Optional. object of class CT
277460
:param structs: Optional. object of class structs
278461
:param slice_num: slice number
@@ -309,8 +492,11 @@ def plot_2d_slice(my_plan: Plan = None, sol: dict = None, ct: CT = None, structs
309492
if sol is not None:
310493
show_dose = True
311494
if show_dose:
312-
dose_1d = sol['inf_matrix'].A @ (sol['optimal_intensity'] * my_plan.get_num_of_fractions())
313-
dose_3d = sol['inf_matrix'].dose_1d_to_3d(dose_1d=dose_1d)
495+
if sol is not None:
496+
dose_1d = sol['inf_matrix'].A @ (sol['optimal_intensity'] * my_plan.get_num_of_fractions())
497+
dose_3d = sol['inf_matrix'].dose_1d_to_3d(dose_1d=dose_1d)
498+
else:
499+
dose_3d = my_plan.inf_matrix.dose_1d_to_3d(dose_1d=dose_1d)
314500
if hasattr(my_plan, 'structures'):
315501
body_mask = my_plan.structures.get_structure_mask_3d('BODY')
316502
masked = np.ma.masked_where(~body_mask[slice_num, :, :].astype(bool), dose_3d[slice_num, :, :])

0 commit comments

Comments
 (0)