pykrait 0.1.0__tar.gz → 0.2.1__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.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Pentabyteman
3
+ Copyright (c) 2025 Phaleanopsis
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,18 +1,23 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pykrait
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Python Calcium Recording Analysis and Inference Toolbox
5
- Author-email: Pentabyteman <22663502+Pentabyteman@users.noreply.github.com>
5
+ Author-email: phaleanopsis <22663502+phaleanopsis@users.noreply.github.com>
6
6
  License: MIT
7
7
  License-File: LICENSE
8
- Requires-Python: >=3.10
9
- Requires-Dist: aicspylibczi>=3.1.1
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: aicspylibczi>=3.3.1
10
+ Requires-Dist: bioio-czi>=2.4.0
11
+ Requires-Dist: bioio-ome-tiff>=1.1.0
12
+ Requires-Dist: bioio-tifffile>=1.3.0
13
+ Requires-Dist: bioio>=3.0.0
10
14
  Requires-Dist: cellpose>=4.0.5
11
15
  Requires-Dist: dask>=2025.5.1
12
16
  Requires-Dist: fsspec>=2022.8.0
13
17
  Requires-Dist: imagecodecs>=2025.3.30
14
18
  Requires-Dist: lxml>=5.4.0
15
19
  Requires-Dist: matplotlib>=3.10.3
20
+ Requires-Dist: numcodecs<0.16
16
21
  Requires-Dist: numpy<2
17
22
  Requires-Dist: ome-types>=0.5.3
18
23
  Requires-Dist: ome-zarr>=0.10.2
@@ -22,11 +27,22 @@ Requires-Dist: pyyaml>=6.0.2
22
27
  Requires-Dist: resource-backed-dask-array>=0.1.0
23
28
  Requires-Dist: scikit-image>=0.25.2
24
29
  Requires-Dist: scipy>=1.15.3
25
- Requires-Dist: tifffile<2023.3.15,>=2021.8.30
26
- Requires-Dist: wrapt>=1.17.2
27
30
  Requires-Dist: xarray>2025
28
- Requires-Dist: xmlschema>=4.0.1
29
- Requires-Dist: zarr<2.16.0,>=2.6
31
+ Provides-Extra: dev
32
+ Requires-Dist: build>=1.2.2.post1; extra == 'dev'
33
+ Requires-Dist: hatch>=1.14.1; extra == 'dev'
34
+ Requires-Dist: ipywidgets>=8.1.7; extra == 'dev'
35
+ Requires-Dist: pydata-sphinx-theme>=0.16.1; extra == 'dev'
36
+ Requires-Dist: pytest-cov; extra == 'dev'
37
+ Requires-Dist: pytest>=8.4.1; extra == 'dev'
38
+ Requires-Dist: ruff>=0.11.13; extra == 'dev'
39
+ Requires-Dist: sphinx-autoapi>=3.6.0; extra == 'dev'
40
+ Requires-Dist: sphinx-favicon>=1.0.1; extra == 'dev'
41
+ Requires-Dist: sphinx>=8.1.3; extra == 'dev'
42
+ Requires-Dist: tomli>=2.2.1; extra == 'dev'
43
+ Requires-Dist: twine>=6.1.0; extra == 'dev'
44
+ Provides-Extra: gui
45
+ Requires-Dist: pyside6>=6.9.1; extra == 'gui'
30
46
  Description-Content-Type: text/markdown
31
47
 
32
48
  # pyKrait - Python Calcium Recording Analysis and Inference Toolbox
@@ -1,21 +1,26 @@
1
1
  [project]
2
2
  name = "pykrait"
3
- version = "0.1.0"
3
+ version = "0.2.1"
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" }
7
7
  authors = [
8
- { name = "Pentabyteman", email = "22663502+Pentabyteman@users.noreply.github.com" }
8
+ { name = "phaleanopsis", email = "22663502+phaleanopsis@users.noreply.github.com" }
9
9
  ]
10
- requires-python = ">=3.10"
10
+ requires-python = ">=3.11"
11
11
  dependencies = [
12
- "aicspylibczi>=3.1.1",
12
+ "aicspylibczi>=3.3.1",
13
+ "bioio>=3.0.0",
14
+ "bioio-czi>=2.4.0",
15
+ "bioio-ome-tiff>=1.1.0",
16
+ "bioio-tifffile>=1.3.0",
13
17
  "cellpose>=4.0.5",
14
18
  "dask>=2025.5.1",
15
19
  "fsspec>=2022.8.0",
16
20
  "imagecodecs>=2025.3.30",
17
21
  "lxml>=5.4.0",
18
22
  "matplotlib>=3.10.3",
23
+ "numcodecs<0.16",
19
24
  "numpy<2",
20
25
  "ome-types>=0.5.3",
21
26
  "ome-zarr>=0.10.2",
@@ -25,11 +30,26 @@ dependencies = [
25
30
  "resource-backed-dask-array>=0.1.0",
26
31
  "scikit-image>=0.25.2",
27
32
  "scipy>=1.15.3",
28
- "tifffile>=2021.8.30,<2023.3.15",
29
- "wrapt>=1.17.2",
30
33
  "xarray>2025",
31
- "xmlschema>=4.0.1",
32
- "zarr>=2.6,<2.16.0",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ gui = [
38
+ "pyside6>=6.9.1"
39
+ ]
40
+ dev = [
41
+ "build>=1.2.2.post1",
42
+ "hatch>=1.14.1",
43
+ "ipywidgets>=8.1.7",
44
+ "pydata-sphinx-theme>=0.16.1",
45
+ "ruff>=0.11.13",
46
+ "sphinx>=8.1.3",
47
+ "sphinx-autoapi>=3.6.0",
48
+ "sphinx-favicon>=1.0.1",
49
+ "tomli>=2.2.1",
50
+ "twine>=6.1.0",
51
+ "pytest>=8.4.1",
52
+ "pytest-cov"
33
53
  ]
34
54
 
35
55
  [build-system]
@@ -55,17 +75,3 @@ source = ["stavromueller_beta"]
55
75
  [tool.coverage.report]
56
76
  show_missing = true
57
77
  skip_covered = true
58
-
59
- [dependency-groups]
60
- dev = [
61
- "build>=1.2.2.post1",
62
- "hatch>=1.14.1",
63
- "ipywidgets>=8.1.7",
64
- "pydata-sphinx-theme>=0.16.1",
65
- "ruff>=0.11.13",
66
- "sphinx>=8.1.3",
67
- "sphinx-autoapi>=3.6.0",
68
- "sphinx-favicon>=1.0.1",
69
- "tomli>=2.2.1",
70
- "twine>=6.1.0",
71
- ]
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,41 @@
1
+ """
2
+ Copyright © 2025 Pentabyteman
3
+ """
4
+ # src/pykrait/__main__.py
5
+
6
+ import sys
7
+ from PySide6.QtWidgets import QApplication
8
+ from pykrait.gui.fileselection import FileSelectionWindow
9
+ from pykrait.gui.mainwindow import MainWindow
10
+
11
+ class AppController:
12
+ def __init__(self):
13
+ self.app = QApplication(sys.argv)
14
+ self.file_selection = FileSelectionWindow()
15
+ self.file_selection.analysis_complete.connect(self.on_analysis_complete)
16
+ self.file_selection.show()
17
+
18
+ # Keep references to windows
19
+ self.main_window = None
20
+
21
+ def on_analysis_complete(self, results):
22
+ # Called when analysis is done in FileSelectionWindow
23
+ # Hide file selection window
24
+ self.file_selection.close()
25
+ # Launch main window, keep reference
26
+ self.main_window = MainWindow(
27
+ frames=results["frames"],
28
+ mask=results["masks"],
29
+ mean_intensities=results["mean_intensities"],
30
+ analysis_output=results["analysis_output"],
31
+ analysis_params=results["analysis_parameters"]
32
+ )
33
+ self.main_window.show()
34
+
35
+ def run(self):
36
+ sys.exit(self.app.exec())
37
+
38
+ # In __main__.py
39
+ if __name__ == "__main__":
40
+ controller = AppController()
41
+ controller.run()
@@ -0,0 +1,167 @@
1
+ from PySide6.QtCore import QObject, Signal, Slot
2
+ import tifffile
3
+ import warnings
4
+ import os
5
+ import traceback
6
+ import numpy as np
7
+
8
+ from pykrait.io.io import load_timelapse_lazy
9
+ from pykrait.preprocessing.segmentation import timelapse_projection, create_cellpose_segmentation
10
+ from pykrait.preprocessing.timeseries_extraction import extract_mean_intensities
11
+ from pykrait.trace_analysis.filtering import detrend_with_sinc_filter
12
+ from pykrait.trace_analysis.peak_analysis import find_peaks
13
+ from pykrait.trace_analysis.oscillations import find_oscillating_rois
14
+ from pykrait.pipeline.pipeline import AnalysisParameters, AnalysisOutput
15
+
16
+ class ExtractIntensitiesWorker(QObject):
17
+ progress_changed = Signal(int, str) # progress percent, status message
18
+ finished = Signal(dict) # analysis results
19
+ error = Signal(str) # error message
20
+
21
+ def __init__(self, analysis_params:AnalysisParameters, output_params:AnalysisOutput, mode:str):
22
+ super().__init__()
23
+ self.analysis_parameters = analysis_params
24
+ self.analysis_output = output_params
25
+ self.mode = mode
26
+
27
+ @Slot()
28
+ def run(self):
29
+ try:
30
+
31
+ self.analysis_output.filename = self.analysis_output.filepath.split("/")[-1]
32
+ print(f"Running analysis on {self.analysis_output.filename}")
33
+ # Create analysis folder in the parent directory of the video file
34
+ parent_dir = os.path.dirname(self.analysis_output.filepath)
35
+ filename_wo_ext = os.path.splitext(self.analysis_output.filename)[0]
36
+ analysis_folder = os.path.join(parent_dir, f"Analysis_{filename_wo_ext}")
37
+ os.makedirs(analysis_folder, exist_ok=True)
38
+ self.analysis_output.analysis_folder = analysis_folder
39
+
40
+ self.progress_changed.emit(5, "Loading timelapse...")
41
+
42
+ timelapse_data, frame_interval, self.analysis_output.pixel_interval_y, self.analysis_output.pixel_interval_x = load_timelapse_lazy(file_path = self.analysis_output.filepath)
43
+
44
+ if frame_interval is None and self.analysis_parameters.frame_interval is None:
45
+ warnings.warn("No frame interval provided in the analysis parameters, and no frame interval could be inferred from the metadata. Defaulting to 1 second.")
46
+ self.analysis_output.frame_interval = 1 # Default to 1 second if not provided
47
+ elif self.analysis_parameters.frame_interval is None and frame_interval is not None:
48
+ self.analysis_output.frame_interval = frame_interval
49
+ elif frame_interval is not None and self.analysis_parameters.frame_interval != frame_interval:
50
+ warnings.warn(f"Provided frame_interval ({self.analysis_parameters.frame_interval}) does not match inferred frame_interval ({frame_interval}) from metadata. Using provided value.")
51
+
52
+ if self.mode == "cellpose":
53
+ # perform the tproj
54
+ self.progress_changed.emit(10, "Performing T Projection...")
55
+ tproj = timelapse_projection(timelapse_data, method=self.analysis_parameters.tproj_type, normalize=self.analysis_parameters.CLAHE_normalize, verbose=True)
56
+ tproj_path = os.path.join(analysis_folder, f"{filename_wo_ext}_tproj.ome.tif")
57
+ self.analysis_output.tproj_path = tproj_path
58
+ tifffile.imwrite(tproj_path, tproj.astype(np.uint16), metadata={'axes': 'CYX'}, compression="zlib")
59
+ self.analysis_output.zproj_path = os.path.join(analysis_folder, f"{filename_wo_ext}_zproj.ome.tif")
60
+
61
+ # create cellpose segmentation
62
+ self.progress_changed.emit(30, "Performing Cellpose Segmentation...")
63
+ masks = create_cellpose_segmentation(tproj, cellpose_model_path=self.analysis_parameters.cellpose_model_path)
64
+ mask_path = os.path.join(analysis_folder, f"{filename_wo_ext}_cp_masks.ome.tif")
65
+ self.analysis_output.mask_path = mask_path
66
+ tifffile.imwrite(mask_path, masks.astype(np.uint16), metadata={'axes': 'YX'}, compression="zlib")
67
+
68
+ elif self.mode == "label_image":
69
+ self.progress_changed.emit(20, "Loading label image...")
70
+ masks = tifffile.imread(self.analysis_output.masks_path)
71
+ tproj = None # Not needed in this mode
72
+
73
+ else:
74
+ raise ValueError("Unknown mode.")
75
+
76
+ # extracts the mean intensities of the masks
77
+ self.progress_changed.emit(60, "Extracting intensities...")
78
+ mean_intensities = extract_mean_intensities(timelapse_data, masks)
79
+ self.analysis_output.number_of_frames, self.analysis_output.number_of_cells = mean_intensities.shape
80
+
81
+ # All done
82
+ self.progress_changed.emit(100, "Done.")
83
+
84
+ results = {
85
+ 'frames': timelapse_data,
86
+ 'analysis_output': self.analysis_output,
87
+ 'analysis_parameters': self.analysis_parameters,
88
+ 'masks': masks,
89
+ 'mean_intensities': mean_intensities
90
+ }
91
+ self.finished.emit(results)
92
+
93
+ except Exception as e:
94
+ tb = traceback.format_exc()
95
+ print(f"Error during analysis: {e}\n{tb}")
96
+ self.error.emit(str(e))
97
+
98
+ class DetrendWorker(QObject):
99
+ finished = Signal(np.ndarray)
100
+ error = Signal(Exception)
101
+
102
+ def __init__(self, intensities, sinc_window, frame_interval):
103
+ super().__init__()
104
+ self.intensities = intensities
105
+ self.sinc_window = sinc_window
106
+ self.frame_interval = frame_interval
107
+
108
+ def run(self):
109
+ try:
110
+ # Call your actual function here
111
+ detrended = detrend_with_sinc_filter(signals=self.intensities,
112
+ cutoff_period=self.sinc_window,
113
+ sampling_interval=self.frame_interval)
114
+ self.finished.emit(detrended)
115
+ except Exception as e:
116
+ self.error.emit(e)
117
+
118
+
119
+ class PeakDetectionWorker(QObject):
120
+ finished = Signal(np.ndarray)
121
+ error = Signal(Exception)
122
+
123
+ def __init__(self, detrended_traces, min_width, max_width, prominence, min_height):
124
+ super().__init__()
125
+ self.traces = detrended_traces
126
+ self.min_width = min_width
127
+ self.max_width = max_width
128
+ self.prominence = prominence
129
+ self.min_height = min_height
130
+
131
+ def run(self):
132
+ try:
133
+ peaks = find_peaks(
134
+ peak_min_width=self.min_width,
135
+ peak_max_width=self.max_width,
136
+ peak_prominence=self.prominence,
137
+ peak_min_height=self.min_height,
138
+ detrended_timeseries=self.traces
139
+ )
140
+ self.finished.emit(peaks)
141
+ except Exception as e:
142
+ self.error.emit(e)
143
+
144
+ class PeriodicCellWorker(QObject):
145
+ finished = Signal(float, int, int, float, int, int)
146
+ error = Signal(Exception)
147
+
148
+ def __init__(self, peaks: np.ndarray, frame_interval: float, std_t: float = 0.01, cov_t: float = 0.01):
149
+ super().__init__()
150
+ self.peaks = peaks
151
+ self.frame_interval = frame_interval
152
+ self.std_t = std_t
153
+ self.cov_t = cov_t
154
+
155
+ def run(self):
156
+ try:
157
+ filtered_peak_series = self.peaks[:, np.sum(self.peaks, axis=0) >= 4]
158
+ result = find_oscillating_rois(
159
+ filtered_peak_series,
160
+ std_threshold=self.std_t,
161
+ cov_threshold=self.cov_t,
162
+ frame_interval=self.frame_interval,
163
+ )
164
+ self.finished.emit(*result)
165
+ except Exception as e:
166
+ self.error.emit(e)
167
+