Skip to content

Commit 18ccf41

Browse files
authored
Merge pull request #4 from gletort/epystar_appose
Epystar appose
2 parents 2c2dc2d + b156903 commit 18ccf41

12 files changed

Lines changed: 616 additions & 27 deletions

File tree

pyproject.toml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "fishfeats"
7-
version = "1.3.10"
7+
version = "1.3.18"
88
description = "Napari plugin for RNA-Fish+cells analysis pipeline"
99
license.file = "LICENSE"
1010
readme = "README.md"
@@ -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 ):
@@ -358,9 +361,13 @@ def separate_junctions_nuclei( self, wth_radxy=4, wth_radz=1, rmoutlier_radxy=5,
358361
self.nucstain = smoothNuclei(self.nucstain, radxy=smoothnucxy, radz=smoothnucz)
359362

360363
def separate_with_sepanet( self, model_dir ):
361-
from fish_feats.Separe import sepanet
362364
bothimage = np.copy(self.image[self.nucchan,])
363-
self.junstain, self.nucstain = sepanet( bothimage, model_dir )
365+
if ut.has_dependency( "tensorflow" ):
366+
from fish_feats.Separe import sepanet_local
367+
self.junstain, self.nucstain = sepanet_local( bothimage, model_dir )
368+
else:
369+
from fish_feats.Separe import sepanet_appose
370+
self.junstain, self.nucstain = sepanet_appose( bothimage, model_dir )
364371

365372
def should_separate( self ):
366373
if self.junchan==self.nucchan:
@@ -606,7 +613,7 @@ def do_segmentation_cellpose(self, diameter, threshold, resample=True, in3D=True
606613
self.pop.setNucleiImage(self.nucmask)
607614

608615
# stardist2D+association 3D
609-
def do_segmentation_stardist(self, threshold, overlap, assoMethod, associationlim, threshold_overlap):
616+
def do_segmentation_stardist(self, threshold, overlap, assoMethod, associationlim, threshold_overlap, progress_bar=None):
610617
ut.show_info("Segmenting nuclei with Stardist2D+association3D")
611618
from fish_feats.SegmentObj import prepNuclei, getNuclei_stardist2DAsso3D
612619
treatedNuclei = prepNuclei(self.nucstain) ## normalize the image
@@ -616,7 +623,7 @@ def do_segmentation_stardist(self, threshold, overlap, assoMethod, associationli
616623
assoMode = assoMethod,
617624
assolim=associationlim,
618625
threshold_overlap=threshold_overlap,
619-
verbose=self.verbose )
626+
verbose=self.verbose, progress_bar=progress_bar )
620627
if self.nucmask is None:
621628
return
622629
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/Naparing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from fish_feats.NapaMix import CheckScale, CropImage, Association, Separation, CytoplasmMeasure, ThresholdChannel
1414
from fish_feats import ClassifyCells as cc
1515
import fish_feats.FishWidgets as fwid
16-
from fish_feats._button_grid import ButtonGrid
16+
#from fish_feats._button_grid import ButtonGrid
1717

1818
"""
1919
Handle the UI through napari plugin

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

0 commit comments

Comments
 (0)