Repository: nilearn/nilearn

Issue:
[BUG] Glm reports does not apply image threshold at the right place
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Operating system

- [X] Linux
- [ ] Mac
- [ ] Windows

### Operating system version

For example one of the following:
- Linux Ubuntu 22.04
- Mac OS Version 12 "monterey"
- Windows 11


### Python version

- [ ] 3.12
- [ ] 3.11
- [X] 3.10
- [ ] 3.9
- [ ] 3.8

### nilearn version

dev version

### Expected behavior

In the example below, the map plotted is all black in report2, because it is reinterpolated after thresholding (compare with report1.html, that displays the expected behavior). 
Interpolation is triggered by the use of a non-diagonal (up to permutation) affine matrix. 
The thresholding should instead occur at  plotting time, so that it would be correctly mask out non-significant area:
for this the `threshold` argument should be frowarded to function `_stat_map_to_svg` in https://github.com/nilearn/nilearn/blob/310f5ca6/nilearn/reporting/glm_reporter.py#L48

### Current behavior & error messages

see report2.html

### Steps and code to reproduce bug

```python
from nilearn.datasets import func
from nilearn.image import mean_img
import numpy as np
import pandas as pd
from nilearn.glm.first_level import FirstLevelModel
from nilearn.reporting import make_glm_report
import nibabel as nib

data = func.fetch_fiac_first_level()
fmri_img = data['func1']
mean_img_ = mean_img(fmri_img)
design_matrices = pd.DataFrame(np.load(data['design_matrix1'])['X'])

fmri_glm = FirstLevelModel(mask_img=data['mask'], minimize_memory=True)
fmri_glm = fmri_glm.fit(fmri_img, design_matrices=design_matrices)
n_columns = design_matrices.shape[1]
contrasts = {'Effects_of_interest': np.eye(n_columns)[:5]}

report = make_glm_report(fmri_glm,
                         contrasts,
                         bg_img=mean_img_,
                         )
report.save_as_html('report1.html')


data_  = nib.load(fmri_img).get_fdata()
affine = np.array([[-2.9, -.1, -.1, 0], [.1, -2.9, -.3, 0], [.1, .3, 3.9, 0], [0, 0, 0, 1]])
fmri_img_ = nib.Nifti1Image(data_, affine)
mean_img_ = mean_img(fmri_img_)
mask_img_ = nib.Nifti1Image(nib.load(data['mask']).get_fdata(), affine=affine)
fmri_glm = FirstLevelModel(mask_img=mask_img_, minimize_memory=True)
fmri_glm = fmri_glm.fit(fmri_img_, design_matrices=design_matrices)
report = make_glm_report(fmri_glm,
                         contrasts,
                         bg_img=mean_img_,
                         )
report.save_as_html('report2.html')
```



Discussion:

Resolving patch:
diff --git a/nilearn/reporting/glm_reporter.py b/nilearn/reporting/glm_reporter.py
--- a/nilearn/reporting/glm_reporter.py
+++ b/nilearn/reporting/glm_reporter.py
@@ -224,8 +224,7 @@ def make_glm_report(
             mask_img = model.masker_.mask_img_
 
     mask_plot_html_code = _mask_to_svg(
-        mask_img=mask_img,
-        bg_img=bg_img,
+        mask_img=mask_img, bg_img=bg_img, cut_coords=cut_coords
     )
     all_components = _make_stat_maps_contrast_clusters(
         stat_img=statistical_maps,
@@ -629,7 +628,7 @@ def _resize_plot_inches(plot, width_change=0, height_change=0):
     return plot
 
 
-def _mask_to_svg(mask_img, bg_img):
+def _mask_to_svg(mask_img, bg_img, cut_coords=None):
     """Plot cuts of an mask image and creates SVG code of it.
 
     Parameters
@@ -656,6 +655,7 @@ def _mask_to_svg(mask_img, bg_img):
             bg_img=bg_img,
             display_mode="z",
             cmap="Set1",
+            cut_coords=cut_coords,
         )
         mask_plot_svg = _plot_to_svg(plt.gcf())
         # prevents sphinx-gallery & jupyter from scraping & inserting plots
@@ -763,13 +763,20 @@ def _make_stat_maps_contrast_clusters(
         components_template_text = html_template_obj.read()
     for contrast_name, stat_map_img in stat_img.items():
         component_text_ = string.Template(components_template_text)
-        thresholded_stat_map, threshold = threshold_stats_img(
+
+        # Only use threshold_stats_img to adjust the threshold
+        # that we will pass to  _clustering_params_to_dataframe
+        # and _stat_map_to_svg
+        # Necessary to avoid :
+        # https://github.com/nilearn/nilearn/issues/4192
+        thresholded_img, threshold = threshold_stats_img(
             stat_img=stat_map_img,
             threshold=threshold,
             alpha=alpha,
             cluster_threshold=cluster_threshold,
             height_control=height_control,
         )
+
         table_details = _clustering_params_to_dataframe(
             threshold,
             cluster_threshold,
@@ -777,16 +784,19 @@ def _make_stat_maps_contrast_clusters(
             height_control,
             alpha,
         )
+
         stat_map_svg = _stat_map_to_svg(
-            stat_img=thresholded_stat_map,
+            stat_img=thresholded_img,
+            threshold=threshold,
             bg_img=bg_img,
             cut_coords=cut_coords,
             display_mode=display_mode,
             plot_type=plot_type,
             table_details=table_details,
         )
+
         cluster_table = get_clusters_table(
-            stat_map_img,
+            thresholded_img,
             stat_threshold=threshold,
             cluster_threshold=cluster_threshold,
             min_distance=min_distance,
@@ -889,6 +899,7 @@ def _clustering_params_to_dataframe(
 @fill_doc
 def _stat_map_to_svg(
     stat_img,
+    threshold,
     bg_img,
     cut_coords,
     display_mode,
@@ -905,6 +916,9 @@ def _stat_map_to_svg(
        to be plotted as slices or glass brain.
        Does not perform any thresholding.
 
+    threshold : float
+       Desired threshold in z-scale.
+
     bg_img : Niimg-like object
         Only used when plot_type is 'slice'.
         See :ref:`extracting_data`.
@@ -945,6 +959,7 @@ def _stat_map_to_svg(
             bg_img=bg_img,
             cut_coords=cut_coords,
             display_mode=display_mode,
+            threshold=threshold,
         )
     elif plot_type == "glass":
         stat_map_plot = plot_glass_brain(
@@ -952,6 +967,7 @@ def _stat_map_to_svg(
             display_mode=display_mode,
             colorbar=True,
             plot_abs=False,
+            threshold=threshold,
         )
     else:
         raise ValueError(


Test patch:
diff --git a/nilearn/reporting/tests/test_glm_reporter.py b/nilearn/reporting/tests/test_glm_reporter.py
--- a/nilearn/reporting/tests/test_glm_reporter.py
+++ b/nilearn/reporting/tests/test_glm_reporter.py
@@ -234,6 +234,7 @@ def test_stat_map_to_svg_slice_z(img_3d_mni, cut_coords):
         display_mode="ortho",
         plot_type="slice",
         table_details=table_details,
+        threshold=2.76,
     )
 
 
@@ -247,6 +248,7 @@ def test_stat_map_to_svg_glass_z(img_3d_mni, cut_coords):
         display_mode="z",
         plot_type="glass",
         table_details=table_details,
+        threshold=2.76,
     )
 
 
@@ -264,6 +266,7 @@ def test_stat_map_to_svg_invalid_plot_type(img_3d_mni, cut_coords):
             display_mode="z",
             plot_type="junk",
             table_details={"junk": 0},
+            threshold=2.76,
         )