Skip to content

Commit 011e388

Browse files
authored
Merge pull request #5 from gletort/main
update the rnanuclei branch to main version
2 parents 4a2628b + 2b608e7 commit 011e388

11 files changed

Lines changed: 614 additions & 25 deletions

File tree

pyproject.toml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,12 @@ dependencies = [
2727
"opencv_python_headless<4.12; python_version< '3.11' ",
2828
"opencv_python_headless; python_version >= '3.11'",
2929
"pyqt6<6.10; python_version < '3.11'",
30-
"epyseg; python_version < '3.11'",
31-
"tensorflow<2.15; python_version < '3.11'",
32-
"tensorflow; python_version >= '3.11'",
3330
"big-fish>=0.6.2",
3431
"napari<0.6.4; python_version < '3.11'",
35-
"napari; python_version >= '3.11'",
32+
"napari;python_version >= '3.11'",
33+
"appose",
3634
"nninteractive",
37-
"torch<=2.6",
35+
"torch<=2.7",
3836
"numpy==1.26.4; python_version < '3.11'",
3937
"numpy; python_version >= '3.11'",
4038
"pyqt5",
@@ -47,6 +45,14 @@ dependencies = [
4745
"lxml",
4846
"packaging",
4947
"cellpose[distributed]",
48+
]
49+
50+
[project.optional-dependencies]
51+
52+
full = [
53+
"epyseg; python_version < '3.11'",
54+
"tensorflow<2.15; python_version < '3.11'",
55+
"tensorflow; python_version >= '3.11'",
5056
"stardist",
5157
]
5258
#dynamic = ["version"]

src/fish_feats/MainImage.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99

1010
try:
1111
from fish_feats.SegmentObj import local_max_proj, prepJunctions, segmentJunctions
12+
except ImportError as e:
13+
print( "Module missing in seg "+e )
14+
try:
1215
from fish_feats.RNASpots import RNASpots
13-
except ImportError:
14-
print("Module missing")
16+
except ImportError as e:
17+
print( "Module missing in RNASpots "+e )
1518

1619
####### Z map functions
1720
def score_each_z( img3d, projimg ):
@@ -366,9 +369,13 @@ def separate_junctions_nuclei( self, wth_radxy=4, wth_radz=1, rmoutlier_radxy=5,
366369
self.nucstain = smoothNuclei(self.nucstain, radxy=smoothnucxy, radz=smoothnucz)
367370

368371
def separate_with_sepanet( self, model_dir ):
369-
from fish_feats.Separe import sepanet
370372
bothimage = np.copy(self.image[self.nucchan,])
371-
self.junstain, self.nucstain = sepanet( bothimage, model_dir )
373+
if ut.has_dependency( "tensorflow" ):
374+
from fish_feats.Separe import sepanet_local
375+
self.junstain, self.nucstain = sepanet_local( bothimage, model_dir )
376+
else:
377+
from fish_feats.Separe import sepanet_appose
378+
self.junstain, self.nucstain = sepanet_appose( bothimage, model_dir )
372379

373380
def should_separate( self ):
374381
if self.junchan==self.nucchan:
@@ -614,7 +621,7 @@ def do_segmentation_cellpose(self, diameter, threshold, resample=True, in3D=True
614621
self.pop.setNucleiImage(self.nucmask)
615622

616623
# stardist2D+association 3D
617-
def do_segmentation_stardist(self, threshold, overlap, assoMethod, associationlim, threshold_overlap):
624+
def do_segmentation_stardist(self, threshold, overlap, assoMethod, associationlim, threshold_overlap, progress_bar=None):
618625
ut.show_info("Segmenting nuclei with Stardist2D+association3D")
619626
from fish_feats.SegmentObj import prepNuclei, getNuclei_stardist2DAsso3D
620627
treatedNuclei = prepNuclei(self.nucstain) ## normalize the image
@@ -624,7 +631,7 @@ def do_segmentation_stardist(self, threshold, overlap, assoMethod, associationli
624631
assoMode = assoMethod,
625632
assolim=associationlim,
626633
threshold_overlap=threshold_overlap,
627-
verbose=self.verbose )
634+
verbose=self.verbose, progress_bar=progress_bar )
628635
if self.nucmask is None:
629636
return
630637
self.nucmask[self.nucmask>0] = self.nucmask[self.nucmask>0] + 1

src/fish_feats/NapaNuclei.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ def go_segnuclei_stardist( self ):
251251
float(self.stardist_nuclei_overlap.text()),
252252
self.stardist_association_method.currentText(),
253253
float(self.stardist_association_distance_limit_micron.text()),
254-
float(self.stardist_threshold_overlap.text()) )
254+
float(self.stardist_threshold_overlap.text()), pbar )
255255
if self.mig.nucmask is None:
256256
ut.close_progress( self.viewer, pbar )
257257
return

src/fish_feats/SegmentObj.py

Lines changed: 115 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,54 @@
99
from fish_feats.Association import associateNucleus, associateNucleusOverlap
1010
from skimage.filters import sato
1111
from skimage.morphology import skeletonize, binary_closing
12-
from skimage.segmentation import find_boundaries
13-
from skimage.segmentation import clear_border
12+
from skimage.segmentation import find_boundaries, clear_border
1413
from skimage.measure import label
1514
from skimage.exposure import adjust_gamma
1615
import time
1716
import tempfile
17+
from importlib import resources
18+
import appose
1819

1920
"""
2021
Functions to segment junctions or nuclei
2122
"""
2223

23-
def run_epyseg(input_folder):
24+
def run_epyseg_appose( input_folder ):
25+
""" Run epyseg on a separate virtual environement through appose """
26+
try:
27+
pixi_file = resources.files("fish_feats.resources").joinpath("pixi.toml")
28+
epyseg_script = resources.files("fish_feats.resources").joinpath("run_epyseg.py")
29+
epyseg_script = epyseg_script.read_text()
30+
ut.show_info("Build/Load tensorflow environment")
31+
env = appose.pixi( pixi_file ).log_debug()
32+
env = env.subscribe_output( lambda line: print("OUT:", line, end="") )
33+
env = env.subscribe_error( lambda line: print("DBG:", line, end="") )
34+
env_name = ut.get_env_name()
35+
env = env.environment(env_name).build()
36+
ut.show_info(f"Environment built at: {env.base()}")
37+
python = env.python().init("import numpy as np; import tensorflow as tf;"\
38+
"from epyseg.deeplearning.deepl import EZDeepLearning; import epyseg.deeplearning.deepl as deepl;")
39+
python.debug(lambda msg: print("[DBG]", msg))
40+
41+
def log_listener(event):
42+
""" Transfer appose task message to the main logger """
43+
if event.message:
44+
print( f"[task] {event.message}" )
45+
46+
try:
47+
task = python.task( epyseg_script )
48+
task.listen( log_listener )
49+
task.inputs["input_folder"] = input_folder
50+
task.wait_for()
51+
except Exception as e:
52+
raise RuntimeError("Running epyseg in separated environement failed") from e
53+
finally:
54+
python.close()
55+
except Exception as e:
56+
print(e)
57+
raise RuntimeError("Epyseg in separated environement failed") from e
58+
59+
def run_epyseg_local(input_folder):
2460
import tensorflow as tf
2561

2662
# libraries loaded checking epyseg to see if everything is functional
@@ -93,12 +129,14 @@ def run_epyseg(input_folder):
93129
#deepTA = None
94130
del deepTA
95131

132+
96133
def run_epyseg_onimage(img, filedir, filename, verbose=True):
134+
""" Run epyseg on an image: create temp dir because of epyseg requirements """
97135
from PIL import Image
98-
import os
99136

100137
binimg = None
101138
tmpdir_path = None
139+
appose = not ut.has_dependency( "epyseg" )
102140
try:
103141
with tempfile.TemporaryDirectory() as tmpdir:
104142
print("tmp dir "+str(tmpdir))
@@ -114,7 +152,10 @@ def run_epyseg_onimage(img, filedir, filename, verbose=True):
114152
print("Warning, issue in creating "+predict_output_folder+" folder")
115153

116154
## run Epyseg on tmp directory (contains current image)
117-
run_epyseg(tmpdir)
155+
if appose:
156+
run_epyseg_appose( tmpdir )
157+
else:
158+
run_epyseg_local(tmpdir)
118159

119160
## return result and delete files
120161
im = Image.open(os.path.join(tmpdir,"predict",inputname))
@@ -132,7 +173,6 @@ def run_epyseg_onimage(img, filedir, filename, verbose=True):
132173

133174
def run_epyseg_onimage_fold(img, filedir, filename, verbose=True):
134175
from PIL import Image
135-
import os
136176
import shutil
137177
import stat
138178

@@ -332,7 +372,13 @@ def prepNuclei(img):
332372
img = (img-quants[0])/(quants[1]-quants[0])
333373
return img
334374

335-
def stardist2D(img, prob, over):
375+
def share_as_ndarray(img: np.ndarray) -> appose.NDArray:
376+
"""Copies a NumPy array into a same-sized newly allocated block of shared memory."""
377+
shared = appose.NDArray(str(img.dtype), img.shape)
378+
shared.ndarray()[:] = img
379+
return shared
380+
381+
def stardist2D_local(img, prob, over, progress_bar=None ):
336382
""" run stardist model, segment nuclei in 2D """
337383
try:
338384
from csbdeep.utils import Path, normalize
@@ -355,13 +401,73 @@ def stardist2D(img, prob, over):
355401
nuclei[ind,] = labels
356402
return nuclei
357403

358-
def getNuclei_stardist2DAsso3D(nucimg, scaleXY, proba=0.55, overlap=0.1, assoMode="Munkres", assolim=3, threshold_overlap=0.25, verbose=True):
404+
def stardist2D_appose( img, prob, over, progress_bar=None ):
405+
""" run stardist model, segment nuclei in 2D """
406+
try:
407+
pixi_file = resources.files("fish_feats.resources").joinpath("pixi.toml")
408+
ut.show_info("Build/Load tensorflow environment")
409+
env = appose.pixi( pixi_file ).log_debug()
410+
env = env.subscribe_output( lambda line: print("OUT:", line, end="") )
411+
env = env.subscribe_error( lambda line: print("DBG:", line, end="") )
412+
env_name = ut.get_env_name()
413+
env = env.environment(env_name).build()
414+
ut.show_info(f"Environment built at: {env.base()}")
415+
python = env.python().init("import numpy as np; import tensorflow as tf;" \
416+
"from stardist.models import StarDist2D")
417+
#python.debug(lambda msg: print("[DBG]", msg))
418+
if progress_bar is None:
419+
progress_bar = ut.start_progress( None, total=1, descr="Stardist segmentation" )
420+
toclose = True
421+
else:
422+
progress_bar.set_description( "Stardist segmentation" )
423+
toclose = False
424+
425+
def log_listener(event):
426+
""" Transfer appose task message to the main logger """
427+
if event.current and event.maximum:
428+
print( f"Segmenting slice {event.current}/{event.maximum}" )
429+
#ut.show_info( f"Segmenting slice {event.current}/{event.maximum}" )
430+
#progress_bar.update( cur )
431+
#progress_bar.total = total
432+
else:
433+
if event.message:
434+
print( f"[task] {event.message} " )
435+
436+
try:
437+
stardist_script = resources.files("fish_feats.resources").joinpath("run_stardist.py")
438+
stardist_script = stardist_script.read_text()
439+
with share_as_ndarray(img) as image:
440+
task = python.task( stardist_script )
441+
task.listen( log_listener )
442+
task.inputs["img"] = image
443+
task.inputs["stardist_probability"] = prob
444+
task.inputs["stardist_overlap"] = over
445+
task.wait_for()
446+
result = image.ndarray()
447+
return np.uint16( result )
448+
except Exception as e:
449+
raise RuntimeError("Running stardist in separated environement failed") from e
450+
finally:
451+
python.close()
452+
if toclose:
453+
ut.close_progress( None, progress_bar=progress_bar )
454+
except Exception as e:
455+
raise RuntimeError("Stardist in separated environement failed") from e
456+
457+
def getNuclei_stardist2DAsso3D(nucimg, scaleXY, proba=0.55, overlap=0.1, assoMode="Munkres", assolim=3, threshold_overlap=0.25, verbose=True, progress_bar=None):
359458
""" Segment nuclei with Stardist2D and reconstruct in 3D - return the nuclei list """
360459

361460
## segment 2D
362-
labnuc = stardist2D(nucimg, prob=proba, over=overlap)
461+
appose = not ut.has_dependency( "stardist" )
462+
if appose:
463+
labnuc = stardist2D_appose(nucimg, prob=proba, over=overlap, progress_bar=progress_bar)
464+
else:
465+
labnuc = stardist2D_local(nucimg, prob=proba, over=overlap, progress_bar=progress_bar)
466+
363467
if labnuc is None:
364468
return None
469+
if progress_bar is not None:
470+
progress_bar.set_description( "Reconstructing now in 3D..." )
365471
## reconstruct 3D
366472
if assoMode == "Munkres":
367473
labels = associateNucleus(labnuc, dlimit=assolim, scaleXY=scaleXY) ## distance 2D in micrcons -2.5

src/fish_feats/Separe.py

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,69 @@
22
import cv2
33
import scipy.ndimage as ndimage
44
from napari.utils.notifications import show_info
5-
from math import floor
65
import time
6+
import fish_feats.Utils as ut
77
from napari.utils import progress
8+
from importlib import resources
9+
import appose
810

9-
def sepanet( img, sepdir, patchsize=256 ):
11+
def share_as_ndarray(img: np.ndarray) -> appose.NDArray:
12+
"""Copies a NumPy array into a same-sized newly allocated block of shared memory."""
13+
shared = appose.NDArray(str(img.dtype), img.shape)
14+
shared.ndarray()[:] = img
15+
return shared
16+
17+
def sepanet_appose( img, sepdir, patchsize=256 ):
18+
""" Separate junctions and nuclei with trained DL """
19+
print("sepaNet with models in "+str(sepdir))
20+
try:
21+
pixi_file = resources.files("fish_feats.resources").joinpath("pixi.toml")
22+
ut.show_info("Build/Load tensorflow environment")
23+
env = appose.pixi( pixi_file ).log_debug()
24+
env = env.subscribe_output( lambda line: print("OUT:", line, end="") )
25+
env_name = ut.get_env_name()
26+
env = env.environment(env_name).build()
27+
ut.show_info(f"Environment built at: {env.base()}")
28+
python = env.python().init("import numpy as np; import tensorflow as tf;"\
29+
"import keras; import scipy.ndimage as ndimage")
30+
#python.debug(lambda msg: print("[DBG]", msg))
31+
progress_bar = ut.start_progress( None, total=1, descr="SepaNet separation" )
32+
33+
def log_listener(event):
34+
""" Transfer appose task message to the main logger """
35+
if event.current and event.maximum:
36+
print( f"Separating slice {event.current}/{event.maximum}" )
37+
#ut.show_info( f"Separiting slice {event.current}/{event.maximum}" )
38+
#progress_bar.update( cur )
39+
#progress_bar.total = total
40+
else:
41+
if event.message:
42+
print( f"[task] {event.message} " )
43+
44+
try:
45+
sepanet_script = resources.files("fish_feats.resources").joinpath("run_sepanet.py")
46+
sepanet_script = sepanet_script.read_text()
47+
result_junc = None
48+
result_nuc = None
49+
with share_as_ndarray(img) as image:
50+
task = python.task( sepanet_script )
51+
task.listen( log_listener )
52+
task.inputs["img"] = image
53+
task.inputs["patchsize"] = patchsize
54+
task.inputs["model_directory"] = sepdir
55+
task.wait_for()
56+
result_junc = np.uint8( task.outputs["junctions"].ndarray().copy() )
57+
result_nuc = np.uint8( task.outputs["nuclei"].ndarray().copy() )
58+
return result_junc, result_nuc
59+
except Exception as e:
60+
raise RuntimeError("Running SepaNet in separated environement failed") from e
61+
finally:
62+
python.close()
63+
ut.close_progress( None, progress_bar=progress_bar )
64+
except Exception as e:
65+
raise RuntimeError("SepaNet in separated environement failed") from e
66+
67+
def sepanet_local( img, sepdir, patchsize=256 ):
1068
""" Separate junctions and nuclei with trained DL """
1169
print("sepaNet with models in "+str(sepdir))
1270

@@ -28,6 +86,7 @@ def sepanet( img, sepdir, patchsize=256 ):
2886
return res[:,:,:,0], res[:,:,:,1]
2987

3088
def run_on_image(imgtest, model, patchsize, step=50):
89+
from math import floor
3190
imgtest.astype(float)
3291
imgtest = normalise(imgtest)
3392
imgtest = smooth(imgtest)
@@ -74,7 +133,6 @@ def run_on_image(imgtest, model, patchsize, step=50):
74133
resimg = np.uint8(resimg*255)
75134
return resimg
76135

77-
78136
def normalise(img):
79137
quants = np.quantile(img, [0.1, 0.99])
80138
img = (img - quants[0]) / (quants[1]-quants[0])
@@ -98,13 +156,23 @@ def both_MSE_percent_1( y_true, y_pred ):
98156
acc0 = keras.metrics.mean_absolute_percentage_error(y_true[:,:,:,1], y_pred[:,:,:,1])
99157
return acc0
100158

101-
102159
def mse_two(y_true, y_pred):
103160
y_true = tf.reshape(y_true, [-1])
104161
y_pred = tf.reshape(y_pred, [-1])
105162
mse = tf.keras.losses.MeanSquaredError()
106163
return mse(y_true, y_pred)
107164

165+
### Separation based on filterings
166+
def junctionsCoherence(img, medblur=3, quant=0.98, dsig=3, cornersig=5, ratio=0.5, niter=4):
167+
## Coherence enhancing diffusion, Weickert et al.
168+
#from skimage import exposure
169+
height, width = img.shape[:2]
170+
qmax = np.quantile(img, quant)
171+
qmin = np.min(img)
172+
img = np.uint8( (img-qmax)/(qmax-qmin)*255 )
173+
#img = cv2.normalize(src=img, dst=None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U)
174+
175+
108176
### Separation based on filterings
109177
def junctionsCoherence(img, medblur=3, quant=0.98, dsig=3, cornersig=5, ratio=0.5, niter=4):
110178
## Coherence enhancing diffusion, Weickert et al.

0 commit comments

Comments
 (0)