Repository: mlcommons/GaNDLF

Issue:
Using patch miner without `Label` header results in failure
**Describe the bug**
If we are not using a `Label` header in the data CSV for [patch miner](https://github.com/mlcommons/GaNDLF/blob/master/gandlf_patchMiner), the code fails.

**To Reproduce**
Steps to reproduce the behavior:
1. Remove the `Label` header from the file `testing/data/train_2d_histo_segmentation.csv`.
2. Run the function `test_train_inference_segmentation_histology_2d`.
3. See error: 
```python-traceback
ERROR: not enough values to unpack (expected 3, got 2)
  File "C:\Projects\GaNDLF\gandlf_run", line 130, in <module>
    test_train_inference_segmentation_histology_2d("cuda")
  File "C:\Projects\GaNDLF\testing\test_full.py", line 2082, in test_train_inference_segmentation_histology_2d
    patch_extraction(
  File "C:\Projects\GaNDLF\GANDLF\cli\patch_extraction.py", line 63, in patch_extraction
    for sid, slide, label in parse_gandlf_csv(input_path):
ValueError: not enough values to unpack (expected 3, got 2)

During handling of the above exception, another exception occurred:

  File "C:\Projects\GaNDLF\gandlf_run", line 142, in <module>
    sys.exit("ERROR: " + str(e))
SystemExit: ERROR: not enough values to unpack (expected 3, got 2)
```
5. If `Label` is present but mask files are not defined, the following error is observed:
```python-traceback
ERROR: expected str, bytes or os.PathLike object, not float
  File "C:\Projects\GaNDLF\gandlf_run", line 130, in <module>
    test_train_inference_segmentation_histology_2d("cuda")
  File "C:\Projects\GaNDLF\testing\test_full.py", line 2082, in test_train_inference_segmentation_histology_2d
    patch_extraction(
  File "C:\Projects\GaNDLF\GANDLF\cli\patch_extraction.py", line 63, in patch_extraction
    for sid, slide, label in parse_gandlf_csv(input_path):
  File "C:\Projects\GaNDLF\GANDLF\cli\patch_extraction.py", line 21, in parse_gandlf_csv
    df, _ = parseTrainingCSV(fpath, train=False)
  File "C:\Projects\GaNDLF\GANDLF\utils\write_parse.py", line 123, in parseTrainingCSV
    convert_relative_paths_in_dataframe(data_full, headers, inputTrainingCSVFile)
  File "C:\Projects\GaNDLF\GANDLF\utils\write_parse.py", line 203, in convert_relative_paths_in_dataframe
    this_path = pathlib.Path(entry)
TypeError: expected str, bytes or os.PathLike object, not float
```

**Expected behavior**
The patch miner should run even without a valid label file.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**GaNDLF Version**
<!-- Put the output of the following command:
python -c 'import GANDLF as g;print(g.__version__)'
-->
0.0.17-dev

**Desktop (please complete the following information):**
 - OS: N.A.
 - Version: N.A.

**Additional context**
Reported by @shubhaminnani


Discussion:

Resolving patch:
diff --git a/GANDLF/cli/patch_extraction.py b/GANDLF/cli/patch_extraction.py
--- a/GANDLF/cli/patch_extraction.py
+++ b/GANDLF/cli/patch_extraction.py
@@ -20,11 +20,17 @@
 def parse_gandlf_csv(fpath):
     df, _ = parseTrainingCSV(fpath, train=False)
     df = df.drop_duplicates()
+    # nans can be easily removed using df.dropna(axis=1, how='all')
+    # we want to keep them because we want the user to check the CSV instead
+    # there might be cases where labels are accidentally removed for some subjects, but not all
+    assert (
+        df.isnull().values.any() == False
+    ), "Data CSV contains null/nan values, please check."
     for _, row in df.iterrows():
         if "Label" in row:
             yield row["SubjectID"], row["Channel_0"], row["Label"]
         else:
-            yield row["SubjectID"], row["Channel_0"]
+            yield row["SubjectID"], row["Channel_0"], None
 
 
 def patch_extraction(input_path, output_path, config=None):
@@ -63,7 +69,8 @@ def patch_extraction(input_path, output_path, config=None):
     for sid, slide, label in parse_gandlf_csv(input_path):
         # Create new instance of slide manager
         manager = PatchManager(slide, os.path.join(output_path, str(sid)))
-        manager.set_label_map(label)
+        if label is not None:
+            manager.set_label_map(label)
         manager.set_subjectID(str(sid))
         manager.set_image_header("Channel_0")
         manager.set_mask_header("Label")
diff --git a/GANDLF/data/patch_miner/opm/patch_manager.py b/GANDLF/data/patch_miner/opm/patch_manager.py
--- a/GANDLF/data/patch_miner/opm/patch_manager.py
+++ b/GANDLF/data/patch_miner/opm/patch_manager.py
@@ -435,9 +435,8 @@ def mine_patches(self, config, output_csv=None):
                 new_df_rows.append(new_row)
 
             new_df = pd.DataFrame(new_df_rows)
-            output_df = pd.concat(
-                [output_df, new_df]
-            )  # Concatenate in case there is a pre-existing dataframe
+            # Concatenate in case there is a pre-existing dataframe
+            output_df = pd.concat([output_df, new_df])
 
         output_df.to_csv(csv_filename, index=False)
 
diff --git a/GANDLF/utils/write_parse.py b/GANDLF/utils/write_parse.py
--- a/GANDLF/utils/write_parse.py
+++ b/GANDLF/utils/write_parse.py
@@ -200,13 +200,14 @@ def convert_relative_paths_in_dataframe(input_dataframe, headers, path_root):
         if (loc == headers["labelHeader"]) or (loc in headers["channelHeaders"]):
             # These entries can be considered as paths to files
             for index, entry in enumerate(input_dataframe[column]):
-                this_path = pathlib.Path(entry)
-                start_path = pathlib.Path(path_root)
-                if start_path.is_file():
-                    start_path = start_path.parent
-                if not this_path.is_file():
-                    if not this_path.is_absolute():
-                        input_dataframe.loc[index, column] = str(
-                            start_path.joinpath(this_path)
-                        )
+                if isinstance(entry, str):
+                    this_path = pathlib.Path(entry)
+                    start_path = pathlib.Path(path_root)
+                    if start_path.is_file():
+                        start_path = start_path.parent
+                    if not this_path.is_file():
+                        if not this_path.is_absolute():
+                            input_dataframe.loc[index, column] = str(
+                                start_path.joinpath(this_path)
+                            )
     return input_dataframe


Test patch: