Skip to content

Commit 90be895

Browse files
Merge pull request #1228 from MouseLand/add_chan2_manual_labeling
Add chan2 manual labeling
2 parents 17fad9a + 0914713 commit 90be895

9 files changed

Lines changed: 101 additions & 83 deletions

File tree

suite2p/detection/detect.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,7 @@ def detection_wrapper(f_reg, diameter=[12., 12.], tau=1., fs=30, meanImg_chan2=N
166166

167167
if mov is None:
168168
nbins = settings["nbins"]
169-
bin_size = int(max(1, n_frames // nbins, np.round(tau * fs)))
170-
#bin_size = int(max(1, np.round(tau * fs)))
169+
bin_size = settings.get("bin_size") or int(max(1, n_frames // nbins, np.round(tau * fs)))
171170
logger.info("Binning movie in chunks of %2.2d frames" % bin_size)
172171
mov = bin_movie(f_reg, bin_size, yrange=yrange, xrange=xrange,
173172
badframes=badframes, nbins=nbins)

suite2p/detection/sparsedetect.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,8 @@ def sparsery(mov, sdmov, highpass_neuropil,
565565
scale, estimate_mode = find_best_scale(I=I, spatial_scale=spatial_scale)
566566

567567
spatscale_pix = 3 * 2**scale
568+
if isinstance(spatscale_pix, np.ndarray):
569+
spatscale_pix = spatscale_pix.item()
568570
mask_window = int(((spatscale_pix * 1.5) // 2) * 2)
569571
Th2 = threshold_scaling * 5 * max(
570572
1, scale) # threshold for accepted peaks (scale it by spatial scale)

suite2p/gui/drawroi.py

Lines changed: 60 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import numpy as np
99
import pyqtgraph as pg
1010
from qtpy import QtGui, QtCore
11-
from qtpy.QtWidgets import QPushButton, QLabel, QLineEdit, QMainWindow, QGridLayout, QButtonGroup, QMessageBox, QWidget
11+
from qtpy.QtWidgets import QPushButton, QLabel, QLineEdit, QMainWindow, QGridLayout, QButtonGroup, QMessageBox, QWidget, QVBoxLayout
1212
from matplotlib.colors import hsv_to_rgb
1313
from scipy import stats
1414
from scipy.ndimage import rotate
@@ -121,8 +121,10 @@ def __init__(self, bid, Text, parent=None):
121121
self.show()
122122

123123
def press(self, parent, bid):
124+
for b in range(len(parent.views)):
125+
parent.viewbtns.button(b).setStyleSheet(parent.styleUnpressed)
126+
parent.viewbtns.button(bid).setStyleSheet(parent.stylePressed)
124127
parent.img0.setImage(parent.masked_images[:, :, :, bid])
125-
126128
parent.win.show()
127129
parent.show()
128130

@@ -141,7 +143,7 @@ def __init__(self, parent):
141143
# layout = QtGui.QFormLayout()
142144
self.cwidget.setLayout(self.l0)
143145
self.stylePressed = ("QPushButton {Text-align: left; "
144-
"background-color: rgb(100,50,100); "
146+
"background-color: rgb(100,100,100); "
145147
"color:white;}")
146148
self.styleUnpressed = ("QPushButton {Text-align: left; "
147149
"background-color: rgb(50,50,50); "
@@ -218,17 +220,28 @@ def __init__(self, parent):
218220
"W: mean img", "E: mean img (enhanced)", "R: correlation map",
219221
"T: max projection"
220222
]
223+
self.has_chan2 = "meanImg_chan2" in parent.ops
224+
if self.has_chan2:
225+
self.views.append("Y: mean img chan2")
221226
b = 0
222227
self.viewbtns = QButtonGroup(self)
228+
view_container = QWidget()
229+
view_vbox = QVBoxLayout()
230+
view_vbox.setContentsMargins(0, 0, 0, 0)
231+
view_vbox.setSpacing(4)
232+
view_container.setLayout(view_vbox)
223233
for names in self.views:
224234
btn = ViewButton(b, "&" + names, self)
225235
self.viewbtns.addButton(btn, b)
226-
self.l0.addWidget(btn, b, 4, 1, 1)
236+
view_vbox.addWidget(btn)
227237
btn.setEnabled(True)
228238
b += 1
229-
b = 0
230-
self.viewbtns.button(b).setChecked(True)
231-
self.viewbtns.button(b).setStyleSheet(self.stylePressed)
239+
view_vbox.addStretch()
240+
self.l0.addWidget(view_container, 0, 4, 3, 1)
241+
for b in range(len(self.views)):
242+
self.viewbtns.button(b).setStyleSheet(self.styleUnpressed)
243+
self.viewbtns.button(0).setChecked(True)
244+
self.viewbtns.button(0).setStyleSheet(self.stylePressed)
232245

233246
self.l0.addWidget(QLabel("neuropil"), 13, 13, 1, 1)
234247

@@ -309,37 +322,38 @@ def close_GUI(self):
309322
self.close()
310323

311324
def normalize_img_add_masks(self):
312-
masked_image = np.zeros(
313-
((self.Ly, self.Lx, 3, 4))) # 3 for RGB and 4 for buttons
314-
for i in np.arange(4): # 4 because 4 buttons
325+
nviews = len(self.views)
326+
masked_image = np.zeros((self.Ly, self.Lx, 3, nviews))
327+
yr = slice(self.parent.ops["yrange"][0], self.parent.ops["yrange"][1])
328+
xr = slice(self.parent.ops["xrange"][0], self.parent.ops["xrange"][1])
329+
for i in np.arange(nviews):
315330
if i == 0:
316-
mimg = np.zeros((self.Ly, self.Lx), np.float32)
317-
mimg[self.parent.ops["yrange"][0]:self.parent.ops["yrange"][1],
318-
self.parent.ops["xrange"][0]:self.parent.ops["xrange"][1]] = self.parent.ops["meanImg"][
319-
self.parent.ops["yrange"][0]:self.parent.ops["yrange"][1],
320-
self.parent.ops["xrange"][0]:self.parent.ops["xrange"][1]]
321-
331+
src = self.parent.ops["meanImg"]
322332
elif i == 1:
323-
mimg = np.zeros((self.Ly, self.Lx), np.float32)
324-
mimg[self.parent.ops["yrange"][0]:self.parent.ops["yrange"][1],
325-
self.parent.ops["xrange"][0]:self.parent.ops["xrange"][1]] = self.parent.ops["meanImgE"][
326-
self.parent.ops["yrange"][0]:self.parent.ops["yrange"][1],
327-
self.parent.ops["xrange"][0]:self.parent.ops["xrange"][1]]
333+
src = self.parent.ops["meanImgE"]
328334
elif i == 2:
329-
mimg = np.zeros((self.Ly, self.Lx), np.float32)
330-
mimg[self.parent.ops["yrange"][0]:self.parent.ops["yrange"][1],
331-
self.parent.ops["xrange"][0]:self.parent.ops["xrange"][1]] = self.parent.ops["Vcorr"]
332-
335+
src = self.parent.ops["Vcorr"]
336+
elif i == 3:
337+
src = self.parent.ops.get("max_proj", None)
338+
elif i == 4 and self.has_chan2:
339+
src = self.parent.ops["meanImg_chan2"]
333340
else:
334-
mimg = np.zeros((self.Ly, self.Lx), np.float32)
335-
if "max_proj" in self.parent.ops:
336-
mimg[self.parent.ops["yrange"][0]:self.parent.ops["yrange"][1],
337-
self.parent.ops["xrange"][0]:self.parent.ops["xrange"][1]] = self.parent.ops["max_proj"]
338-
339-
mimg1 = np.percentile(mimg, 1)
340-
mimg99 = np.percentile(mimg, 99)
341-
mimg = (mimg - mimg1) / (mimg99 - mimg1)
342-
mimg = np.maximum(0, np.minimum(1, mimg))
341+
src = None
342+
343+
mimg = np.zeros((self.Ly, self.Lx), np.float32)
344+
if src is not None:
345+
mimg1 = np.percentile(src, 1)
346+
mimg99 = np.percentile(src, 99)
347+
if mimg99 > mimg1:
348+
src = (src - mimg1) / (mimg99 - mimg1)
349+
else:
350+
src = np.zeros_like(src)
351+
src = np.clip(src, 0, 1).astype(np.float32)
352+
if src.shape[0] == self.Ly and src.shape[1] == self.Lx:
353+
mimg = src
354+
else:
355+
mimg[yr, xr] = src
356+
343357
masked_image[:, :, :, i] = self.create_masks_of_cells(mimg)
344358

345359
return masked_image
@@ -387,6 +401,9 @@ def keyPressEvent(self, event):
387401
elif event.key() == QtCore.Qt.Key_T:
388402
self.viewbtns.button(3).setChecked(True)
389403
self.viewbtns.button(3).press(self, 3)
404+
elif event.key() == QtCore.Qt.Key_Y and self.has_chan2:
405+
self.viewbtns.button(4).setChecked(True)
406+
self.viewbtns.button(4).press(self, 4)
390407

391408
def add_ROI(self, pos=None):
392409
self.iROI = len(self.ROIs)
@@ -557,13 +574,13 @@ def remove(self, parent):
557574
parent.win.show()
558575
parent.show()
559576

560-
def rotate_ROI(self, parent, ellipse, xrange, yrange, posx, posy):
577+
def rotate_ROI(self, parent, ellipse, xrange, yrange, center_x, center_y):
561578
#Rotates ROI depending on Rotatehandle degree
562579
ellipse = rotate(ellipse, angle=math.floor(self.ROI.angle()), order=0)
563580
ellipse = np.flip(ellipse, axis=0)
564-
xrange = (np.arange(-1 * int(ellipse.shape[1] - 1), 1) + int(posx)).astype(np.int32)
565-
yrange = (np.arange(-1 * int(ellipse.shape[0] - 1), 1) + int(posy)).astype(np.int32)
566-
yrange += int(np.floor(ellipse.shape[0] / 2)) + 1
581+
w, h = ellipse.shape[1], ellipse.shape[0]
582+
xrange = (np.arange(-(w // 2), w - w // 2) + int(center_x)).astype(np.int32)
583+
yrange = (np.arange(-(h // 2), h - h // 2) + int(center_y)).astype(np.int32)
567584
return ellipse, xrange, yrange
568585

569586
def position(self, parent):
@@ -579,13 +596,16 @@ def position(self, parent):
579596
yrange = (np.arange(-1 * int(sizey), 1) + int(posy)).astype(np.int32)
580597
yrange += int(np.floor(sizey / 2)) + 1
581598
# what is ellipse circling?
582-
br = self.ROI.boundingRect()
583599
ellipse = np.zeros((yrange.size, xrange.size), "bool")
584600
x, y = np.meshgrid(np.arange(0, xrange.size, 1), np.arange(0, yrange.size, 1))
585601
ellipse = ((y - br.center().y())**2 / (br.height() / 2)**2 +
586602
(x - br.center().x())**2 / (br.width() / 2)**2) <= 1
603+
center_scene = self.ROI.mapToScene(br.center())
604+
center_view = parent.p0.mapSceneToView(center_scene)
605+
center_x = center_view.x()
606+
center_y = center_view.y()
587607
if self.ROI.angle() not in (0, 180, -180):
588-
ellipse, xrange, yrange = self.rotate_ROI(parent, ellipse, xrange, yrange, posx, posy)
608+
ellipse, xrange, yrange = self.rotate_ROI(parent, ellipse, xrange, yrange, center_x, center_y)
589609
#ensures that ROI is not placed outside of movie coordinates
590610
ellipse = ellipse[:, np.logical_and(xrange >= 0, xrange < parent.Lx)]
591611
xrange = xrange[np.logical_and(xrange >= 0, xrange < parent.Lx)]

suite2p/gui/io.py

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -457,29 +457,12 @@ def save_iscell(parent):
457457

458458
def save_mat(parent):
459459
print("saving to mat")
460-
matpath = os.path.join(parent.basename, "Fall.mat")
461-
if "date_proc" in parent.ops:
462-
parent.ops["date_proc"] = []
463-
scipy.io.savemat(
464-
matpath, {
465-
"stat":
466-
parent.stat,
467-
"settings":
468-
parent.ops,
469-
"F":
470-
parent.Fcell,
471-
"Fneu":
472-
parent.Fneu,
473-
"spks":
474-
parent.Spks,
475-
"iscell":
476-
np.concatenate(
477-
(parent.iscell[:, np.newaxis], parent.probcell[:, np.newaxis]),
478-
axis=1),
479-
"redcell":
480-
np.concatenate((np.expand_dims(parent.redcell, axis=1),
481-
np.expand_dims(parent.probredcell, axis=1)), axis=1)
482-
})
460+
iscell = np.concatenate(
461+
(parent.iscell[:, np.newaxis], parent.probcell[:, np.newaxis]), axis=1)
462+
redcell = np.concatenate(
463+
(np.expand_dims(parent.redcell, axis=1), np.expand_dims(parent.probredcell, axis=1)), axis=1)
464+
ops = {**parent.ops, "save_path": parent.basename}
465+
io.save_mat(ops, parent.stat, parent.Fcell, parent.Fneu, parent.Spks, iscell, redcell)
483466

484467

485468
def save_merge(parent):

suite2p/io/h5.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def h5py_to_binary(dbs, settings, reg_file, reg_file_chan2):
8282
else:
8383
irange = np.arange(
8484
ik / ncp, min(ik / ncp + nbatch / ncp, nframes_all / ncp),
85-
1)
85+
1).astype(int)
8686
if irange.size == 0:
8787
break
8888
im = f[key][irange, ...]

suite2p/io/save.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,15 @@ def save_mat(ops, stat, F, Fneu, spks, iscell, redcell,
6060
stat = np.array(stat, dtype=object)
6161

6262

63-
# Check for None values in ops_matlab and replace with empty arrays
64-
for k, v in ops_matlab.items():
65-
if v is None:
66-
logger.warning(f"ops_matlab['{k}'] is None, replacing with empty array")
67-
ops_matlab[k] = np.array([])
63+
# Check for None values in ops_matlab and replace with empty arrays in recursive manner
64+
def replace_none(d):
65+
for k, v in d.items():
66+
if v is None:
67+
logger.warning(f"'{k}' is None, replacing with empty array")
68+
d[k] = np.array([])
69+
elif isinstance(v, dict):
70+
replace_none(v)
71+
replace_none(ops_matlab)
6872

6973
# Handle None variables by replacing with empty arrays
7074
if redcell is None:
@@ -201,6 +205,12 @@ def combined(save_folder, save=True):
201205
plane_folders = natsorted([
202206
f.path for f in os.scandir(save_folder) if f.is_dir() and f.name[:5] == "plane"
203207
])
208+
top_db_path = os.path.join(save_folder, "db.npy")
209+
if os.path.exists(top_db_path):
210+
top_db = np.load(top_db_path, allow_pickle=True).item()
211+
ignore_flyback = set(top_db.get("ignore_flyback") or [])
212+
if ignore_flyback:
213+
plane_folders = [f for i, f in enumerate(plane_folders) if i not in ignore_flyback]
204214
dbs = [
205215
np.load(os.path.join(f, "db.npy"), allow_pickle=True).item()
206216
for f in plane_folders

suite2p/parameters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@
544544
"min": 1,
545545
"max": np.inf,
546546
"default": None,
547-
"description": "Size of bins for cell detection (default is tau * fs).",
547+
"description": "Size of bins for cell detection. If None, computed as max(1, nframes // nbins, round(tau * fs)), where nframes is the total number of frames in the registered movie.",
548548
},
549549
"highpass_time": {
550550
"gui_name": "Highpass time",

suite2p/registration/register.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -487,11 +487,11 @@ def normalize_reference_image(refImg):
487487
return refImg, rmin, rmax
488488

489489

490-
def register_frames(f_align_in, refImg, f_align_out=None, batch_size=100,
491-
bidiphase=0, norm_frames=True, smooth_sigma=1.15, spatial_taper=3.45,
492-
block_size=(128,128), nonrigid=True, maxregshift=0.1,
490+
def register_frames(f_align_in, refImg, f_align_out=None, batch_size=100,
491+
bidiphase=0, norm_frames=True, smooth_sigma=1.15, spatial_taper=3.45,
492+
block_size=(128,128), nonrigid=True, maxregshift=0.1,
493493
smooth_sigma_time=0, snr_thresh=1.2, maxregshiftNR=5,
494-
device=torch.device("cuda"), tif_root=None, apply_shifts=True,
494+
subpixel=10, device=torch.device("cuda"), tif_root=None, apply_shifts=True,
495495
upsample_meanImg=False):
496496
"""
497497
Register frames to a reference image using rigid and optionally nonrigid shifts.
@@ -578,11 +578,11 @@ def register_frames(f_align_in, refImg, f_align_out=None, batch_size=100,
578578
else:
579579
nZ = 1
580580

581-
refAndMasks = compute_filters_and_norm(refImg, norm_frames=norm_frames,
581+
refAndMasks = compute_filters_and_norm(refImg, norm_frames=norm_frames,
582582
spatial_smooth=smooth_sigma,
583-
spatial_taper=spatial_taper,
584-
block_size=block_size if nonrigid else None,
585-
device=device)
583+
spatial_taper=spatial_taper,
584+
block_size=block_size if nonrigid else None,
585+
subpixel=subpixel, device=device)
586586
blocks = refAndMasks[-3] if nZ==1 else refAndMasks[0][-3]
587587
rmin = refAndMasks[-2] if nZ==1 else [refAndMasks[z][-2] for z in range(nZ)]
588588
rmax = refAndMasks[-1] if nZ==1 else [refAndMasks[z][-1] for z in range(nZ)]
@@ -829,8 +829,8 @@ def assign_reg_io(f_reg, f_raw=None, f_reg_chan2=None,
829829
if f_align_in.shape[0] != f_alt_in.shape[0]:
830830
raise ValueError("number of frames in f_align_in and f_alt_in must match")
831831

832+
tif_root_align, tif_root_alt = None, None
832833
if save_path:
833-
tif_root_align, tif_root_alt = None, None
834834
if reg_tif:
835835
tifroot = os.path.join(save_path, "reg_tif")
836836
os.makedirs(tifroot, exist_ok=True)
@@ -954,8 +954,9 @@ def registration_wrapper(f_reg, f_raw=None, f_reg_chan2=None, f_raw_chan2=None,
954954
spatial_taper=settings["spatial_taper"], block_size=settings["block_size"],
955955
nonrigid=settings["nonrigid"],
956956
maxregshift=settings["maxregshift"], smooth_sigma_time=settings["smooth_sigma_time"],
957-
snr_thresh=settings["snr_thresh"], maxregshiftNR=settings["maxregshiftNR"],
958-
device=device, upsample_meanImg=settings.get("upsample_meanImg", False))
957+
snr_thresh=settings["snr_thresh"], maxregshiftNR=settings["maxregshiftNR"],
958+
subpixel=settings["subpixel"],
959+
device=device, upsample_meanImg=settings.get("upsample_meanImg", False))
959960
rmin, rmax, mean_img, offsets_all, blocks, mean_img_ups, counts_ups, meanImg_ups = outputs
960961
yoff, xoff, corrXY, yoff1, xoff1, corrXY1, zest, cmax_all = offsets_all
961962

suite2p/run_s2p.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,9 @@ def run_s2p(db={}, settings=default_settings(), server={}):
378378
plane_folders = natsorted([
379379
f.path for f in os.scandir(save_folder) if f.is_dir() and f.name[:5] == "plane"
380380
])
381+
ignore_flyback = set(db.get("ignore_flyback") or [])
382+
if ignore_flyback:
383+
plane_folders = [f for i, f in enumerate(plane_folders) if i not in ignore_flyback]
381384
else:
382385
plane_folders = []
383386

0 commit comments

Comments
 (0)