Skip to content

Commit 7ca7fd2

Browse files
Improvement documentation and testing (#70)
* Update to uv setup. * improve test coverage * improve documentation * update uv.lock * add more docstrings * update documentation * update documentation * update tests * make rfest optional * update readme * update workflow * update dependecies * move mb test file * update workflow * update Python version requirements in workflow and project configuration
1 parent 4b3a181 commit 7ca7fd2

88 files changed

Lines changed: 20437 additions & 1192 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/python-app.yml

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,33 @@ jobs:
1212

1313
strategy:
1414
matrix:
15-
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
15+
python-version: [ "3.10", "3.11", "3.12" ]
1616

1717
steps:
18-
# Checkout
1918
- name: Checkout repo
20-
uses: actions/checkout@v2
21-
# Setup Python
22-
- name: Set Up Python ${{ matrix.python-version }}
23-
uses: actions/setup-python@v2
19+
uses: actions/checkout@v4
20+
21+
- name: Install uv
22+
uses: astral-sh/setup-uv@v5
23+
24+
- name: Set up Python ${{ matrix.python-version }}
25+
uses: actions/setup-python@v5
2426
with:
2527
python-version: ${{ matrix.python-version }}
26-
# Display Python version
28+
2729
- name: Display Python version
2830
run: python -c "import sys; print(sys.version)"
29-
# Install dependencies
31+
3032
- name: Install dependencies
3133
run: |
32-
python -m pip install --upgrade pip
33-
pip install flake8 pytest
34-
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
35-
# Run tests
34+
uv pip install --system flake8 pytest
35+
uv pip install --system -e .
36+
3637
- name: Lint with flake8
3738
run: |
38-
# stop the build if there are Python syntax errors or undefined names
3939
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
40-
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
4140
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
41+
4242
- name: Test with pytest
4343
run: |
4444
pytest -v

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ __pycache__/
66
# Files
77
*.pickle
88
*.pkl
9+
test_data/*
910

1011
# C extensions
1112
*.so
@@ -119,4 +120,4 @@ djconfig/
119120

120121
# Exclude user stuff
121122
user/
122-
!user/README.md
123+
!user/README.md

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,18 @@ Download the package:
2121
git clone https://github.com/eulerlab/djimaging.git
2222
````
2323

24-
Install the package e.g. using pip as an editable package:
24+
Install the package as an editable package using [uv](https://docs.astral.sh/uv/):
2525

2626
```bash
2727
cd djimaging
28-
pip install -r requirements.txt
29-
pip install -e .
28+
uv pip install -e .
3029
```
3130

32-
If you want to use autorois, also install the following:
33-
```
34-
pip install -r requirements-autorois.txt
31+
If you want to use autorois or receptive fields, install with the optional extras:
32+
```bash
33+
uv pip install -e ".[autorois]"
34+
uv pip install -e ".[rf]"
35+
uv pip install -e ".[autorois,rf]"
3536
```
3637

3738
> ❗ To test if the package was successfully installed, e.g.

djimaging/autorois/autorois_utils.py

Lines changed: 107 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,58 @@
1010

1111

1212
@lru_cache(2)
13-
def _get_neighbor_kernel(device: torch.device):
13+
def _get_neighbor_kernel(device: "torch.device") -> "torch.Tensor":
14+
"""Return a 4-connected neighbor kernel (cross-shaped) as a cached tensor.
15+
16+
Parameters
17+
----------
18+
device : torch.device
19+
The device on which the kernel tensor will be placed.
20+
21+
Returns
22+
-------
23+
torch.Tensor
24+
A 4D tensor of shape (1, 1, 3, 3) with the 4-connected neighbor pattern.
25+
"""
1426
return torch.tensor([[[[0, 1, 0], [1, 0, 1], [0, 1, 0]]]],
1527
dtype=torch.float32,
1628
device=device)
1729

1830

1931
@lru_cache(2)
20-
def _get_diagonal_neighbor_kernel(device: torch.device):
32+
def _get_diagonal_neighbor_kernel(device: "torch.device") -> "torch.Tensor":
33+
"""Return an 8-connected neighbor kernel (full 3x3 except center) as a cached tensor.
34+
35+
Parameters
36+
----------
37+
device : torch.device
38+
The device on which the kernel tensor will be placed.
39+
40+
Returns
41+
-------
42+
torch.Tensor
43+
A 4D tensor of shape (1, 1, 3, 3) with the 8-connected neighbor pattern.
44+
"""
2145
return torch.tensor([[[[1, 1, 1], [1, 0, 1], [1, 1, 1]]]],
2246
dtype=torch.float32,
2347
device=device)
2448

2549

26-
def _run_convolution(mask: torch.Tensor, kernel: torch.Tensor) -> torch.Tensor:
50+
def _run_convolution(mask: "torch.Tensor", kernel: "torch.Tensor") -> "torch.Tensor":
51+
"""Convolve a 2D boolean mask with the given kernel and return a boolean output.
52+
53+
Parameters
54+
----------
55+
mask : torch.Tensor
56+
2D boolean tensor to convolve.
57+
kernel : torch.Tensor
58+
4D convolution kernel of shape (1, 1, kH, kW).
59+
60+
Returns
61+
-------
62+
torch.Tensor
63+
2D boolean tensor of the same shape as ``mask``.
64+
"""
2765
mask_float = mask.to(torch.float32)
2866
mask4d = mask_float.unsqueeze(dim=0).unsqueeze(dim=0)
2967
conv_out = torch.nn.functional.conv2d(mask4d, kernel, padding=1)[0, 0]
@@ -32,7 +70,26 @@ def _run_convolution(mask: torch.Tensor, kernel: torch.Tensor) -> torch.Tensor:
3270
return conv_out_bool
3371

3472

35-
def make_mask_ids_consecutive(mask: np.array) -> np.array:
73+
def make_mask_ids_consecutive(mask: np.ndarray) -> np.ndarray:
74+
"""Remap ROI mask values to a consecutive integer range starting at 0.
75+
76+
Parameters
77+
----------
78+
mask : np.ndarray
79+
Integer array where 0 is background and positive integers are ROI IDs.
80+
Must contain 0 as the minimum value (cellpose format).
81+
82+
Returns
83+
-------
84+
np.ndarray
85+
Mask with the same shape as ``mask`` but with IDs remapped to
86+
``{0, 1, 2, ..., n_rois}``.
87+
88+
Raises
89+
------
90+
AssertionError
91+
If the minimum value of ``mask`` is not 0.
92+
"""
3693
values = np.unique(mask)
3794
assert values.min() == 0, "Mask has to be in cellpose format"
3895
if (values.max() + 1) == values.shape[0]:
@@ -46,27 +103,44 @@ def make_mask_ids_consecutive(mask: np.array) -> np.array:
46103

47104

48105
def create_mask(
49-
cell_probs: torch.Tensor,
50-
offsets: torch.Tensor,
51-
center_mask: torch.Tensor,
106+
cell_probs: "torch.Tensor",
107+
offsets: "torch.Tensor",
108+
center_mask: "torch.Tensor",
52109
cell_prob_threshold: float = 0.5,
53110
kernel_size: int = 5,
54111
center_prob_threshold: float = 0.1,
55112
max_number_of_cells: int = 99999,
56-
) -> torch.Tensor:
57-
"""
58-
Create a roi mask from the outputs of the instance unet model.
113+
) -> "torch.Tensor":
114+
"""Create a ROI mask from the outputs of the instance UNet model.
115+
59116
See https://github.com/bowenc0221/panoptic-deeplab/blob/master/segmentation/model/post_processing/instance_post_processing.py
60-
for another implementation
61-
62-
Args:
63-
cell_probs: probability that a pixel contains a cell, shape: [dim_x, dim_y]
64-
offsets: offset to the cell center in x and y direction for each pixel, shape [2, dim_x, dim_y]
65-
center_mask: 'probability' that a pixel is the center of the cell, shape: [dim_x, dim_y]
66-
cell_prob_threshold: threshold that we consider a pixel to be a cell
67-
kernel_size: mentioned parameter in section 3.2 of https://arxiv.org/pdf/1911.10194.pdf, changed to 5
68-
center_prob_threshold: probability threshold that to determine whether a pixel is a center
69-
max_number_of_cells: maximum number of cells per mask
117+
for another implementation.
118+
119+
Parameters
120+
----------
121+
cell_probs : torch.Tensor
122+
Probability that a pixel contains a cell, shape: (dim_x, dim_y).
123+
offsets : torch.Tensor
124+
Offset to the cell center in x and y direction for each pixel,
125+
shape: (2, dim_x, dim_y).
126+
center_mask : torch.Tensor
127+
Probability that a pixel is the center of a cell, shape: (dim_x, dim_y).
128+
cell_prob_threshold : float, optional
129+
Threshold above which a pixel is considered a cell. Default is 0.5.
130+
kernel_size : int, optional
131+
Max-pooling kernel size used to find center peaks (Section 3.2 of
132+
https://arxiv.org/pdf/1911.10194.pdf). Default is 5.
133+
center_prob_threshold : float, optional
134+
Probability threshold for a pixel to be considered a center.
135+
Default is 0.1.
136+
max_number_of_cells : int, optional
137+
Maximum number of cells per mask. Default is 99999.
138+
139+
Returns
140+
-------
141+
torch.Tensor
142+
Integer tensor of the same spatial shape as ``cell_probs`` where each
143+
pixel is labelled with its instance ID (0 = background).
70144
"""
71145
# determine pixels that are cells
72146
pixel_is_cell = cell_probs > cell_prob_threshold
@@ -123,11 +197,19 @@ def create_mask(
123197
return roi_mask_consecutive
124198

125199

126-
def clean_roi_mask(roi_mask: torch.Tensor) -> torch:
127-
""""
128-
(1) filter pixel of a cell that don't have a horizontal and vertical neighboring pixel
129-
(2) throw out cells that have less than 3 pixels
130-
Possible Todo: throw out pixels of a cell that are horizontal or vertical neighbors of another cell
200+
def clean_roi_mask(roi_mask: "torch.Tensor") -> None:
201+
"""Remove isolated pixels and small ROIs from a ROI mask in-place.
202+
203+
Steps applied:
204+
(1) Remove pixels of a cell that have no horizontal or vertical neighbour
205+
belonging to the same cell.
206+
(2) Remove cells that have fewer than 3 pixels.
207+
208+
Parameters
209+
----------
210+
roi_mask : torch.Tensor
211+
Integer 2D tensor where 0 is background and positive integers are ROI
212+
IDs. Modified in-place.
131213
"""
132214
# Filter pixels that don't have any horizontal or vertical neighboring pixel
133215
kernel = _get_neighbor_kernel(roi_mask.device)

0 commit comments

Comments
 (0)