Skip to content

Commit

Permalink
Merge pull request #184 from stardist/dev
Browse files Browse the repository at this point in the history
Pending changes for 0.8.0 release
  • Loading branch information
uschmidt83 committed Mar 10, 2022
2 parents 0d9dffb + 0e98863 commit b3780c3
Show file tree
Hide file tree
Showing 21 changed files with 490 additions and 105 deletions.
23 changes: 12 additions & 11 deletions examples/other2D/bioimageio.ipynb
Expand Up @@ -144,7 +144,7 @@
"\n",
"**Important**: \n",
"- Image normalization is included as a pre-processing step of the exported model (percentile parameters). Hence, input images do not need to be normalized beforehand if this model is used for prediction elsewhere.\n",
"- StarDist's post-processing step (i.e. non-maximum supression to yield an instance segmentation from the output of the neural network) is generally *not included* as part of the exported model. The only exception are 2D models run via [DeepImageJ](https://deepimagej.github.io/deepimagej/), for which the post-processing step is performed by the [StarDist ImageJ/Fiji plugin](https://github.com/stardist/stardist-imagej) (needs to be installed beforehand)."
"- StarDist's post-processing step (i.e. non-maximum supression to yield an instance segmentation from the output of the neural network) is generally *not included* as part of the exported model. The only exception are currently 2D models run via [DeepImageJ](https://deepimagej.github.io/deepimagej/), for which the post-processing step can be performed by the [StarDist ImageJ/Fiji plugin](https://github.com/stardist/stardist-imagej) (needs to be installed beforehand). Note that the post-processing step is done via an ImageJ macro that's bundled with the exported model, but it is currently not run automatically."
]
},
{
Expand All @@ -158,7 +158,7 @@
"text": [
"Help on function export_bioimageio in module stardist.bioimageio_utils:\n",
"\n",
"export_bioimageio(model, outpath, test_input, test_input_axes=None, test_input_norm_axes='ZYX', name='bioimageio_model', mode='tensorflow_saved_model_bundle', min_percentile=1.0, max_percentile=99.8, overwrite_spec_kwargs=None)\n",
"export_bioimageio(model, outpath, test_input, test_input_axes=None, test_input_norm_axes='ZYX', name=None, mode='tensorflow_saved_model_bundle', min_percentile=1.0, max_percentile=99.8, overwrite_spec_kwargs=None)\n",
" Export stardist model into bioimage.io format, https://github.com/bioimage-io/spec-bioimage-io.\n",
" \n",
" Parameters\n",
Expand All @@ -170,13 +170,14 @@
" test_input: np.ndarray\n",
" input image for generating test data\n",
" test_input_axes: str or None\n",
" the axes of the test input, for example 'YX' for a 2d image or 'ZYX' for a 3d volume\n",
" using None assumes that axes of test_input are the same as those of model\n",
" the axes of the test input, for example 'YX' for a 2d image or 'ZYX' for a 3d volume\n",
" using None assumes that axes of test_input are the same as those of model\n",
" test_input_norm_axes: str\n",
" the axes of the test input which will be jointly normalized, for example 'ZYX' for all spatial dimensions ('Z' ignored for 2D input)\n",
" use 'ZYXC' to also jointly normalize channels (e.g. for RGB input images)\n",
" the axes of the test input which will be jointly normalized, for example 'ZYX' for all spatial dimensions ('Z' ignored for 2D input)\n",
" use 'ZYXC' to also jointly normalize channels (e.g. for RGB input images)\n",
" name: str\n",
" the name of this model (default: \"bioimageio_model\")\n",
" the name of this model (default: None)\n",
" if None, uses the (folder) name of the model (i.e. `model.name`)\n",
" mode: str\n",
" the export type for this model (default: \"tensorflow_saved_model_bundle\")\n",
" min_percentile: float\n",
Expand Down Expand Up @@ -211,14 +212,14 @@
"text": [
"INFO:tensorflow:No assets to save.\n",
"INFO:tensorflow:No assets to write.\n",
"INFO:tensorflow:SavedModel written to: /tmp/tmpvd14cwz9/model/saved_model.pb\n",
"INFO:tensorflow:SavedModel written to: /tmp/tmpn7mta850/model/saved_model.pb\n",
"\n",
"bioimage.io model exported to 'bioimageio_model.zip'\n"
"bioimage.io model with name '2D_versatile_fluo' exported to 'stardist_model.zip'\n"
]
}
],
"source": [
"modelfile = Path('bioimageio_model.zip')\n",
"modelfile = Path('stardist_model.zip')\n",
"\n",
"export_bioimageio(model, modelfile, img)"
]
Expand Down Expand Up @@ -262,7 +263,7 @@
"data": {
"text/plain": [
"StarDist2D(imported_model): YXC → YXC\n",
"├─ Directory: /home/uwe/research/stardist/stardist-feature-branch/examples/other2D/imported_model\n",
"├─ Directory: /home/uwe/research/stardist/stardist/examples/other2D/imported_model\n",
"└─ Config2D(axes='YXC', backbone='unet', grid=(2, 2), n_channel_in=1, n_channel_out=33, n_classes=None, n_dim=2, n_rays=32, net_conv_after_unet=128, net_input_shape=[None, None, 1], net_mask_shape=[None, None, 1], train_background_reg=0.0001, train_batch_size=8, train_checkpoint='weights_best.h5', train_checkpoint_epoch='weights_now.h5', train_checkpoint_last='weights_last.h5', train_class_weights=[1, 1], train_completion_crop=32, train_dist_loss='mae', train_epochs=800, train_foreground_only=0.9, train_learning_rate=0.0003, train_loss_weights=[1, 0.2], train_n_val_patches=None, train_patch_size=[256, 256], train_reduce_lr={'factor': 0.5, 'min_delta': 0, 'patience': 80}, train_sample_cache=True, train_shape_completion=False, train_steps_per_epoch=400, train_tensorboard=True, unet_activation='relu', unet_batch_norm=False, unet_dropout=0.0, unet_kernel_size=[3, 3], unet_last_activation='relu', unet_n_conv_per_depth=2, unet_n_depth=3, unet_n_filter_base=32, unet_pool=[2, 2], unet_prefix='', use_gpu=False)"
]
},
Expand Down
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -109,6 +109,7 @@ def build_extension(self, ext):
'csbdeep>=0.6.3',
'scikit-image',
'numba',
'imageio',
],

extras_require={
Expand Down
4 changes: 2 additions & 2 deletions stardist/big.py
Expand Up @@ -437,7 +437,7 @@ def __init__(self, coord, bbox=None, shape_max=None):
self.slice = tuple(slice(*r) for r in self.bbox)
self.shape = tuple(r[1]-r[0] for r in self.bbox)
rr,cc = polygon(*self.coord, self.shape)
self.mask = np.zeros(self.shape, np.bool)
self.mask = np.zeros(self.shape, bool)
self.mask[rr,cc] = True

@staticmethod
Expand All @@ -459,7 +459,7 @@ def __init__(self, dist, origin, rays, bbox=None, shape_max=None):
self.slice = tuple(slice(*r) for r in self.bbox)
self.shape = tuple(r[1]-r[0] for r in self.bbox)
_origin = origin.reshape(1,3) - np.array([r[0] for r in self.bbox]).reshape(1,3)
self.mask = polyhedron_to_label(dist[np.newaxis], _origin, rays, shape=self.shape, verbose=False).astype(np.bool)
self.mask = polyhedron_to_label(dist[np.newaxis], _origin, rays, shape=self.shape, verbose=False).astype(bool)

@staticmethod
def coords_bbox(*dist_origin, rays, shape_max=None):
Expand Down
56 changes: 42 additions & 14 deletions stardist/bioimageio_utils.py
Expand Up @@ -3,6 +3,7 @@
from zipfile import ZipFile
import numpy as np
import tempfile
from distutils.version import LooseVersion
from csbdeep.utils import axes_check_and_normalize, normalize, _raise


Expand Down Expand Up @@ -54,9 +55,9 @@
def _import(error=True):
try:
from importlib_metadata import metadata
from bioimageio.core.build_spec import build_model
from bioimageio.core.build_spec import build_model # type: ignore
import xarray as xr
import bioimageio.core
import bioimageio.core # type: ignore
except ImportError:
if error:
raise RuntimeError(
Expand All @@ -70,12 +71,30 @@ def _import(error=True):


def _create_stardist_dependencies(outdir):
from ruamel.yaml import YAML
from tensorflow import __version__ as tf_version
from . import __version__ as stardist_version
pkg_info = get_distribution("stardist")
reqs = ("tensorflow",) + tuple(map(str, pkg_info.requires()))
path = outdir / "requirements.txt"
# dependencies that start with the name "bioimageio" will be added as conda dependencies
reqs_conda = [str(req) for req in pkg_info.requires(extras=['bioimageio']) if str(req).startswith('bioimageio')]
# only stardist and tensorflow as pip dependencies
tf_major, tf_minor = LooseVersion(tf_version).version[:2]
reqs_pip = (f"stardist>={stardist_version}", f"tensorflow>={tf_major}.{tf_minor},<{tf_major+1}")
# conda environment
env = dict(
name = 'stardist',
channels = ['defaults', 'conda-forge'],
dependencies = [
('python>=3.7,<3.8' if tf_major == 1 else 'python>=3.7'),
*reqs_conda,
'pip', {'pip': reqs_pip},
],
)
yaml = YAML(typ='safe')
path = outdir / "environment.yaml"
with open(path, "w") as f:
f.write("\n".join(reqs))
return f"pip:{path}"
yaml.dump(env, f)
return f"conda:{path}"


def _create_stardist_doc(outdir):
Expand All @@ -95,9 +114,13 @@ def _get_stardist_metadata(outdir, model):
package_data = metadata("stardist")
doi_2d = "https://doi.org/10.1007/978-3-030-00934-2_30"
doi_3d = "https://doi.org/10.1109/WACV45572.2020.9093435"
authors = {
'Martin Weigert': dict(name='Martin Weigert', github_user='maweigert'),
'Uwe Schmidt': dict(name='Uwe Schmidt', github_user='uschmidt83'),
}
data = dict(
description=package_data["Summary"],
authors=list(dict(name=name.strip()) for name in package_data["Author"].split(",")),
authors=list(authors.get(name.strip(),dict(name=name.strip())) for name in package_data["Author"].split(",")),
git_repo=package_data["Home-Page"],
license=package_data["License"],
dependencies=_create_stardist_dependencies(outdir),
Expand Down Expand Up @@ -316,7 +339,7 @@ def export_bioimageio(
test_input,
test_input_axes=None,
test_input_norm_axes='ZYX',
name="bioimageio_model",
name=None,
mode="tensorflow_saved_model_bundle",
min_percentile=1.0,
max_percentile=99.8,
Expand All @@ -333,13 +356,14 @@ def export_bioimageio(
test_input: np.ndarray
input image for generating test data
test_input_axes: str or None
the axes of the test input, for example 'YX' for a 2d image or 'ZYX' for a 3d volume
using None assumes that axes of test_input are the same as those of model
the axes of the test input, for example 'YX' for a 2d image or 'ZYX' for a 3d volume
using None assumes that axes of test_input are the same as those of model
test_input_norm_axes: str
the axes of the test input which will be jointly normalized, for example 'ZYX' for all spatial dimensions ('Z' ignored for 2D input)
use 'ZYXC' to also jointly normalize channels (e.g. for RGB input images)
the axes of the test input which will be jointly normalized, for example 'ZYX' for all spatial dimensions ('Z' ignored for 2D input)
use 'ZYXC' to also jointly normalize channels (e.g. for RGB input images)
name: str
the name of this model (default: "bioimageio_model")
the name of this model (default: None)
if None, uses the (folder) name of the model (i.e. `model.name`)
mode: str
the export type for this model (default: "tensorflow_saved_model_bundle")
min_percentile: float
Expand All @@ -354,6 +378,10 @@ def export_bioimageio(
isinstance(model, (StarDist2D, StarDist3D)) or _raise(ValueError("not a valid model"))
0 <= min_percentile < max_percentile <= 100 or _raise(ValueError("invalid percentile values"))

if name is None:
name = model.name
name = str(name)

outpath = Path(outpath)
if outpath.suffix == "":
outdir = outpath
Expand All @@ -375,7 +403,7 @@ def export_bioimageio(
kwargs.update(overwrite_spec_kwargs)

build_model(name=name, output_path=zip_path, add_deepimagej_config=(model.config.n_dim==2), root=tmp_dir, **kwargs)
print(f"\nbioimage.io model exported to '{zip_path}'")
print(f"\nbioimage.io model with name '{name}' exported to '{zip_path}'")


def import_bioimageio(source, outpath):
Expand Down
16 changes: 16 additions & 0 deletions stardist/data/__init__.py
Expand Up @@ -5,6 +5,10 @@ def abspath(path):


def test_image_nuclei_2d(return_mask=False):
""" Fluorescence microscopy image and mask from the 2018 kaggle DSB challenge
Caicedo et al. "Nucleus segmentation across imaging experiments: the 2018 Data Science Bowl." Nature methods 16.12
"""
from tifffile import imread
img = imread(abspath("images/img2d.tif"))
mask = imread(abspath("images/mask2d.tif"))
Expand All @@ -13,7 +17,19 @@ def test_image_nuclei_2d(return_mask=False):
else:
return img


def test_image_he_2d():
""" H&E stained RGB example image from the Cancer Imaging Archive
https://www.cancerimagingarchive.net
"""
from imageio import imread
img = imread(abspath("images/histo.jpg"))
return img


def test_image_nuclei_3d(return_mask=False):
""" synthetic nuclei
"""
from tifffile import imread
img = imread(abspath("images/img3d.tif"))
mask = imread(abspath("images/mask3d.tif"))
Expand Down
Binary file added stardist/data/images/histo.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 9 additions & 5 deletions stardist/geometry/geom2d.py
Expand Up @@ -127,18 +127,22 @@ def _polygons_to_label_old(coord, prob, points, shape=None, thr=-np.inf):
return lbl


def dist_to_coord(dist, points):
def dist_to_coord(dist, points, scale_dist=(1,1)):
"""convert from polar to cartesian coordinates for a list of distances and center points
dist.shape = (n_polys, n_rays)
points.shape = (n_polys, 2)
len(scale_dist) = 2
return coord.shape = (n_polys,2,n_rays)
"""
dist = np.asarray(dist)
points = np.asarray(points)
assert dist.ndim==2 and points.ndim==2 and len(dist)==len(points) and points.shape[1]==2
assert dist.ndim==2 and points.ndim==2 and len(dist)==len(points) \
and points.shape[1]==2 and len(scale_dist)==2
n_rays = dist.shape[1]
phis = ray_angles(n_rays)
coord = points[...,np.newaxis] + (dist[:,np.newaxis]*np.array([np.sin(phis),np.cos(phis)]))
coord = (dist[:,np.newaxis]*np.array([np.sin(phis),np.cos(phis)])).astype(np.float32)
coord *= np.asarray(scale_dist).reshape(1,2,1)
coord += points[...,np.newaxis]
return coord


Expand All @@ -162,7 +166,7 @@ def polygons_to_label_coord(coord, shape, labels=None):
return lbl


def polygons_to_label(dist, points, shape, prob=None, thr=-np.inf):
def polygons_to_label(dist, points, shape, prob=None, thr=-np.inf, scale_dist=(1,1)):
"""converts distances and center points to label image
dist.shape = (n_polys, n_rays)
Expand All @@ -188,7 +192,7 @@ def polygons_to_label(dist, points, shape, prob=None, thr=-np.inf):
points = points[ind]
dist = dist[ind]

coord = dist_to_coord(dist, points)
coord = dist_to_coord(dist, points, scale_dist=scale_dist)

return polygons_to_label_coord(coord, shape=shape, labels=ind)

Expand Down
84 changes: 83 additions & 1 deletion stardist/matching.py
Expand Up @@ -3,11 +3,13 @@
from numba import jit
from tqdm import tqdm
from scipy.optimize import linear_sum_assignment
from skimage.measure import regionprops
from collections import namedtuple
from csbdeep.utils import _raise

matching_criteria = dict()


def label_are_sequential(y):
""" returns true if y has only sequential labels from 1... """
labels = np.unique(y)
Expand Down Expand Up @@ -49,6 +51,7 @@ def _label_overlap(x, y):
overlap[x[i],y[i]] += 1
return overlap


def _safe_divide(x,y, eps=1e-10):
"""computes a safe divide which returns 0 if y is zero"""
if np.isscalar(x) and np.isscalar(y):
Expand Down Expand Up @@ -167,7 +170,8 @@ def matching(y_true, y_pred, thresh=0.5, criterion='iou', report_matches=False):
n_matched = min(n_true, n_pred)

def _single(thr):
not_trivial = n_matched > 0 and np.any(scores >= thr)
# not_trivial = n_matched > 0 and np.any(scores >= thr)
not_trivial = n_matched > 0
if not_trivial:
# compute optimal matching with scores as tie-breaker
costs = -(scores >= thr).astype(float) - scores / (2*n_matched)
Expand Down Expand Up @@ -399,3 +403,81 @@ def relabel_sequential(label_field, offset=1):
inverse_map[offset:] = labels0
relabeled = forward_map[label_field]
return relabeled, forward_map, inverse_map



def group_matching_labels(ys, thresh=1e-10, criterion='iou'):
"""
Group matching objects (i.e. assign the same label id) in a
list of label images (e.g. consecutive frames of a time-lapse).
Uses function `matching` (with provided `criterion` and `thresh`) to
iteratively/greedily match and group objects/labels in consecutive images of `ys`.
To that end, matching objects are grouped together by assigning the same label id,
whereas unmatched objects are assigned a new label id.
At the end of this process, each label group will have been assigned a unique id.
Note that the label images `ys` will not be modified. Instead, they will initially
be duplicated and converted to data type `np.int32` before objects are grouped and the result
is returned. (Note that `np.int32` limits the number of label groups to at most 2147483647.)
Example
-------
import numpy as np
from stardist.data import test_image_nuclei_2d
from stardist.matching import group_matching_labels
_y = test_image_nuclei_2d(return_mask=True)[1]
labels = np.stack([_y, 2*np.roll(_y,10)], axis=0)
labels_new = group_matching_labels(labels)
Parameters
----------
ys : np.ndarray or list/tuple of np.ndarray
list/array of integer labels (2D or 3D)
"""
# check 'ys' without making a copy
len(ys) > 1 or _raise(ValueError("'ys' must have 2 or more entries"))
if isinstance(ys, np.ndarray):
_check_label_array(ys, 'ys')
ys.ndim > 1 or _raise(ValueError("'ys' must be at least 2-dimensional"))
ys_grouped = np.empty_like(ys, dtype=np.int32)
else:
all(_check_label_array(y, 'ys') for y in ys) or _raise(ValueError("'ys' must be a list of label images"))
all(y.shape==ys[0].shape for y in ys) or _raise(ValueError("all label images must have the same shape"))
ys_grouped = np.empty((len(ys),)+ys[0].shape, dtype=np.int32)

def _match_single(y_prev, y, next_id):
y = y.astype(np.int32, copy=False)
res = matching(y_prev, y, report_matches=True, thresh=thresh, criterion=criterion)
# relabel dict (for matching labels) that maps label ids from y -> y_prev
relabel = dict(reversed(res.matched_pairs[i]) for i in res.matched_tps)
y_grouped = np.zeros_like(y)
for r in regionprops(y):
m = (y[r.slice] == r.label)
if r.label in relabel:
y_grouped[r.slice][m] = relabel[r.label]
else:
y_grouped[r.slice][m] = next_id
next_id += 1
return y_grouped, next_id

ys_grouped[0] = ys[0]
next_id = ys_grouped[0].max() + 1
for i in range(len(ys)-1):
ys_grouped[i+1], next_id = _match_single(ys_grouped[i], ys[i+1], next_id)
return ys_grouped



def _shuffle_labels(y):
_check_label_array(y, 'y')
y2 = np.zeros_like(y)
ids = tuple(set(np.unique(y)) - {0})
relabel = dict(zip(ids,np.random.permutation(ids)))
for r in regionprops(y):
m = (y[r.slice] == r.label)
y2[r.slice][m] = relabel[r.label]
return y2

0 comments on commit b3780c3

Please sign in to comment.