exhale-lung 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. exhale_lung-1.3.0/LICENSE +21 -0
  2. exhale_lung-1.3.0/MANIFEST.in +21 -0
  3. exhale_lung-1.3.0/PKG-INFO +48 -0
  4. exhale_lung-1.3.0/README.md +21 -0
  5. exhale_lung-1.3.0/exhale/__init__.py +8 -0
  6. exhale_lung-1.3.0/exhale/__main__.py +7 -0
  7. exhale_lung-1.3.0/exhale/analysisdialog.py +113 -0
  8. exhale_lung-1.3.0/exhale/analysisworker.py +127 -0
  9. exhale_lung-1.3.0/exhale/cluster_analysis/__init__.py +7 -0
  10. exhale_lung-1.3.0/exhale/cluster_analysis/xrf_clustering.py +169 -0
  11. exhale_lung-1.3.0/exhale/cluster_analysis/xrf_general_functions.py +65 -0
  12. exhale_lung-1.3.0/exhale/cluster_analysis/xrf_interface.py +140 -0
  13. exhale_lung-1.3.0/exhale/cluster_analysis/xrf_main.py +320 -0
  14. exhale_lung-1.3.0/exhale/constants.py +17 -0
  15. exhale_lung-1.3.0/exhale/elementsettings.py +183 -0
  16. exhale_lung-1.3.0/exhale/exceptiondialog.py +33 -0
  17. exhale_lung-1.3.0/exhale/exhale.py +150 -0
  18. exhale_lung-1.3.0/exhale/exhale_qt.py +446 -0
  19. exhale_lung-1.3.0/exhale/exhalewindow.py +1414 -0
  20. exhale_lung-1.3.0/exhale/imagecomposer.py +440 -0
  21. exhale_lung-1.3.0/exhale/imagedialog.py +154 -0
  22. exhale_lung-1.3.0/exhale/imagesettings.py +212 -0
  23. exhale_lung-1.3.0/exhale/listwidgets.py +214 -0
  24. exhale_lung-1.3.0/exhale/naparihelper.py +243 -0
  25. exhale_lung-1.3.0/exhale/overridecursor.py +18 -0
  26. exhale_lung-1.3.0/exhale/plotwidgets.py +12 -0
  27. exhale_lung-1.3.0/exhale/projectio.py +276 -0
  28. exhale_lung-1.3.0/exhale/resources/icons/document-open.svg +2 -0
  29. exhale_lung-1.3.0/exhale/resources/icons/lungs.png +0 -0
  30. exhale_lung-1.3.0/exhale/resources/models/2D_versatile_fluo_copy/config.json +1 -0
  31. exhale_lung-1.3.0/exhale/resources/models/2D_versatile_fluo_copy/thresholds.json +1 -0
  32. exhale_lung-1.3.0/exhale/resources/models/2D_versatile_fluo_copy/weights_best.h5 +0 -0
  33. exhale_lung-1.3.0/exhale/source_refs.py +203 -0
  34. exhale_lung-1.3.0/exhale/xrf_refcopy/xrf_element_channel.py +102 -0
  35. exhale_lung-1.3.0/exhale/xrf_refcopy/xrf_other_channel.py +189 -0
  36. exhale_lung-1.3.0/exhale/xrf_refcopy/xrf_sample_class.py +562 -0
  37. exhale_lung-1.3.0/exhale/xrf_refcopy/xrf_utils.py +302 -0
  38. exhale_lung-1.3.0/exhale_lung.egg-info/PKG-INFO +48 -0
  39. exhale_lung-1.3.0/exhale_lung.egg-info/SOURCES.txt +45 -0
  40. exhale_lung-1.3.0/exhale_lung.egg-info/dependency_links.txt +1 -0
  41. exhale_lung-1.3.0/exhale_lung.egg-info/entry_points.txt +2 -0
  42. exhale_lung-1.3.0/exhale_lung.egg-info/requires.txt +17 -0
  43. exhale_lung-1.3.0/exhale_lung.egg-info/top_level.txt +3 -0
  44. exhale_lung-1.3.0/pyproject.toml +52 -0
  45. exhale_lung-1.3.0/run_exhale.py +7 -0
  46. exhale_lung-1.3.0/setup.cfg +4 -0
  47. exhale_lung-1.3.0/version_bump.sh +93 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Carl Troein, Tom Delaire, Bryan Falcones, Emanuel Larsson, Karina Thånell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ include README.md
2
+ include README.md
3
+ include pyproject.toml
4
+ include MANIFEST.in
5
+ include run_exhale.py
6
+
7
+ recursive-include exhale *.py
8
+ recursive-include exhale/resources/icons *.png *.svg
9
+ recursive-include exhale/resources/models *.json *.h5
10
+
11
+ exclude .gitignore
12
+ exclude win_build.py
13
+
14
+ prune .github
15
+ prune build
16
+ prune dist
17
+ prune packaging
18
+ prune exhale.egg-info
19
+ prune exhale/resources/ui
20
+
21
+ global-exclude __pycache__ *.py[cod]
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: exhale-lung
3
+ Version: 1.3.0
4
+ Summary: EXHALE, Efficient X-ray Hub Aiding Lung Explorations
5
+ Author: Tom Delaire, Emanuel Larsson, Bryan Falcones, Karina Thånell
6
+ Author-email: Carl Troein <carl.troein@cec.lu.se>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/ctroein/exhale
9
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: numpy
14
+ Requires-Dist: scipy
15
+ Requires-Dist: scikit-learn
16
+ Requires-Dist: pandas
17
+ Requires-Dist: h5py
18
+ Requires-Dist: napari
19
+ Requires-Dist: imageio
20
+ Requires-Dist: silx
21
+ Requires-Dist: csbdeep
22
+ Requires-Dist: pyqt5
23
+ Requires-Dist: tensorflow==2.15.*; sys_platform == "win32"
24
+ Requires-Dist: tensorflow; sys_platform != "win32"
25
+ Requires-Dist: stardist
26
+ Dynamic: license-file
27
+
28
+ # EXHALE
29
+ ## Efficient X-ray Hub Aiding Lung Explorations
30
+
31
+ This project aims to develop a specialized tool for investigating elemental aspects of chronic lung diseases.
32
+
33
+ From the [VINNOVA project
34
+ description](https://www.vinnova.se/en/p/exhale--efficient-x-ray-hub-aiding-lung-explorations/):
35
+ The purpose of this project is to advance understanding and treatment of
36
+ chronic lung diseases, leading causes of mortality worldwide, by
37
+ characterizing elements in lung tissue suspected to impact their
38
+ pathogenesis. The industrial partners TrulyLabs and AnaMar in collaboration
39
+ with MAX IV, Lung Biology and Correlative Image Processing and Analysis
40
+ (CIPA) fom Lund University, aim to streamline a workflow of X-ray imaging
41
+ studies for lung diseases, making these methods more accessible for
42
+ biomedical companies and clinical researchers.
43
+
44
+ ## Currect functionality
45
+
46
+ * Loading HDF5 files with XRF data from the NanoMAX beamline at MAX IV.
47
+ * Composing element maps into publication-ready figures.
48
+ * Segmentation of cells and clustering of areas in element maps.
@@ -0,0 +1,21 @@
1
+ # EXHALE
2
+ ## Efficient X-ray Hub Aiding Lung Explorations
3
+
4
+ This project aims to develop a specialized tool for investigating elemental aspects of chronic lung diseases.
5
+
6
+ From the [VINNOVA project
7
+ description](https://www.vinnova.se/en/p/exhale--efficient-x-ray-hub-aiding-lung-explorations/):
8
+ The purpose of this project is to advance understanding and treatment of
9
+ chronic lung diseases, leading causes of mortality worldwide, by
10
+ characterizing elements in lung tissue suspected to impact their
11
+ pathogenesis. The industrial partners TrulyLabs and AnaMar in collaboration
12
+ with MAX IV, Lung Biology and Correlative Image Processing and Analysis
13
+ (CIPA) fom Lund University, aim to streamline a workflow of X-ray imaging
14
+ studies for lung diseases, making these methods more accessible for
15
+ biomedical companies and clinical researchers.
16
+
17
+ ## Currect functionality
18
+
19
+ * Loading HDF5 files with XRF data from the NanoMAX beamline at MAX IV.
20
+ * Composing element maps into publication-ready figures.
21
+ * Segmentation of cells and clustering of areas in element maps.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Created on Wed May 29 12:23:17 2024
5
+
6
+ @author: carl
7
+ """
8
+
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from .exhale import main
5
+
6
+ if __name__ == "__main__":
7
+ main()
@@ -0,0 +1,113 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Form implementation generated from reading ui file 'analysisdialog.ui'
4
+ #
5
+ # Created by: PyQt5 UI code generator 5.15.9
6
+ #
7
+ # WARNING: Any manual changes made to this file will be lost when pyuic5 is
8
+ # run again. Do not edit this file unless you know what you are doing.
9
+
10
+
11
+ from PyQt5 import QtCore, QtGui, QtWidgets
12
+
13
+
14
+ class Ui_AnalysisDialog(object):
15
+ def setupUi(self, AnalysisDialog):
16
+ AnalysisDialog.setObjectName("AnalysisDialog")
17
+ AnalysisDialog.resize(415, 314)
18
+ self.verticalLayout = QtWidgets.QVBoxLayout(AnalysisDialog)
19
+ self.verticalLayout.setObjectName("verticalLayout")
20
+ self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
21
+ self.horizontalLayout_6.setObjectName("horizontalLayout_6")
22
+ self.label_2 = QtWidgets.QLabel(AnalysisDialog)
23
+ self.label_2.setObjectName("label_2")
24
+ self.horizontalLayout_6.addWidget(self.label_2)
25
+ self.clusterMinK = QtWidgets.QSpinBox(AnalysisDialog)
26
+ self.clusterMinK.setMinimum(1)
27
+ self.clusterMinK.setStepType(QtWidgets.QAbstractSpinBox.AdaptiveDecimalStepType)
28
+ self.clusterMinK.setProperty("value", 3)
29
+ self.clusterMinK.setObjectName("clusterMinK")
30
+ self.horizontalLayout_6.addWidget(self.clusterMinK)
31
+ self.label_10 = QtWidgets.QLabel(AnalysisDialog)
32
+ self.label_10.setObjectName("label_10")
33
+ self.horizontalLayout_6.addWidget(self.label_10)
34
+ self.clusterMaxK = QtWidgets.QSpinBox(AnalysisDialog)
35
+ self.clusterMaxK.setMinimum(1)
36
+ self.clusterMaxK.setMaximum(999)
37
+ self.clusterMaxK.setStepType(QtWidgets.QAbstractSpinBox.AdaptiveDecimalStepType)
38
+ self.clusterMaxK.setProperty("value", 10)
39
+ self.clusterMaxK.setObjectName("clusterMaxK")
40
+ self.horizontalLayout_6.addWidget(self.clusterMaxK)
41
+ self.horizontalLayout_6.setStretch(0, 1)
42
+ self.horizontalLayout_6.setStretch(1, 1)
43
+ self.horizontalLayout_6.setStretch(3, 1)
44
+ self.verticalLayout.addLayout(self.horizontalLayout_6)
45
+ self.horizontalLayout = QtWidgets.QHBoxLayout()
46
+ self.horizontalLayout.setObjectName("horizontalLayout")
47
+ self.label = QtWidgets.QLabel(AnalysisDialog)
48
+ self.label.setObjectName("label")
49
+ self.horizontalLayout.addWidget(self.label)
50
+ self.clusterNInit = QtWidgets.QSpinBox(AnalysisDialog)
51
+ self.clusterNInit.setMinimum(1)
52
+ self.clusterNInit.setMaximum(999)
53
+ self.clusterNInit.setStepType(QtWidgets.QAbstractSpinBox.AdaptiveDecimalStepType)
54
+ self.clusterNInit.setProperty("value", 50)
55
+ self.clusterNInit.setObjectName("clusterNInit")
56
+ self.horizontalLayout.addWidget(self.clusterNInit)
57
+ self.horizontalLayout.setStretch(0, 2)
58
+ self.horizontalLayout.setStretch(1, 1)
59
+ self.verticalLayout.addLayout(self.horizontalLayout)
60
+ self.gridLayout = QtWidgets.QGridLayout()
61
+ self.gridLayout.setObjectName("gridLayout")
62
+ self.label_3 = QtWidgets.QLabel(AnalysisDialog)
63
+ self.label_3.setObjectName("label_3")
64
+ self.gridLayout.addWidget(self.label_3, 0, 0, 1, 1)
65
+ self.label_4 = QtWidgets.QLabel(AnalysisDialog)
66
+ self.label_4.setObjectName("label_4")
67
+ self.gridLayout.addWidget(self.label_4, 0, 2, 1, 1)
68
+ self.nucleiExpansion = QtWidgets.QSpinBox(AnalysisDialog)
69
+ self.nucleiExpansion.setMinimum(1)
70
+ self.nucleiExpansion.setMaximum(999)
71
+ self.nucleiExpansion.setStepType(QtWidgets.QAbstractSpinBox.AdaptiveDecimalStepType)
72
+ self.nucleiExpansion.setProperty("value", 15)
73
+ self.nucleiExpansion.setObjectName("nucleiExpansion")
74
+ self.gridLayout.addWidget(self.nucleiExpansion, 0, 1, 1, 1)
75
+ self.label_5 = QtWidgets.QLabel(AnalysisDialog)
76
+ self.label_5.setObjectName("label_5")
77
+ self.gridLayout.addWidget(self.label_5, 1, 0, 1, 1)
78
+ self.nucleiMinArea = QtWidgets.QSpinBox(AnalysisDialog)
79
+ self.nucleiMinArea.setMinimum(1)
80
+ self.nucleiMinArea.setMaximum(999)
81
+ self.nucleiMinArea.setStepType(QtWidgets.QAbstractSpinBox.AdaptiveDecimalStepType)
82
+ self.nucleiMinArea.setProperty("value", 100)
83
+ self.nucleiMinArea.setObjectName("nucleiMinArea")
84
+ self.gridLayout.addWidget(self.nucleiMinArea, 1, 1, 1, 1)
85
+ self.label_6 = QtWidgets.QLabel(AnalysisDialog)
86
+ self.label_6.setObjectName("label_6")
87
+ self.gridLayout.addWidget(self.label_6, 1, 2, 1, 1)
88
+ self.gridLayout.setColumnStretch(0, 2)
89
+ self.gridLayout.setColumnStretch(1, 1)
90
+ self.verticalLayout.addLayout(self.gridLayout)
91
+ spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
92
+ self.verticalLayout.addItem(spacerItem)
93
+ self.buttonBox = QtWidgets.QDialogButtonBox(AnalysisDialog)
94
+ self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
95
+ self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Close)
96
+ self.buttonBox.setObjectName("buttonBox")
97
+ self.verticalLayout.addWidget(self.buttonBox)
98
+
99
+ self.retranslateUi(AnalysisDialog)
100
+ self.buttonBox.accepted.connect(AnalysisDialog.accept) # type: ignore
101
+ self.buttonBox.rejected.connect(AnalysisDialog.reject) # type: ignore
102
+ QtCore.QMetaObject.connectSlotsByName(AnalysisDialog)
103
+
104
+ def retranslateUi(self, AnalysisDialog):
105
+ _translate = QtCore.QCoreApplication.translate
106
+ AnalysisDialog.setWindowTitle(_translate("AnalysisDialog", "Analysis options"))
107
+ self.label_2.setText(_translate("AnalysisDialog", "Clustering K"))
108
+ self.label_10.setText(_translate("AnalysisDialog", "to"))
109
+ self.label.setText(_translate("AnalysisDialog", "Clustering restarts"))
110
+ self.label_3.setText(_translate("AnalysisDialog", "Nuclei expansion"))
111
+ self.label_4.setText(_translate("AnalysisDialog", "px"))
112
+ self.label_5.setText(_translate("AnalysisDialog", "Nuclei min area"))
113
+ self.label_6.setText(_translate("AnalysisDialog", "px"))
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Created on Fri Mar 27 00:34:39 2026
5
+
6
+ @author: carl
7
+ """
8
+
9
+ import importlib
10
+ from .elementsettings import ElementSettings
11
+ from silx.gui import qt
12
+ import contextlib
13
+
14
+
15
+ class AnalysisWorker(qt.QObject):
16
+ progress = qt.Signal(str)
17
+ finished = qt.Signal(object) # XRFSample
18
+ failed = qt.Signal(str)
19
+
20
+ def __init__(self, nuclei_es: ElementSettings, tissue_es: ElementSettings,
21
+ element_settings: list, **process_args):
22
+ super().__init__()
23
+ print("AW init", self)
24
+ self.nuclei_es = nuclei_es
25
+ self.tissue_es = tissue_es
26
+ self.element_settings = element_settings
27
+ self.process_args = process_args
28
+ self._abort = False
29
+
30
+ @qt.Slot()
31
+ def run(self):
32
+ print("AW run", self)
33
+ try:
34
+ sample = self.xrf_analysis()
35
+ except InterruptedError:
36
+ self.failed.emit("")
37
+ except Exception:
38
+ import traceback
39
+ self.failed.emit(traceback.format_exc())
40
+ else:
41
+ self.finished.emit(sample)
42
+
43
+ @qt.Slot()
44
+ def abort(self):
45
+ print("AW abort", self)
46
+ self._abort = True
47
+
48
+ # def init_libs(self):
49
+ # "Defers loading of TF, stardist and such"
50
+ # # Uncomment in case we need to load from . instead of a specified path
51
+ # # _cwd = os.getcwd()
52
+ # # import exhale.resources
53
+ # # os.chdir(next(iter(exhale.resources.__path__)))
54
+ # from .xrf_refcopy import xrf_utils
55
+ # xrf_utils.set_model_basedir(
56
+ # importlib.resources.files("exhale").joinpath("resources"))
57
+ # # os.chdir(_cwd)
58
+ # # assert xrf_utils.model
59
+
60
+ def xrf_analysis(self):
61
+ # init_libs()
62
+
63
+ # self._abort = False
64
+ def progress_or_abort(msg):
65
+ if self._abort:
66
+ raise InterruptedError("Aborted")
67
+ self.progress.emit(msg)
68
+
69
+ # Try to intercept logging
70
+ import logging
71
+ class LogHandler(logging.Handler):
72
+ "Minimal logging handler"
73
+ def __init__(self):
74
+ logging.Handler.__init__(self, logging.DEBUG)
75
+ self.setFormatter(logging.Formatter(
76
+ "[%(name)s] %(levelname)s: %(message)s"))
77
+ def emit(self, record):
78
+ try:
79
+ msg = self.format(record)
80
+ except Exception:
81
+ msg = record.getMessage()
82
+ progress_or_abort(msg)
83
+ tfLogHandler = LogHandler()
84
+ tf_log = logging.getLogger("tensorflow")
85
+ tf_log.setLevel(logging.DEBUG)
86
+ tf_log.addHandler(tfLogHandler)
87
+
88
+ class EmitStream():
89
+ "Stream that collects entire lines and emits them as progress"
90
+ def __init__(self):
91
+ self._s = []
92
+ def write(self, text):
93
+ self._s.append(text)
94
+ if "\n" in text:
95
+ self.flush()
96
+ def flush(self):
97
+ s = "".join(self._s)
98
+ for t in s.split("\n"):
99
+ if t:
100
+ progress_or_abort(t)
101
+ self._s = []
102
+ def close(self):
103
+ ...
104
+ estream = EmitStream()
105
+
106
+ # Late loading of TF and stardist
107
+ with (contextlib.redirect_stdout(estream),
108
+ contextlib.redirect_stderr(estream)):
109
+ from .xrf_refcopy import xrf_utils
110
+ xrf_utils.set_model_basedir(
111
+ importlib.resources.files("exhale").joinpath("resources/models"))
112
+ from .xrf_refcopy.xrf_sample_class import XRFSample
113
+ from .xrf_refcopy.xrf_other_channel import NucleiChannel, TissueChannel
114
+
115
+ sample = XRFSample("Default_sample")
116
+ sample.nuclei = NucleiChannel(self.nuclei_es.data)
117
+ sample.tissue = TissueChannel(self.tissue_es.data)
118
+ for es in self.element_settings:
119
+ sample.add_element(es.name, es.data)
120
+
121
+ progress_or_abort("Preparing to process")
122
+ sample.process(callback=progress_or_abort, **self.process_args)
123
+ progress_or_abort("Processed; combining")
124
+ sample.combine(callback=progress_or_abort)
125
+ progress_or_abort("Analysis complete")
126
+ return sample
127
+
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Created on Sun Oct 12 21:26:36 2025
5
+
6
+ @author: carl
7
+ """
@@ -0,0 +1,169 @@
1
+ from skimage import measure
2
+ from sklearn.cluster import KMeans
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ # import dask.array as da
7
+ # from dask.diagnostics import ProgressBar
8
+ # import dask.dataframe as dd
9
+ # import tensorflow as tf
10
+
11
+ from sklearn.metrics import silhouette_score
12
+ # from sklearn.cluster import KMeans
13
+ from collections import Counter
14
+ import dask
15
+ from dask import delayed
16
+
17
+ from . import xrf_general_functions as xgf
18
+
19
+ @delayed
20
+ def run_clustering(X, min_k=3, max_k=5, n_init=1):
21
+ best_k, best_score = None, -1
22
+ for k in range(min_k, max_k+1):
23
+ labels = KMeans(n_clusters=k, init='k-means++', max_iter=100, n_init=n_init).fit_predict(X)
24
+ score = silhouette_score(X, labels)
25
+ if score > best_score:
26
+ best_k, best_score = k, score
27
+ return best_k
28
+
29
+ def measure_number_cluster_legacy(image, num_runs = 100):
30
+ # Assuming element_img is defined somewhere in your code
31
+
32
+ # Number of times to run the experiment
33
+
34
+
35
+ # Create a list of delayed tasks
36
+ tasks = [run_clustering(image) for _ in range(num_runs)]
37
+
38
+ # Compute the results in parallel
39
+ best_clusters_list = dask.compute(*tasks)
40
+
41
+ # Determine the most frequent number of clusters
42
+ most_frequent_clusters = Counter(best_clusters_list).most_common(1)[0][0]
43
+ #print("The most frequent best number of clusters is %i" % most_frequent_clusters)
44
+ return most_frequent_clusters
45
+
46
+ def measure_number_cluster(X, min_k=3, max_k=5, n_init=100):
47
+ """Find best number of clusters in [min_k, max_k] using silhouette score."""
48
+ best_score, best_k = -1, min_k
49
+ for n_clusters in range(min_k, max_k + 1):
50
+ model = KMeans(n_clusters=n_clusters, init='k-means++', max_iter=100, n_init=n_init)
51
+ labels = model.fit_predict(X)
52
+ score = silhouette_score(X, labels)
53
+ if score > best_score:
54
+ best_score, best_k = score, n_clusters
55
+ return best_k
56
+
57
+ def run_kmeans(img, num_clusters):
58
+ """Run KMeans clustering on the flattened image."""
59
+ kmeans = KMeans(n_clusters=num_clusters, init="k-means++")
60
+ flat_img = img.reshape(-1, 1)
61
+ kmeans.fit(flat_img)
62
+ return kmeans.labels_.reshape(img.shape)
63
+
64
+ def extract_cluster_positions(labels, max_cluster_size=10000):
65
+ """Extract pixel positions for clusters below size threshold."""
66
+ cluster_positions = {}
67
+ cluster_sizes = np.bincount(labels.ravel())
68
+
69
+ for label, size in enumerate(cluster_sizes):
70
+ if size <= max_cluster_size:
71
+ coords = np.column_stack(np.where(labels == label))
72
+ cluster_positions[label] = coords
73
+
74
+ return cluster_positions
75
+
76
+ def build_mask(img_shape, cluster_positions):
77
+ """Build boolean mask of all cluster positions."""
78
+ if not cluster_positions:
79
+ return np.zeros(img_shape, dtype=bool)
80
+
81
+ all_points = np.vstack(list(cluster_positions.values()))
82
+ mask = np.zeros(img_shape, dtype=bool)
83
+ mask[tuple(all_points.T)] = True
84
+ return mask
85
+
86
+ def compute_cluster_properties(segmented_img, raw_img, min_area=1):
87
+ """Compute cluster properties and filter by area."""
88
+ labels = measure.label(segmented_img, connectivity=2)
89
+ props = measure.regionprops_table(
90
+ labels,
91
+ raw_img,
92
+ properties=["label", "area", "mean_intensity", "centroid"]
93
+ )
94
+ df = pd.DataFrame(props)
95
+ filtered_df = df[df["area"] > min_area]
96
+ return labels, filtered_df
97
+
98
+ def measure_clusters_properties(img, num_clusters, raw_img, max_cluster_size=10000):
99
+ """Main pipeline: KMeans clustering + property extraction + filtering."""
100
+ # Step 1: Run clustering
101
+ k_labels = run_kmeans(img, num_clusters)
102
+
103
+ # Step 2: Extract cluster positions
104
+ cluster_positions = extract_cluster_positions(k_labels, max_cluster_size=max_cluster_size)
105
+
106
+ # Step 3: Build mask and segmented image
107
+ mask = build_mask(img.shape, cluster_positions)
108
+ segmented_img = xgf.build_segmented_image(img.shape, mask)
109
+
110
+ # Step 4: Compute cluster properties
111
+ labels, filtered_df = compute_cluster_properties(segmented_img, raw_img)
112
+
113
+ # Step 5: Filter image by properties
114
+ filtered_image = xgf.draw_filtered_img(labels, filtered_df)
115
+
116
+ return filtered_image, filtered_df
117
+
118
+
119
+ #def measure_clusters_properties_v2(img, num_clusters, raw_img):
120
+ # Apply K-Means clustering
121
+ kmeans = KMeans(n_clusters=num_clusters, init='k-means++')
122
+ kmeans.fit(img.reshape(-1, 1))
123
+ # Extract cluster positions and calculate intensity and size
124
+ cluster_positions = {}
125
+
126
+ # Define a maximum size threshold for clusters
127
+ max_cluster_size = 10000 # Adjust this threshold as needed
128
+ # Get cluster labels
129
+ k_labels = kmeans.labels_
130
+ # cluster_sizes = np.bincount(k_labels)
131
+ for label in set(k_labels):
132
+ cluster_indices = np.where(k_labels == label)[0]
133
+ cluster_size = len(cluster_indices)
134
+
135
+ if cluster_size <= max_cluster_size:
136
+ # Collect cluster points
137
+ cluster_coords = np.unravel_index(cluster_indices, img.shape)
138
+ cluster_positions[label] = np.column_stack(cluster_coords)
139
+
140
+
141
+ # Prepare data for Napari
142
+ points_list = []
143
+ for cluster_id, positions in cluster_positions.items():
144
+ points_list.append(positions)
145
+
146
+ # If there are points to display, concatenate them into a single array
147
+ if points_list:
148
+ all_points = np.vstack(points_list)
149
+ else:
150
+ all_points = np.empty((0, 2))
151
+
152
+ # Create a mask for the segmented points
153
+ mask = np.zeros_like(img, dtype=bool)
154
+ for point in all_points:
155
+ mask[tuple(point)] = True
156
+
157
+ # Overlay the mask on the original image
158
+ segmented_img = np.zeros_like(img) #element_img.copy()
159
+ segmented_img[mask] = 1 #element_img[mask]#.max() # Highlight segmented pixels
160
+
161
+ k_space_labels = measure.label(segmented_img, connectivity=2)
162
+ k_props = measure.regionprops_table(k_space_labels, raw_img, properties=['label', 'area', 'mean_intensity', 'centroid'])
163
+ k_df = pd.DataFrame(k_props)
164
+ filtered_k_df = k_df[(k_df['area']>1) ] #& (k_df['mean_intensity']>(np.exp(element_img)+c).mean())
165
+ mask = np.isin(k_space_labels, filtered_k_df)
166
+ filtered_image = np.zeros_like(k_space_labels)
167
+ filtered_image[mask] = k_space_labels[mask]
168
+
169
+ return filtered_image, filtered_k_df
@@ -0,0 +1,65 @@
1
+ # from stardist.models import StarDist2D
2
+ from csbdeep.utils import normalize
3
+ # import tensorflow as tf
4
+
5
+ # import os
6
+ # from skimage import measure, io
7
+ from skimage.segmentation import expand_labels
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+ # os.environ["CUDA_VISIBLE_DEVICES"] = "0"
12
+ # print("Num GPUs Available: ", len(tf.config.list_physical_devices('GPU')))
13
+ # model = StarDist2D(None, '2D_versatile_fluo_copy', basedir='.')
14
+
15
+ def log_img(img):
16
+ # Logarithmic transformation inspired by Fiji's "log" function
17
+ # Scale factor ensures result is normalized in [0,255]
18
+ c = 255 / np.log1p(img.max())
19
+ log_image = c * np.log1p(img.astype(np.float32))
20
+ # Subtract mean, clip negatives to zero, and cast back to original dtype
21
+ element_img = np.clip(log_image - log_image.mean(), 0, None).astype(img.dtype)
22
+ return element_img
23
+
24
+ def draw_filtered_img(labels, df):
25
+ # Create a boolean mask where label IDs are in the filtered dataframe
26
+ mask = np.isin(labels, df['label'])
27
+ # Initialize empty mask with same shape as labels
28
+ filtered_image = np.zeros_like(labels)
29
+ filtered_image[mask] = labels[mask]
30
+ return filtered_image
31
+
32
+ def build_segmented_image(img_shape, mask):
33
+ """Create binary image of segmented clusters."""
34
+ segmented_img = np.zeros(img_shape, dtype=np.uint8)
35
+ segmented_img[mask] = 1
36
+ return segmented_img
37
+
38
+ def segment_nuclei(img_nuclei, model):
39
+ # Run deep learning segmentation model on nuclei channel
40
+ labels, _ = model.predict_instances(normalize(img_nuclei))
41
+ return labels
42
+
43
+ def create_membrane_nuclei(labels_nuclei, expansion_size = 15, return_expanded = True):
44
+ # Expand nuclei labels by a fixed distance
45
+ expanded_labels = expand_labels(labels_nuclei, distance=expansion_size)
46
+ membrane_labels = expanded_labels - labels_nuclei
47
+ if return_expanded:
48
+ return expanded_labels, membrane_labels
49
+ else:
50
+ return membrane_labels
51
+
52
+ def flatten_df(df, location):
53
+ # Flatten per-cell lists of cluster sizes and intensities into long format
54
+ flat_cluster_sizes = [item for sublist in df['cluster_sizes'] for item in sublist]
55
+ flat_cluster_intensities = [item for sublist in df['cluster_intensities'] for item in sublist]
56
+ elements = [df['element'][i] for i in range(len(df)) for _ in df['cluster_sizes'][i]]
57
+
58
+ # Construct normalized (flattened) dataframe
59
+ df_flat = pd.DataFrame({
60
+ 'element': elements,
61
+ 'cluster_size': flat_cluster_sizes,
62
+ 'cluster_intensity': flat_cluster_intensities,
63
+ 'location': location
64
+ })
65
+ return df_flat