diff --git a/docs/_static/adaptive_mask.png b/docs/_static/adaptive_mask.png new file mode 100644 index 000000000..32a09df7a Binary files /dev/null and b/docs/_static/adaptive_mask.png differ diff --git a/docs/outputs.rst b/docs/outputs.rst index 55369191b..e1f1a82ff 100644 --- a/docs/outputs.rst +++ b/docs/outputs.rst @@ -528,11 +528,30 @@ should not overly focus on carpet plots and should examine these results in cont :height: 400px +************************** +Adaptive Mask Summary Plot +************************** + +Below the carpet plots is a summary plot of the adaptive mask. + +This figure overlays contours reflecting the boundaries of the following masks onto the mean optimally combined data: + +- **Base**: The base mask, either provided by the user or generated automatically using ``compute_epi_mask``. +- **Optimal combination**: The mask used for optimal combination and denoising. + This corresponds to values greater than or equal to 1 (at least 1 good echo) in the adaptive mask. +- **Classification**: The mask used for the decomposition and component classification steps. + This corresponds to values greather than or equal to 3 (at least 3 good echoes) in the adaptive mask. + +.. image:: /_static/adaptive_mask.png + :align: center + :height: 400px + + ************************ T2* and S0 Summary Plots ************************ -Below the carpet plots are summary plots for the T2* and S0 maps. +Below the adaptive mask plot are summary plots for the T2* and S0 maps. Each map has two figures: a spatial map of the values and a histogram of the voxelwise values. The T2* map should look similar to T2 maps and be brightest in the ventricles and darkest in areas of largest susceptibility. The S0 map should roughly follow the signal-to-noise ratio and will be brightest near the surface near RF coils. diff --git a/tedana/reporting/data/html/report_body_template.html b/tedana/reporting/data/html/report_body_template.html index 008713715..9328c815a 100644 --- a/tedana/reporting/data/html/report_body_template.html +++ b/tedana/reporting/data/html/report_body_template.html @@ -180,21 +180,25 @@

Carpet plots

+
+

Adaptive mask

+ +

T2* and S0

T2*

- +
- +

S0

- +
- +
diff --git a/tedana/reporting/html_report.py b/tedana/reporting/html_report.py index 519aefe50..8f01263f5 100644 --- a/tedana/reporting/html_report.py +++ b/tedana/reporting/html_report.py @@ -143,6 +143,9 @@ def _update_template_bokeh(bokeh_id, info_table, about, prefix, references, boke # Initial carpet plot (default one) initial_carpet = f"./figures/{prefix}carpet_optcom.svg" + # Adaptive mask image + adaptive_mask = f"./figures/{prefix}adaptive_mask.svg" + # T2* and S0 images t2star_brain = f"./figures/{prefix}t2star_brain.svg" t2star_histogram = f"./figures/{prefix}t2star_histogram.svg" @@ -165,6 +168,7 @@ def _update_template_bokeh(bokeh_id, info_table, about, prefix, references, boke about=about, prefix=prefix, initialCarpet=initial_carpet, + adaptiveMask=adaptive_mask, t2starBrainPlot=t2star_brain, t2starHistogram=t2star_histogram, s0BrainPlot=s0_brain, diff --git a/tedana/reporting/static_figures.py b/tedana/reporting/static_figures.py index d9b9abdaa..8d155e155 100644 --- a/tedana/reporting/static_figures.py +++ b/tedana/reporting/static_figures.py @@ -616,3 +616,82 @@ def plot_t2star_and_s0( annotate=False, output_file=os.path.join(io_generator.out_dir, "figures", s0_plot), ) + + +def plot_adaptive_mask( + *, + optcom: np.ndarray, + base_mask: np.ndarray, + io_generator: io.OutputGenerator, +): + """Create a figure to show the adaptive mask. + + This figure shows the base mask, the adaptive mask thresholded for denoising (threshold >= 1), + and the adaptive mask thresholded for classification (threshold >= 3), + overlaid on the mean optimal combination image. + + Parameters + ---------- + optcom : (S x T) :obj:`numpy.ndarray` + Optimal combination of components. + The mean image over time is used as the underlay for the figure. + base_mask : (S,) :obj:`numpy.ndarray` + Base mask used in tedana. + This is the original mask either provided by the user or generated with `compute_epi_mask`. + io_generator : :obj:`~tedana.io.OutputGenerator` + The output generator for this workflow. + """ + from matplotlib.lines import Line2D + from nilearn import image + + adaptive_mask_img = io_generator.get_name("adaptive mask img") + mean_optcom_img = io.new_nii_like(io_generator.reference_img, np.mean(optcom, axis=1)) + + # Concatenate the three masks used in tedana to treat as a probabilistic atlas + base_mask = io.new_nii_like(io_generator.reference_img, base_mask) + mask_denoise = image.math_img("(img >= 1).astype(np.uint8)", img=adaptive_mask_img) + mask_clf = image.math_img("(img >= 3).astype(np.uint8)", img=adaptive_mask_img) + all_masks = image.concat_imgs((base_mask, mask_denoise, mask_clf)) + # Set values to 0.5 for probabilistic atlas plotting + all_masks = image.math_img("img * 0.5", img=all_masks) + + cmap = plt.cm.gist_rainbow + discrete_cmap = cmap.resampled(3) # colors matching the mask lines in the image + color_dict = { + "Base": discrete_cmap(0), + "Optimal combination": discrete_cmap(0.4), + "Classification": discrete_cmap(0.9), + } + + ob = plotting.plot_prob_atlas( + maps_img=all_masks, + bg_img=mean_optcom_img, + view_type="contours", + threshold=0.2, + annotate=False, + draw_cross=False, + cmap=cmap, + display_mode="mosaic", + cut_coords=4, + ) + + legend_elements = [] + for k, v in color_dict.items(): + line = Line2D([0], [0], color=v, label=k, markersize=10) + legend_elements.append(line) + + fig = ob.frame_axes.get_figure() + width = fig.get_size_inches()[0] + + ob.frame_axes.set_zorder(100) + ob.frame_axes.legend( + handles=legend_elements, + facecolor="white", + ncols=3, + loc="lower center", + fancybox=True, + shadow=True, + fontsize=width, + ) + adaptive_mask_plot = f"{io_generator.prefix}adaptive_mask.svg" + fig.savefig(os.path.join(io_generator.out_dir, "figures", adaptive_mask_plot)) diff --git a/tedana/tests/data/cornell_three_echo_outputs.txt b/tedana/tests/data/cornell_three_echo_outputs.txt index 44a6e660f..5c1d538dd 100644 --- a/tedana/tests/data/cornell_three_echo_outputs.txt +++ b/tedana/tests/data/cornell_three_echo_outputs.txt @@ -23,6 +23,7 @@ desc-adaptiveGoodSignal_mask.nii.gz desc-denoised_bold.nii.gz desc-optcom_bold.nii.gz figures +figures/adaptive_mask.svg figures/carpet_optcom.svg figures/carpet_denoised.svg figures/carpet_accepted.svg diff --git a/tedana/tests/data/fiu_four_echo_outputs.txt b/tedana/tests/data/fiu_four_echo_outputs.txt index 221bc66e7..0da67ba15 100644 --- a/tedana/tests/data/fiu_four_echo_outputs.txt +++ b/tedana/tests/data/fiu_four_echo_outputs.txt @@ -61,6 +61,7 @@ sub-01_echo-4_desc-Rejected_bold.nii.gz sub-01_report.txt sub-01_tedana_report.html figures +figures/sub-01_adaptive_mask.svg figures/sub-01_carpet_optcom.svg figures/sub-01_carpet_denoised.svg figures/sub-01_carpet_accepted.svg diff --git a/tedana/tests/data/nih_five_echo_outputs_verbose.txt b/tedana/tests/data/nih_five_echo_outputs_verbose.txt index 9c86dee80..613690c77 100644 --- a/tedana/tests/data/nih_five_echo_outputs_verbose.txt +++ b/tedana/tests/data/nih_five_echo_outputs_verbose.txt @@ -82,6 +82,7 @@ sub-01_references.bib sub-01_report.txt sub-01_tedana_report.html figures +figures/sub-01_adaptive_mask.svg figures/sub-01_carpet_optcom.svg figures/sub-01_carpet_denoised.svg figures/sub-01_carpet_accepted.svg diff --git a/tedana/workflows/tedana.py b/tedana/workflows/tedana.py index 79f61ff13..3a315c97f 100644 --- a/tedana/workflows/tedana.py +++ b/tedana/workflows/tedana.py @@ -595,12 +595,13 @@ def tedana_workflow( t2s_limited_sec = utils.reshape_niimg(t2smap) t2s_limited = utils.sec2millisec(t2s_limited_sec) t2s_full = t2s_limited.copy() - mask = utils.reshape_niimg(mask) + mask = utils.reshape_niimg(mask).astype(int) mask[t2s_limited == 0] = 0 # reduce mask based on T2* map else: LGR.info("Computing EPI mask from first echo") first_echo_img = io.new_nii_like(io_generator.reference_img, catd[:, 0, :]) - mask = compute_epi_mask(first_echo_img) + mask = compute_epi_mask(first_echo_img).get_fdata() + mask = utils.reshape_niimg(mask).astype(int) RepLGR.info( "An initial mask was generated from the first echo using " "nilearn's compute_epi_mask function." @@ -899,6 +900,11 @@ def tedana_workflow( dn_ts, hikts, lowkts = io.denoise_ts(data_oc, mmix, mask_denoise, comptable) + reporting.static_figures.plot_adaptive_mask( + optcom=data_oc, + base_mask=mask, + io_generator=io_generator, + ) reporting.static_figures.carpet_plot( optcom_ts=data_oc, denoised_ts=dn_ts,