Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add brainmapper analysis functionality #24

Merged
merged 5 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
290 changes: 290 additions & 0 deletions brainglobe_utils/brainmapper/analysis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
"""
Parts based on https://github.com/SainsburyWellcomeCentre/cell_count_analysis
by Charly Rousseau (https://github.com/crousseau).
"""

from dataclasses import dataclass
from pathlib import Path
from typing import List, Set, Union

import numpy as np
import pandas as pd
from bg_atlasapi import BrainGlobeAtlas

from brainglobe_utils.general.system import ensure_directory_exists
from brainglobe_utils.pandas.misc import safe_pandas_concat, sanitise_df


@dataclass
class Point:
"""
A class to represent a point in both raw and atlas coordinate spaces,
along with associated anatomical information.

Attributes
----------
raw_coordinate : np.ndarray
A numpy array representing the raw coordinates of the point
in the original image space.
atlas_coordinate : np.ndarray
A numpy array representing the coordinates of the point in atlas space.
structure : str
The name of the atlas structure associated with the point.
structure_id : int
The numerical ID of the anatomical structure associated with the point.
hemisphere : str
The hemisphere ('left' or 'right') in which the point is located.

"""

raw_coordinate: np.ndarray
atlas_coordinate: np.ndarray
structure: str
structure_id: int
hemisphere: str


def calculate_densities(
counts: pd.DataFrame, volume_csv_path: Union[str, Path]
) -> pd.DataFrame:
"""
Use region volumes from registration to calculate cell densities.

Parameters
----------
counts : pd.DataFrame
Dataframe with cell counts.
volume_csv_path : Union[str, Path]
Path of the CSV file containing the volumes of each brain region.

Returns
-------
pd.DataFrame
A dataframe containing the original cell counts and the calculated cell
densities per mm³ for each brain region.

"""

volumes = pd.read_csv(volume_csv_path, sep=",", header=0, quotechar='"')
df = pd.merge(counts, volumes, on="structure_name", how="outer")
df = df.fillna(0)
df["left_cells_per_mm3"] = df.left_cell_count / df.left_volume_mm3
df["right_cells_per_mm3"] = df.right_cell_count / df.right_volume_mm3
return df


def combine_df_hemispheres(df: pd.DataFrame) -> pd.DataFrame:
"""
Combine left and right hemisphere data onto a single row

Parameters
----------
df : pd.DataFrame
A pandas DataFrame with separate rows for left and
right hemisphere data.

Returns
-------
pd.DataFrame
A DataFrame with combined hemisphere data. Each row
represents the combined data of left and right hemispheres for
each brain region.

"""
left = df[df["hemisphere"] == "left"]
right = df[df["hemisphere"] == "right"]
left = left.drop(["hemisphere"], axis=1)
right = right.drop(["hemisphere"], axis=1)
left.rename(columns={"cell_count": "left_cell_count"}, inplace=True)
right.rename(columns={"cell_count": "right_cell_count"}, inplace=True)
both = pd.merge(left, right, on="structure_name", how="outer")
both = both.fillna(0)
both["total_cells"] = both.left_cell_count + both.right_cell_count
both = both.sort_values("total_cells", ascending=False)
return both


def create_all_cell_csv(
points: List[Point], output_filename: Union[str, Path]
) -> None:
"""
Create a CSV file with cell data from a list of Point objects.

This function takes a list of Point objects, each representing cell
coordinates and brain region and converts this into a pandas DataFrame.
The DataFrame is then saved to a CSV file at the specified filename.

Parameters
----------
points : List[Point]
A list of Point objects, each containing cell data such as
raw and atlas coordinates,
structure name, and hemisphere information.
output_filename : Union[str, Path]
The filename (including path) where the CSV file will be saved.
Can be a string or a Path object.

Returns
-------
None
"""

ensure_directory_exists(Path(output_filename).parent)
df = pd.DataFrame(

Check warning on line 133 in brainglobe_utils/brainmapper/analysis.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_utils/brainmapper/analysis.py#L132-L133

Added lines #L132 - L133 were not covered by tests
columns=(
"coordinate_raw_axis_0",
"coordinate_raw_axis_1",
"coordinate_raw_axis_2",
"coordinate_atlas_axis_0",
"coordinate_atlas_axis_1",
"coordinate_atlas_axis_2",
"structure_name",
"hemisphere",
)
)

temp_matrix = [[] for i in range(len(points))]
for i, point in enumerate(points):
temp_matrix[i].append(point.raw_coordinate[0])
temp_matrix[i].append(point.raw_coordinate[1])
temp_matrix[i].append(point.raw_coordinate[2])
temp_matrix[i].append(point.atlas_coordinate[0])
temp_matrix[i].append(point.atlas_coordinate[1])
temp_matrix[i].append(point.atlas_coordinate[2])
temp_matrix[i].append(point.structure)
temp_matrix[i].append(point.hemisphere)

Check warning on line 155 in brainglobe_utils/brainmapper/analysis.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_utils/brainmapper/analysis.py#L146-L155

Added lines #L146 - L155 were not covered by tests

df = pd.DataFrame(temp_matrix, columns=df.columns, index=None)
df.to_csv(output_filename, index=False)

Check warning on line 158 in brainglobe_utils/brainmapper/analysis.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_utils/brainmapper/analysis.py#L157-L158

Added lines #L157 - L158 were not covered by tests


def count_points_per_brain_region(
points: List[Point],
structures_with_points: Set[str],
brainreg_volume_csv_path: Union[str, Path],
output_filename: Union[str, Path],
) -> None:
"""
Count the number of points per brain region.

Parameters
----------
points : List[Point]
A list of Point objects.
structures_with_points : Set[str]
A set of strings representing the names of all atlas structures
represented
brainreg_volume_csv_path : Union[str, Path]
The path to the CSV file containing volume information from the
brainreg registration.
output_filename : Union[str, Path]
The path where the summary of points by atlas region will be saved.

Returns
-------
None
"""

structures_with_points = list(structures_with_points)

point_numbers = pd.DataFrame(
columns=("structure_name", "hemisphere", "cell_count")
)
for structure in structures_with_points:
for hemisphere in ("left", "right"):
n_points = len(
[
point
for point in points
if point.structure == structure
and point.hemisphere == hemisphere
]
)
if n_points:
point_numbers = safe_pandas_concat(
point_numbers,
pd.DataFrame(
data=[[structure, hemisphere, n_points]],
columns=[
"structure_name",
"hemisphere",
"cell_count",
],
),
)
sorted_point_numbers = point_numbers.sort_values(
by=["cell_count"], ascending=False
)

combined_hemispheres = combine_df_hemispheres(sorted_point_numbers)
df = calculate_densities(combined_hemispheres, brainreg_volume_csv_path)
df = sanitise_df(df)

df.to_csv(output_filename, index=False)


def summarise_points_by_atlas_region(
points_in_raw_data_space: np.ndarray,
points_in_atlas_space: np.ndarray,
atlas: BrainGlobeAtlas,
brainreg_volume_csv_path: Union[str, Path],
points_list_output_filename: Union[str, Path],
summary_filename: Union[str, Path],
) -> None:
"""
Summarise points data by atlas region.

This function takes points in both raw data space and atlas space,
and generates a summary of these points based on the BrainGlobe atlas
region they are found in.
The summary is saved to a CSV file.

Parameters
----------
points_in_raw_data_space : np.ndarray
A numpy array representing points in the raw data space.
points_in_atlas_space : np.ndarray
A numpy array representing points in the atlas space.
atlas : BrainGlobeAtlas
The BrainGlobe atlas object used for the analysis
brainreg_volume_csv_path : Union[str, Path]
The path to the CSV file containing volume information from the
brainreg registration.
points_list_output_filename : Union[str, Path]
The path where the detailed points list will be saved.
summary_filename : Union[str, Path]
The path where the summary of points by atlas region will be saved.

Returns
-------
None
"""

points = []
structures_with_points = set()
for idx, point in enumerate(points_in_atlas_space):
try:
structure_id = atlas.structure_from_coords(point)
structure = atlas.structures[structure_id]["name"]
hemisphere = atlas.hemisphere_from_coords(point, as_string=True)
points.append(

Check warning on line 270 in brainglobe_utils/brainmapper/analysis.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_utils/brainmapper/analysis.py#L263-L270

Added lines #L263 - L270 were not covered by tests
Point(
points_in_raw_data_space[idx],
point,
structure,
structure_id,
hemisphere,
)
)
structures_with_points.add(structure)
except Exception:
continue

Check warning on line 281 in brainglobe_utils/brainmapper/analysis.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_utils/brainmapper/analysis.py#L279-L281

Added lines #L279 - L281 were not covered by tests

create_all_cell_csv(points, points_list_output_filename)

Check warning on line 283 in brainglobe_utils/brainmapper/analysis.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_utils/brainmapper/analysis.py#L283

Added line #L283 was not covered by tests

count_points_per_brain_region(

Check warning on line 285 in brainglobe_utils/brainmapper/analysis.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_utils/brainmapper/analysis.py#L285

Added line #L285 was not covered by tests
points,
structures_with_points,
brainreg_volume_csv_path,
summary_filename,
)
33 changes: 33 additions & 0 deletions brainglobe_utils/brainmapper/export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from pathlib import Path
from typing import Union

Check warning on line 2 in brainglobe_utils/brainmapper/export.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_utils/brainmapper/export.py#L1-L2

Added lines #L1 - L2 were not covered by tests

import numpy as np

Check warning on line 4 in brainglobe_utils/brainmapper/export.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_utils/brainmapper/export.py#L4

Added line #L4 was not covered by tests


def export_points_to_brainrender(

Check warning on line 7 in brainglobe_utils/brainmapper/export.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_utils/brainmapper/export.py#L7

Added line #L7 was not covered by tests
points: np.ndarray,
resolution: float,
output_filename: Union[str, Path],
) -> None:
"""
Export points in atlas space for visualization in brainrender.

Points are scaled from atlas coordinates to real units and saved
as a numpy file.

Parameters
----------
points : np.ndarray
A numpy array containing the points in atlas space.
resolution : float
A numerical value representing the resolution scale to be
applied to the points.
output_filename : Union[str, Path]
The path where the numpy file will be saved. Can be a string
or a Path object.

Returns
-------
None
"""
np.save(output_filename, points * resolution)

Check warning on line 33 in brainglobe_utils/brainmapper/export.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_utils/brainmapper/export.py#L33

Added line #L33 was not covered by tests
Empty file.
Loading