5
5
import dask
6
6
import geopandas as gpd
7
7
import numpy as np
8
+ import numpy .ma as ma
8
9
import shapely
9
10
from dask .diagnostics import ProgressBar
10
11
from shapely .geometry import Polygon , box
16
17
17
18
log = logging .getLogger (__name__ )
18
19
20
+ AVAILABLE_MODES = ["average" , "min" , "max" ]
21
+
19
22
20
23
def average_channels (
24
+ sdata : SpatialData , image_key : str = None , shapes_key : str = None , expand_radius_ratio : float = 0
25
+ ) -> np .ndarray :
26
+ log .warning ("average_channels is deprecated, use `aggregate_channels` instead" )
27
+ return aggregate_channels (sdata , image_key , shapes_key , expand_radius_ratio , mode = "average" )
28
+
29
+
30
+ def aggregate_channels (
21
31
sdata : SpatialData ,
22
32
image_key : str = None ,
23
33
shapes_key : str = None ,
24
34
expand_radius_ratio : float = 0 ,
35
+ mode : str = "average" ,
25
36
) -> np .ndarray :
26
- """Average channel intensities per cell.
37
+ """Aggregate the channel intensities per cell (either `"average"`, or take the `"min"` / `"max"`) .
27
38
28
39
Args:
29
40
sdata: A `SpatialData` object
30
41
image_key: Key of `sdata` containing the image. If only one `images` element, this does not have to be provided.
31
42
shapes_key: Key of `sdata` containing the cell boundaries. If only one `shapes` element, this does not have to be provided.
32
43
expand_radius_ratio: Cells polygons will be expanded by `expand_radius_ratio * mean_radius`. This help better aggregate boundary stainings.
44
+ mode: Aggregation mode. One of `"average"`, `"min"`, `"max"`. By default, average the intensity inside the cell mask.
33
45
34
46
Returns:
35
47
A numpy `ndarray` of shape `(n_cells, n_channels)`
36
48
"""
49
+ assert mode in AVAILABLE_MODES , f"Invalid { mode = } . Available modes are { AVAILABLE_MODES } "
50
+
37
51
image = get_spatial_image (sdata , image_key )
38
52
39
53
geo_df = get_boundaries (sdata , key = shapes_key )
40
54
geo_df = to_intrinsic (sdata , geo_df , image )
41
55
geo_df = expand_radius (geo_df , expand_radius_ratio )
42
56
43
- log .info (f"Averaging channels intensity over { len (geo_df )} cells with expansion { expand_radius_ratio = } " )
44
- return _average_channels_aligned (image , geo_df )
57
+ return _aggregate_channels_aligned (image , geo_df , mode )
45
58
46
59
47
- def _average_channels_aligned (image : DataArray , geo_df : gpd .GeoDataFrame | list [Polygon ]) -> np .ndarray :
60
+ def _aggregate_channels_aligned (image : DataArray , geo_df : gpd .GeoDataFrame | list [Polygon ], mode : str ) -> np .ndarray :
48
61
"""Average channel intensities per cell. The image and cells have to be aligned, i.e. be on the same coordinate system.
49
62
50
63
Args:
@@ -54,11 +67,17 @@ def _average_channels_aligned(image: DataArray, geo_df: gpd.GeoDataFrame | list[
54
67
Returns:
55
68
A numpy `ndarray` of shape `(n_cells, n_channels)`
56
69
"""
70
+ log .info (f"Aggregating channels intensity over { len (geo_df )} cells with { mode = } " )
71
+
57
72
cells = geo_df if isinstance (geo_df , list ) else list (geo_df .geometry )
58
73
tree = shapely .STRtree (cells )
59
74
60
- intensities = np . zeros (( len (cells ), len ( image .coords ["c" ])) )
75
+ n_channels = len (image .coords ["c" ])
61
76
areas = np .zeros (len (cells ))
77
+ if mode == "min" :
78
+ aggregation = np .full ((len (cells ), n_channels ), fill_value = np .inf )
79
+ else :
80
+ aggregation = np .zeros ((len (cells ), n_channels ))
62
81
63
82
chunk_sizes = image .data .chunks
64
83
offsets_y = np .cumsum (np .pad (chunk_sizes [1 ], (1 , 0 ), "constant" ))
@@ -86,9 +105,20 @@ def _average_chunk_inside_cells(chunk, iy, ix):
86
105
87
106
mask = rasterize (cell , sub_image .shape [1 :], bounds )
88
107
89
- intensities [index ] += np .sum (sub_image * mask , axis = (1 , 2 ))
90
108
areas [index ] += np .sum (mask )
91
109
110
+ if mode == "min" :
111
+ masked_image = ma .masked_array (sub_image , 1 - np .repeat (mask [None ], n_channels , axis = 0 ))
112
+ aggregation [index ] = np .minimum (aggregation [index ], masked_image .min (axis = (1 , 2 )))
113
+ elif mode in ["average" , "max" ]:
114
+ func = np .sum if mode == "average" else np .max
115
+ values = func (sub_image * mask , axis = (1 , 2 ))
116
+
117
+ if mode == "average" :
118
+ aggregation [index ] += values
119
+ else :
120
+ aggregation [index ] = np .maximum (aggregation [index ], values )
121
+
92
122
with ProgressBar ():
93
123
tasks = [
94
124
dask .delayed (_average_chunk_inside_cells )(chunk , iy , ix )
@@ -97,4 +127,7 @@ def _average_chunk_inside_cells(chunk, iy, ix):
97
127
]
98
128
dask .compute (tasks )
99
129
100
- return intensities / areas [:, None ].clip (1 )
130
+ if mode == "average" :
131
+ return aggregation / areas [:, None ].clip (1 )
132
+ else :
133
+ return aggregation
0 commit comments