Skip to content

rsiegemit/fluorostats

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

44 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FluoroStats

Turn fluorescence microscopy images into publication-ready quantitative data.


FluoroStats is a Python CLI tool and library that converts microscopy images — confocal z-stacks, widefield fluorescence, any major microscope format — into the numbers, statistics, and figures needed for publication. No ImageJ macros, no manual thresholding, no spreadsheet wrangling.

Why FluoroStats?

Fluorescence microscopy experiments often produce clear qualitative differences between conditions — more cells, denser networks, better coverage. But manuscripts require quantitative evidence: volume fractions, connectivity metrics, error bars, and p-values.

FluoroStats bridges that gap with a single command:

fluorostats quant3d --input ./my_confocal_data/ --output ./results/

This produces:

  • Per-file CSV with volume fraction, connectivity metrics, and skeleton analysis
  • Per-condition summary with mean, std, and median across replicates
  • QC overlays for visual verification of segmentation accuracy
  • Publication plots — bar charts with SEM and boxplots with individual data points
  • Statistical comparisons — Mann-Whitney U p-values between all condition pairs

What Can It Quantify?

3D Confocal Z-Stacks

Well suited for bioprinted constructs, tissue sections, organoids, and spheroids — any volumetric data where cell presence and network structure matter.

Metrics:

  • Volume fraction — percentage of the volume containing cells
  • Euler number — topological measure of network interconnectedness (more negative = more loops and tunnels)
  • Largest component fraction — whether the structure forms one connected network (e.g., 97%) or many scattered clusters (e.g., 19%)
  • Skeleton length, branches, junctions — extent and branching complexity of the cell network
  • Depth-penetration profiles — mean intensity vs depth, blank-subtracted and surface-normalised, with area-under-curve over a physical depth window (probe diffusion / permeability assays)

2D Fluorescence Images

Well suited for endothelial coverage, monolayer confluence, wound healing assays, and other planar cell coverage measurements.

Metrics:

  • Area fraction — percentage of the surface covered by cells
  • Cluster count and sizes — whether cells form one confluent sheet or many scattered patches
  • Largest component fraction — degree of coverage fragmentation

Getting Started

Install

pip install fluorostats

For microscope-specific proprietary formats, add the corresponding extra:

pip install fluorostats[olympus]    # .oib, .oif files
pip install fluorostats[zeiss]      # .czi files
pip install fluorostats[nikon]      # .nd2 files
pip install fluorostats[leica]      # .lif files
pip install fluorostats[all]        # all formats

TIFF, OME-TIFF, PNG, JPEG, and BMP are supported out of the box.

Organize Data

Each experimental condition should be in its own folder, with replicate files inside:

my_experiment/
  GelMA/
    sample1.oib
    sample2.oib
    sample3.oib
  Hybrid/
    sample1.oib
    sample2.oib
    sample3.oib
  Control/
    sample1.oib
    sample2.oib
    sample3.oib

Run

# 3D confocal data
fluorostats quant3d --input ./my_experiment/ --output ./results_3d/

# 2D fluorescence images
fluorostats quant2d --input ./endothelial_images/ --output ./results_2d/

# depth-penetration / permeability (probe diffusion vs depth) — manifest driven
fluorostats depth ./my_experiment/manifest.json

Review Results

Start with the overlays/ folder — each image gets a QC overlay (grayscale intensity + magenta segmentation mask) for immediate visual verification.

Then inspect:

  • per_file.csv — measurements for every file
  • per_condition.csv — summary statistics grouped by condition
  • plots/summary_panel.png — all metrics in one composite figure
  • plots/pvalues.csv — statistical comparisons (available when replicates are present)

Tuning

Defaults are optimized for typical fluorescence microscopy, but two parameters are worth adjusting for specific datasets:

Threshold methodotsu (default for 3D) works well for bright, high-contrast signal. li (default for 2D) is better suited to dim or diffuse signal such as endothelial monolayers.

fluorostats quant3d --input ./data/ --output ./results/ --threshold li

Threshold scale — scales the computed threshold by a multiplicative factor. Lower values capture more dim signal. The 3D default of 0.9 slightly favors sensitivity over specificity.

fluorostats quant3d --input ./data/ --output ./results/ --threshold-scale 0.8

A recommended workflow: run with defaults, review the overlays, then adjust if needed.

Supported Formats

fluorostats formats   # check availability on the current system
Format Microscope Install
.tif .tiff Universal / OME-TIFF / ImageJ included
.png .jpg .bmp Exported snapshots included
.npy NumPy arrays included
.oib .oif Olympus FluoView fluorostats[olympus]
.czi Zeiss ZEN fluorostats[zeiss]
.nd2 Nikon NIS-Elements fluorostats[nikon]
.lif Leica LAS X fluorostats[leica]

Python API

FluoroStats can also be used as a library for custom analysis workflows:

from fluorostats.io import load_auto
from fluorostats.preprocess import select_green_channel, denoise
from fluorostats.segment import binarize
from fluorostats.metrics_3d import volume_fraction, connectivity_metrics, skeleton_metrics

# Load any supported format (auto-detected)
arr, meta = load_auto("my_sample.czi")

# Extract the fluorescence channel (auto-detects 488nm, FITC, GFP, etc.)
green = select_green_channel(arr, meta["channel_names"])
green = denoise(green)

# Segment — otsu/li/isodata/triangle/yen/mean, or "auto" (picks per histogram) /
# "consensus" (majority vote of all six). Otsu under-segments dim/sparse signal;
# "auto" switches to Li there, "consensus" is a no-guess robust fallback.
mask = binarize(green, method="otsu", threshold_scale=0.9)

# Measure
print(f"Volume fraction: {volume_fraction(mask):.1%}")
print(f"Connectivity: {connectivity_metrics(mask)}")
print(f"Skeleton: {skeleton_metrics(mask, meta['voxel_size_um'])}")

Advanced Analysis

Beyond the per-file metrics, FluoroStats exposes the toolkit needed to run a full statistical analysis directly from the library — homogeneity, density normalisation, multi-group statistics, power planning, publication styling, and 3D / slide-style rendering.

Spatial homogeneity and depth (no segmentation needed)

from fluorostats.morphometry import (
    lateral_homogeneity, depth_profile, depth_span, depth_centroid,
)

hom = lateral_homogeneity(green, tiles=8)        # Gini + CV across an 8x8 XY grid
prof = depth_profile(green)                       # mean intensity vs z slice
span = depth_span(green, voxel_size_um=meta["voxel_size_um"])
cent = depth_centroid(green, voxel_size_um=meta["voxel_size_um"])

Useful for "is the signal uniformly distributed?" and "how deep do cells penetrate?" questions without committing to a binary mask.

Depth-penetration profiling (probe diffusion / permeability)

from fluorostats import depth

# mean intensity vs physical depth (slice 0 = surface)
sig   = depth.intensity_depth_profile(vol, meta["voxel_size_um"][0])
blank = depth.intensity_depth_profile(no_fluo_vol, blank_meta["voxel_size_um"][0])

sig = depth.subtract_background(sig, blank)        # depth-matched blank subtraction
sig = depth.normalize_to_surface(sig, n_surface=3) # surface (first 3 slices) = 1
auc = depth.auc_depth(sig.depth_um, sig.normalized, z_min=0, z_max=100)  # µm·units

Turns a z-stack into mean-intensity-vs-depth, subtracts a no-fluo control (interpolated depth-for-depth, so blank and signal need not share slicing), normalises to the near-surface signal, and integrates over a physical depth window. Built for probe-penetration / permeability assays (e.g. FITC-dextran diffusion, "does material A retain signal deeper than material B?").

For a batch, manifest-driven workflow — many stacks grouped by condition, matched blanks, multiple AUC windows, tidy CSVs + publication figures — run:

fluorostats depth manifest.json      # or: from fluorostats.depth_batch import run

The JSON manifest lists the groups, stacks, blanks, and AUC windows (fully generic; nothing hardcoded). See fluorostats.depth_batch for the schema.

Per-object measurements

from fluorostats.objects import (
    label_3d, equivalent_diameters_um, object_density_per_mm3, centroid_homogeneity,
)

labels, n = label_3d(mask, min_size=64)
diams_um = equivalent_diameters_um(labels, meta["voxel_size_um"])
density = object_density_per_mm3(n, mask.shape, meta["voxel_size_um"])
centroids = object_centroids(labels)
spatial = centroid_homogeneity(centroids, mask.shape, tiles=8)

Right tool for nuclei sizing (DAPI), cluster-size distributions, and centroid-based homogeneity checks.

FOV-normalised densities (digital-zoom-safe)

from fluorostats.metrics_3d import normalise_skeleton_metrics

skel = skeleton_metrics(mask, meta["voxel_size_um"])
skel = normalise_skeleton_metrics(skel, mask.shape, meta["voxel_size_um"])
# adds length_density_um_per_mm3, junction_density_per_mm3, branch_density_per_mm3

Use whenever stacks have different voxel sizes — counts/lengths per FOV are not comparable, but per-mm³ densities are.

Skeleton morphometry (spur pruning + field-standard branchpoints)

from fluorostats.skeleton import skeleton_metrics, prune_skeleton, n_junction_nodes

# Opt-in spur pruning (removes short branches like AnalyzeSkeleton/AngioTool/REAVER)
m = skeleton_metrics(mask, meta["voxel_size_um"], prune=True, min_branch_length_um=5)
m["n_junction_nodes"]   # degree>=3 nodes — the AngioTool/REAVER branchpoint definition
m["n_junctions"]        # junction-to-junction branches (skan convention)

Works on 2D or 3D masks. prune=False (default) preserves raw skeleton behaviour; prune=True removes short spurs before counting so junction/branch counts match the conventions used by Fiji AnalyzeSkeleton, AngioTool, and REAVER. n_junction_nodes is the field-standard branchpoint count (degree ≥ 3 nodes).

Method-comparison / agreement statistics

from fluorostats.agreement import agreement_report, bland_altman, lins_ccc, icc

rep = agreement_report(fluorostats_values, ground_truth, "fluorostats", "manual")
# -> bias, 95% limits of agreement, Lin's CCC, ICC, Spearman, Pearson, MAPE

For validating fluorostats (or any method) against ground truth or another tool — Bland-Altman limits of agreement, Lin's concordance correlation coefficient, and ICC(A,1) absolute agreement in one call.

Live/Dead viability quantification

from fluorostats.viability import (
    live_dead_fractions, live_dead_by_count, viability_depth_profile,
    viability_2d_vs_3d, attenuation_correct,
)

frac = live_dead_fractions(live_channel, dead_channel)   # area-based live/dead fraction + viability
prof = viability_depth_profile(live_channel, dead_channel)  # per-z live/dead — depth gradient
cmp  = viability_2d_vs_3d(live_channel)   # how much a mid-plane / MIP overestimates the true 3D fraction
corr = attenuation_correct(volume)        # per-z normalisation: biological death vs optical decay

# count-based viability — robust to crowding (area under-reports when cells overlap)
cnt  = live_dead_by_count(live_channel, dead_channel, method="all")
#   -> {'by_method': {cc/watershed/maxima}, 'consensus': median, 'spread': disagreement}

Quantifies the Calcein-AM / PI (live/dead) assay in a depth-aware way. In thick 3D samples a single plane or a maximum-intensity projection overestimates viability and misses depth-dependent core death — viability_2d_vs_3d measures that overestimation and viability_depth_profile exposes the gradient. live_dead_by_count counts cells instead of area (via cc, watershed, prominence-maxima, a transparent auto, or all+consensus); on a published synthetic Live/Dead set the maxima mode matches the reference Fiji macro exactly (CCC 0.987). No single counting mode is universally best — maxima excels on crowded single-peak cells, cc is most noise-robust — so pick by regime or use all.

Instance-segmentation validation

from fluorostats.validate import instance_f1, average_precision

f1 = instance_f1(pred_labels, gt_labels, iou_threshold=0.5)   # F1, precision, recall, mean IoU
ap = average_precision(pred_labels, gt_labels)                # mean AP over IoU 0.5–0.9 (DSB2018 convention)

Score a labeled prediction against a labeled ground truth with the standard DSB2018 / Cell Tracking Challenge instance metrics — lets fluorostats validate itself (or any tool) against reference annotations in-library.

Splitting touching objects

from fluorostats.objects import watershed_split, clear_border_labels

labels, n = watershed_split(mask, min_distance=5)   # separate touching nuclei/cells
labels, n = clear_border_labels(labels)             # drop partial edge objects for unbiased counts

Connected-component labeling merges touching objects; watershed_split seeds a distance-transform watershed to separate them (the training-free fix for crowded fields), and clear_border_labels removes edge objects that would bias counts.

Multi-group statistics

from fluorostats.stats import (
    stratified_mann_whitney, bootstrap_fold_change_ci,
    stouffer_combine, scheirer_ray_hare, cliffs_delta, bh_fdr,
)

# Stratified Mann-Whitney + BH-FDR across (region × metric)
stats_df = stratified_mann_whitney(
    df, value_cols=["volume_fraction", "length_density_um_per_mm3"],
    group_col="material", group_a="GelMA", group_b="Hybrid",
    strata=["day", "region"],
)

# Distribution-free fold-change interval
ci = bootstrap_fold_change_ci(gelma_vf, hybrid_vf, n_boot=5000)
# {"fold_change_median": 9.5, "ci_low": 2.5, "ci_high": 38.0, ...}

# Pool independent evidence (e.g. across modalities)
pooled = stouffer_combine([p_live_dead, p_immuno])

# Non-parametric 2-way ANOVA on ranks (Scheirer-Ray-Hare)
anova = scheirer_ray_hare(df, value_col="lateral_gini",
                          factor_a="material", factor_b="day")

Bootstrap power analysis

from fluorostats.power import bootstrap_power, power_curve, fdr_power_curve

# How many replicates do I need to clear FDR at q < 0.05?
curve = power_curve(samples_a, samples_b,
                    ns=[4, 6, 8, 10, 12, 15, 20], n_sims=1000)

# Joint power under BH-FDR across multiple metrics
multi = fdr_power_curve(samples_per_metric_a, samples_per_metric_b,
                        ns=[4, 8, 12, 20], n_sims=500)

Publication styling

from fluorostats.style import apply_style, PALETTE, material_color

apply_style()                       # clean typography, spines, grid, palette
color = material_color("Hybrid")    # consistent per-condition colors

apply_style() opts every subsequent figure into a modern publication look — call it once at the top of a script. PALETTE / MATERIAL_COLORS give a consistent color story, and DARK_PALETTE drives the black-background 3D look below.

3D reconstruction (isosurface, voxel cloud)

from fluorostats.render3d import render_isosurface, render_voxel_cloud, save_isosurface

# Light publication look on a physical-µm grid
save_isosurface(mask, "out/iso.png", voxel_size_um=meta["voxel_size_um"],
                color="#d62728", downsample=(1, 4, 4), scalebar_um=100)

# Dark, smooth, shaded mesh (reference-figure style)
render_isosurface(mask, voxel_size_um=meta["voxel_size_um"],
                  style="dark", smooth_iter=6, shade=True,
                  color="#F5C518", scalebar_um=100)

Marching-cubes isosurface on a physical-micrometre grid. style="dark" renders on black with a faint box and white scalebar; smooth_iter applies Laplacian mesh smoothing for continuous tubes; shade=True adds Lambertian per-face shading for depth. render_voxel_cloud is a chunky voxel variant.

Slide-style image panels (no segmentation)

from fluorostats.render3d import live_dead_mip, mip_grid, layer_split_mip, depth_coded_mip

# Two-channel raw MIP (green live + red dead), gamma + percentile clipped
live_dead_mip(live_channel, dead_channel, ax=ax)

# Grid of MIPs — any cell renderer, any {(row, col): kwargs} mapping
fig = mip_grid(cells, rows=["top", "middle", "bottom"], cols=conditions,
               render_func=live_dead_mip)

# Top vs middle layer split of one volume (depth-resolved comparison)
layer_split_mip(volume, ax_top=ax1, ax_bot=ax2, split=0.5)

# Depth-coded MIP — single-panel 3D intuition via colormap
depth_coded_mip(volume, ax=ax, cmap="viridis")

These build the qualitative image panels (raw Live/Dead grids, top-vs-middle layer views, depth maps) straight from the intensity volume — useful alongside the quantitative figures.

Effect-size grids and forest plots

from fluorostats.plots import effect_size_heatmap, forest_plot, modality_panel

effect_size_heatmap(stats_df, "out/heatmap.png",
                    row_col="metric", col_col="region",
                    value_col="cliffs_delta", sig_col="sig_q05")

forest_plot(bootstrap_ci_df, "out/forest.png",
            label_col="metric", center_col="fold_change_median",
            lo_col="ci_low", hi_col="ci_high", log_scale=True)

modality_panel(df, metrics=["volume_fraction", "length_density_um_per_mm3"],
               modality_col="modality", out_path="out/modality.png")

All CLI Options

fluorostats quant3d
Option Default Description
--input required Folder containing volume files
--output required Output folder for results
--condition-from parent Label source: parent folder, grandparent, or filename
--channel auto Force channel by index or name
--threshold otsu Thresholding method: otsu or li
--threshold-scale 0.9 Scale threshold (lower = more sensitive)
--min-size 64 Min object size in voxels
--sigma 1.0 Gaussian blur sigma
--bg-radius 0 Background subtraction radius (0 = off)
--no-skeleton off Skip skeleton analysis (faster)
--no-overlays off Skip QC overlay images
--no-plots off Skip comparison plots
fluorostats quant2d
Option Default Description
--input required Folder containing image files
--output required Output folder for results
--condition-from parent Label source: parent folder, grandparent, or filename
--channel auto Force channel by index or name
--threshold li Thresholding method: otsu or li
--threshold-scale 1.0 Scale threshold (lower = more sensitive)
--min-size 64 Min object size in pixels
--sigma 1.0 Gaussian blur sigma
--bg-radius 15 Background subtraction radius (0 = off)
--auto-crop on Auto-crop microscope software borders
--no-overlays off Skip QC overlay images
--no-plots off Skip comparison plots

License

MIT

About

Universal fluorescence microscopy image quantification — 2D coverage, 3D volume fraction, connectivity, and skeleton analysis

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors