This repository describes the methods and provides the Python code for generating brain flatmaps based on the Allen Mouse Brain atlas.
This is a Python reimplementation of an existing method developed for the isocortex. This code will run on other regions in the near future.
Note: this is a work in progress. Only part of the method (streamlines) has been implemented so far.
A powerful computer is required to handle the 10um Allen atlas volume.
- At least 64GB of RAM
- At least 250GB of free space on an SSD
- A NVIDIA graphics processing unit (GPU) with at least 8GB of video memory
- Python 3
- NumPy
- SciPy
- Numba
- Cupy
- nrrd
- h5py
- tqdm
The code requires data files that we do not provide directly. If needed, ask Cyrille Rossant for more guidance.
The input files are to put in input/, the code will generate output files in regions/isocortex/.
-
Put the following input files in the
input/subfolder:isocortex_boundary_10.nrrdisocortex_mask_10.nrrd
-
Run
python surface.py. This script should create:regions/isocortex/mask.npy(a volume with labels indicating the surfaces and the regions between them)regions/isocortex/normal.npy(a 3D vector field with the surface normal vectors)
-
Run
python laplacian.py. This script should create:regions/isocortex/laplacian.npy(a 3D scalar field within the region volume)
-
Run
python gradient.py. This script should create:regions/isocortex/gradient.npy(a 3D vector field with the gradient to Laplace's equation's solution)
-
Run
python streamlines.py. This script should create:regions/isocortex/streamlines.npy(a 3D array with N 3D paths of size 100 in the 3D coordinate space of the 10um volume)
-
Visualize the generated streamlines in 2D with the plotting Jupyter notebook, or in 3D with
python plotting.py(requires Datoviz) -
The next steps for generating the flatmaps using the streamlines are not yet implemented.
Some useful constants are defined in common.py, including:
# Volume shape for 10um Allen Mouse Brain Atlas.
N, M, P = 1320, 800, 1140
# Values used in the mask file
V_OUTSIDE = 0 # voxels outside of the surfaces and brain region
V_ST = 1 # top (outer) surface
V_VOLUME = 2 # volume between the two surfaces
V_SB = 3 # bottom (inter) surface
V_SE = 4 # intermediate surfacesThis section describes the method for generating the streamlines and flatmaps.
The method consists of computing streamlines between a bottom and top surface around a brain region by solving Laplace's partial differential equation
The streamlines allow one to generate flatmaps by mapping every pixel of the flattened surface to an average value along the streamline that starts at that voxel.
We start by defining some notations.
- The 1-norm of a vector
$\mathbf p=(x,y,z)$ is$\lVert\mathbf p\rVert_1 = |x|+|y|+|z|$ . - The Euclidean norm of a vector
$\mathbf p=(x,y,z)$ is$\lVert\mathbf p\rVert_2 = \sqrt{x^2+y^2+z^2}$ . - The gradient of a scalar field
$u$ is$\displaystyle\nabla u =\left(\frac{\partial u}{\partial x}, \frac{\partial u}{\partial y}, \frac{\partial u}{\partial z}\right)$ . - The Laplacian of a scalar field
$u$ is$\displaystyle\Delta u =\frac{\partial^2 u}{\partial x^2} + \frac{\partial^2 u}{\partial y^2} + \frac{\partial^2 u}{\partial z^2}$ .
-
$\Omega = \left[0, N\right] \times \left[0, M\right] \times \left[0, P\right]$ is the 3D volume containing the brain atlas. -
$\mathcal V \subset \Omega$ is the brain region to flatten -
$\mathcal S = \partial\mathcal V \subset \Omega$ is the boundary surface of the volume -
$\mathcal S_T \subset \mathcal S$ is the top (outer) surface of the brain region$\mathcal V$ -
$\mathcal S_B \subset \mathcal S$ is the bottom (inner) surface of the brain region$\mathcal V$ -
$\mathcal S_E \subset \mathcal S$ is the edge surface of the brain region$\mathcal V$
The topological boundary of the volume is the union of these three non-intersecting surfaces:
We use the Allen CCF coordinate system:
-
$p = (i, j, k) \in \Omega$ is a voxel in the volume -
$p_x^- = (i-1, j, k) \in \Omega$ is the neighbor voxel in front of$p$ -
$p_x^+ = (i+1, j, k) \in \Omega$ is the neighbor voxel behind$p$ -
$p_y^- = (i, j-1, k) \in \Omega$ is the neighbor voxel on top of$p$ -
$p_y^+ = (i, j+1, k) \in \Omega$ is the neighbor voxel below$p$ -
$p_z^- = (i, j, k-1) \in \Omega$ is the neighbor voxel to the left of$p$ -
$p_z^+ = (i, j, k+1) \in \Omega$ is the neighbor voxel to the right of$p$
For each subset
The mask
Implementation notes: The mask
$\mu$ is stored inmask.npythat is computed in the first step below, from the input nrrd files. This file is a 3D array with shape(N, M, P)and data typeuint8.
The first step is to estimate the normal to the surface at every surface voxel. The normals will be used as boundary conditions when simulating the partial differential equation in Step 2.
We can make a first estimation of the surface normals thanks to the
On each axis, the component of the vector
Once this crude local estimate is obtained, we can smoothen it and normalize it to improve the accuracy of the boundary conditions in Step 2.
We define a Gaussian kernel as follows:
We smoothen the crude normal estimate with a partial Gaussian convolution on the surface:
Finally, we normalize the normal vectors:
Implementation notes: this convolution is implemented with nested
forloops in Python accelerated with JIT compilation using Numba.
Step 2 is the most complex and computationally intensive step of the process. It requires a GPU to be tractable on the 10
Mathematically, the goal is to solve the following partial differential equation (PDE), called Laplace's equation, with a mixture of Dirichlet and Neumann boundary conditions:
An approximate solution of this equation can be obtained with an iterative numerical scheme.
We start from
- Update
$u^{n+1}$ on$\mathcal V$ . - Update
$u^{n+1}$ on$\mathcal S$ .
On
On
- On
$\mathcal S_T$ , we just use the following equation for the Dirichlet boundary condition:
- On
$\mathcal S_B$ and$\mathcal S_E$ , we need to implement the Neuman boundary conditions as explained below.
We use central, forward, or backward finite difference schemes for
We note
and similarly for the other components,
Then, we find the following scheme for the Neumann boundary condition:
We wrote a GPU implementation with the Cupy Python package leveraging the NVIDIA CUDA API. There are a few tricks:
-
We use two CUDA kernels: one for the numerical scheme in the brain region
$\mathcal V$ , another for the one on the surfaces$\mathcal S_B$ and$\mathcal S_E$ (Neumann conditions). Every iteration involves a call to both kernels. -
We use two 3D arrays for the solution to Laplace's equation,
U_1andU_2. The CUDA kernels use one array to read the old values ($u^n$ ), another one to write the new values ($u^{n+1}$ ). At each iteration, we swapU_1andU_2. -
To avoid using too much GPU memory (there are wide empty spaces around a given brain region
$\mathcal V$ ), we compute the axis boundaries of the mask array and we pad each side with a few voxels. -
To ensure all arrays fit in GPU memory, we cut the brain in half (two hemispheres), which is possible as long as the streamlines are not expected to cross the sagittal midline within the brain region.
-
We achieve about 1000 iterations per minute on an NVIDIA Geforce RTX 2070 SUPER (for one hemisphere).
-
Empirically, a total of 10,000 iterations per hemisphere seems to be necessary for proper convergence of the algorithm.
-
In total, the entire method (steps 1-4) should run under one or two hours with a GPU.
Note: an alternative would be to use sparse data structures instead of dense ones, but it would require a bit more work.
Once the solution of Laplace's equation has been obtained, we can estimate its gradient that will be used to integrate the streamlines in Step 4.
We use central, forward, or backward differences for the numerical scheme of the derivative of
We get:
and similarly for
Finally, we normalize the gradient:
To compute streamlines, we start from voxels in the bottom surface
More precisely, we solve an ordinary differential equation (ODE) with
which must satisfy:
with initial conditions:
We use the forward Euler method to integrate this ODE numerically.
At every time step, we use a linear interpolation to estimate the gradient at a position between voxels.
We also stop the integration for streamlines that go beyond the volume
Finally, once obtained, we resample the streamlines to reparametrize them in 100 steps.
TO DO.
Some references:
- Jones, S. E., Buchbinder, B. R., & Aharon, I. (2000). Three‐dimensional mapping of cortical thickness using Laplace's equation. Human brain mapping, 11(1), 12-32.
- Lerch, J. P., Carroll, J. B., Dorr, A., Spring, S., Evans, A. C., Hayden, M. R., ... & Henkelman, R. M. (2008). Cortical thickness measured from MRI in the YAC128 mouse model of Huntington's disease. Neuroimage, 41(2), 243-251.
Other implementations: