Example of 1p online analysis using a Ring CNN + OnACID

The demo shows how to perform online analysis on one photon data using a Ring CNN for extracting the background followed by processing using the OnACID algorithm. The algorithm relies on the usage a GPU to efficiently estimate and apply the background model so it is recommended to have access to a GPU when running this notebook.


In [ ]:
get_ipython().magic('load_ext autoreload')
get_ipython().magic('autoreload 2')

from IPython.display import display, clear_output
import glob
import logging
import numpy as np
import os
import cv2

logging.basicConfig(format=
                          "%(relativeCreated)12d [%(filename)s:%(funcName)20s():%(lineno)s] [%(process)d] %(message)s",
                    # filename="/tmp/caiman.log",
                    level=logging.WARNING)

import caiman as cm
from caiman.source_extraction import cnmf as cnmf
from caiman.paths import caiman_datadir
from caiman.utils.utils import download_demo
import matplotlib.pyplot as plt

import bokeh.plotting as bpl
bpl.output_notebook()

First specify the data file(s) to be analyzed

The download_demo method will download the file (if not already present) and store it inside your caiman_data/example_movies folder. You can specify any path to files you want to analyze.


In [ ]:
fnames=download_demo('blood_vessel_10Hz.mat')

Set up some parameters

Here we set up some parameters for specifying the ring model and running OnACID. We use the same params object as in batch processing with CNMF.


In [ ]:
reuse_model = False                                                 # set to True to re-use an existing ring model
path_to_model = None                                                # specify a pre-trained model here if needed 
gSig = (7, 7)                                                       # expected half size of neurons
gnb = 2                                                             # number of background components for OnACID
init_batch = 500                                                    # number of frames for initialization and training

params_dict = {'fnames': fnames,
               'var_name_hdf5': 'Y',                                # name of variable inside mat file where the data is stored
               'fr': 10,                                            # frame rate (Hz)
               'decay_time': 0.5,                                   # approximate length of transient event in seconds
               'gSig': gSig,
               'p': 0,                                              # order of AR indicator dynamics
               'ring_CNN': True,                                    # SET TO TRUE TO USE RING CNN 
               'min_SNR': 2.65,                                     # minimum SNR for accepting new components
               'SNR_lowest': 0.75,                                  # reject components with SNR below this value
               'use_cnn': False,                                    # do not use CNN based test for components
               'use_ecc': True,                                     # test eccentricity
               'max_ecc': 2.625,                                    # reject components with eccentricity above this value
               'rval_thr': 0.70,                                    # correlation threshold for new component inclusion
               'rval_lowest': 0.25,                                 # reject components with corr below that value
               'ds_factor': 1,                                      # spatial downsampling factor (increases speed but may lose some fine structure)
               'nb': gnb,
               'motion_correct': False,                             # Flag for motion correction
               'init_batch': init_batch,                            # number of frames for initialization (presumably from the first file)
               'init_method': 'bare',
               'normalize': False,
               'expected_comps': 1100,                               # maximum number of expected components used for memory pre-allocation (exaggerate here)
               'sniper_mode': False,                                 # flag using a CNN to detect new neurons (o/w space correlation is used)
               'dist_shape_update' : True,                           # flag for updating shapes in a distributed way
               'min_num_trial': 5,                                   # number of candidate components per frame
               'epochs': 3,                                          # number of total passes over the data
               'stop_detection': True,                               # Run a last epoch without detecting new neurons  
               'K': 50,                                              # initial number of components
               'lr': 6e-4,
               'lr_scheduler': [0.9, 6000, 10000],
               'pct': 0.01,
               'path_to_model': path_to_model,                       # where the ring CNN model is saved/loaded
               'reuse_model': reuse_model                            # flag for re-using a ring CNN model          
              }
opts = cnmf.params.CNMFParams(params_dict=params_dict)

Now run the Ring-CNN + CaImAn online algorithm (OnACID).

The first initbatch frames are used for training the ring-CNN model. Once the model is trained the background is subtracted and the different is used for initialization purposes. The initialization method chosen here bare will only search for a small number of neurons and is mostly used to initialize the background components. Initialization with the full CNMF can also be used by choosing cnmf.

We first create an OnACID object located in the module online_cnmf and we pass the parameters similarly to the case of batch processing. We then run the algorithm using the fit_online method. We then save the results inside the folder where the Ring_CNN model is saved.


In [ ]:
run_onacid = True

if run_onacid:
    cnm = cnmf.online_cnmf.OnACID(params=opts)
    cnm.fit_online()
    fld_name = os.path.dirname(cnm.params.ring_CNN['path_to_model'])
    res_name_nm = os.path.join(fld_name, 'onacid_results_nm.hdf5')
    cnm.save(res_name_nm)                # save initial results (without any postprocessing)
else:
    fld_name = os.path.dirname(path_to_model)
    res_name = os.path.join(fld_name, 'onacid_results.hdf5')
    cnm = cnmf.online_cnmf.load_OnlineCNMF(res_name)
    cnm.params.data['fnames'] = fnames

Check speed

Create some plots that show the speed per frame and cumulatively


In [ ]:
ds = 10             # plot every ds frames to make more manageable figures
init_batch = 500
dims, T = cnmf.utilities.get_file_size(fnames, var_name_hdf5='Y')
T = np.array(T).sum()
n_epochs = cnm.params.online['epochs']
T_detect = 1e3*np.hstack((np.zeros(init_batch), cnm.t_detect))
T_shapes = 1e3*np.hstack((np.zeros(init_batch), cnm.t_shapes))
T_online = 1e3*np.hstack((np.zeros(init_batch), cnm.t_online)) - T_detect - T_shapes
plt.figure()
plt.stackplot(np.arange(len(T_detect))[::ds], T_online[::ds], T_detect[::ds], T_shapes[::ds],
              colors=['tab:red', 'tab:purple', 'tab:brown'])
plt.legend(labels=['process', 'detect', 'shapes'], loc=2)
plt.title('Processing time allocation')
plt.xlabel('Frame #')
plt.ylabel('Processing time [ms]')
max_val = 80
plt.ylim([0, max_val]);
plt.plot([init_batch, init_batch], [0, max_val], '--k')
for i in range(n_epochs - 1):
    plt.plot([(i+1)*T, (i+1)*T], [0, max_val], '--k')
plt.xlim([0, n_epochs*T]);
plt.savefig(os.path.join(fld_name, 'time_per_frame_ds.pdf'), bbox_inches='tight', pad_inches=0)

In [ ]:
init_batch = 500
plt.figure()
tc_init = cnm.t_init*np.ones(T*n_epochs)
ds = 10
#tc_mot = np.hstack((np.zeros(init_batch), np.cumsum(T_motion)/1000))
tc_prc = np.cumsum(T_online)/1000#np.hstack((np.zeros(init_batch), ))
tc_det = np.cumsum(T_detect)/1000#np.hstack((np.zeros(init_batch), ))
tc_shp = np.cumsum(T_shapes)/1000#np.hstack((np.zeros(init_batch), ))
plt.stackplot(np.arange(len(tc_init))[::ds], tc_init[::ds], tc_prc[::ds], tc_det[::ds], tc_shp[::ds],
              colors=['g', 'tab:red', 'tab:purple', 'tab:brown'])
plt.legend(labels=['initialize', 'process', 'detect', 'shapes'], loc=2)
plt.title('Processing time allocation')
plt.xlabel('Frame #')
plt.ylabel('Processing time [s]')
max_val = (tc_prc[-1] + tc_det[-1] + tc_shp[-1] + cnm.t_init)*1.05
for i in range(n_epochs - 1):
    plt.plot([(i+1)*T, (i+1)*T], [0, max_val], '--k')
plt.xlim([0, n_epochs*T]);
plt.ylim([0, max_val])
plt.savefig(os.path.join(fld_name, 'time_cumulative_ds.pdf'), bbox_inches='tight', pad_inches=0)

In [ ]:
print('Cost of estimating model and running first epoch: {:.2f}s'.format(tc_prc[T] + tc_det[T] + tc_shp[T] + tc_init[T]))

Do some initial plotting


In [ ]:
# first compute background summary images
images = cm.load(fnames, var_name_hdf5='Y', subindices=slice(None, None, 2))
cn_filter, pnr = cm.summary_images.correlation_pnr(images, gSig=3, swap_dim=False) # change swap dim if output looks weird, it is a problem with tiffile

In [ ]:
plt.figure(figsize=(15, 7))
plt.subplot(1,2,1); plt.imshow(cn_filter); plt.colorbar()
plt.subplot(1,2,2); plt.imshow(pnr); plt.colorbar()

In [ ]:
cnm.estimates.plot_contours_nb(img=cn_filter, idx=cnm.estimates.idx_components, line_color='white', thr=0.3)

View components

Now inspect the components extracted by OnACID. Note that if single pass was used then several components would be non-zero only for the part of the time interval indicating that they were detected online by OnACID.

Note that if you get data rate error you can start Jupyter notebooks using: 'jupyter notebook --NotebookApp.iopub_data_rate_limit=1.0e10'


In [ ]:
cnm.estimates.nb_view_components(img=cn_filter, denoised_color='red')

Load ring model to filter the data

Filter the data with the learned Ring CNN model and a create memory mapped file with the background subtracted data. We will use this to run the quality tests and screen for false positive components.


In [ ]:
save_file = True
if save_file:
    from caiman.utils.nn_models import create_LN_model
    model_LN = create_LN_model(images, shape=opts.data['dims'] + (1,), n_channels=opts.ring_CNN['n_channels'],
                               width=opts.ring_CNN['width'], use_bias=opts.ring_CNN['use_bias'], gSig=gSig[0],
                               use_add=opts.ring_CNN['use_add'])
    model_LN.load_weights(cnm.params.ring_CNN['path_to_model'])

    # Load the data in batches and save them

    m = []
    saved_files = []
    batch_length = 256
    for i in range(0, T, batch_length):
        images = cm.load(fnames, var_name_hdf5='Y', subindices=slice(i, i + batch_length))
        images_filt = np.squeeze(model_LN.predict(np.expand_dims(images, axis=-1)))
        temp_file = os.path.join(fld_name, 'pfc_back_removed_' + format(i, '05d') + '.h5')
        saved_files.append(temp_file)
        m = cm.movie(np.maximum(images - images_filt, 0))
        m.save(temp_file)
else:
    saved_files = glob.glob(os.path.join(fld_name, 'pfc_back_removed_*'))
    saved_files.sort()

In [ ]:
fname_mmap = cm.save_memmap([saved_files], order='C', border_to_0=0)
Yr, dims, T = cm.load_memmap(fname_mmap)
images_mmap = Yr.T.reshape((T,) + dims, order='F')

Merge components


In [ ]:
cnm.params.merging['merge_thr'] = 0.7
cnm.estimates.c1 = np.zeros(cnm.estimates.A.shape[-1])
cnm.estimates.bl = np.zeros(cnm.estimates.A.shape[-1])
cnm.estimates.neurons_sn = np.zeros(cnm.estimates.A.shape[-1])
cnm.estimates.g = None #np.ones((cnm.estimates.A.shape[-1], 1))*.9
cnm.estimates.merge_components(Yr, cnm.params)

Evaluate components and compare again

We run the component evaluation tests to screen for false positive components.


In [ ]:
cnm.params.quality

In [ ]:
cnm.estimates.evaluate_components(imgs=images_mmap, params=cnm.params)

In [ ]:
cnm.estimates.plot_contours_nb(img=cn_filter, idx=cnm.estimates.idx_components, line_color='white')

In [ ]:
cnm.estimates.nb_view_components(idx=cnm.estimates.idx_components, img=cn_filter)

Compare against CNMF-E results

We download the results of CNMF-E on the same dataset and compare.


In [ ]:
cnmfe_results = download_demo('online_vs_offline.npz')
locals().update(np.load(cnmfe_results, allow_pickle=True))
A_patch_good = A_patch_good.item()
estimates_gt = cnmf.estimates.Estimates(A=A_patch_good, C=C_patch_good, dims=dims)

In [ ]:
maxthr=0.01
cnm.estimates.A_thr=None
cnm.estimates.threshold_spatial_components(maxthr=maxthr)
estimates_gt.A_thr=None
estimates_gt.threshold_spatial_components(maxthr=maxthr*10)
min_size = np.pi*(gSig[0]/1.5)**2
max_size = np.pi*(gSig[0]*1.5)**2
ntk = cnm.estimates.remove_small_large_neurons(min_size_neuro=min_size, max_size_neuro=2*max_size)
gtk = estimates_gt.remove_small_large_neurons(min_size_neuro=min_size, max_size_neuro=2*max_size)

In [ ]:
m1, m2, nm1, nm2, perf = cm.base.rois.register_ROIs(estimates_gt.A_thr[:, estimates_gt.idx_components],
                                                  cnm.estimates.A_thr[:, cnm.estimates.idx_components],
                                                  dims, align_flag=False, thresh_cost=.7, plot_results=True,
                                                  Cn=cn_filter, enclosed_thr=None)[:-1]

In [ ]:
for k, v in perf.items():
    print(k + ':', '%.4f' % v, end='  ')

Save the results


In [ ]:
res_name = os.path.join(fld_name, 'onacid_results.hdf5')
cnm.save(res_name)

Make some plots


In [ ]:
import matplotlib.lines as mlines
lp, hp = np.nanpercentile(cn_filter, [5, 98])
A_onacid = cnm.estimates.A_thr.toarray().copy()
A_onacid /= A_onacid.max(0)

A_TP = estimates_gt.A[:, m1].toarray() #cnm.estimates.A[:, cnm.estimates.idx_components[m2]].toarray()
A_TP = A_TP.reshape(dims + (-1,), order='F').transpose(2,0,1)
A_FN = estimates_gt.A[:, nm1].toarray()
A_FN = A_FN.reshape(dims + (-1,), order='F').transpose(2,0,1)
A_FP = A_onacid[:,cnm.estimates.idx_components[nm2]]
A_FP = A_FP.reshape(dims + (-1,), order='F').transpose(2,0,1)


plt.figure(figsize=(15, 12))
plt.imshow(cn_filter, vmin=lp, vmax=hp, cmap='viridis')
plt.colorbar();

for aa in A_TP:
    plt.contour(aa, [0.05], colors='k');
for aa in A_FN:
    plt.contour(aa, [0.05], colors='r');
for aa in A_FP:
    plt.contour(aa, [0.25], colors='w');
cl = ['k', 'r', 'w']
lb = ['both', 'CNMF-E only', 'ring CNN only']
day = [mlines.Line2D([], [], color=cl[i], label=lb[i]) for i in range(3)]
plt.legend(handles=day, loc=3)
plt.axis('off');
plt.margins(0, 0);
plt.savefig(os.path.join(fld_name, 'ring_CNN_contours_gSig_3.pdf'), bbox_inches='tight', pad_inches=0)

In [ ]:
A_rej = cnm.estimates.A[:, cnm.estimates.idx_components_bad].toarray()
A_rej = A_rej.reshape(dims + (-1,), order='F').transpose(2,0,1)
plt.figure(figsize=(15, 15))
plt.imshow(cn_filter, vmin=lp, vmax=hp, cmap='viridis')
plt.title('Rejected Components')
for aa in A_rej:
    plt.contour(aa, [0.05], colors='w');

Show the learned filters


In [ ]:
from caiman.utils.nn_models import create_LN_model
model_LN = create_LN_model(images, shape=opts.data['dims'] + (1,), n_channels=opts.ring_CNN['n_channels'],
                           width=opts.ring_CNN['width'], use_bias=opts.ring_CNN['use_bias'], gSig=gSig[0],
                           use_add=opts.ring_CNN['use_add'])
model_LN.load_weights(cnm.params.ring_CNN['path_to_model'])

In [ ]:
W = model_LN.get_weights()

In [ ]:
plt.figure(figsize=(10, 10))
plt.subplot(2,2,1); plt.imshow(np.squeeze(W[0][:,:,:,0])); plt.colorbar(); plt.title('Ring Kernel 1')
plt.subplot(2,2,2); plt.imshow(np.squeeze(W[0][:,:,:,1])); plt.colorbar(); plt.title('Ring Kernel 2')
plt.subplot(2,2,3); plt.imshow(np.squeeze(W[-1][:,:,0])); plt.colorbar(); plt.title('Multiplicative Layer 1')
plt.subplot(2,2,4); plt.imshow(np.squeeze(W[-1][:,:,1])); plt.colorbar(); plt.title('Multiplicative Layer 2');

Make a movie


In [ ]:
m1 = cm.load(fnames, var_name_hdf5='Y')  # original data
m2 = cm.load(fname_mmap)  # background subtracted data
m3 = m1 - m2  # estimated background
m4 = cm.movie(cnm.estimates.A[:,cnm.estimates.idx_components].dot(cnm.estimates.C[cnm.estimates.idx_components])).reshape(dims + (T,)).transpose(2,0,1)
              # estimated components

nn = 0.01
mm = 1 - nn/4   # normalize movies by quantiles
m1 = (m1 - np.quantile(m1[:1000], nn))/(np.quantile(m1[:1000], mm) - np.quantile(m1[:1000], nn))
m2 = (m2 - np.quantile(m2[:1000], nn))/(np.quantile(m2[:1000], mm) - np.quantile(m2[:1000], nn))
m3 = (m3 - np.quantile(m3[:1000], nn))/(np.quantile(m3[:1000], mm) - np.quantile(m3[:1000], nn))
m4 = (m4 - np.quantile(m4[:1000], nn))/(np.quantile(m4[:1000], mm) - np.quantile(m4[:1000], nn))

m = cm.concatenate((cm.concatenate((m1.transpose(0,2,1), m3.transpose(0,2,1)), axis=2),
                    cm.concatenate((m2.transpose(0,2,1), m4), axis=2)), axis=1)

m[:3000].play(magnification=2, q_min=1, plot_text=True,
              save_movie=True, movie_name=os.path.join(fld_name, 'movie.avi'))