pykrait 0.2.2__tar.gz → 0.2.4__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.
- {pykrait-0.2.2 → pykrait-0.2.4}/.gitignore +2 -0
- {pykrait-0.2.2 → pykrait-0.2.4}/PKG-INFO +7 -2
- {pykrait-0.2.2 → pykrait-0.2.4}/README.md +6 -1
- {pykrait-0.2.2 → pykrait-0.2.4}/pyproject.toml +11 -5
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/gui/app.py +1 -1
- pykrait-0.2.4/src/pykrait/gui/async_workers.py +218 -0
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/gui/fileselection.py +288 -103
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/gui/mainwindow.py +99 -40
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/io/io.py +44 -3
- pykrait-0.2.4/src/pykrait/pipeline/pipeline.py +407 -0
- pykrait-0.2.4/src/pykrait/trace_analysis/oscillations.py +129 -0
- pykrait-0.2.2/src/pykrait/gui/async_workers.py +0 -258
- pykrait-0.2.2/src/pykrait/pipeline/pipeline.py +0 -278
- pykrait-0.2.2/src/pykrait/trace_analysis/oscillations.py +0 -114
- {pykrait-0.2.2 → pykrait-0.2.4}/LICENSE +0 -0
- {pykrait-0.2.2 → pykrait-0.2.4}/src/.python-version +0 -0
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/__init__.py +0 -0
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/__main__.py +0 -0
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/gui/resources/logo.png +0 -0
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/io/__init__.py +0 -0
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/pipeline/__init__.py +0 -0
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/preprocessing/__init__.py +0 -0
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/preprocessing/segmentation.py +0 -0
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/preprocessing/timeseries_extraction.py +0 -0
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/trace_analysis/__init__.py +0 -0
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/trace_analysis/filtering.py +0 -0
- {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/trace_analysis/peak_analysis.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pykrait
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Python Calcium Recording Analysis and Inference Toolbox
|
|
5
5
|
Author-email: phaleanopsis <22663502+phaleanopsis@users.noreply.github.com>
|
|
6
6
|
License: MIT
|
|
@@ -61,13 +61,18 @@ Description-Content-Type: text/markdown
|
|
|
61
61
|
pip install pykrait
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
To use the GUI, install it with
|
|
65
|
+
```bash
|
|
66
|
+
pip install 'pykrait[gui]'
|
|
67
|
+
```
|
|
68
|
+
|
|
64
69
|
## Documentation
|
|
65
70
|
|
|
66
71
|
See the [docs](docs/) folder for detailed guides and API reference.
|
|
67
72
|
|
|
68
73
|
## License
|
|
69
74
|
|
|
70
|
-
This project is licensed under the
|
|
75
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
71
76
|
|
|
72
77
|
## Contributing
|
|
73
78
|
|
|
@@ -12,13 +12,18 @@
|
|
|
12
12
|
pip install pykrait
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
To use the GUI, install it with
|
|
16
|
+
```bash
|
|
17
|
+
pip install 'pykrait[gui]'
|
|
18
|
+
```
|
|
19
|
+
|
|
15
20
|
## Documentation
|
|
16
21
|
|
|
17
22
|
See the [docs](docs/) folder for detailed guides and API reference.
|
|
18
23
|
|
|
19
24
|
## License
|
|
20
25
|
|
|
21
|
-
This project is licensed under the
|
|
26
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
22
27
|
|
|
23
28
|
## Contributing
|
|
24
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pykrait"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "Python Calcium Recording Analysis and Inference Toolbox"
|
|
5
5
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -58,14 +58,15 @@ dev = [
|
|
|
58
58
|
requires = ["hatchling"]
|
|
59
59
|
build-backend = "hatchling.build"
|
|
60
60
|
|
|
61
|
-
[tool.hatch.
|
|
62
|
-
|
|
61
|
+
[tool.hatch.metadata]
|
|
62
|
+
allow-direct-references = true
|
|
63
63
|
|
|
64
64
|
[tool.hatch.build.targets.wheel]
|
|
65
65
|
only-include = ["src"]
|
|
66
|
+
packages = ["src/pykrait"]
|
|
66
67
|
|
|
67
|
-
[tool.hatch.
|
|
68
|
-
|
|
68
|
+
[tool.hatch.build.targets.sdist]
|
|
69
|
+
include = ["src/**", "README.md", "pyproject.toml"]
|
|
69
70
|
|
|
70
71
|
[tool.pytest.ini_options]
|
|
71
72
|
pythonpath = ["src"]
|
|
@@ -80,3 +81,8 @@ skip_covered = true
|
|
|
80
81
|
|
|
81
82
|
[tool.setuptools.package-data]
|
|
82
83
|
"pykrait.gui.resources" = ["logo.png"]
|
|
84
|
+
|
|
85
|
+
[tool.ruff]
|
|
86
|
+
src = ["src"]
|
|
87
|
+
line-length = 88
|
|
88
|
+
target-version = "py311"
|
|
@@ -16,7 +16,7 @@ class AppController:
|
|
|
16
16
|
from importlib.resources import files
|
|
17
17
|
icon_path = files("pykrait.gui.resources").joinpath("logo.png")
|
|
18
18
|
os.path.exists(icon_path)
|
|
19
|
-
except Exception
|
|
19
|
+
except Exception:
|
|
20
20
|
icon_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "pykrait/gui/resources", "logo.png"))
|
|
21
21
|
os.path.exists(icon_path)
|
|
22
22
|
if os.path.exists(icon_path):
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from PySide6.QtCore import QObject, Signal, Slot
|
|
2
|
+
import traceback
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from skimage.measure import find_contours
|
|
6
|
+
|
|
7
|
+
from pykrait.io.io import read_label_image, read_Txnrois
|
|
8
|
+
from pykrait.preprocessing.timeseries_extraction import extract_mean_intensities
|
|
9
|
+
from pykrait.trace_analysis.filtering import detrend_with_sinc_filter
|
|
10
|
+
from pykrait.trace_analysis.peak_analysis import find_peaks
|
|
11
|
+
from pykrait.pipeline.pipeline import AnalysisParameters, AnalysisOutput, create_analysis_folder, load_timelapse, create_masks, calculate_periodic_cells, save_analysis_results
|
|
12
|
+
|
|
13
|
+
def get_pixel_contour_from_label_img(label_img:np.ndarray, orig_shape:tuple, target_shape:tuple) -> list:
|
|
14
|
+
"""
|
|
15
|
+
Generates pixel contours from a labeled image, scaled to a target shape.
|
|
16
|
+
|
|
17
|
+
:param label_img: a label image where each pixel is labeled with an integer representing the ROI it belongs to
|
|
18
|
+
:type label_img: np.ndarray
|
|
19
|
+
:param orig_shape: original shape of the image (height, width)
|
|
20
|
+
:type orig_shape: tuple
|
|
21
|
+
:param target_shape: target shape to which the contours should be scaled (height, width)
|
|
22
|
+
:type target_shape: tuple
|
|
23
|
+
:return: returns a list of polygons representing the scaled contours of each ROI
|
|
24
|
+
:rtype: list
|
|
25
|
+
"""
|
|
26
|
+
roi_polygons = []
|
|
27
|
+
orig_height, orig_width = orig_shape
|
|
28
|
+
target_height, target_width = target_shape
|
|
29
|
+
scale_y = target_height / orig_height
|
|
30
|
+
scale_x = target_width / orig_width
|
|
31
|
+
for index in range(1, label_img.max() + 1): # skip background (0)
|
|
32
|
+
contours = find_contours(label_img == index, 0.5)
|
|
33
|
+
if contours:
|
|
34
|
+
contour = contours[0]
|
|
35
|
+
y, x = contour.T
|
|
36
|
+
y, x = y * scale_y, x * scale_x # rescales coordinates to target shape
|
|
37
|
+
polygon = list(zip(x, y))
|
|
38
|
+
roi_polygons.append(polygon)
|
|
39
|
+
return roi_polygons
|
|
40
|
+
|
|
41
|
+
class ExtractIntensitiesWorker(QObject):
|
|
42
|
+
"""
|
|
43
|
+
Worker for extracting mean intensities from a timelapse dataset, using either Cellpose segmentation or a label image.
|
|
44
|
+
"""
|
|
45
|
+
progress_changed = Signal(int, str) # progress percent, status message
|
|
46
|
+
finished = Signal(dict) # analysis results
|
|
47
|
+
error = Signal(str) # error message
|
|
48
|
+
|
|
49
|
+
def __init__(self, analysis_params:AnalysisParameters, output_params:AnalysisOutput, mode:str, mean_intensities_path:str=None):
|
|
50
|
+
super().__init__()
|
|
51
|
+
self.analysis_parameters = analysis_params
|
|
52
|
+
self.analysis_output = output_params
|
|
53
|
+
self.mode = mode
|
|
54
|
+
self.mean_intensities_path = mean_intensities_path
|
|
55
|
+
|
|
56
|
+
@Slot()
|
|
57
|
+
def run(self):
|
|
58
|
+
try:
|
|
59
|
+
|
|
60
|
+
self.analysis_output = create_analysis_folder(self.analysis_output)
|
|
61
|
+
print(f"Running analysis on {self.analysis_output.filename}")
|
|
62
|
+
# loading the timelapse data and analysing frame interval
|
|
63
|
+
self.timelapse_data, self.analysis_output = load_timelapse(self.analysis_output, self.analysis_parameters)
|
|
64
|
+
|
|
65
|
+
if self.mode == "cellpose":
|
|
66
|
+
# perform the tproj
|
|
67
|
+
self.progress_changed.emit(10, "Performing T Projection and Cellpose Segmentation...")
|
|
68
|
+
# creating the masks and saving them to the analysis folder
|
|
69
|
+
self.masks, self.analysis_output = create_masks(self.timelapse_data, self.analysis_output, self.analysis_parameters)
|
|
70
|
+
# extracts the mean intensities of the masks
|
|
71
|
+
self.progress_changed.emit(60, "Extracting intensities...")
|
|
72
|
+
mean_intensities = extract_mean_intensities(self.timelapse_data, self.masks)
|
|
73
|
+
elif self.mode == "label_image":
|
|
74
|
+
self.progress_changed.emit(20, "Loading label image...")
|
|
75
|
+
self.masks = read_label_image(self.analysis_output.masks_path)
|
|
76
|
+
# extracts the mean intensities of the masks
|
|
77
|
+
self.progress_changed.emit(60, "Extracting intensities...")
|
|
78
|
+
mean_intensities = extract_mean_intensities(self.timelapse_data, self.masks)
|
|
79
|
+
elif self.mode == "csv":
|
|
80
|
+
self.progress_changed.emit(20, "Loading label image...")
|
|
81
|
+
self.masks = read_label_image(self.analysis_output.masks_path)
|
|
82
|
+
self.progress_changed.emit(60, "Reading intensities...")
|
|
83
|
+
mean_intensities = read_Txnrois(self.mean_intensities_path, n_frames=self.timelapse_data.shape[0], n_rois=self.masks.max())
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError("Unknown mode.")
|
|
86
|
+
|
|
87
|
+
self.analysis_output.number_of_frames, self.analysis_output.number_of_cells = mean_intensities.shape
|
|
88
|
+
|
|
89
|
+
# All done
|
|
90
|
+
self.progress_changed.emit(90, "Calculating ROI boundaries...")
|
|
91
|
+
rois = get_pixel_contour_from_label_img(self.masks, orig_shape=self.masks.shape, target_shape=(512, 512))
|
|
92
|
+
|
|
93
|
+
self.progress_changed.emit(100, "Done.")
|
|
94
|
+
|
|
95
|
+
results = {
|
|
96
|
+
'frames': self.timelapse_data,
|
|
97
|
+
'analysis_output': self.analysis_output,
|
|
98
|
+
'analysis_parameters': self.analysis_parameters,
|
|
99
|
+
'masks': self.masks,
|
|
100
|
+
'rois': rois,
|
|
101
|
+
'mean_intensities': mean_intensities
|
|
102
|
+
}
|
|
103
|
+
self.finished.emit(results)
|
|
104
|
+
|
|
105
|
+
except Exception as e:
|
|
106
|
+
tb = traceback.format_exc()
|
|
107
|
+
print(f"Error during analysis: {e}\n{tb}")
|
|
108
|
+
self.error.emit(str(e))
|
|
109
|
+
|
|
110
|
+
class DetrendWorker(QObject):
|
|
111
|
+
"""
|
|
112
|
+
_summary_
|
|
113
|
+
|
|
114
|
+
:param QObject: _description_
|
|
115
|
+
:type QObject: _type_
|
|
116
|
+
"""
|
|
117
|
+
finished = Signal(np.ndarray)
|
|
118
|
+
error = Signal(Exception)
|
|
119
|
+
|
|
120
|
+
def __init__(self, intensities, sinc_window, frame_interval):
|
|
121
|
+
super().__init__()
|
|
122
|
+
self.intensities = intensities
|
|
123
|
+
self.sinc_window = sinc_window
|
|
124
|
+
self.frame_interval = frame_interval
|
|
125
|
+
|
|
126
|
+
def run(self):
|
|
127
|
+
try:
|
|
128
|
+
# Call your actual function here
|
|
129
|
+
detrended = detrend_with_sinc_filter(signals=self.intensities,
|
|
130
|
+
cutoff_period=self.sinc_window,
|
|
131
|
+
sampling_interval=self.frame_interval)
|
|
132
|
+
self.finished.emit(detrended)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
self.error.emit(e)
|
|
135
|
+
|
|
136
|
+
class PeakDetectionWorker(QObject):
|
|
137
|
+
"""
|
|
138
|
+
Worker for detecting peaks in detrended traces, wraps the `find_peaks` function.
|
|
139
|
+
"""
|
|
140
|
+
finished = Signal(np.ndarray)
|
|
141
|
+
error = Signal(Exception)
|
|
142
|
+
|
|
143
|
+
def __init__(self, detrended_traces, min_width, max_width, prominence, min_height):
|
|
144
|
+
super().__init__()
|
|
145
|
+
self.traces = detrended_traces
|
|
146
|
+
self.min_width = min_width
|
|
147
|
+
self.max_width = max_width
|
|
148
|
+
self.prominence = prominence
|
|
149
|
+
self.min_height = min_height
|
|
150
|
+
|
|
151
|
+
def run(self):
|
|
152
|
+
try:
|
|
153
|
+
peaks = find_peaks(
|
|
154
|
+
peak_min_width=self.min_width,
|
|
155
|
+
peak_max_width=self.max_width,
|
|
156
|
+
peak_prominence=self.prominence,
|
|
157
|
+
peak_min_height=self.min_height,
|
|
158
|
+
detrended_timeseries=self.traces
|
|
159
|
+
)
|
|
160
|
+
self.finished.emit(peaks)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
self.error.emit(e)
|
|
163
|
+
|
|
164
|
+
class PeriodicCellWorker(QObject):
|
|
165
|
+
"""
|
|
166
|
+
Worker for finding oscillating ROIs in the peak series, wraps the `find_oscillating_rois` function.
|
|
167
|
+
"""
|
|
168
|
+
finished = Signal(AnalysisOutput, np.ndarray, np.ndarray)
|
|
169
|
+
error = Signal(Exception)
|
|
170
|
+
|
|
171
|
+
def __init__(self, peaks: np.ndarray, analysis_params, analysis_output):
|
|
172
|
+
super().__init__()
|
|
173
|
+
self.peaks = peaks
|
|
174
|
+
self.analysis_output = analysis_output
|
|
175
|
+
self.analysis_params = analysis_params
|
|
176
|
+
|
|
177
|
+
def run(self):
|
|
178
|
+
try:
|
|
179
|
+
periodicity_results, stds, covs = calculate_periodic_cells(self.peaks, self.analysis_params, self.analysis_output)
|
|
180
|
+
self.finished.emit(periodicity_results, stds, covs)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
self.error.emit(e)
|
|
183
|
+
|
|
184
|
+
class AnalysisSaveWorker(QObject):
|
|
185
|
+
"""
|
|
186
|
+
Worker for saving the analysis results, wraps the `save_analysis_results` function.
|
|
187
|
+
"""
|
|
188
|
+
finished = Signal(str) # emit output file path or summary message
|
|
189
|
+
error = Signal(Exception)
|
|
190
|
+
|
|
191
|
+
def __init__(self, analysis_output, analysis_params, peaks: np.ndarray, detrended_intensities: np.ndarray, intensities: np.ndarray,
|
|
192
|
+
stds: np.ndarray = None, covs: np.ndarray = None, overwrite: bool = False):
|
|
193
|
+
super().__init__()
|
|
194
|
+
self.analysis_output = analysis_output
|
|
195
|
+
self.analysis_params = analysis_params
|
|
196
|
+
self.peaks = peaks
|
|
197
|
+
self.detrended_intensities = detrended_intensities
|
|
198
|
+
self.intensities = intensities
|
|
199
|
+
self.overwrite = overwrite
|
|
200
|
+
self.stds = stds
|
|
201
|
+
self.covs = covs
|
|
202
|
+
|
|
203
|
+
def run(self):
|
|
204
|
+
try:
|
|
205
|
+
save_analysis_results(
|
|
206
|
+
analysis_output=self.analysis_output,
|
|
207
|
+
analysis_params=self.analysis_params,
|
|
208
|
+
detrended_intensities=self.detrended_intensities,
|
|
209
|
+
peaks=self.peaks,
|
|
210
|
+
intensities=self.intensities,
|
|
211
|
+
stds=self.stds,
|
|
212
|
+
covs=self.covs,
|
|
213
|
+
overwrite=self.overwrite
|
|
214
|
+
)
|
|
215
|
+
self.finished.emit(f"Saved to {self.analysis_output.analysis_folder}")
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
self.error.emit(e)
|