Skip to content
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

MVP for morpohology module #866

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft

Conversation

timtreis
Copy link
Member

@timtreis timtreis commented Aug 7, 2024

MRE:

import squidpy as sq
import spatialdata as sd
from spatialdata.datasets import raccoon
import spatialdata_plot
from scipy.stats import entropy

sdata = raccoon()

def my_func(regionmask, intensity_image):
    
    masked_values = intensity_image[regionmask]
    histogram, bin_edges = np.histogram(masked_values, bins=256, range=(0, 255))
    probabilities = histogram / np.sum(histogram)

    return entropy(probabilities, base=2)

sq.im.quantify_morphology(
    sdata,
    label="segmentation",
    image="raccoon",
    methods=["area", "perimeter", "circularity", "intensity_mean", my_func],
    split_by_channels=True,
)

image

image

Notes

  • circularity is a non-skimage method but internal method (we need a library of those, can "steal" from CellProfiler + X)
  • my_func is an external method that gets fed to skimage.measure.regionprops. We need to provide doc on how to make such method. They take in a mask of the respective cell as well as a (potentially multi-channel) intensity image (both np.ndarrays).
  • add back to adata instead, align with rest of codebase
  • now thing of how to integrate custom callables that are external to the codebase, f.e. huggingface models
  • Needs to respect transformations & deal with different scales
  • Needs notebook in squidpy-notebooks

From discussion on 2024-08-13

  • Workstreams
      1. "baseline" regionprops: what skimage has natively as functions, can query by string
      1. "non-baseline" regionprops: other metrics we can "steal" from CellProfiler etc and make available by string internally
      1. feed in methods that take in mask / intensity image
      1. test performance of regionprops on actual data - does it work at scale at all?
      • if not, can we hijack the method and parallelize across chunks (parallel io) and collect
      • can we lazy-compute? dask?
      1. respect transformations and datatree "zoom" - which matches, what does the user want?
      1. my_func wrapper for model inference, f.e. from some HF-model?
      • download model etc?
      • they might require specific resolution, might have to scale all cells because area contains info

@codecov-commenter
Copy link

codecov-commenter commented Aug 7, 2024

Codecov Report

Attention: Patch coverage is 20.33898% with 47 lines in your changes missing coverage. Please review.

Project coverage is 69.52%. Comparing base (4a632d6) to head (bbecec3).
Report is 5 commits behind head on main.

Files Patch % Lines
src/squidpy/im/_feature.py 20.33% 47 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #866      +/-   ##
==========================================
- Coverage   69.99%   69.52%   -0.47%     
==========================================
  Files          39       39              
  Lines        5532     5667     +135     
  Branches     1037     1063      +26     
==========================================
+ Hits         3872     3940      +68     
- Misses       1367     1429      +62     
- Partials      293      298       +5     
Files Coverage Δ
src/squidpy/im/_feature.py 51.81% <20.33%> (-37.08%) ⬇️

... and 2 files with indirect coverage changes

@npeschke
Copy link
Collaborator

I had to remove the code that called the label function from skimage as it was creating additional labels that we cannot trace back to the original labels.

@@ -3,7 +3,7 @@
from __future__ import annotations

from squidpy.im._container import ImageContainer
from squidpy.im._feature import calculate_image_features
from squidpy.im._feature import calculate_image_features, quantify_morphology
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @npeschke @timtreis was just taking a look at this, was wondering why the duplication of the function? It seems that quantify_morphology does something very similar to calculate_image_features. Do you plan to discontinue the latter? wouldn't it be better to adapt the latter to use spatialdata + adapt? thank you

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh not sure yet what the optimal solution will be. There is clear redundancy and a strong overlap, but when I wrote the original MVP, it didn't clearly fit with the current calculate_image_features. On the one hand, some of these features don't need an image but only the label, but then also the structure of the function was going to be quite different.

Even if we merge them into the calculate_image_features function, the parameters of that function would have to change. So yeah, not fully sure yet.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally wanted to have sth that takes an arbitrary callable with a region_props like footprint so we could also inject f.e. some HugginfFace featuriser or sth

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, that's a good point. The calculate_image_features does support arbitrary functions, see https://squidpy.readthedocs.io/en/stable/api/squidpy.im.calculate_image_features.html and https://squidpy.readthedocs.io/en/stable/api/squidpy.im.calculate_image_features.html and I understand that the current implementation doesn't operate on masks (or not only on mask), but maybe it would be ok to just change the input to "image" to not be just an Image but also a Label?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the parallelization/out of core functionality I think it's also important. The current implementation relies on joblib parallel to iterate over spots, and understand it's not optimal (e.g. how to do it with raster labels and raster image?). But maybe a combination of that and dask, e.g. https://examples.dask.org/applications/image-processing.html could be useful. Basically I think we should strive to implement scalability in time/memory.

Copy link
Member

@giovp giovp Sep 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at this, not maintained but maybe some ideas could be reused
https://github.com/jrussell25/dask-regionprops/blob/main/dask_regionprops/regionprops.py

EDIT: looking more deeply, not sure anymore 😅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this ties into the larger topic of moving things to the GPU

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for context, the current implementation takes 20 seconds on my machine to calculate all available features of the MIBI-TOF dataset. So parallelization might not need to be that urgent.

Copy link
Member Author

@timtreis timtreis Sep 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be if we add more, potentially more expensive to compute, features and analyse datasets like Xenium with 100k+ cells ;)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @npeschke , thanks for sharing the time. If the mibi-tof dataset you are referring to is the one from squidpy, that is a toy dataset that does not really recapitulate real data complexity and size. I'd be curious to see the performance on a e.g. xenium dataset.

# if we didn't get any properties, we'll do the bare minimum
props = ["label"]

np_rgb_image = image_element.values.transpose(1, 2, 0) # (c, y, x) -> (y, x, c)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like, stuff like this doesn't work with real data. Everything needs to be either lazy, or looped/parallelized

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants