calmono 0.0.3__py3-none-any.whl
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/__init__.py +0 -0
- calmono/ifmatch.py +445 -0
- calmono/imgimport.py +139 -0
- calmono/imgseg.py +740 -0
- calmono/siganalysis.py +637 -0
- calmono-0.0.3.dist-info/METADATA +29 -0
- calmono-0.0.3.dist-info/RECORD +9 -0
- calmono-0.0.3.dist-info/WHEEL +5 -0
- calmono-0.0.3.dist-info/top_level.txt +1 -0
calmono/__init__.py
ADDED
|
File without changes
|
calmono/ifmatch.py
ADDED
|
@@ -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
|
calmono/imgimport.py
ADDED
|
@@ -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
|
+
|