Getting started#

PyNutil is a Python toolkit for transforming section-based data into atlas space and quantifying it by brain region.

PyNutil requires Python 3.8 or above.

Installation#

Install the Python package from PyPI:

pip install PyNutil

If you are working from the repository and want to run the bundled demos, install the package in editable mode from the repository root:

pip install -e .

PyNutil can be used with:

  • BrainGlobe atlases from the BrainGlobe Atlas API

  • Custom atlas volumes in .nrrd format, for example the sample data in tests/test_data/allen_mouse_2017_atlas

If you want the desktop GUI instead of the Python API, download the Windows or macOS executable from the GitHub releases page.

Choose your workflow#

Most PyNutil runs follow the same pattern:

  1. Load an atlas

  2. Load a registration JSON with read_alignment()

  3. Convert your input data into atlas-space coordinates

  4. Quantify by atlas region with quantify_coords()

  5. Save reports and exports with save_analysis()

Choose the coordinate extraction step based on your input data:

Your input

Function to use

Typical use case

Segmentation images

seg_to_coords()

Count labeled objects or pixels by region

Source images

image_to_coords()

Measure image intensity by region

DataFrame of detections

xy_to_coords()

Quantify points produced by another tool

Choose the atlas based on where it comes from:

Atlas source

How to load it

BrainGlobe atlas

BrainGlobeAtlas("allen_mouse_25um")

Local atlas files

pnt.load_custom_atlas(...)

First successful run#

If you are running from the repository, the quickest way to verify that your environment works is to reproduce the standard segmentation workflow using the bundled test data:

import os

from brainglobe_atlasapi import BrainGlobeAtlas
import PyNutil as pnt

repo_root = os.path.abspath(".")
segmentation_folder = os.path.join(
    repo_root, "tests/test_data/nonlinear_allen_mouse/segmentations"
)
alignment_json = os.path.join(
    repo_root, "tests/test_data/nonlinear_allen_mouse/alignment.json"
)
output_folder = os.path.join(repo_root, "test_result/getting_started_example")

atlas = BrainGlobeAtlas("allen_mouse_25um")
alignment = pnt.read_alignment(alignment_json)
segmentation = pnt.read_segmentation_dir(
  segmentation_folder,
  pixel_id=[0, 0, 0],
  segmentation_format="binary",
)

result = pnt.seg_to_coords(segmentation, alignment, atlas)
label_df = pnt.quantify_coords(result, atlas)
pnt.save_analysis(output_folder, result, atlas, label_df=label_df)

This example does four things:

  1. Loads a BrainGlobe atlas

  2. Reads a registration JSON

  3. Converts segmentation images into atlas-space coordinates

  4. Writes the quantified output to test_result/getting_started_example

Inputs PyNutil expects#

Atlas#

PyNutil supports both BrainGlobe atlases and local custom atlases.

With a BrainGlobe atlas:

from brainglobe_atlasapi import BrainGlobeAtlas
import PyNutil as pnt

atlas = BrainGlobeAtlas("allen_mouse_25um")
alignment = pnt.read_alignment("path/to/alignment.json")

With a custom atlas:

import PyNutil as pnt

atlas = pnt.load_custom_atlas(
    atlas_path="path/to/annotation.nrrd",
    hemi_path=None,
    label_path="path/to/labels.csv",
)
alignment = pnt.read_alignment("path/to/alignment.json")

For custom atlases:

  • atlas_path should point to an annotation volume, typically a .nrrd file

  • hemi_path is optional and can be None if you do not have a hemisphere map

  • label_path should point to a CSV containing region labels and colors

  • the bundled sample file tests/test_data/allen_mouse_2017_atlas/allen2017_colours.csv includes columns such as idx, name, r, g, and b

Registration JSON#

Use read_alignment() to load the section-to-atlas alignment:

alignment = pnt.read_alignment("path/to/alignment.json")

PyNutil will try to detect the registration format automatically. The same entry point supports registration data produced by QuickNII, VisuAlign, and BrainGlobe registration workflows.

By default, read_alignment() also tries to:

  • apply non-linear deformation when the registration source provides it

  • apply damage masks when they are available

If you want to disable those steps explicitly:

alignment = pnt.read_alignment(
    "path/to/alignment.json",
    apply_deformation=False,
    apply_damage=False,
)

Core workflows#

Segmentation images#

Use seg_to_coords() when each section has a segmentation image and the class of interest is encoded as a specific RGB value.

from brainglobe_atlasapi import BrainGlobeAtlas
import PyNutil as pnt

atlas = BrainGlobeAtlas("allen_mouse_25um")
alignment = pnt.read_alignment("path/to/alignment.json")

segs = pnt.read_segmentation_dir(
    "path/to/segmentations/",
    pixel_id=[0, 0, 0],
    segmentation_format="binary",
)
result = pnt.seg_to_coords(segs, alignment, atlas)

label_df = pnt.quantify_coords(result, atlas)
pnt.save_analysis("path/to/output", result, atlas, label_df=label_df)

Common parameters for read_segmentation_dir:

  • pixel_id: RGB value to quantify, for example [0, 0, 0]

  • segmentation_format="binary": standard binary segmentation images

  • segmentation_format="cellpose": Cellpose-style segmentation input

Common parameter for seg_to_coords:

  • object_cutoff: minimum object size to keep

Intensity images#

Use image_to_coords() when you want to quantify image intensity rather than segmented objects.

from brainglobe_atlasapi import BrainGlobeAtlas
import PyNutil as pnt

atlas = BrainGlobeAtlas("allen_mouse_25um")
alignment = pnt.read_alignment("path/to/alignment.json")

images = pnt.read_image_dir("path/to/images/")
result = pnt.image_to_coords(
    images,
    alignment,
    atlas,
)

label_df = pnt.quantify_coords(result, atlas)
pnt.save_analysis("path/to/output", result, atlas, label_df=label_df)

This workflow is useful when your input images contain signal intensity that should be aggregated by atlas region instead of counted as discrete objects.

Pre-extracted coordinates#

Use xy_to_coords() when a different tool has already detected points in image space. Pass the detections as a pandas.DataFrame with columns X, Y, image_width, image_height, and section number.

import pandas as pd
from brainglobe_atlasapi import BrainGlobeAtlas
import PyNutil as pnt

atlas = BrainGlobeAtlas("allen_mouse_25um")
alignment = pnt.read_alignment("path/to/alignment.json")
df = pd.read_csv("path/to/coordinates.csv")

result = pnt.xy_to_coords(
    df,
    alignment,
    atlas,
)

label_df = pnt.quantify_coords(result, atlas)
pnt.save_analysis("path/to/output", result, atlas, label_df=label_df)

The input CSV should contain these columns:

  • X

  • Y

  • image_width

  • image_height

  • section number

The section number values must match the section numbers used in the registration JSON.

Outputs#

quantify_coords() returns a pandas DataFrame. The exact columns depend on the workflow and the atlas, but segmentation-based runs commonly include columns like:

idx

name

region_area

object_count

area_fraction

0

Clear Label

15234

0

0.0000

8

Basic cell groups and regions

8421

17

0.0315

567

Cerebrum

12984

42

0.0648

688

Cerebral cortex

10327

31

0.0521

The exact columns depend on the workflow:

  • segmentation workflows commonly include region_area, object_count, object_pixels, object_area, and area_fraction

  • intensity workflows commonly include sum_intensity and mean_intensity

  • hemisphere-aware atlases add left/right hemisphere columns

  • damage-aware inputs add damaged/undamaged columns

After save_analysis(), the output folder typically contains:

  • whole_series_report/counts.csv for segmentation or coordinate workflows

  • whole_series_report/intensity.csv for intensity workflows

  • whole_series_meshview/pixels_meshview.json

  • whole_series_meshview/objects_meshview.json when object-level outputs exist

Interpolated 3D volumes#

PyNutil can also project section-based data into a 3D atlas-space volume with interpolate_volume().

This is useful when you want:

  • a 3D heatmap of segmented objects or image intensity

  • a volume that can be viewed in downstream NIfTI tools

  • a per-voxel sampling-frequency volume alongside the main signal volume

The function returns a VolumeResult object with three fields:

  • value: the main reconstructed value volume

  • frequency: how many section-derived samples contributed to each voxel

  • damage: a binary volume marking damaged regions

Example:

from brainglobe_atlasapi import BrainGlobeAtlas
import PyNutil as pnt

atlas = BrainGlobeAtlas("allen_mouse_25um")
registration = pnt.read_alignment("path/to/alignment.json")
image_series = pnt.read_segmentation_dir(
    "path/to/segmentations/",
    pixel_id=[0, 0, 0],
)

volumes = pnt.interpolate_volume(
    image_series=image_series,
    registration=registration,
    colour=[0, 0, 0],
    atlas=atlas,
    value_mode="pixel_count",
    segmentation_mode=True,
)

pnt.save_volume_niftis(
    output_folder="path/to/output",
    volumes=volumes,
    atlas=atlas,
)

Common value_mode options:

  • pixel_count: number of segmented pixels projected into each voxel

  • object_count: number of segmented objects projected into each voxel

  • mean: mean sampled intensity, useful for intensity-image workflows

If you are interpolating from source images instead of segmentation masks, set segmentation_mode=False and use pnt.read_image_dir() to build the ImageSeries. You can also use intensity_channel, min_intensity, and max_intensity to control how intensity values are sampled.

save_volume_niftis() writes the generated volumes into:

  • interpolated_volume/interpolated_volume.nii.gz

  • interpolated_volume/frequency_volume.nii.gz

  • interpolated_volume/damage_volume.nii.gz

These NIfTI exports are scaled to 8-bit on write and can be opened in tools such as ITK-SNAP or siibra explorer.

Worked examples#

The main PyNutil repository includes several runnable scripts in demos/ that show the same patterns with real paths and test data. This documentation repository also keeps a copy of those scripts in examples/ for reference. These examples assume PyNutil is installed in the current environment:

pip install -e .

Useful starting points:

  • examples/basic_example.py: standard segmentation workflow with a BrainGlobe atlas

  • examples/basic_example_custom_atlas.py: segmentation workflow with a custom atlas

  • examples/basic_example_intensity.py: intensity quantification workflow

  • examples/coordinate_example.py: coordinate CSV workflow

  • examples/brainglobe_coordinate_example.py: coordinate CSV workflow using BrainGlobe registration output

  • examples/brainglobe_registration_usage.py: BrainGlobe registration examples

Troubleshooting#

If a first run does not behave as expected, these are the most common checks:

  • If no objects are found, make sure pixel_id matches the RGB value present in the segmentation images

  • If section files are skipped, make sure section numbers in filenames or CSV rows match the registration JSON

  • If quantification looks incomplete, check whether damage masking is filtering points and whether that is desirable for your workflow

  • If you are using a custom atlas, verify that the annotation volume and label table refer to the same region IDs