Skip to content

Commit e65af25

Browse files
committed
Merge branch 'main' of https://github.com/MouseLand/suite2p
2 parents 0df2cc3 + 20ea7e1 commit e65af25

5 files changed

Lines changed: 125 additions & 38 deletions

File tree

docs/index.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ includes the following modules:
1515
For examples of how the output looks and how the GUI works, check out
1616
this twitter [thread](https://twitter.com/marius10p/status/1032804776633880583).
1717

18-
This code was written by Carsen Stringer and Marius Pachitariu. For
19-
support, please open an [issue](https://github.com/MouseLand/suite2p/issues).
18+
For software support, please open an [issue](https://github.com/MouseLand/suite2p/issues). For general questions, please use [forum.image.sc](https://forum.image.sc)
2019

2120
The reference paper is [here](https://www.biorxiv.org/content/10.64898/2026.02.04.703741v1). The deconvolution algorithm is based on
2221
[this paper](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1005423), with settings based on [this

notebooks/example_run_suite2p_2026.ipynb

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,20 @@
88
"source": [
99
"# Running suite2p on example data\n",
1010
"\n",
11+
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/MouseLand/suite2p/blob/main/notebooks/example_run_suite2p_2026.ipynb)\n",
12+
"\n",
1113
"This notebook will guide you through the various stages and outputs of suite2p by running it on a real-life dataset. This is data collected from a wild-type mouse injected with GCaMP6s in primary visual cortex. The recording was collected at 13Hz (there were 3 planes in the recording, 1 is included here).\n",
1214
"\n",
1315
"The code cell below installs and imports the necessary packages to use suite2p in this Colab notebook."
1416
]
1517
},
18+
{
19+
"cell_type": "markdown",
20+
"metadata": {},
21+
"source": [
22+
"To use the GPU-acceleration, connect to a hosted runtime with a GPU in colab."
23+
]
24+
},
1625
{
1726
"cell_type": "code",
1827
"execution_count": null,
@@ -198,6 +207,7 @@
198207
"settings['detection']['threshold_scaling'] = 2.0 # we are increasing the threshold for finding ROIs to limit the number of non-cell ROIs found (sometimes useful in gcamp injections)\n",
199208
"settings['fs'] = 13 # sampling rate of recording, determines binning for cell detection\n",
200209
"settings['tau'] = 1.25 # timescale of gcamp to use for deconvolution\n",
210+
"settings['device'] = 'cuda' if torch.cuda.is_available() else 'cpu' # use GPU if available for faster processing\n",
201211
"print(settings)"
202212
]
203213
},
@@ -234,6 +244,8 @@
234244
"## Run entire suite2p pipeline on data\n",
235245
"The suite2p.run_s2p function runs the pipeline and returns a list of output dictionaries containing the pipeline parameters used and extra data calculated along the way, one for each plane.\n",
236246
"\n",
247+
"To see the logs during running, you now need to run `logger_setup`, optionally providing the `save_path` for the logs to be written to a text file.\n",
248+
"\n",
237249
"The following cell might take a couple of minutes to run."
238250
]
239251
},
@@ -249,6 +261,8 @@
249261
},
250262
"outputs": [],
251263
"source": [
264+
"from suite2p.run_s2p import logger_setup\n",
265+
"logger_setup()\n",
252266
"suite2p.run_s2p(settings=settings, db=db)"
253267
]
254268
},
@@ -355,7 +369,8 @@
355369
},
356370
"outputs": [],
357371
"source": [
358-
"device = torch.device(\"cuda\")"
372+
"settings['device'] = 'cuda' if torch.cuda.is_available() else 'cpu' # use GPU if available for faster processing\n",
373+
"device = torch.device(settings['device'])"
359374
]
360375
},
361376
{
@@ -607,13 +622,9 @@
607622
"outputs": [],
608623
"source": [
609624
"# Baseline correction of fluorescence traces\n",
610-
"if settings[\"dcnv_preprocess\"][\"baseline\"] == \"maximin\":\n",
611-
" dF = extraction.baseline_maximin(dF, win_baseline=settings[\"dcnv_preprocess\"][\"win_baseline\"],\n",
612-
" sig_baseline=settings[\"dcnv_preprocess\"][\"sig_baseline\"],\n",
613-
" fs=settings[\"fs\"], device=device)\n",
614-
"else:\n",
615-
" dF = extraction.preprocess(F=dF, fs=settings[\"fs\"],\n",
616-
" **settings[\"dcnv_preprocess\"])"
625+
"dF = extraction.preprocess(F=dF, fs=settings[\"fs\"], device=device,\n",
626+
" batch_size=settings[\"extraction\"][\"batch_size\"],\n",
627+
" **settings[\"dcnv_preprocess\"])"
617628
]
618629
},
619630
{

suite2p/parameters.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,15 @@
495495
"default": False,
496496
"description": "Whether to save chan2 registered tiffs.",
497497
},
498-
498+
"upsample_meanImg": {
499+
"gui_name": "Upsample meanImg",
500+
"type": list,
501+
"min": None,
502+
"max": None,
503+
"default": None,
504+
"description": "Upsampling factor for super-resolution meanImg computation. If 0 or None, no upsampling is performed.",
505+
},
506+
499507
},
500508
"detection": {
501509
"algorithm": {

suite2p/registration/nonrigid.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,8 @@ def phasecorr(data, blocks, maskMul, maskOffset, cfRefImg, snr_thresh,
370370

371371
return ymax1.T.float(), xmax1.T.float(), cmax1.T, ccsm, ccb
372372

373-
def transform_data(data, nblocks, xblock, yblock, ymax1, xmax1):
373+
def transform_data(data, nblocks, xblock, yblock, ymax1, xmax1,
374+
data_ups=None, counts_ups=None):
374375
"""
375376
Apply bilinear interpolation to transform image data using block-wise shifts.
376377
This function performs non-rigid image registration by interpolating block-wise
@@ -399,7 +400,7 @@ def transform_data(data, nblocks, xblock, yblock, ymax1, xmax1):
399400
Shifted image data of shape (nimg, Ly, Lx) with dtype int16 (short).
400401
The input images are warped according to the interpolated displacement field.
401402
"""
402-
_, Ly, Lx = data.shape
403+
n_frames, Ly, Lx = data.shape
403404
#device = torch.device("cuda")
404405
#data = torch.from_numpy(data).to(device).float()
405406
device = data.device
@@ -414,12 +415,33 @@ def transform_data(data, nblocks, xblock, yblock, ymax1, xmax1):
414415
size=(Lyc, Lxc), mode="bilinear", align_corners=True)
415416
yxup = F.pad(yxup, (int(xb.min()), Lx - int(xb.max()),
416417
int(yb.min()), Ly - int(yb.max())), mode="replicate")
418+
419+
if data_ups is not None and counts_ups is not None:
420+
ups = torch.Tensor([data_ups.shape[0] // Ly, data_ups.shape[1] // Lx]).to(device)
421+
yxup_round = -1*yxup.clone() + torch.stack((mshy, mshx), dim=0)
422+
yxup_round = torch.floor(0.5 + (yxup_round * ups.unsqueeze(-1).unsqueeze(-1))).long()
423+
yxup_round[:,0] = torch.clamp(yxup_round[:,0], min=0, max=Ly*ups[0] - 1)
424+
yxup_round[:,1] = torch.clamp(yxup_round[:,1], min=0, max=Lx*ups[1] - 1)
425+
for t in range(n_frames):
426+
mat = torch.sparse_coo_tensor(indices=yxup_round[t].reshape(2, -1), values=data[t].flatten(),
427+
size=(int(Ly*ups[0]), int(Lx*ups[1]))).to_dense()
428+
data_ups += mat
429+
mat = torch.sparse_coo_tensor(indices=yxup_round[t].reshape(2, -1),
430+
values=torch.ones(data[t].numel(), device=device, dtype=torch.long),
431+
size=(int(Ly*ups[0]), int(Lx*ups[1]))).to_dense()
432+
counts_ups += mat
433+
434+
# data_ups[yxup_round[t,0], yxup_round[t,1]] += data[t]
435+
# counts_ups[yxup_round[t,0], yxup_round[t,1]] += 1
436+
437+
# rescale for grid_sample
417438
yxup[:,0] += mshy
418439
yxup[:,1] += mshx
419440
yxup /= torch.Tensor([Ly-1, Lx-1]).to(device).unsqueeze(-1).unsqueeze(-1)
420441
yxup *= 2
421442
yxup -= 1
422443
yxup = yxup.permute(0, 2, 3, 1)
444+
423445
if device.type == "mps":
424446
# Manually pad the input tensor with the border values
425447
data_padded = F.pad(data.float().unsqueeze(1), (1, 1, 1, 1), mode="replicate")
@@ -437,4 +459,6 @@ def transform_data(data, nblocks, xblock, yblock, ymax1, xmax1):
437459
else:
438460
fr_shift = F.grid_sample(data.float().unsqueeze(1), yxup[:,:,:,[1,0]],
439461
mode="bilinear", padding_mode="border", align_corners=True)
462+
463+
440464
return fr_shift.squeeze().short()#.cpu().numpy()

suite2p/registration/register.py

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import numpy as np
1212
import torch
1313
from scipy.signal import medfilt
14+
from scipy.ndimage import gaussian_filter
1415

1516
import logging
1617
logger = logging.getLogger(__name__)
@@ -402,7 +403,8 @@ def compute_shifts(refAndMasks, fr_reg, maxregshift=0.1, smooth_sigma_time=0,
402403

403404
return ymax, xmax, cmax, ymax1, xmax1, cmax1, None, None
404405

405-
def shift_frames(fr_torch, yoff, xoff, yoff1=None, xoff1=None, blocks=None, device=torch.device("cuda")):
406+
def shift_frames(fr_torch, yoff, xoff, yoff1=None, xoff1=None, blocks=None,
407+
mean_img_ups=None, counts_ups=None, device=torch.device("cuda")):
406408
"""
407409
Apply rigid and optionally nonrigid shifts to frames and return as numpy int16.
408410
@@ -432,7 +434,7 @@ def shift_frames(fr_torch, yoff, xoff, yoff1=None, xoff1=None, blocks=None, devi
432434
"""
433435
fr_torch = torch.stack([torch.roll(frame, shifts=(-dy, -dx), dims=(0, 1))
434436
for frame, dy, dx in zip(fr_torch, yoff, xoff)], axis=0)
435-
437+
436438
if yoff1 is not None:
437439
if isinstance(yoff1, np.ndarray):
438440
if fr_torch.device.type == "cuda":
@@ -441,7 +443,9 @@ def shift_frames(fr_torch, yoff, xoff, yoff1=None, xoff1=None, blocks=None, devi
441443
else:
442444
yoff1 = torch.from_numpy(yoff1).to(device)
443445
xoff1 = torch.from_numpy(xoff1).to(device)
444-
fr_torch = nonrigid.transform_data(fr_torch, blocks[2], blocks[1], blocks[0], yoff1, xoff1)
446+
447+
fr_torch = nonrigid.transform_data(fr_torch, blocks[2], blocks[1], blocks[0],
448+
yoff1, xoff1, data_ups=mean_img_ups, counts_ups=counts_ups)
445449

446450
frames_out = np.empty(fr_torch.shape, dtype="int16")
447451
frames_out = fr_torch.cpu().numpy()
@@ -472,11 +476,11 @@ def normalize_reference_image(refImg):
472476

473477

474478
def register_frames(f_align_in, refImg, f_align_out=None, batch_size=100,
475-
bidiphase=0,
476-
norm_frames=True, smooth_sigma=1.15, spatial_taper=3.45,
479+
bidiphase=0, norm_frames=True, smooth_sigma=1.15, spatial_taper=3.45,
477480
block_size=(128,128), nonrigid=True, maxregshift=0.1,
478481
smooth_sigma_time=0, snr_thresh=1.2, maxregshiftNR=5,
479-
device=torch.device("cuda"), tif_root=None, apply_shifts=True):
482+
device=torch.device("cuda"), tif_root=None, apply_shifts=True,
483+
upsample_meanImg=False):
480484
"""
481485
Register frames to a reference image using rigid and optionally nonrigid shifts.
482486
@@ -523,6 +527,12 @@ def register_frames(f_align_in, refImg, f_align_out=None, batch_size=100,
523527
If provided, save registered frames as tiffs in this directory.
524528
apply_shifts : bool
525529
If True, apply computed shifts to frames. If False, only compute shifts.
530+
upsample_meanImg : bool, int, list, or tuple
531+
Upsampling factor for super-resolution mean image computation.
532+
If False or None, no upsampling is performed. If int, same factor is used
533+
for both Y and X. If list/tuple of length 2, specifies [Y_factor, X_factor].
534+
The mean image is computed by accumulating registered frames at subpixel
535+
locations and normalizing by pixel counts.
526536
527537
Returns
528538
-------
@@ -537,6 +547,15 @@ def register_frames(f_align_in, refImg, f_align_out=None, batch_size=100,
537547
concatenated across all batches.
538548
blocks : list
539549
Block definitions from nonrigid.make_blocks.
550+
mean_img_ups : torch.Tensor or None
551+
Raw upsampled mean image tensor of shape (Ly*upsample[0], Lx*upsample[1])
552+
before normalization. None if upsample_meanImg is False.
553+
counts_ups : torch.Tensor or None
554+
Pixel counts tensor of shape (Ly*upsample[0], Lx*upsample[1]) indicating
555+
how many frames contributed to each upsampled pixel. None if upsample_meanImg is False.
556+
meanImg_ups : np.ndarray or None
557+
Super-resolution mean image of shape (Ly*upsample[0], Lx*upsample[1])
558+
after Gaussian smoothing and normalization by counts. None if upsample_meanImg is False.
540559
"""
541560

542561
n_frames, Ly, Lx = f_align_in.shape
@@ -558,6 +577,13 @@ def register_frames(f_align_in, refImg, f_align_out=None, batch_size=100,
558577
### ------------- register frames to reference image ------------ ###
559578

560579
mean_img = np.zeros((Ly, Lx), "float32")
580+
if upsample_meanImg:
581+
if not isinstance(upsample_meanImg, (np.ndarray, list, tuple)):
582+
upsample_meanImg = [upsample_meanImg, upsample_meanImg]
583+
mean_img_ups = torch.zeros((int(Ly*upsample_meanImg[0]), int(Lx*upsample_meanImg[1])), dtype=torch.double, device=device)
584+
counts_ups = torch.zeros((int(Ly*upsample_meanImg[0]), int(Lx*upsample_meanImg[1])), dtype=torch.int, device=device)
585+
else:
586+
mean_img_ups, counts_ups, meanImg_ups = None, None, None
561587

562588
n_batches = int(np.ceil(n_frames / batch_size))
563589
logger.info(f"Registering {n_frames} frames in {n_batches} batches")
@@ -580,8 +606,9 @@ def register_frames(f_align_in, refImg, f_align_out=None, batch_size=100,
580606
ymax, xmax, cmax, ymax1, xmax1, cmax1, zest, cmax_all = offsets
581607

582608
if apply_shifts:
583-
frames = shift_frames(fr_torch, ymax, xmax, ymax1, xmax1, blocks, device)
584-
609+
frames = shift_frames(fr_torch, ymax, xmax, ymax1, xmax1, blocks,
610+
mean_img_ups=mean_img_ups, counts_ups=counts_ups, device=device)
611+
585612
# convert to numpy and concatenate offsets
586613
ymax, xmax, cmax = ymax.cpu().numpy(), xmax.cpu().numpy(), cmax.cpu().numpy()
587614
if ymax1 is not None:
@@ -608,7 +635,16 @@ def register_frames(f_align_in, refImg, f_align_out=None, batch_size=100,
608635
fname = os.path.join(tif_root, f"file{n : 05d}.tif")
609636
save_tiff(mov=frames, fname=fname)
610637

611-
return rmin, rmax, mean_img, offsets_all, blocks
638+
if upsample_meanImg:
639+
# apply Gaussian smoothing and normalize by counts
640+
mimg = mean_img_ups.cpu().numpy()
641+
cimg = counts_ups.cpu().numpy()
642+
sig = 1
643+
mimg = gaussian_filter(mimg, sig)
644+
cimg = gaussian_filter(cimg, sig)
645+
meanImg_ups = mimg / cimg
646+
647+
return rmin, rmax, mean_img, offsets_all, blocks, mean_img_ups, counts_ups, meanImg_ups
612648

613649
def check_offsets(yoff, xoff, yoff1, xoff1, n_frames):
614650
"""
@@ -844,8 +880,9 @@ def registration_wrapper(f_reg, f_raw=None, f_reg_chan2=None, f_raw_chan2=None,
844880
Dictionary containing registration results with keys: "refImg", "rmin",
845881
"rmax", "meanImg", "yoff", "xoff", "corrXY", "yoff1", "xoff1",
846882
"corrXY1", "meanImg_chan2", "badframes", "badframes0", "yrange",
847-
"xrange", "bidiphase", "meanImgE", and optionally "zpos_registration"
848-
and "cmax_registration".
883+
"xrange", "bidiphase", "meanImgE", and optionally "zpos_registration",
884+
"cmax_registration", "meanImg_upsample", "mean_img_ups",
885+
and "counts_ups".
849886
"""
850887
out = assign_reg_io(f_reg, f_raw, f_reg_chan2, f_raw_chan2, align_by_chan2,
851888
save_path, settings["reg_tif"], settings["reg_tif_chan2"])
@@ -874,7 +911,7 @@ def registration_wrapper(f_reg, f_raw=None, f_reg_chan2=None, f_raw_chan2=None,
874911
bidiphase = settings["bidiphase"]
875912

876913
if bidiphase != 0 and refImg is None:
877-
frames = bidi.shift(frames, int(settings["bidiphase"]))
914+
frames = bidi.shift(frames, bidiphase)
878915

879916
if refImg is None:
880917
t0 = time.time()
@@ -894,15 +931,15 @@ def registration_wrapper(f_reg, f_raw=None, f_reg_chan2=None, f_raw_chan2=None,
894931

895932
### ----- register frames to reference image -------------- ###
896933
outputs = register_frames(f_align_in, f_align_out=f_align_out, bidiphase=bidiphase,
897-
refImg=refImg, tif_root=tif_root_align,
898-
batch_size=settings["batch_size"],
899-
norm_frames=settings["norm_frames"], smooth_sigma=settings["smooth_sigma"],
900-
spatial_taper=settings["spatial_taper"], block_size=settings["block_size"],
934+
refImg=refImg, tif_root=tif_root_align,
935+
batch_size=settings["batch_size"],
936+
norm_frames=settings["norm_frames"], smooth_sigma=settings["smooth_sigma"],
937+
spatial_taper=settings["spatial_taper"], block_size=settings["block_size"],
901938
nonrigid=settings["nonrigid"],
902939
maxregshift=settings["maxregshift"], smooth_sigma_time=settings["smooth_sigma_time"],
903940
snr_thresh=settings["snr_thresh"], maxregshiftNR=settings["maxregshiftNR"],
904-
device=device)
905-
rmin, rmax, mean_img, offsets_all, blocks = outputs
941+
device=device, upsample_meanImg=settings.get("upsample_meanImg", False))
942+
rmin, rmax, mean_img, offsets_all, blocks, mean_img_ups, counts_ups, meanImg_ups = outputs
906943
yoff, xoff, corrXY, yoff1, xoff1, corrXY1, zest, cmax_all = offsets_all
907944

908945
# compute valid region and timepoints to exclude
@@ -932,16 +969,24 @@ def registration_wrapper(f_reg, f_raw=None, f_reg_chan2=None, f_raw_chan2=None,
932969
else:
933970
meanImg_chan2 = None
934971

935-
reg_outputs = registration_outputs_to_dict(refImg_orig, rmin, rmax, meanImg,
936-
(yoff, xoff, corrXY),
937-
(yoff1, xoff1, corrXY1),
938-
(zest, cmax_all), meanImg_chan2,
939-
badframes, badframes0,
940-
yrange, xrange, bidiphase)
941-
972+
reg_outputs = registration_outputs_to_dict(refImg_orig, rmin, rmax, meanImg,
973+
(yoff, xoff, corrXY),
974+
(yoff1, xoff1, corrXY1),
975+
(zest, cmax_all), meanImg_chan2,
976+
badframes, badframes0,
977+
yrange, xrange, bidiphase,
978+
)
979+
942980
# add enhanced mean image
943981
meanImgE = utils.highpass_mean_image(meanImg.astype("float32"), aspect=aspect)
944982
reg_outputs["meanImgE"] = meanImgE
983+
984+
# add upsampled mean image if computed
985+
if mean_img_ups is not None and counts_ups is not None:
986+
reg_outputs["meanImg_upsample"] = meanImg_ups
987+
reg_outputs["mean_img_ups"] = mean_img_ups.cpu().numpy()
988+
reg_outputs["counts_ups"] = counts_ups.cpu().numpy()
989+
945990
return reg_outputs
946991

947992
def registration_outputs_to_dict(refImg, rmin, rmax, meanImg, rigid_offsets,

0 commit comments

Comments
 (0)