flatironinstitute / CaImAn

Computational toolbox for large scale Calcium Imaging Analysis, including movie handling, motion correction, source extraction, spike deconvolution and result visualization.
https://caiman.readthedocs.io
GNU General Public License v2.0
612 stars 362 forks source link

all these correlation functions #1236

Open kushalkolar opened 7 months ago

kushalkolar commented 7 months ago

There are a lot of correlation functions in caiman.summary_images and it's not easy to understand them from the names and docstrings. I think it would be important to make a comparison that considers:

pgunn commented 7 months ago

Usage of all functions in that namespace:

ryanirl commented 3 months ago

To add to the chaos, here's a pretty significant upgrade (speed and memory) to these functions that I wrote some time ago for my research. You might have to look over it a bit to verify it's validity mathematically, but huge gains in terms of speed and memory.

It's also pretty easy to generalize this optimization idea to other local-correlation-like functions.

from tqdm.auto import tqdm
import numpy as np

from scipy.ndimage import convolve

def local_correlation_projection(frames: np.ndarray, kernel_size: int = 5, *, verbose: bool = True) -> np.ndarray:
    """Memory and time efficient local correlation algorithm. It makes use of
    the fact that a correlation between two signals is a normalized dot product,
    and that the dot product is an inherently iterative procedure. From the dot
    product perspective, a correlation between one pixel's temporal trace and 
    anothers is just a running sum of their time points multiplied by each other
    after mean and unit-one normalization. Thus a local correlation is just this
    operation applied with every pixel and it's neighbors within some pre-defined 
    radius, followed by a mean at the end. This can further be optimized when you
    realize that a convolution with a 0 center uniform filter was just described.

    The extra space required for this algorithm is roughly `frames[i] * 5` for 
    some arbitrary `i`. Unlike other algorithms, this implementation does not
    scale with the size of `frames`, but instead with the size of each individual 
    frame given that it's iterative.

    In my testing, for a 20 gigabyte video, running this function didn't raise 
    my RAM over extra gigabyte using a kernel size of 5. Furthermore, it was
    faster than any other implementation I have used. 

    Args:
        frames (np.ndarray): A numpy movie of shape (n_frames, height, width).
        kernel_size (int): The size in diameter (not radius) of the nearest 
            neighbor kernel to use. 
        verbose (bool): Whether to show the TQDM loading bars or not.

    Returns:
        np.ndarray: A two-dimensional array representing the local correlation
            projection of `frames`.

    """
    if kernel_size % 2 == 0:
        raise ValueError("'kernel_size' must be odd.")

    n_frames, height, width = frames.shape

    # Compute the kernel for the per-frame convolution. 
    kernel = np.ones((kernel_size, kernel_size), dtype = np.float32)
    kernel[kernel_size // 2, kernel_size // 2] = 0
    kernel = kernel / kernel.sum()

    mean = frames.mean(axis = 0)

    # Using np.linalg.norm(frames, axis = 0) is not memory efficient so we
    # vectorize it only over one dimension.
    norm = np.zeros((height, width), dtype = np.float32) 
    for i in tqdm(range(frames.shape[1]), disable = not verbose):
        norm[i] = np.linalg.norm(frames[:, i] - mean[i], axis = 0)

    # Now compute the local correlation as a running sum of normalized dot
    # products of each pixel with it's nearest neighbors.
    local_corr = np.zeros((height, width), dtype = np.float32)
    for i in tqdm(range(n_frames), disable = not verbose):
        image = (frames[i].copy() - mean) / norm
        image = image * convolve(image, kernel, mode = "constant")
        local_corr += image

    return local_corr
pgunn commented 3 months ago

Thanks; we'd need to stare at it a bit more and test it out a bit, but if it holds up then giving it as an option could be reasonable, particularly if it's faster and more memory-efficient.