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.
Files changed (27) hide show
  1. {pykrait-0.2.2 → pykrait-0.2.4}/.gitignore +2 -0
  2. {pykrait-0.2.2 → pykrait-0.2.4}/PKG-INFO +7 -2
  3. {pykrait-0.2.2 → pykrait-0.2.4}/README.md +6 -1
  4. {pykrait-0.2.2 → pykrait-0.2.4}/pyproject.toml +11 -5
  5. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/gui/app.py +1 -1
  6. pykrait-0.2.4/src/pykrait/gui/async_workers.py +218 -0
  7. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/gui/fileselection.py +288 -103
  8. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/gui/mainwindow.py +99 -40
  9. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/io/io.py +44 -3
  10. pykrait-0.2.4/src/pykrait/pipeline/pipeline.py +407 -0
  11. pykrait-0.2.4/src/pykrait/trace_analysis/oscillations.py +129 -0
  12. pykrait-0.2.2/src/pykrait/gui/async_workers.py +0 -258
  13. pykrait-0.2.2/src/pykrait/pipeline/pipeline.py +0 -278
  14. pykrait-0.2.2/src/pykrait/trace_analysis/oscillations.py +0 -114
  15. {pykrait-0.2.2 → pykrait-0.2.4}/LICENSE +0 -0
  16. {pykrait-0.2.2 → pykrait-0.2.4}/src/.python-version +0 -0
  17. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/__init__.py +0 -0
  18. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/__main__.py +0 -0
  19. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/gui/resources/logo.png +0 -0
  20. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/io/__init__.py +0 -0
  21. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/pipeline/__init__.py +0 -0
  22. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/preprocessing/__init__.py +0 -0
  23. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/preprocessing/segmentation.py +0 -0
  24. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/preprocessing/timeseries_extraction.py +0 -0
  25. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/trace_analysis/__init__.py +0 -0
  26. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/trace_analysis/filtering.py +0 -0
  27. {pykrait-0.2.2 → pykrait-0.2.4}/src/pykrait/trace_analysis/peak_analysis.py +0 -0
@@ -181,3 +181,5 @@ uv.lock
181
181
  /cellpose_models/
182
182
  notebooks/developing.ipynb
183
183
  notebooks/benchmark_adjacency_matrix.ipynb
184
+
185
+ .vscode/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pykrait
3
- Version: 0.2.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 GNU Lesser General Public License (GLP). See the [LICENSE](LICENSE) file for details.
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 GNU Lesser General Public License (GLP). See the [LICENSE](LICENSE) file for details.
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.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.build.targets.sdist]
62
- include = ["src/**", "pyproject.toml", "README.md"]
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.metadata]
68
- allow-direct-references = true
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 as e:
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)