calsipro 0.10.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.
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.1
2
+ Name: calsipro
3
+ Version: 0.10.0
4
+ Summary:
5
+ Author: Simon Haendeler
6
+ Author-email: simon.ac@haend.de
7
+ Requires-Python: >=3.8,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.8
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Requires-Dist: Pillow (>=9.2.0,<10.0.0)
14
+ Requires-Dist: aicsimageio (>=4.9.4,<5.0.0)
15
+ Requires-Dist: aicspylibczi (>=3.0.5)
16
+ Requires-Dist: bokeh (>=3.0.0,<4.0.0)
17
+ Requires-Dist: click (>=8.1.3,<9.0.0)
18
+ Requires-Dist: datashader (>=0.14.2,<0.15.0)
19
+ Requires-Dist: ffmpeg-python (>=0.2.0,<0.3.0)
20
+ Requires-Dist: fsspec (>=2022.7.1)
21
+ Requires-Dist: matplotlib (>=3.5.3,<4.0.0)
22
+ Requires-Dist: numba (>=0.56.0,<0.57.0) ; python_version >= "3.8" and python_version < "3.11"
23
+ Requires-Dist: numba (>=0.57,<0.58) ; python_version >= "3.11"
24
+ Requires-Dist: numpy (>=1.18)
25
+ Requires-Dist: openpyxl (>=3.0.10,<4.0.0)
26
+ Requires-Dist: polars (>=0.16.14,<0.17.0)
27
+ Requires-Dist: pywavelets (>=1.4.1,<2.0.0)
28
+ Requires-Dist: s3fs (>=2023.5.0,<2024.0.0)
29
+ Requires-Dist: scikit-learn (>=1.2.1,<2.0.0)
30
+ Requires-Dist: scipy (>=1.9.0,<2.0.0)
31
+ Requires-Dist: syn-bokeh-helpers (>=0.5.0,<0.6.0)
@@ -0,0 +1,50 @@
1
+ [tool.ruff]
2
+ line-length = 120
3
+
4
+ [tool.poetry]
5
+ name = "calsipro"
6
+ version = "0.10.0"
7
+ description = ""
8
+ authors = ["Simon Haendeler <simon.ac@haend.de>"]
9
+
10
+ [tool.poetry.scripts]
11
+ calsipro = 'calsipro.cli:cli'
12
+
13
+ [tool.poetry.dependencies]
14
+ python = "^3.8"
15
+ openpyxl = "^3.0.10"
16
+ datashader = "^0.14.2"
17
+ numba = [{version = "^0.56.0", python = ">=3.8,<3.11"}, {version = "^0.57", python=">=3.11"}]
18
+ Pillow = "^9.2.0"
19
+ click = "^8.1.3"
20
+ numpy = ">=1.18"
21
+ scipy = "^1.9.0"
22
+ matplotlib = "^3.5.3"
23
+ bokeh = "^3.0.0"
24
+ aicsimageio = "^4.9.4"
25
+ aicspylibczi = ">=3.0.5"
26
+ fsspec = ">=2022.7.1"
27
+ syn-bokeh-helpers = {version = "^0.5.0", source = "syntonym"}
28
+ pywavelets = "^1.4.1"
29
+ ffmpeg-python = "^0.2.0"
30
+ scikit-learn = "^1.2.1"
31
+ polars = "^0.16.14"
32
+ s3fs = "^2023.5.0"
33
+
34
+ [tool.poetry.dev-dependencies]
35
+
36
+ [tool.poetry.group.dev.dependencies]
37
+ pytest = "^7.2.0"
38
+ mypy = "^0.991"
39
+ popy = {version = "^0.1.1", source = "syntonym"}
40
+
41
+
42
+ [[tool.poetry.source]]
43
+ name = "syntonym"
44
+ url = "http://localhost:8080/"
45
+ default = false
46
+ secondary = false
47
+
48
+ [build-system]
49
+ requires = ["poetry-core>=1.0.0"]
50
+ build-backend = "poetry.core.masonry.api"
File without changes
@@ -0,0 +1,365 @@
1
+ import numpy as np
2
+ import polars as pl
3
+ import scipy.ndimage
4
+ import numba
5
+
6
+ def moving_average(a, n=3):
7
+ if n == 0:
8
+ return a
9
+ ret = np.cumsum(a, axis=2, dtype=float)
10
+ ret[:, :, n:] = ret[:, :, n:] - ret[:, :, :-n]
11
+ return ret[:, :, n - 1:] / n
12
+
13
+
14
+ def normalize(data):
15
+ m, mm = np.min(data), np.max(data)
16
+ data = (data - m) / (mm - m)
17
+ return data
18
+
19
+
20
+ def _calculate_bf_threshold_and_mask(data, min_size=30, border_size=5):
21
+
22
+ calculation_needed = True
23
+ pick = 1
24
+ while calculation_needed and pick < 80:
25
+ threshold = calculate_threshold(data, pick=pick)
26
+ mask = calculate_mask(data, th=threshold, raw=True, larger=False)
27
+ mask_size = mask.sum()
28
+ if mask_size == 0:
29
+ calculation_needed = False
30
+ elif mask_size < min_size:
31
+ pick += 1
32
+ else:
33
+ calculation_needed = False
34
+
35
+ return mask
36
+
37
+
38
+ def calculate_mask(t, th=0.25, raw=False, labelling=True, larger=True):
39
+ if not raw:
40
+ t = np.max(t, axis=0)
41
+ t = normalize(t)
42
+ if larger:
43
+ mask = t >= th
44
+ else:
45
+ mask = t <= th
46
+ if not labelling:
47
+ return mask
48
+ image = np.ones(mask.shape)
49
+ image[~mask] = 0
50
+ image[mask] = 1
51
+ label, count = scipy.ndimage.label(image)
52
+ if count == 1:
53
+ return mask
54
+ else:
55
+ sizes = []
56
+ for k in range(1, count+1):
57
+ size = np.sum(label[mask] == k)
58
+ sizes.append(size)
59
+ if len(sizes) > 0:
60
+ biggest = np.argmax(sizes)+1
61
+ else:
62
+ biggest = 1
63
+ return label == biggest
64
+
65
+
66
+ def calculate_threshold(data, pick=1):
67
+ if np.min(data) == 0:
68
+ offset = 1
69
+ else:
70
+ offset = 0
71
+ try:
72
+ counts, bins = np.histogram(np.log(data+offset), 80)
73
+ except Exception as e:
74
+ d1 = data+offset
75
+ d2 = np.log(d1)
76
+ print('data+offset', d1)
77
+ print('log(data+offset)', d2)
78
+ print('offset', offset)
79
+ print('data min', np.min(data))
80
+ print('data max', np.max(data))
81
+ print('data+offset min', np.min(d1))
82
+ print('data+offset max', np.max(d1))
83
+ print('log(data+offset min)', np.min(d2))
84
+ print('log(data+offset max)', np.max(d2))
85
+ raise e
86
+
87
+
88
+ for i in range(len(counts)):
89
+ if counts[i] <= 0:
90
+ counts[i] = 1
91
+ else:
92
+ break
93
+
94
+ for i in range(1, len(counts)):
95
+ if counts[-i] <= 0:
96
+ counts[-i] = 1
97
+ else:
98
+ break
99
+
100
+
101
+ freq = np.log(1+counts)
102
+ left_flood = freq.copy()
103
+ right_flood = freq.copy()
104
+ flood = freq.copy()
105
+
106
+ for i in range(1, len(freq)):
107
+ left_flood[i] = max(left_flood[i], left_flood[i-1])
108
+
109
+ for i in list(range(0, len(freq)-1))[::-1]:
110
+ right_flood[i] = max(right_flood[i], right_flood[i+1])
111
+
112
+ for i in range(0, len(freq)):
113
+ flood[i] = min(left_flood[i], right_flood[i])
114
+
115
+ f = flood - freq
116
+ if pick != 1:
117
+ idxs = np.argsort(flood-freq)
118
+ idx = idxs[-pick]
119
+ else:
120
+ idx = np.argmax(flood-freq)
121
+
122
+ rest = freq[idx:]
123
+ low = freq[idx]
124
+ high = np.max(rest)
125
+
126
+ idx_offset = max(0, np.argmax(rest >= (low + (high-low)*0.10))-1)
127
+ idx = idx + idx_offset
128
+
129
+
130
+ pixels = np.sum(counts[idx:]) / np.sum(counts)
131
+ if pixels < 0.001:
132
+ return np.max(data)+1
133
+ if 0.999 < pixels:
134
+ return np.max(data)+1
135
+ if idx == 0:
136
+ return np.max(data)+1
137
+ return np.exp(bins[idx+1])-offset
138
+
139
+
140
+ def _find_biggest(mask):
141
+ label, count = scipy.ndimage.label(mask)
142
+ if count == 1:
143
+ return mask
144
+ else:
145
+ sizes = list(np.bincount(label[mask]))
146
+ assert sizes[0] == 0
147
+ sizes = sizes[1:]
148
+ assert len(sizes) == count
149
+ if len(sizes) > 0:
150
+ biggest = np.argmax(sizes)+1
151
+ else:
152
+ biggest = 1
153
+ return label == biggest
154
+
155
+
156
+ def time_analysis(t, intensity_cutoff=0.5):
157
+ t = t - np.min(t, axis=0).reshape((1, t.shape[1], t.shape[2]))
158
+ t = t / np.max(t, axis=0).reshape((1, t.shape[1], t.shape[2]))
159
+
160
+ time = np.argmax(t >= intensity_cutoff, axis=0)
161
+ return time
162
+
163
+
164
+ def push_low_pixels(time, mask):
165
+
166
+ m = np.min(time[mask])
167
+ mm = np.max(time[mask])
168
+
169
+ time[~mask] = mm+1
170
+
171
+ for i in range(m, mm+1):
172
+ if np.sum(time == i) < 30:
173
+ time[time == i] = i+1
174
+ m = m+1
175
+ else:
176
+ break
177
+
178
+ for i in range(m, mm+1)[::-1]:
179
+ if np.sum(time == i) < 30:
180
+ time[time == i] = i-1
181
+ mm = mm-1
182
+ else:
183
+ break
184
+ time[~mask] = np.max(time)
185
+ return time
186
+
187
+
188
+ def find_times(data, mask, times, as_index=True):
189
+ data = data.copy()
190
+ data[~mask] = np.min(data)-1
191
+ if as_index:
192
+ return [(data == time).nonzero() for time in times]
193
+ else:
194
+ return [(data == time) for time in times]
195
+
196
+
197
+ def find_ori_cluster(data, mask, as_index=False):
198
+ cluster_mask = np.zeros(data.shape, dtype=np.bool_)
199
+ ori_time = np.min(data[mask])
200
+ cluster_mask[data == ori_time] = 1
201
+ cluster_mask[~mask] = 0
202
+ label, count = scipy.ndimage.label(cluster_mask, scipy.ndimage.generate_binary_structure(2, 2))
203
+
204
+ if count > 1:
205
+ sizes = []
206
+ for k in range(1, count+1):
207
+ size = np.sum(label[mask] == k)
208
+ sizes.append(size)
209
+ biggest = np.argmax(sizes)+1
210
+ cluster_mask = (label == biggest)
211
+
212
+ if as_index:
213
+ xs, ys = cluster_mask.nonzero()
214
+ return np.array((np.average(xs), np.average(ys))).reshape((2, 1))
215
+ else:
216
+ return cluster_mask
217
+
218
+
219
+ def calculate_speed(time, mask):
220
+ m, mm = np.min(time[mask]), np.max(time[mask])
221
+
222
+ ori_pos = np.array(find_ori_cluster(time, mask, as_index=True)).reshape((2, 1))
223
+
224
+ timepoints = list(range(m+1, mm+1))
225
+ locations = [np.stack([x, y]) for x, y in find_times(time, mask, timepoints, as_index=True)]
226
+
227
+ dts = [0]
228
+ speeds = [-1]
229
+ ns = [np.sum(time == m)]
230
+ total_speed = 0
231
+ total_ns = 0
232
+ for t, l in zip(timepoints, locations):
233
+ dists = np.sqrt(np.sum((l - ori_pos)**2, axis=0))
234
+ dt = t-m
235
+ dl = np.sum(dists)
236
+ n = dists.shape[0]
237
+ if n == 0:
238
+ continue
239
+ total_speed += dl/dt
240
+ total_ns += n
241
+ speeds.append(dl/(n*dt))
242
+ ns.append(n)
243
+ dts.append(dt)
244
+ if total_ns == 0:
245
+ total_ns = 1
246
+
247
+ r = (pl.DataFrame({'time': np.array(dts, dtype=np.int64),
248
+ 'speed': np.array(speeds, dtype=np.float64),
249
+ 'n': np.array(ns, dtype=np.int64)}),
250
+ total_speed/total_ns)
251
+ return r
252
+
253
+
254
+ def calculate_speed_better(time, mask):
255
+ ori_pos = find_ori_cluster(time, mask, as_index=True)
256
+
257
+ x_dist = np.repeat((np.arange(time.shape[0]) - ori_pos[0]).reshape((time.shape[0], 1)), time.shape[1], axis=1)
258
+ y_dist = np.repeat((np.arange(time.shape[1]) - ori_pos[1]).reshape((1, time.shape[1])), time.shape[0], axis=0)
259
+
260
+ dists = np.sqrt(x_dist**2 - y_dist**2)
261
+
262
+ time = time - np.min(time)
263
+ speed = dists / time
264
+ speed[time == 0] = 0
265
+
266
+ return speed
267
+
268
+
269
+ def tabularize_speed(time, speed, mask):
270
+ m, mm = np.min(time), np.max(time)
271
+ timepoints = list(range(m+1, mm+1))
272
+
273
+ time[mask] = mm+1
274
+
275
+ ns = [np.sum(time == timepoint) for timepoint in timepoints]
276
+ speed = [np.average(speed[time == timepoint]) for timepoint in timepoints]
277
+
278
+ r = (pl.DataFrame({'time': np.array(timepoints, dtype=np.int64),
279
+ 'speed': np.array(speed, dtype=np.float64),
280
+ 'n': np.array(ns, dtype=np.int64)}),
281
+ np.average(speed[mask]))
282
+ return r
283
+
284
+
285
+ def reachability(data, threshold=0.02):
286
+ data = normalize(data)
287
+ mask = np.zeros(shape=data.shape, dtype=np.bool_)
288
+ scheduled = np.zeros(shape=data.shape, dtype=np.bool_)
289
+ _reachability(data, scheduled, mask, threshold, 1, 1)
290
+ return mask
291
+
292
+
293
+ @numba.njit(cache=True)
294
+ def _reachability(data, scheduled, mask, threshold, dx, dy):
295
+ next = []
296
+ x_len, y_len = data.shape
297
+ for y in range(y_len-1):
298
+ x = 0
299
+ mask[x, y] = True
300
+ next.append((x, y))
301
+ scheduled[x, y] = True
302
+
303
+ x = x_len-1
304
+ mask[x, y] = True
305
+ next.append((x, y))
306
+ scheduled[x, y] = True
307
+
308
+ for x in range(x_len-1):
309
+ y = 0
310
+ mask[x, y] = True
311
+ next.append((x, y))
312
+ scheduled[x, y] = True
313
+
314
+ y = y_len-1
315
+ mask[x, y] = True
316
+ next.append((x, y))
317
+ scheduled[x, y] = True
318
+
319
+ while (len(next) != 0):
320
+ x, y = next.pop(0)
321
+ b_v = data[x,y]
322
+
323
+ nx, ny = x+dx, y
324
+ if 0 <= nx < x_len:
325
+ v = data[nx, ny]
326
+ if abs(v-b_v) <= threshold:
327
+ mask[nx, ny] = True
328
+ if not scheduled[nx, ny]:
329
+ scheduled[nx, ny] = True
330
+ next.append((nx, ny))
331
+
332
+ nx, ny = x-dx, y
333
+ if 0 <= nx < x_len:
334
+ v = data[nx, ny]
335
+ if abs(v-b_v) <= threshold:
336
+ mask[nx, ny] = True
337
+ if not scheduled[nx, ny]:
338
+ scheduled[nx, ny] = True
339
+ next.append((nx, ny))
340
+
341
+ nx, ny = x, y+dy
342
+ if 0 <= ny < y_len:
343
+ v = data[nx, ny]
344
+ if abs(v-b_v) <= threshold:
345
+ mask[nx, ny] = True
346
+ if not scheduled[nx, ny]:
347
+ scheduled[nx, ny] = True
348
+ next.append((nx, ny))
349
+
350
+ nx, ny = x, y-dy
351
+ if 0 <= ny < y_len:
352
+ v = data[nx, ny]
353
+ if abs(v-b_v) <= threshold:
354
+ mask[nx, ny] = True
355
+ if not scheduled[nx, ny]:
356
+ scheduled[nx, ny] = True
357
+ next.append((nx, ny))
358
+
359
+
360
+ def calculate_bf_mask(data):
361
+ data = data.copy()
362
+ reachability_mask = reachability(data)
363
+ data[reachability_mask] = np.mean(data[reachability_mask])
364
+ mask = _calculate_bf_threshold_and_mask(data)
365
+ return mask