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.
- exhale_lung-1.3.0/LICENSE +21 -0
- exhale_lung-1.3.0/MANIFEST.in +21 -0
- exhale_lung-1.3.0/PKG-INFO +48 -0
- exhale_lung-1.3.0/README.md +21 -0
- exhale_lung-1.3.0/exhale/__init__.py +8 -0
- exhale_lung-1.3.0/exhale/__main__.py +7 -0
- exhale_lung-1.3.0/exhale/analysisdialog.py +113 -0
- exhale_lung-1.3.0/exhale/analysisworker.py +127 -0
- exhale_lung-1.3.0/exhale/cluster_analysis/__init__.py +7 -0
- exhale_lung-1.3.0/exhale/cluster_analysis/xrf_clustering.py +169 -0
- exhale_lung-1.3.0/exhale/cluster_analysis/xrf_general_functions.py +65 -0
- exhale_lung-1.3.0/exhale/cluster_analysis/xrf_interface.py +140 -0
- exhale_lung-1.3.0/exhale/cluster_analysis/xrf_main.py +320 -0
- exhale_lung-1.3.0/exhale/constants.py +17 -0
- exhale_lung-1.3.0/exhale/elementsettings.py +183 -0
- exhale_lung-1.3.0/exhale/exceptiondialog.py +33 -0
- exhale_lung-1.3.0/exhale/exhale.py +150 -0
- exhale_lung-1.3.0/exhale/exhale_qt.py +446 -0
- exhale_lung-1.3.0/exhale/exhalewindow.py +1414 -0
- exhale_lung-1.3.0/exhale/imagecomposer.py +440 -0
- exhale_lung-1.3.0/exhale/imagedialog.py +154 -0
- exhale_lung-1.3.0/exhale/imagesettings.py +212 -0
- exhale_lung-1.3.0/exhale/listwidgets.py +214 -0
- exhale_lung-1.3.0/exhale/naparihelper.py +243 -0
- exhale_lung-1.3.0/exhale/overridecursor.py +18 -0
- exhale_lung-1.3.0/exhale/plotwidgets.py +12 -0
- exhale_lung-1.3.0/exhale/projectio.py +276 -0
- exhale_lung-1.3.0/exhale/resources/icons/document-open.svg +2 -0
- exhale_lung-1.3.0/exhale/resources/icons/lungs.png +0 -0
- exhale_lung-1.3.0/exhale/resources/models/2D_versatile_fluo_copy/config.json +1 -0
- exhale_lung-1.3.0/exhale/resources/models/2D_versatile_fluo_copy/thresholds.json +1 -0
- exhale_lung-1.3.0/exhale/resources/models/2D_versatile_fluo_copy/weights_best.h5 +0 -0
- exhale_lung-1.3.0/exhale/source_refs.py +203 -0
- exhale_lung-1.3.0/exhale/xrf_refcopy/xrf_element_channel.py +102 -0
- exhale_lung-1.3.0/exhale/xrf_refcopy/xrf_other_channel.py +189 -0
- exhale_lung-1.3.0/exhale/xrf_refcopy/xrf_sample_class.py +562 -0
- exhale_lung-1.3.0/exhale/xrf_refcopy/xrf_utils.py +302 -0
- exhale_lung-1.3.0/exhale_lung.egg-info/PKG-INFO +48 -0
- exhale_lung-1.3.0/exhale_lung.egg-info/SOURCES.txt +45 -0
- exhale_lung-1.3.0/exhale_lung.egg-info/dependency_links.txt +1 -0
- exhale_lung-1.3.0/exhale_lung.egg-info/entry_points.txt +2 -0
- exhale_lung-1.3.0/exhale_lung.egg-info/requires.txt +17 -0
- exhale_lung-1.3.0/exhale_lung.egg-info/top_level.txt +3 -0
- exhale_lung-1.3.0/pyproject.toml +52 -0
- exhale_lung-1.3.0/run_exhale.py +7 -0
- exhale_lung-1.3.0/setup.cfg +4 -0
- 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,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,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
|