calmono 0.0.3__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.
calmono-0.0.3/PKG-INFO ADDED
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.4
2
+ Name: calmono
3
+ Version: 0.0.3
4
+ Summary: Calcium imaging analysis of bovine DRG neurons
5
+ Author-email: Junxuan Ma <Junxuan.Ma@aofoundation.org>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: czifile>=2026.3.0
12
+ Requires-Dist: numpy>=1.26
13
+ Requires-Dist: scipy>=1.11
14
+ Requires-Dist: pandas>=2.2
15
+ Requires-Dist: matplotlib>=3.8
16
+ Requires-Dist: seaborn>=0.13
17
+ Requires-Dist: scikit-learn>=1.4
18
+ Requires-Dist: opencv-python>=4.9
19
+
20
+ The package CalMono
21
+
22
+ &#x09;Perform calcium imaging and immunofluorescent labeling images in Carl Zeiss Image format.
23
+
24
+ &#x09;Segment neuronal pixels from calcium imaging image.
25
+
26
+ &#x09;Match to the pixels from immunofluorescent image.
27
+
28
+ &#x09;Perform signal processing of the calcium imaging data and quantify neuronal activities.
29
+
@@ -0,0 +1,10 @@
1
+ The package CalMono
2
+
3
+ &#x09;Perform calcium imaging and immunofluorescent labeling images in Carl Zeiss Image format.
4
+
5
+ &#x09;Segment neuronal pixels from calcium imaging image.
6
+
7
+ &#x09;Match to the pixels from immunofluorescent image.
8
+
9
+ &#x09;Perform signal processing of the calcium imaging data and quantify neuronal activities.
10
+
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=82.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+
6
+ [project]
7
+ name = "calmono"
8
+ version = "0.0.3"
9
+ description = "Calcium imaging analysis of bovine DRG neurons"
10
+ authors = [
11
+ { name="Junxuan Ma", email="Junxuan.Ma@aofoundation.org" }
12
+ ]
13
+ dependencies = [
14
+ "czifile>=2026.3.0",
15
+ "numpy >= 1.26",
16
+ "scipy >= 1.11",
17
+ "pandas >= 2.2",
18
+ "matplotlib >= 3.8",
19
+ "seaborn >= 0.13",
20
+ "scikit-learn >= 1.4",
21
+ "opencv-python >= 4.9",
22
+ ]
23
+ readme = "README.md"
24
+ requires-python = ">=3.12"
25
+ classifiers = [
26
+ "Programming Language :: Python :: 3",
27
+ "Operating System :: OS Independent"
28
+ ]
29
+ license = {text = "MIT"}
30
+
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
34
+ namespaces = false
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,445 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Sat Mar 7 19:49:45 2026
4
+
5
+ @author: Junxuan Ma
6
+ """
7
+
8
+ #Semi-automatically matching calcium imaging pixels with pixels in the immunofluoresent (IF) stained images
9
+ """
10
+ The matching is performed through a registration process involving stretching and shifting,
11
+ while torsion and rotation are not considered. The goal is to align the pixel locations
12
+ in the calcium imaging image with those in the IF image. In other words, the objective
13
+ is to maximize the number of co-localized pixels (i.e., pixels with the same XY coordinates)
14
+ that are positive in both the calcium imaging and IF images. For example, if pixels
15
+ identified as cells (positive) in the calcium imaging image correspond to cell pixels
16
+ in the IF image, the matching is considered successful.
17
+
18
+ In this regard, IFmatch is composed of the following steps:
19
+ 1. Downsize the IF images if the resolution is different from that of calcium imaging.
20
+
21
+ 2. Binarize the IF images by optimizing the IF intensity threshold to obtain cell shapes
22
+ comparable to those in the calcium imaging.
23
+ Matching_plot_IF_threshold is helpful for deciding the IF intensity threshold.
24
+
25
+ 3. Use manual shifting and stretching and plot to understand the stretching factor.
26
+ This stretching is usually needed because of microscope errors, where the downsizing
27
+ performed in step one is insufficient to achieve equal scaling.
28
+ It can be assumed that IF images taken via the same microscope can use the same stretching factor
29
+ Manual_regi_plot is to decide on the stretching factors based on manual registration.
30
+ This is by inputting stretching factor a and c (fold change) in horizontal and vertical dimension
31
+ with corresponding shifter factor b and d (unit pixel) in horizontal and vertical dimension.
32
+
33
+ 4. Use automatic shifting correction of IF image through a simple optimization function.
34
+ The objective function maximizes the number of shared positive pixels.
35
+ 5. Optionally check the matching between calcium imaging and IF.
36
+ 6. Output a dataframe of X and Y coordinates of calcium imaging and an additional column of
37
+ IF intensities.
38
+ """
39
+ import cv2
40
+ import numpy as np
41
+ import pandas as pd
42
+ import matplotlib.pyplot as plt
43
+ from scipy.ndimage import map_coordinates
44
+ class IFmatch:
45
+ def __init__(
46
+ self,
47
+ Calcium_imaging_df: pd.DataFrame,
48
+ CGRP_raw: np.ndarray,
49
+ NF_raw: np.ndarray,
50
+ Calcium_height: int = 292,
51
+ Calcium_width: int = 384 ):
52
+
53
+ self.df_clust= Calcium_imaging_df
54
+ self.height= Calcium_height
55
+ self.width= Calcium_width
56
+
57
+ #The IF image is scaled to match the dimensions of the calcium imaging image.
58
+ self.cgrp= cv2.resize(CGRP_raw,dsize=(self.width,self.height),interpolation=cv2.INTER_AREA)
59
+ self.nf= cv2.resize(NF_raw,dsize=(self.width,self.height),interpolation=cv2.INTER_AREA)
60
+
61
+ def _if_threshold(self, channel, IF_threshold) -> np.ndarray:
62
+ return (channel >= IF_threshold).astype(np.uint8)
63
+
64
+ def _mask_matrix_to_df(self, mask: np.ndarray) -> pd.DataFrame:
65
+ ys, xs = np.nonzero(mask)
66
+ IF_df = pd.DataFrame({'X': xs.astype(float), 'Y': ys.astype(float)})
67
+ return IF_df
68
+
69
+ def Matching_plot_IF_threshold(self,
70
+ IF_threshold_list: list,
71
+ n_rows: int,
72
+ which_channel: str = 'NF'):
73
+ if which_channel == 'NF':
74
+ channel = self.nf
75
+ elif which_channel == 'CGRP':
76
+ channel = self.cgrp
77
+ else: raise ValueError(f"channel must be 'NF' or 'CGRP'. Got: {which_channel}")
78
+
79
+ n_plots = len(IF_threshold_list)
80
+ n_cols = (n_plots + n_rows - 1) // n_rows
81
+
82
+ fig, axes = plt.subplots(n_rows, n_cols, figsize=(n_cols * 5, n_rows * 4), squeeze=False)
83
+ for i, thresh in enumerate(IF_threshold_list):
84
+ row = i // n_cols
85
+ col = i % n_cols
86
+ ax = axes[row, col]
87
+ mask = self._if_threshold(channel, thresh) # shape (h,w), values 0 or 1
88
+ ys, xs = np.nonzero(mask) # Only coordinates where pixel == 1 (usually << 112k)
89
+ ax.scatter(xs, ys,
90
+ color='blue', s=1, lw=0,
91
+ label='IF positive')
92
+ ax.scatter(self.df_clust['X'], self.df_clust['Y'],
93
+ color='red', s=1, alpha=0.3, edgecolor='none',
94
+ label='Ca²⁺ clusters')
95
+ ax.set_title(f'Threshold = {thresh}')
96
+ ax.invert_yaxis()
97
+ ax.set_aspect('equal')
98
+ ax.axis('off')
99
+ # Hide unused panels
100
+ for j in range(i+1, n_rows * n_cols):
101
+ r = j // n_cols
102
+ c = j % n_cols
103
+ axes[r, c].set_visible(False)
104
+ fig.tight_layout()
105
+
106
+
107
+
108
+ def _registrate_linear(self, IF_bi_df, a,b, c,d):
109
+ #Input IF_bi_df should be a binary mask
110
+ IF_regi= IF_bi_df.copy()
111
+ IF_regi["X"]= IF_regi["X"]*a + b
112
+ IF_regi["Y"]= IF_regi["Y"]*c + d
113
+ return IF_regi
114
+
115
+ def Manual_regi_plot(self, IF_threshold, a,b, c,d, which_channel: str = 'NF',):
116
+ if which_channel == 'NF':
117
+ channel = self.nf
118
+ elif which_channel == 'CGRP':
119
+ channel = self.cgrp
120
+ else: raise ValueError(f"channel must be 'NF' or 'CGRP'. Got: {which_channel}")
121
+
122
+ mask = self._if_threshold(channel, IF_threshold)
123
+ IF_df= self._mask_matrix_to_df(mask)
124
+ IF_regi= self._registrate_linear(IF_df, a,b, c,d)
125
+
126
+
127
+ fig, (ax1,ax2) = plt.subplots(1,2, figsize=(self.width/10, self.height/20))
128
+
129
+ ax1.scatter( IF_regi['X'], IF_regi['Y'], color='blue', s=1)
130
+ ax1.scatter( self.df_clust["X"], self.df_clust["Y"], color='red',alpha=0.3, s=2)
131
+ ax1.set_title('Post-registration', fontsize=11)
132
+ ax1.set(xlim= (0,self.width-1), ylim= (0,self.height-1))
133
+ ax1.invert_yaxis()
134
+
135
+ ax2.scatter(IF_df['X'], IF_df['Y'], color='blue', s=1)
136
+ ax2.scatter( self.df_clust["X"], self.df_clust["Y"], color='red',alpha=0.3, s=2)
137
+ ax2.set_title('Pre-registration', fontsize=11)
138
+ ax2.set(xlim= (0,self.width-1), ylim= (0,self.height-1))
139
+ ax2.invert_yaxis()
140
+
141
+ plt.tight_layout()
142
+
143
+
144
+
145
+ def _registration_obj_fun(self, IF_reg_df, cal_df):
146
+ #This is a common algorithm in calculating mask overlapse
147
+ # zip() gives tupples of all combinations of X and Y. set() outputs set format removes duplicates
148
+ IF_set = set(zip(IF_reg_df['X'].round().astype(int), IF_reg_df['Y'].round().astype(int)))
149
+ Cal_set = set(zip(cal_df['X'].round().astype(int), cal_df['Y'].round().astype(int)))
150
+
151
+ intersection = len(IF_set & Cal_set)#& is only between sets
152
+ return intersection / len(Cal_set)
153
+
154
+ def Registration_optimize(self,
155
+ IF_threshold,
156
+ a = 1.0, # fixed scale in X
157
+ c = 1.0, # fixed scale in Y
158
+ step = 1.0,
159
+ start_X = -30.0,
160
+ end_X = 30.0,
161
+ start_Y = -30.0,
162
+ end_Y = 30.0,
163
+ plot_result = True,
164
+ which_channel: str = 'NF'):
165
+ """
166
+ Grid search over translation (b, d) with fixed stretching scale (a, c).
167
+ Returns best (X_shift, Y_shift) and corresponding score.
168
+ """
169
+ if which_channel == 'NF':
170
+ channel = self.nf
171
+ elif which_channel == 'CGRP':
172
+ channel = self.cgrp
173
+ else: raise ValueError(f"channel must be 'NF' or 'CGRP'. Got: {which_channel}")
174
+
175
+ if step <= 0:
176
+ raise ValueError("step must be positive")
177
+ # Generate empty data containers for shifts, scores, best shift and best score.
178
+ x_shifts = np.arange(start_X, end_X + step/2, step)
179
+ y_shifts = np.arange(start_Y, end_Y + step/2, step)
180
+ scores = np.full((len(y_shifts), len(x_shifts)), np.nan) # 2D grid for heatmap
181
+ best_score = -np.inf
182
+ best_shift = (0.0, 0.0)
183
+ shift_record = []
184
+ obj_values = []
185
+
186
+ mask = self._if_threshold(channel, IF_threshold)
187
+ IF_df= self._mask_matrix_to_df(mask)
188
+ for i_x, dx in enumerate(x_shifts):#enumerate output both indices and values
189
+ for i_y, dy in enumerate(y_shifts):
190
+ # Apply registration
191
+ IF_reg = self._registrate_linear(IF_df, a, dx, c, dy)
192
+
193
+ # Compute score
194
+ score = self._registration_obj_fun(IF_reg, self.df_clust)
195
+ scores[i_y, i_x] = score
196
+
197
+ obj_values.append(score)
198
+ shift_record.append((dx, dy))
199
+
200
+ if score > best_score:
201
+ best_score = score
202
+ best_shift = (dx, dy)
203
+
204
+ if plot_result and len(obj_values) > 1:
205
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5.5))
206
+
207
+ # Heatmap of scores
208
+ im = ax1.imshow(
209
+ scores,
210
+ extent=[x_shifts.min(), x_shifts.max(), y_shifts.min(), y_shifts.max()],
211
+ origin='lower',
212
+ aspect='auto',
213
+ cmap='magma',
214
+ interpolation='nearest'
215
+ )
216
+ ax1.plot(best_shift[0], best_shift[1], 'c*', markersize=18, label='Best')
217
+ ax1.set_title('Objective function landscape')
218
+ ax1.set_xlabel('X shift (pixels)')
219
+ ax1.set_ylabel('Y shift (pixels)')
220
+ plt.colorbar(im, ax=ax1, label='Overlap ratio')
221
+
222
+ # 1D progression (useful for debugging order)
223
+ ax2.plot(obj_values, '.-', color='teal', lw=1)
224
+ ax2.set_xlabel('Evaluation #')
225
+ ax2.set_ylabel('Score')
226
+ ax2.set_title('Score during grid search')
227
+ ax2.grid(True, alpha=0.3)
228
+
229
+ plt.tight_layout()
230
+
231
+
232
+ return best_shift, best_score
233
+
234
+
235
+
236
+
237
+
238
+ def _matrix_to_df(self,
239
+ Coordinates: pd.DataFrame,
240
+ image: np.ndarray,
241
+ name: str = 'value') -> pd.DataFrame:
242
+ ys = Coordinates['Y'].astype(int).to_numpy()
243
+ xs = Coordinates['X'].astype(int).to_numpy()
244
+ cluster= Coordinates['Cluster'].to_numpy()
245
+ df = pd.DataFrame({'Y': ys, 'X': xs, "Cluster":cluster, name: image[ys, xs] })
246
+ return df
247
+
248
+ """The updated pixel should be
249
+ IF_regi["X1"]= IF_["X"]*a + b
250
+ IF_regi["Y1"]= IF_["Y"]*c + d
251
+ Now we know IF_regi["X1"] and IF_regi["Y1"], for they are the calcium imaging coordinates
252
+ we need to solve IF_["X"] and IF_["Y"] that are the pixels in IF corresponding to the calcium pixel
253
+ """
254
+
255
+ def Merge_channels_in_df(self, a, b, c, d):
256
+
257
+ merged = self.df_clust.copy()
258
+
259
+ # calculate the transformed coordinates for indexing
260
+ x_idx = ((merged["X"] - b) / a).clip(0, self.width-1).astype(int).copy()
261
+ y_idx = ((merged["Y"] - d) / c).clip(0, self.height-1).astype(int).copy()
262
+
263
+ # add IF values
264
+ merged["NF"] = map_coordinates(self.nf, [y_idx, x_idx], order=1, mode='nearest')
265
+ merged["CGRP"] = map_coordinates(self.cgrp, [y_idx, x_idx], order=1, mode='nearest')
266
+
267
+ return merged
268
+
269
+
270
+
271
+
272
+ """---------------------------------------------------------------------------------------------------------
273
+ A more efficient way is to do the registration in matrix"""
274
+
275
+ import cv2
276
+ import numpy as np
277
+ import pandas as pd
278
+ import matplotlib.pyplot as plt
279
+
280
+
281
+ class IFmatchMatrix:
282
+
283
+ def __init__(
284
+ self,
285
+ Calcium_imaging_df: pd.DataFrame,
286
+ CGRP_raw: np.ndarray,
287
+ NF_raw: np.ndarray,
288
+ Calcium_height: int = 292,
289
+ Calcium_width: int = 384
290
+ ):
291
+
292
+ self.df_clust = Calcium_imaging_df
293
+ self.height = Calcium_height
294
+ self.width = Calcium_width
295
+
296
+ # resize IF images
297
+ self.cgrp = cv2.resize(CGRP_raw, (self.width, self.height),
298
+ interpolation=cv2.INTER_AREA)
299
+
300
+ self.nf = cv2.resize(NF_raw, (self.width, self.height),
301
+ interpolation=cv2.INTER_AREA)
302
+
303
+ # build calcium mask once
304
+ self.cal_mask = np.zeros((self.height, self.width), dtype=np.uint8)
305
+
306
+ ys = self.df_clust["Y"].astype(int).to_numpy()
307
+ xs = self.df_clust["X"].astype(int).to_numpy()
308
+
309
+ self.cal_mask[ys, xs] = 1
310
+
311
+
312
+ def _if_threshold(self, image, threshold):
313
+
314
+ return (image >= threshold).astype(np.uint8)
315
+
316
+
317
+ def _affine_transform(self, mask, a, b, c, d):
318
+
319
+ M = np.array([
320
+ [a, 0, b],
321
+ [0, c, d]
322
+ ], dtype=np.float32)
323
+
324
+ transformed = cv2.warpAffine(
325
+ mask,
326
+ M,
327
+ (self.width, self.height),
328
+ flags=cv2.INTER_NEAREST
329
+ )
330
+
331
+ return transformed
332
+
333
+
334
+ def _registration_score(self, IF_mask):
335
+
336
+ overlap = np.sum(IF_mask & self.cal_mask)
337
+
338
+ return overlap / np.sum(self.cal_mask)
339
+
340
+
341
+ def Registration_optimize(
342
+ self,
343
+ IF_threshold,
344
+ a=1.0,
345
+ c=1.0,
346
+ step=1,
347
+ start_X=-30,
348
+ end_X=30,
349
+ start_Y=-30,
350
+ end_Y=30,
351
+ which_channel="NF",
352
+ plot_result=True
353
+ ):
354
+
355
+ if which_channel == "NF":
356
+ channel = self.nf
357
+ elif which_channel == "CGRP":
358
+ channel = self.cgrp
359
+ else:
360
+ raise ValueError("channel must be NF or CGRP")
361
+
362
+ IF_mask = self._if_threshold(channel, IF_threshold)
363
+
364
+ x_shifts = np.arange(start_X, end_X + step, step)
365
+ y_shifts = np.arange(start_Y, end_Y + step, step)
366
+
367
+ scores = np.zeros((len(y_shifts), len(x_shifts)))
368
+
369
+ best_score = -1
370
+ best_shift = (0, 0)
371
+
372
+ for ix, dx in enumerate(x_shifts):
373
+ for iy, dy in enumerate(y_shifts):
374
+
375
+ IF_trans = self._affine_transform(IF_mask, a, dx, c, dy)
376
+
377
+ score = self._registration_score(IF_trans)
378
+
379
+ scores[iy, ix] = score
380
+
381
+ if score > best_score:
382
+ best_score = score
383
+ best_shift = (dx, dy)
384
+
385
+ if plot_result:
386
+
387
+ plt.figure(figsize=(6,5))
388
+ plt.imshow(
389
+ scores,
390
+ extent=[x_shifts.min(), x_shifts.max(),
391
+ y_shifts.min(), y_shifts.max()],
392
+ origin="lower",
393
+ cmap="magma"
394
+ )
395
+
396
+ plt.scatter(best_shift[0], best_shift[1], c="cyan", s=120)
397
+
398
+ plt.xlabel("X shift")
399
+ plt.ylabel("Y shift")
400
+ plt.title("Registration objective")
401
+ plt.colorbar(label="overlap ratio")
402
+
403
+ plt.show()
404
+
405
+ return best_shift, best_score
406
+
407
+
408
+ def Merge_channels_in_df(self, a, b, c, d):
409
+
410
+ coords = self.df_clust.copy()
411
+
412
+ # inverse transform
413
+ coords["X"] = (coords["X"] - b) / a
414
+ coords["Y"] = (coords["Y"] - d) / c
415
+
416
+ xs = coords["X"].clip(0, self.width-1).astype(int)
417
+ ys = coords["Y"].clip(0, self.height-1).astype(int)
418
+
419
+ merged = self.df_clust.copy()
420
+
421
+ merged["NF"] = self.nf[ys, xs]
422
+ merged["CGRP"] = self.cgrp[ys, xs]
423
+
424
+ return merged
425
+
426
+
427
+
428
+
429
+ """Combine the IF with calcium imaging analysis outcome
430
+ A good merge function explaination
431
+ https://www.geeksforgeeks.org/python-pandas-merging-joining-and-concatenating/
432
+ 1. The first step is an "outer" merge of TU and CGRP, the output is all X,Y that
433
+ either positive for TU or CGRP, if only positive in TU, in CGRP it will be NA
434
+ 2. The second step is to do a "right" merge that calcium (Position) is on the right.
435
+ In this way, only pixels of calcium will remain
436
+ calcium pixels without either TU or CGRP will be NA at corresponding position
437
+ 3. The input is already segmented and registerred (regarding calcium)"""
438
+ def merge_df_channels(IF_TU_regi, IF_CGRP_regi, Calcium):
439
+ IF_com =pd.merge(IF_TU_regi.rename(columns={'IF':'TU'})[['X1','Y1','TU']],
440
+ IF_CGRP_regi.rename(columns={'IF':'CGRP'})[['X1','Y1','CGRP']],
441
+ how="outer", on=['X1','Y1'])
442
+ Cal_IF =pd.merge(Calcium[['X','Y','Neuron_indice','Base','Bra','CZP', 'Cap','Kcl']],
443
+ IF_com.rename(columns={'X1':'X','Y1':'Y'}),
444
+ how="left", on=['X','Y'])
445
+ return Cal_IF
@@ -0,0 +1,139 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Fri Feb 20 20:14:33 2026
4
+
5
+ @author: Junxuan Ma
6
+ """
7
+
8
+ import glob
9
+ import czifile
10
+ import numpy as np
11
+ import cv2
12
+ import os
13
+
14
+ #Read confocal files
15
+ """
16
+ To automate import, match IF (immunofluorescence) with calcium imaging with the same Sample Name.
17
+ Specifically: IF is in a form of "IF_sample_name.czi".
18
+ And Calcium imaging is in a form of "Cal_sample_name.czi".
19
+ The sample name would be "sample_name.czi" .
20
+ Note that the IF_ or Cal_ tag has to be before the same sample name.
21
+
22
+ All the calcium imaging files and IF files are stored in the folder defined by the input Local_path
23
+ Since the files are very large and there is no memory to store all simutaneously, it has to be read one and analyze, then read the next.
24
+
25
+ Thus the Auto_ImportCZI contains 2 steps:
26
+ 1. Fetch_Files: list all files representing all samples
27
+ Inputs (parameters):
28
+ --------
29
+ Local_path: the local directory of the image files
30
+ Cal_tag: the string pattern in the file name that can be used to distinguish Calcium imaging from IF
31
+ IF_tag: the string pattern specific for IF file
32
+ Note: calcium imaging file name can be switched to IF file name
33
+ by changing Cal_tag to IF_tag
34
+
35
+ Outputs (attributes):
36
+ ---------
37
+ Get_cal_files: a function to call all the calcium imaging file names.
38
+ Get_if_files: a function to call all the IF file names
39
+
40
+
41
+ 2. ImportCZI: choose one to import and make it easy for iteration
42
+ Notice that the IF and calcium imaging file names have to be well matched.
43
+ The only difference is their tags before the sample name.
44
+ Inputs (parameters):
45
+ --------
46
+ local_path: the local directory of the image files
47
+ Cal_name: the filename of one calcium imaging chosen for analysis
48
+ Cal_tag: the string pattern in the file name that can be used to distinguish Calcium imaging from IF
49
+ IF_tag: the string pattern specific for IF file
50
+ Note: calcium imaging file name can be switched to IF file name
51
+ by changing Cal_tag to IF_tag
52
+
53
+ Outputs (attributes):
54
+ ---------
55
+ Read_if: input which channel index is "CGRP" and which is "NF".
56
+ output the CGRP and NF image file in np.ndarray, respectively.
57
+ Read_cal: output the raw calcium imaging data as np.ndarray
58
+ and the pixel size of the image in a list of (height, width).
59
+
60
+
61
+ Note the read_image function needs to be adapted based on the format of image
62
+ For the following analysis the imported calcium imaging data should be np.array and need to have time in 0th dimention
63
+
64
+ """
65
+ #Step 1, list all files representing all samples
66
+ from pathlib import Path
67
+ class FetchFiles:
68
+ def __init__(
69
+ self,
70
+ local_path: str,
71
+ cal_tag: str = "Cal",
72
+ if_tag: str = "IF",
73
+ ) -> None:
74
+ self._path = Path(local_path)
75
+ self._cal_tag = cal_tag
76
+ self._if_tag = if_tag
77
+
78
+ if not self._path.exists():
79
+ raise FileNotFoundError(f"Path does not exist: {self._path}")
80
+
81
+ # ---------- Internal logic ----------
82
+ def _get_filenames(self, tag: str):
83
+ return [f.name for f in self._path.glob(f"{tag}*.czi")]
84
+
85
+ # ---------- Public API ----------
86
+ def Get_cal_files(self):
87
+ return self._get_filenames(self._cal_tag)
88
+
89
+ def Get_if_files(self):
90
+ return self._get_filenames(self._if_tag)
91
+
92
+
93
+ #Step 2, choose one to import and make it easy for iteration
94
+ class ImportCZI():
95
+ def __init__(self,
96
+ local_path: str,
97
+ Cal_name: str = 'Cal_Donor1_Day2_G5_b5_2.czi',
98
+ cal_tag: str = "Cal",
99
+ if_tag: str = "IF"):
100
+ self._path = Path(local_path)
101
+ self.Cal_name = self._path / Cal_name #Full path object, not just file name
102
+ self._cal_tag = cal_tag
103
+ self._if_tag = if_tag
104
+
105
+ #To ensure correspondence, IF path is derived from Cal by changing tag
106
+ self.IF_name = self.Cal_name.with_name(#Full path object
107
+ self.Cal_name.name.replace(self._cal_tag, self._if_tag)
108
+ )#Full path object, not just file name
109
+
110
+ self._if_data = np.squeeze(czifile.imread(self.IF_name))
111
+ self._cal_data = np.squeeze(czifile.imread(self.Cal_name))
112
+
113
+ # ---------- Internal logic ----------
114
+ def _read_IF(self, CGRP: int, NF: int) ->tuple[np.ndarray,np.ndarray]:
115
+ if CGRP >= self._if_data.shape[0] or NF >= self._if_data.shape[0]:
116
+ raise IndexError("CGRP or NF index out of bounds of channels")
117
+ return self._if_data[CGRP,:,:], self._if_data[NF,:,:]
118
+
119
+ def _read_Cal(self):
120
+ """The same as:
121
+ return np.reshape(self._cal_data,(self._cal_data.shape[0],
122
+ self._cal_data.shape[1]*self._cal_data.shape[2]))"""
123
+ return np.reshape(self._cal_data,(self._cal_data.shape[0],-1))
124
+
125
+ def _cal_shape(self):
126
+ return self._cal_data.shape
127
+
128
+ # ---------- Public API ----------
129
+
130
+ def Read_if(self, CGRP: int, NF: int)->tuple[np.ndarray,np.ndarray]:
131
+ return self._read_IF(CGRP,NF)
132
+
133
+ def Read_cal(self) -> np.ndarray:
134
+ return self._read_Cal(), self._cal_shape()
135
+
136
+
137
+
138
+
139
+