-
Notifications
You must be signed in to change notification settings - Fork 7
Add tutorial roi regions #330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4ba2ff8
8b224eb
33ff5b3
3fc534b
d20b5ac
fd66e0f
14b0bc3
ec810f8
c67cade
bee60c7
60b1f52
f6d636a
7efb1ff
ba50312
471a8e6
04ca7e8
544e4c5
edd376c
9823f94
37bef6d
d3f488e
e746ca5
c3ab5b5
6c35a42
f95f48f
15cc8ae
3b85f15
3fa889b
8e1b423
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import dask | ||
| import numpy as np | ||
| import rasterio | ||
| import spatialdata as sd | ||
| from napari_spatialdata import Interactive | ||
| from rasterio.features import geometry_mask | ||
| from scipy import ndimage | ||
| from shapely import unary_union | ||
| from shapely.geometry import Polygon, mapping | ||
| from skimage.draw import disk | ||
| from skimage.measure import find_contours | ||
| from skimage.segmentation import watershed | ||
| from spatialdata.models import Image2DModel | ||
|
|
||
|
|
||
| def mask_image(sdata, image, mask, invert, automatic_masking, threshold, overwrite, masked_image_name): | ||
| """ | ||
| Given an image and mask, either masks or crops the image. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| sdata : sd.SpatialData | ||
| spatialdata object containing the image and mask. | ||
| image : str | ||
| Name of the image in sdata.images to mask. | ||
| mask : str | shapely.geometry.Polygon | ||
| Mask, either str of the name of the shape in sdata.shapes or a shapely polygon. | ||
| invert : bool | ||
| If True, inverts the mask, such that only pixels within mask remain, while the rest gets cropped. | ||
| automatic_masking : bool | ||
| If True, uses threshold + watershed to automatically create a mask based on shapes. Threshold needs to be adjusted manually. | ||
| threshold : float | ||
| Threshold for pixel intensity values at which to segment image into foreground and background. | ||
| overwrite : bool | ||
| Whether to overwrite the image in sdata.images. | ||
| masked_image_name : None | str | ||
| Name of the masked image in sdata.images if overwrite==True. Defaults to f"{image}_masked". | ||
| Returns | ||
| ------- | ||
| sd.SpatialData | ||
| spatialdata object with masked image | ||
| """ | ||
| channels, height, width = sdata.images[image].data.shape | ||
|
|
||
| if automatic_masking: | ||
| polygon = _draw_polygons(sdata.images[image].data, threshold) | ||
| elif isinstance(mask, str): | ||
| polygon = sdata.shapes[mask].iloc[0].geometry | ||
| else: | ||
| polygon = mask | ||
|
|
||
| polygon_geom = [mapping(polygon)] | ||
|
|
||
| transform = rasterio.transform.Affine(1, 0, 0, 0, 1, 0) # identity transform | ||
|
|
||
| image_mask = geometry_mask(polygon_geom, invert=invert, out_shape=(height, width), transform=transform) | ||
|
|
||
| if channels > 1: | ||
| image_mask = dask.array.broadcast_to(image_mask, (channels, height, width)) | ||
|
|
||
| masked_image = sdata.images[image].data * image_mask | ||
| images = {} | ||
| images["masked_image"] = Image2DModel.parse(masked_image) | ||
|
|
||
| if overwrite: | ||
| sdata.images[image] = images["masked_image"] | ||
| else: | ||
| if masked_image_name is None: | ||
| masked_image_name = f"{image}_masked" | ||
| sdata.images[masked_image_name] = images["masked_image"] | ||
|
|
||
|
|
||
| def _draw_polygons(image, threshold): | ||
| """ | ||
| Given an image, detect regions to turn into polygon shapes, which are then used as a mask. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| image : np.ndarray | ||
| Image to find regions in. | ||
| threshold : float | ||
| Threshold for pixel intensity values at which to segment image into foreground and background. | ||
| Returns | ||
| ------- | ||
| shapely.geometry.Polygon | ||
| Polygon containing the detected regions. | ||
| """ | ||
| if image.shape[0] == 1: | ||
| image = image[0] | ||
| binary_image = image > np.percentile(image.flatten(), threshold) | ||
|
|
||
| distance = ndimage.distance_transform_edt(binary_image) | ||
| markers, _ = ndimage.label(distance) | ||
|
|
||
| segmented = watershed(-distance, markers, mask=binary_image) | ||
|
|
||
| contours = find_contours(segmented, level=0.5) | ||
|
|
||
| polygons = [Polygon(contour) for contour in contours if len(contour) > 2] | ||
| polygon = unary_union(polygons) | ||
|
|
||
| return polygon |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -123,6 +123,9 @@ class Project(Logable): | |||||||||||||||||||||||||
| DEFAULT_SINGLE_CELL_IMAGE_DTYPE = DEFAULT_SINGLE_CELL_IMAGE_DTYPE | ||||||||||||||||||||||||||
| DEFAULT_CELL_ID_NAME = DEFAULT_CELL_ID_NAME | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| _h5sc_handle = None | ||||||||||||||||||||||||||
| _h5sc_adata = None | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| PALETTE = [ | ||||||||||||||||||||||||||
| "blue", | ||||||||||||||||||||||||||
| "green", | ||||||||||||||||||||||||||
|
|
@@ -232,6 +235,15 @@ def __exit__(self): | |||||||||||||||||||||||||
| def __del__(self): | ||||||||||||||||||||||||||
| self._clear_temp_dir() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def __getstate__(self): | ||||||||||||||||||||||||||
| state = self.__dict__.copy() | ||||||||||||||||||||||||||
| state["_h5sc_handle"] = None # ensure closed before pickling | ||||||||||||||||||||||||||
| return state | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def __setstate__(self, state): | ||||||||||||||||||||||||||
| self.__dict__.update(state) | ||||||||||||||||||||||||||
| self._h5sc_handle = None # will be reopened lazily | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||
| def sdata_path(self) -> str: | ||||||||||||||||||||||||||
| return self._get_sdata_path() | ||||||||||||||||||||||||||
|
|
@@ -243,14 +255,22 @@ def sdata(self) -> SpatialData: | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||
| def h5sc(self) -> AnnData: | ||||||||||||||||||||||||||
| if self.extraction_f is None: | ||||||||||||||||||||||||||
| raise ValueError("No extraction method has been set.") | ||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||
| if self.extraction_f.output_path is None: | ||||||||||||||||||||||||||
| path = self.extraction_f.extraction_file | ||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||
| path = self.extraction_f.output_path | ||||||||||||||||||||||||||
| return read_h5sc(path) | ||||||||||||||||||||||||||
| # Always safely close previous handle if it exists | ||||||||||||||||||||||||||
| if hasattr(self, "_h5sc_handle") and self._h5sc_handle is not None: | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| self._h5sc_handle.close() | ||||||||||||||||||||||||||
| except (ValueError, RuntimeError): | ||||||||||||||||||||||||||
| # handle was already closed or in invalid state | ||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||
| self._h5sc_handle = None | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Load a fresh AnnData | ||||||||||||||||||||||||||
| adata = read_h5sc(self.extraction_f.output_path or self.extraction_f.extraction_file) | ||||||||||||||||||||||||||
|
Comment on lines
+267
to
+268
|
||||||||||||||||||||||||||
| # Load a fresh AnnData | |
| adata = read_h5sc(self.extraction_f.output_path or self.extraction_f.extraction_file) | |
| # Ensure extraction configuration is available before accessing its attributes | |
| extraction_f = getattr(self, "extraction_f", None) | |
| if extraction_f is None: | |
| raise ValueError( | |
| "Extraction configuration 'extraction_f' is not set. " | |
| "Run the extraction step before accessing 'h5sc'." | |
| ) | |
| # Load a fresh AnnData | |
| adata = read_h5sc(extraction_f.output_path or extraction_f.extraction_file) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -3,8 +3,10 @@ | |||||
| import warnings | ||||||
| from collections.abc import Iterable | ||||||
|
|
||||||
| import h5py | ||||||
| import matplotlib.pyplot as plt | ||||||
| import numpy as np | ||||||
| import pandas as pd | ||||||
| from anndata import AnnData | ||||||
| from matplotlib.axes import Axes | ||||||
| from matplotlib.figure import Figure | ||||||
|
|
@@ -272,7 +274,20 @@ def cell_grid_single_channel( | |||||
| fig = ax.get_figure() | ||||||
|
|
||||||
| spacing = spacing * single_cell_size | ||||||
| images = get_image_with_cellid(adata, _cell_ids, channel_id) | ||||||
|
|
||||||
| # Collect images in a list | ||||||
| if isinstance(adata.obsm["single_cell_images"], h5py.Dataset): | ||||||
| # if working on a memory-backed array | ||||||
| images = get_image_with_cellid(adata, _cell_ids, channel_id) | ||||||
|
|
||||||
| else: | ||||||
| # non backed h5sc adata objects can be accessed directly | ||||||
| # these are created by slicing original h5sc objects | ||||||
| col = "scportrait_cell_id" | ||||||
|
||||||
| col = "scportrait_cell_id" | |
| col = DEFAULT_CELL_ID_NAME |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Debug print statement should be removed or replaced with proper logging. For production code, use the logging module to allow users to control output verbosity.