Repository: nilearn/nilearn

Issue:
SecondLevelModel is late to notice Target and Reference image mismatch
<!--Provide a brief description what you would like changes and why.-->
 I think that the SecondLevelModel object when operating on a list of FirstLevelModels should first check whether all FirstLevelModels are in the same Affine and Shape before running because in my case it fails after processing multiple models.

In addition it would be nice if there was a feature to force resample everything at the 2nd level as well.

<!--Please fill in the following information, to the best of your ability.-->
### Benefits to the change
User would know in advanced that data is not fit for 2nd level analysis prior to it starting.

### Pseudocode for the new behavior, if applicable

```python

```



Discussion:
Thx for raising this. Indeed there are two points.
* checking a priori in indeed important
*  Regarding forced resampling, I am less enthusiastic: what referential should be taken by default ? I think that we resample safely only if something like target_affine and target_shape or target_img are provided ?

Right - with regards to "forced resampling" - I'm only suggesting it as an additional option of course - when I faced this issue recently I chose my first image as reference, but maybe a more elegant thing would be to choose the reference of the majority of the 1st level images/FirstLevelModels?

Indeed I was hoping that by specifying a target_affine and target_shape in my SecondLevelModel I could trigger the resampling to happen however I got the following error

>   File "/home/user/anaconda3/envs/spyder-env/lib/python3.8/site-packages/nilearn/image/resampling.py", line 418, in resample_img
>     if target_shape is not None and target_affine.shape == (3, 3):
> 
> AttributeError: 'list' object has no attribute 'shape'

I haven't fully tracked it down, but my intuition is that I have multi-session/multi-run data and the resampling procedure can't deal with that.
> but maybe a more elegant thing would be to choose the reference of the majority of the 1st level images/FirstLevelModels?

No, we should particularly avoid fancy behavior that are eventually hard to figure out.
To me, forcing resampling is only possible whenever target affine and shape and explicitly disclosed.
Yes, that sounds fair. 

Should I try to write a PR for trying to fix the fact that is fails to resample multi-run data?
Yes, thx for proposing ! 

Resolving patch:
diff --git a/nilearn/glm/second_level/second_level.py b/nilearn/glm/second_level/second_level.py
--- a/nilearn/glm/second_level/second_level.py
+++ b/nilearn/glm/second_level/second_level.py
@@ -111,7 +111,21 @@ def _check_input_as_type(
         _check_input_as_dataframe(second_level_input)
 
 
+INF = 1000 * np.finfo(np.float32).eps
+
+
 def _check_input_as_first_level_model(second_level_input, none_confounds):
+    """Check that all all first level models are valid.
+
+    - must have been fit
+    - must all have a subject label in case confounds are passed
+    - must all have the same affine / shape
+      (checking all against those of the first model)
+
+    """
+    ref_affine = None
+    ref_shape = None
+
     for model_idx, first_level in enumerate(second_level_input):
         if first_level.labels_ is None or first_level.results_ is None:
             raise ValueError(
@@ -120,12 +134,43 @@ def _check_input_as_first_level_model(second_level_input, none_confounds):
             )
         if not none_confounds and first_level.subject_label is None:
             raise ValueError(
-                "In case confounds are provided, first level "
-                "objects need to provide the attribute "
-                "subject_label to match rows appropriately."
+                "In case confounds are provided, "
+                "first level objects need to provide "
+                "the attribute 'subject_label' to match rows appropriately.\n"
                 f"Model at idx {model_idx} does not provide it. "
-                "To set it, you can do "
-                "first_level.subject_label = '01'"
+                "To set it, you can do first_level.subject_label = '01'"
+            )
+
+        affine = None
+        shape = None
+        if first_level.mask_img is not None:
+            if isinstance(first_level.mask_img, NiftiMasker):
+                affine = first_level.mask_img.affine_
+                shape = first_level.mask_img.mask_img_.shape
+            elif isinstance(first_level.mask_img, Nifti1Image):
+                affine = first_level.mask_img.affine
+                shape = first_level.mask_img.shape
+
+        # take as reference the first values we found
+        if ref_affine is None:
+            ref_affine = affine
+        if ref_shape is None:
+            ref_shape = shape
+
+        if ref_affine is not None and abs(affine - ref_affine).max() > INF:
+            raise ValueError(
+                "All first level models must have the same affine.\n"
+                f"Model {first_level.subject_label} "
+                f"at index {model_idx} has a different affine "
+                "from the previous ones."
+            )
+
+        if shape != ref_shape:
+            raise ValueError(
+                "All first level models must have the same shape.\n"
+                f"Model {first_level.subject_label} "
+                f"at index {model_idx} has a different shape "
+                "from the previous ones."
             )
 
 


Test patch:
diff --git a/nilearn/glm/tests/test_second_level.py b/nilearn/glm/tests/test_second_level.py
--- a/nilearn/glm/tests/test_second_level.py
+++ b/nilearn/glm/tests/test_second_level.py
@@ -25,6 +25,7 @@
     _check_design_matrix,
     _check_effect_maps,
     _check_first_level_contrast,
+    _check_input_as_first_level_model,
     _check_output_type,
     _check_second_level_input,
     _get_contrast,
@@ -173,6 +174,75 @@ def test_process_second_level_input_as_firstlevelmodels():
     assert sample_map.shape == (7, 8, 9)
 
 
+def test_check_affine_first_level_models():
+    shapes, rk = [(7, 8, 9, 15)], 3
+    mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
+        shapes, rk
+    )
+    list_of_flm = [
+        FirstLevelModel(mask_img=mask, subject_label=f"sub-{i}").fit(
+            fmri_data[0], design_matrices=design_matrices[0]
+        )
+        for i in range(3)
+    ]
+    # should pass
+    _check_input_as_first_level_model(
+        second_level_input=list_of_flm, none_confounds=False
+    )
+
+    # add a model with a different affine
+    # should raise an error
+    mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
+        shapes, rk, affine=np.eye(4) * 2
+    )
+    list_of_flm.append(
+        FirstLevelModel(mask_img=mask, subject_label="sub-4").fit(
+            fmri_data[0], design_matrices=design_matrices[0]
+        )
+    )
+    with pytest.raises(
+        ValueError, match="All first level models must have the same affine"
+    ):
+        _check_input_as_first_level_model(
+            second_level_input=list_of_flm, none_confounds=False
+        )
+
+
+def test_check_shape_first_level_models():
+    shapes, rk = [(7, 8, 9, 15)], 3
+    mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
+        shapes, rk
+    )
+    list_of_flm = [
+        FirstLevelModel(mask_img=mask, subject_label=f"sub-{i}").fit(
+            fmri_data[0], design_matrices=design_matrices[0]
+        )
+        for i in range(3)
+    ]
+    # should pass
+    _check_input_as_first_level_model(
+        second_level_input=list_of_flm, none_confounds=False
+    )
+
+    # add a model with a different shape
+    # should raise an error
+    shapes, rk = [(8, 9, 10, 15)], 3
+    mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
+        shapes, rk
+    )
+    list_of_flm.append(
+        FirstLevelModel(mask_img=mask, subject_label="sub-4").fit(
+            fmri_data[0], design_matrices=design_matrices[0]
+        )
+    )
+    with pytest.raises(
+        ValueError, match="All first level models must have the same shape"
+    ):
+        _check_input_as_first_level_model(
+            second_level_input=list_of_flm, none_confounds=False
+        )
+
+
 def test_check_second_level_input():
     with pytest.raises(
         TypeError,
@@ -209,8 +279,7 @@ def test_check_second_level_input():
         with pytest.raises(
             ValueError,
             match="In case confounds are provided, first level "
-            "objects need to provide the attribute "
-            "subject_label",
+            "objects need to provide the attribute 'subject_label'",
         ):
             _check_second_level_input(
                 input_models * 2, pd.DataFrame(), confounds=pd.DataFrame()