Repository: fmi-faim/faim-wako-searchfirst

Issue:
Allow specifying bounding box for segmentation
Similar to these parameters in previous ImageJ macros:

```java
// Define box in which for objects should be searched
bBx = 30; //x size of FOV is 2560 (for CV7000), 2000 (for CV8000)
bBy = 30; //y size of the FOV is 2160 (for CV7000), 2000 (for CV8000)
width = 2500;
height = 2100;
```

... we should implement these config parameters:

```yaml
bounding-box:
  - min_x: 0
  - min_y: 0
  - max_x: 2000
  - max_y: 2000
```


Comments:

Patch:
diff --git a/README.md b/README.md
index 4afc3c9..2223146 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,14 @@ segmentation:
     threshold: 128
     include_holes: yes
     min_size: 10
+    max_size: 99999999999
+    min_eccentricity: 0.0
     max_eccentricity: 0.4
+bounding_box:
+    min_x: 0
+    min_y: 0
+    max_x: 256
+    max_y: 256
 additional_analysis:
     enabled: yes
     target_channel: C03
diff --git a/config.yml b/config.yml
index 2dad472..5f94267 100644
--- a/config.yml
+++ b/config.yml
@@ -11,6 +11,11 @@ segmentation:
     max_size: 99999999999
     min_eccentricity: 0.0
     max_eccentricity: 0.4
+bounding_box:
+    min_x: 0
+    min_y: 0
+    max_x: 256
+    max_y: 256
 additional_analysis:
     enabled: yes
     target_channel: C03
diff --git a/src/faim_wako_searchfirst/segment.py b/src/faim_wako_searchfirst/segment.py
index 1d979b3..abee273 100644
--- a/src/faim_wako_searchfirst/segment.py
+++ b/src/faim_wako_searchfirst/segment.py
@@ -49,6 +49,7 @@ def run(folder: Union[str, Path], configfile: str):
         folder_path,
         file_selection_params=config["file_selection"].get(),
         segmentation_params=config["segmentation"].get(),
+        bounding_box_params=config["bounding_box"].get(),
         additional_analysis_params=config["additional_analysis"].get(),
         output_params=config["output"].get(),
         grid_sampling_params=config["grid_sampling"].get(),
@@ -179,10 +180,20 @@ def save_segmentation_image(folder_path, filename, img, labels):
     imsave(destination_folder / (Path(filename).stem + '.png'), preview)
 
 
+def apply_bounding_box(labels, min_x: int, min_y: int, max_x: int, max_y: int):
+    """Set everything outside the bounding box to zero."""
+    labels[0:max(min_y, 0), :] = 0
+    labels[:, 0:max(min_x, 0)] = 0
+    y, x = labels.shape
+    labels[min(max_y, y):y, :] = 0
+    labels[:, min(max_x, x):x] = 0
+
+
 def process(
         folder: Path,
         file_selection_params: dict,
         segmentation_params: dict,
+        bounding_box_params: dict,
         additional_analysis_params: dict,
         output_params: dict,
         grid_sampling_params: dict,
@@ -202,6 +213,9 @@ def process(
         # file -> segmentation mask and image
         img, labels = segment_file(tif_file, segment, **segmentation_params)
 
+        # mask everything outside bounding box
+        apply_bounding_box(labels, **bounding_box_params)
+
         # addition analysis (e.g. filter by intensity in other channel)
         labels = additional_analysis(
             tif_file, labels, filter_objects_by_intensity,


Test patch:
diff --git a/tests/test_segment.py b/tests/test_segment.py
index 1d5b225..11a19aa 100644
--- a/tests/test_segment.py
+++ b/tests/test_segment.py
@@ -66,6 +66,12 @@ def test_process_test_set(_data_path):
         "min_eccentricity": 0.0,
         "max_eccentricity": 0.9,
     }
+    bounding_box_params = {
+        "min_x": 0,
+        "min_y": 0,
+        "max_x": 256,
+        "max_y": 256,
+    }
     additional_analysis_params = {
         "enabled": False,
     }
@@ -76,11 +82,12 @@ def test_process_test_set(_data_path):
 
     process(
         _data_path,
-        file_selection_params,
-        segmentation_params,
-        additional_analysis_params,
-        output_params,
-        grid_sampling_params,
+        file_selection_params=file_selection_params,
+        segmentation_params=segmentation_params,
+        bounding_box_params=bounding_box_params,
+        additional_analysis_params=additional_analysis_params,
+        output_params=output_params,
+        grid_sampling_params=grid_sampling_params,
     )
 
     csv_path = _data_path / "TestSet_D07_T0001F002L01A02Z01C01.csv"
@@ -119,6 +126,12 @@ def test_process_invalid_second_channel(_data_path):
         "min_eccentricity": 0.0,
         "max_eccentricity": 0.9,
     }
+    bounding_box_params = {
+        "min_x": 0,
+        "min_y": 0,
+        "max_x": 256,
+        "max_y": 256,
+    }
     additional_analysis_params = {
         "enabled": True,
         "target_channel": "C04",
@@ -129,9 +142,60 @@ def test_process_invalid_second_channel(_data_path):
     with pytest.raises(FileNotFoundError):
         process(
             _data_path,
-            file_selection_params,
-            segmentation_params,
-            additional_analysis_params,
-            output_params,
-            grid_sampling_params,
+            file_selection_params=file_selection_params,
+            segmentation_params=segmentation_params,
+            bounding_box_params=bounding_box_params,
+            additional_analysis_params=additional_analysis_params,
+            output_params=output_params,
+            grid_sampling_params=grid_sampling_params,
         )
+
+
+def test_process_bounding_box(_data_path):
+    """Test bounding box for segmentation."""
+    file_selection_params = {
+        "channel": "C01",
+    }
+    segmentation_params = {
+        "threshold": 1,
+        "include_holes": False,
+        "min_size": 10,
+        "max_size": 9999999,
+        "min_eccentricity": 0.0,
+        "max_eccentricity": 0.9,
+    }
+    bounding_box_params = {
+        "min_x": 180,
+        "min_y": 30,
+        "max_x": 1000000,
+        "max_y": 110,
+    }
+    additional_analysis_params = {
+        "enabled": False,
+    }
+    output_params = {
+        "type": "centers",
+    }
+    grid_sampling_params = {}
+
+    process(
+        _data_path,
+        file_selection_params=file_selection_params,
+        segmentation_params=segmentation_params,
+        bounding_box_params=bounding_box_params,
+        additional_analysis_params=additional_analysis_params,
+        output_params=output_params,
+        grid_sampling_params=grid_sampling_params,
+    )
+
+    csv_path = _data_path / "TestSet_D07_T0001F002L01A02Z01C01.csv"
+    assert csv_path.exists()
+
+    with open(csv_path, 'r') as csv_file:
+        reader = csv.reader(csv_file)
+        entries = list(reader)
+        assert len(entries) == 1, "Incorrect number of objects detected."
+        assert entries[0] == ['2', '209.6689930209372', '67.93419740777667']
+
+    segmentation_folder = _data_path.parent / (_data_path.name + "_segmentation")
+    assert sum(1 for _ in segmentation_folder.glob("*")) == 1