Repository: Project-MONAI/MONAI

Issue:
Duplicated logging messages in the call of the ensemble method in EnsembleRunner 
**Describe the bug**

The message `the output_dir is not specified. /ensemble_output will be used to save ensemble predictions.`

**To Reproduce**
Steps to reproduce the behavior:

python tests/test_integration_autorunner.py

2023-04-26 06:54:37,178 - INFO - The output_dir is not specified. /tmp/tmpqv1zas88/work_dir/ensemble_output will be used to save ensemble predictions.
2023-04-26 06:54:37,182 - INFO - The output_dir is not specified. /tmp/tmpqv1zas88/work_dir/ensemble_output will be used to save ensemble predictions.

**Expected behavior**

Should print once in mGPU settings.

remove deprecated 'by_case' in data analyzer .yaml output
**Is your feature request related to a problem? Please describe.**
https://github.com/Project-MONAI/MONAI/blob/6a7f35b7e271cdf3b2c763ae35acf4ab25d85d23/monai/apps/auto3dseg/data_analyzer.py#L239

`len(result[DataStatsKeys.BY_CASE])` is deprecating because the BY_CASE results are moved from `.yaml` to `_by_case.yaml` for performance reasons. the new way to get number of cases from `.yaml` is `n_cases = data_stats["stats_summary#n_cases"]`.

`[None] * n_cases` can be removed when all the algorithms adopt the new way.


Discussion:

Resolving patch:
diff --git a/monai/apps/auto3dseg/auto_runner.py b/monai/apps/auto3dseg/auto_runner.py
--- a/monai/apps/auto3dseg/auto_runner.py
+++ b/monai/apps/auto3dseg/auto_runner.py
@@ -563,12 +563,12 @@ def set_image_save_transform(self, **kwargs: Any) -> None:
 
         """
 
-        are_all_args_present, missing_args = check_kwargs_exist_in_class_init(SaveImage, kwargs)
+        are_all_args_present, extra_args = check_kwargs_exist_in_class_init(SaveImage, kwargs)
         if are_all_args_present:
             self.kwargs.update(kwargs)
         else:
             raise ValueError(
-                f"{missing_args} are not supported in monai.transforms.SaveImage,"
+                f"{extra_args} are not supported in monai.transforms.SaveImage,"
                 "Check https://docs.monai.io/en/stable/transforms.html#saveimage for more information."
             )
 
diff --git a/monai/apps/auto3dseg/data_analyzer.py b/monai/apps/auto3dseg/data_analyzer.py
--- a/monai/apps/auto3dseg/data_analyzer.py
+++ b/monai/apps/auto3dseg/data_analyzer.py
@@ -238,7 +238,6 @@ def get_all_case_stats(self, key="training", transform_list=None):
         n_cases = len(result_bycase[DataStatsKeys.BY_CASE])
         result[DataStatsKeys.SUMMARY] = summarizer.summarize(cast(list, result_bycase[DataStatsKeys.BY_CASE]))
         result[DataStatsKeys.SUMMARY]["n_cases"] = n_cases
-        result[DataStatsKeys.BY_CASE] = [None] * n_cases
         result_bycase[DataStatsKeys.SUMMARY] = result[DataStatsKeys.SUMMARY]
         if not self._check_data_uniformity([ImageStatsKeys.SPACING], result):
             logger.info("Data spacing is not completely uniform. MONAI transforms may provide unexpected result")
diff --git a/monai/apps/auto3dseg/ensemble_builder.py b/monai/apps/auto3dseg/ensemble_builder.py
--- a/monai/apps/auto3dseg/ensemble_builder.py
+++ b/monai/apps/auto3dseg/ensemble_builder.py
@@ -31,6 +31,7 @@
 from monai.bundle import ConfigParser
 from monai.data import partition_dataset
 from monai.transforms import MeanEnsemble, SaveImage, VoteEnsemble
+from monai.utils import RankFilter
 from monai.utils.enums import AlgoKeys
 from monai.utils.misc import check_kwargs_exist_in_class_init, prob2class
 from monai.utils.module import look_up_option, optional_import
@@ -406,9 +407,6 @@ def __init__(
             "CMD_PREFIX": os.environ.get("CMD_PREFIX"),  # type: ignore
         }
 
-        # self.kwargs needs to pop out args for set_image_save_transform
-        self.save_image: dict[str, Any] = self._pop_kwargs_to_get_image_save_transform(**self.kwargs)
-
     def set_ensemble_method(self, ensemble_method_name: str = "AlgoEnsembleBestByFold", **kwargs: Any) -> None:
         """
         Set the bundle ensemble method
@@ -474,7 +472,7 @@ def _pop_kwargs_to_get_image_save_transform(self, **kwargs):
 
         return save_image
 
-    def set_image_save_transform(self, **kwargs):
+    def set_image_save_transform(self, **kwargs: Any) -> None:
         """
         Set the ensemble output transform.
 
@@ -483,8 +481,14 @@ def set_image_save_transform(self, **kwargs):
                 transform. For more information, check https://docs.monai.io/en/stable/transforms.html#saveimage .
 
         """
-        kwargs_copy = deepcopy(kwargs)
-        self.save_image = self._pop_kwargs_to_get_image_save_transform(**kwargs_copy)
+        are_all_args_present, extra_args = check_kwargs_exist_in_class_init(SaveImage, kwargs)
+        if are_all_args_present:
+            self.kwargs.update(kwargs)
+        else:
+            raise ValueError(
+                f"{extra_args} are not supported in monai.transforms.SaveImage,"
+                "Check https://docs.monai.io/en/stable/transforms.html#saveimage for more information."
+            )
 
     def set_num_fold(self, num_fold: int = 5) -> None:
         """
@@ -504,18 +508,20 @@ def ensemble(self):
             dist.init_process_group(backend="nccl", init_method="env://")
             self.world_size = dist.get_world_size()
             self.rank = dist.get_rank()
+            logger.addFilter(RankFilter())
         # set params after init_process_group to know the rank
         self.set_num_fold(num_fold=self.num_fold)
         self.set_ensemble_method(self.ensemble_method_name, **self.kwargs)
+        # self.kwargs needs to pop out args for set_image_save_transform
+        save_image = self._pop_kwargs_to_get_image_save_transform(**self.kwargs)
 
         history = import_bundle_algo_history(self.work_dir, only_trained=False)
         history_untrained = [h for h in history if not h[AlgoKeys.IS_TRAINED]]
         if history_untrained:
-            if self.rank == 0:
-                warn(
-                    f"Ensembling step will skip {[h[AlgoKeys.ID] for h in history_untrained]} untrained algos."
-                    "Generally it means these algos did not complete training."
-                )
+            logger.warning(
+                f"Ensembling step will skip {[h[AlgoKeys.ID] for h in history_untrained]} untrained algos."
+                "Generally it means these algos did not complete training."
+            )
             history = [h for h in history if h[AlgoKeys.IS_TRAINED]]
         if len(history) == 0:
             raise ValueError(
@@ -534,13 +540,12 @@ def ensemble(self):
         self.ensembler.infer_files = infer_files
         # add rank to pred_params
         self.kwargs["rank"] = self.rank
-        self.kwargs["image_save_func"] = self.save_image
-        if self.rank == 0:
-            logger.info("Auto3Dseg picked the following networks to ensemble:")
-            for algo in self.ensembler.get_algo_ensemble():
-                logger.info(algo[AlgoKeys.ID])
-            output_dir = self.save_image["output_dir"]
-            logger.info(f"Auto3Dseg ensemble prediction outputs will be saved in {output_dir}.")
+        self.kwargs["image_save_func"] = save_image
+        logger.info("Auto3Dseg picked the following networks to ensemble:")
+        for algo in self.ensembler.get_algo_ensemble():
+            logger.info(algo[AlgoKeys.ID])
+        output_dir = save_image["output_dir"]
+        logger.info(f"Auto3Dseg ensemble prediction outputs will be saved in {output_dir}.")
         self.ensembler(pred_param=self.kwargs)
 
         if self.mgpu:


Test patch:
diff --git a/tests/test_auto3dseg_ensemble.py b/tests/test_auto3dseg_ensemble.py
--- a/tests/test_auto3dseg_ensemble.py
+++ b/tests/test_auto3dseg_ensemble.py
@@ -180,10 +180,10 @@ def test_ensemble_runner(self) -> None:
         self.assertTrue(runner.ensemble_method.n_best == 3)
 
         save_output = os.path.join(self.test_dir.name, "workdir")
-        runner.set_image_save_transform(
+        save_image = runner._pop_kwargs_to_get_image_save_transform(
             output_dir=save_output, output_postfix="test_ensemble", output_dtype=float, resample=True
         )
-        self.assertIsInstance(ConfigParser(runner.save_image).get_parsed_content(), SaveImage)
+        self.assertIsInstance(ConfigParser(save_image).get_parsed_content(), SaveImage)
 
     def tearDown(self) -> None:
         set_determinism(None)