77import matplotlib .pyplot as plt
88import napari
99import numpy as np
10- import sparse
1110import xarray as xr
12- import zarr
1311from ethology .io .annotations import load_bboxes
1412from PIL import Image
13+ from skimage .measure import regionprops
1514
1615# %%
1716# %matplotlib widget
2726# %%%%%%%%%%%%%%%%%%%%%%%%%%
2827# Helpers
2928
29+
3030class ImageArrayLazy :
3131 """A lazy array for images in a list."""
3232
3333 def __init__ (self , img_paths ):
3434 self .img_paths = sorted (img_paths )
35- # add image shape, assuming all have same as
35+ # add image shape and dtype , assuming all have same as
3636 # first sample
37- sample = np .array (Image .open (img_paths [0 ])) # H, W, C
37+ sample = np .array (Image .open (self . img_paths [0 ])) # H, W, C
3838 self .img_h , self .img_w , self .img_c = sample .shape
39+ self .dtype = sample .dtype
3940
4041 def __len__ (self ):
4142 return len (self .img_paths )
4243
4344 def __getitem__ (self , idx ):
44- return np .array (Image .open (self .img_paths [idx ]))
45+ # napari indexes with a tuple, e.g. (frame, slice, slice)
46+ frame_idx , * rest = idx if isinstance (idx , tuple ) else (idx ,)
47+ if isinstance (frame_idx , slice ):
48+ frames = np .stack (
49+ [np .array (Image .open (p )) for p in self .img_paths [frame_idx ]]
50+ )
51+ return frames [(slice (None ), * rest )]
52+ return np .array (Image .open (self .img_paths [frame_idx ]))[tuple (rest )]
4553
4654 @property
4755 def shape (self ):
56+ """Shape of array (B, H, W, C)."""
4857 return (len (self .img_paths ), self .img_h , self .img_w , self .img_c )
49- # B, H, W, C
58+
59+ @property
60+ def ndim (self ):
61+ """Number of dimensions in array."""
62+ return len (self .shape )
5063
5164
5265def ellipses_from_labels (label_image ):
@@ -63,6 +76,7 @@ def ellipses_from_labels(label_image):
6376 Endpoint pairs for the major axis (y, x).
6477 minor_axes : list of (2, 2) arrays
6578 Endpoint pairs for the minor axis (y, x).
79+
6680 """
6781 ellipse_corners = []
6882 major_axes = []
@@ -124,12 +138,12 @@ def ellipses_from_labels(label_image):
124138
125139# %%%%%%%%%%%%%%%%%%%%%%%
126140# Load masks
127- zarr_root = zarr . open (
128- DATA_DIR / f" { LABEL_NAME } masks.zarr" ,
129- mode = "r" ,
130- )
141+ # ID-encoded masks (int16): 0 = background, k + 1 = the k-th box
142+ # in the frame. Set the store name to the output of
143+ # scripts/generate_masks_from_bboxes.py
144+ MASKS_ZARR = DATA_DIR / "annotations" / "masks_20261009_133849.zarr"
131145
132- mask_da_array = da .from_zarr (zarr_root [ "masks" ] )
146+ mask_da_array = da .from_zarr (MASKS_ZARR , mode = "r" ) # (image_id, H, W )
133147
134148
135149# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@@ -138,14 +152,20 @@ def ellipses_from_labels(label_image):
138152# CONS: can be slow to compute
139153n_ids = ds_bboxes .sizes ["id" ]
140154
155+
141156# for one task per chunk:
142157def _apply_one_hot_encoding_for_IDs (block , n_ids ):
143- """Transform a block of shape (chunk_frames, H, W)
158+ """Apply one hot encoding to ID-encoded block.
159+
160+ Transform an ID-encoded block of shape (chunk_frames, H, W)
144161 into a boolean array of shape (chunk_frames, n_ids, H, W).
145162
163+ Label k + 1 in the block is the mask of the box at position k
164+ along the `id` dimension.
165+
146166 This is most efficient if chunk_frames=1.
147167 """
148- labels = np .arange (1 , n_ids + 1 )[None , :, None , None ] # (1, n_ids, 1, 1)
168+ labels = np .arange (1 , n_ids + 1 )[None , :, None , None ] # (1, n_ids, 1, 1)
149169 # compare every pixel against every label simultaneously
150170 # via broadcasting (i.e. for each pixel, we have a 1-hot encoding
151171 # vector indicate which label/id it is)
@@ -154,16 +174,16 @@ def _apply_one_hot_encoding_for_IDs(block, n_ids):
154174
155175# Apply expand labels chunk by chunk
156176# we define one chunk per image
157- mask_da_array = mask_da_array .rechunk ({0 : 1 })
177+ mask_da_array = mask_da_array .rechunk ({0 : 1 })
158178mask_4d = mask_da_array .map_blocks (
159179 _apply_one_hot_encoding_for_IDs ,
160180 n_ids = n_ids ,
161- new_axis = 1 , # position of new_axis id
162- chunks = ( # sizes of output chunks (if diff from input)
163- mask_da_array .chunks [0 ], # same as input chunk axis=0
164- (n_ids ,), # ids
165- mask_da_array .chunks [1 ], # same as input chunk axis=1
166- mask_da_array .chunks [2 ], # same as input chunk axis=2
181+ new_axis = 1 , # position of new_axis id
182+ chunks = ( # sizes of output chunks (if diff from input)
183+ mask_da_array .chunks [0 ], # same as input chunk axis=0
184+ (n_ids ,), # ids
185+ mask_da_array .chunks [1 ], # same as input chunk axis=1
186+ mask_da_array .chunks [2 ], # same as input chunk axis=2
167187 ),
168188 dtype = bool ,
169189) # (image_id, id, img_h, img_w)
@@ -201,32 +221,32 @@ def _apply_one_hot_encoding_for_IDs(block, n_ids):
201221)
202222ax .contour (single_mask , levels = [0.5 ], colors = "red" , linewidths = 0.5 )
203223
204- # plot all masks in one frame (boolean masks, all same color! )
205- all_masks = ds_bboxes . masks_bool . sel ( image_id = image_id ). any ( dim = "id" )
224+ # plot all masks in one frame (one color per ID )
225+ label_frame = mask_da_array [ image_id ]. compute ( )
206226ax .imshow (
207- all_masks ,
227+ label_frame ,
208228 cmap = "turbo" ,
209- alpha = all_masks . astype ( float ) * 0.5 ,
229+ alpha = ( label_frame > 0 ) * 0.5 ,
210230)
211231
212232# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
213233# Visualise frames and masks in napari viewer
214234mask_array = mask_da_array
215235viewer = napari .Viewer ()
216236
217- # viewer.add_image(np.asarray(image_array).moveaxis(0, -1, 1, 2), name="image")
218-
219- viewer .add_labels (np . asarray ( mask_array ) , name = f"{ LABEL_NAME } masks" )
237+ # both layers are lazy, so napari only loads the frame displayed
238+ viewer . add_image ( image_array , name = "frames" , rgb = True )
239+ viewer .add_labels (mask_array , name = f"{ LABEL_NAME } masks" )
220240
221241# %%
222242# build ellipses and axis lines for napari Shapes layers
223243all_ellipses = []
224244all_major_axes = []
225245all_minor_axes = []
226246for frame_idx in range (mask_array .shape [0 ]):
227- ellipse_corners , major_axes , minor_axes = ellipses_from_labels (
228- np .asarray (mask_array [frame_idx ])
229- )
247+ # masks are already ID-encoded, so each ID is one region
248+ label_frame = np .asarray (mask_array [frame_idx ])
249+ ellipse_corners , major_axes , minor_axes = ellipses_from_labels ( label_frame )
230250 for corners in ellipse_corners :
231251 # Prepend frame index as first column for nD shapes
232252 nd = np .column_stack (
@@ -266,3 +286,5 @@ def _apply_one_hot_encoding_for_IDs(block, n_ids):
266286 edge_width = 4 ,
267287 name = "major axes" ,
268288)
289+
290+ # %%
0 commit comments