Running the membrane thickness estimation pipeline#

Table of contents#

  1. Introduction

  2. Changes from the previous pipeline version

  3. Input data

  4. Setup

  5. Parameters

  6. Run full pipeline

  7. Run step 1: surface extraction

  8. Run step 2: surface point matching

  9. Run step 3: intensity profile analysis

Companion tutorials:
Membrane thickness: analysis — loading results, plotting thickness distributions, 3D spatial maps, intensity profile summaries, and motl export.
Membrane thickness: preprocessing — how the choice of tomogram reconstruction and denoising (WBP, Wiener-like filter deconvolution, cryoCARE-denoising) affects the measured membrane thickness.

Introduction#

This tutorial covers the cryocat.analysis.memthick pipeline for measuring membrane thickness from cryo-electron tomography (cryo-ET) membrane segmentations as described in [1].

The pipeline runs in three sequential steps:

Step

What it does

Output

1 — Surface extraction

Runs marching cubes on each membrane label; refines surface normals; separates the two segmentation surfaces into surface 1 and surface 2

*_vertices_normals.csv

2 — Geometric matching

Pairs each surface-1 vertex with its nearest surface-2 neighbor within a given distance and angle deviation from the vertex normal; records the Euclidean distance between paired points (match_distance_nm)

*_matched_points.csv

3 — Intensity profile analysis

Extracts short 1-D intensity profiles from the tomogram along each matched point pair; detects bilayer minima and outward maxima; computes inflection-point-based membrane thickness

*_thickness.csv, *_int_profiles.pkl

run_full_pipeline runs all three steps in one call. Each step can also be called independently, allowing you to re-enter the pipeline at any point.

The input data used throughout this tutorial is a Chlamydomonas reinhardtii tomogram [2], with membrane segmentations generated using MemBrain-seg [3].


Changes from the previous pipeline version#

The previous version of the pipeline used tomogram intensity profiles as a validation step: thickness was first measured geometrically between matched segmentation surface points, and intensity profiles were then used to filter out unreliable measurements by checking whether both bilayer minima and a central maximum were detectable.

The current implementation uses the tomogram intensity profiles as the primary source of the membrane thickness measurement.

  • Steps 1 and 2 (surface extraction and geometric matching) are unchanged. The matched-point distance (match_distance_nm) is still recorded and reflects the segmentation geometry.

  • In step 3, intensity profiles are extracted along each matched pair. The pipeline then identifies the two leaflet minima (phosphate headgroups) and searches outward from each for an intensity maximum (where the density returns to baseline). The inflection point on the steepest slope between each minimum and its outward maximum defines the membrane boundary, and the distance between the two inflection points is the reported membrane_thickness_nm.

  • This means membrane_thickness_nm is measured from the tomogram densities. The segmentation surface points are used only to anchor the profile extraction and guide the minima search windows.

For a detailed description of the detection logic, output columns, and detection_mode values (max_max, max_anchor, minima_only), see Section 1 of Membrane thickness: analysis.


Input data#

  • Segmentation MRC — instance segmentation with integer labels, one per membrane type. We used MemBrain-seg [3] for automated membrane segmentation.

  • Tomogram MRC — the reconstruction used for intensity profile analysis (step 3). Must be on the same grid (same dimensions and voxel size) as the segmentation. See Membrane thickness: preprocessing for how the choice of reconstruction method affects measured thickness.

The example data used in this tutorial can be downloaded here. It contains an inputs/ folder (segmentation and tomogram) and an outputs/ folder (the expected results). Place both folders next to this notebook to run it as is.

References#

[1] D. Glushkova, S. Böhm, M. Beck. Systematic membrane thickness variation across cellular organelles revealed by cryo-ET. J. Cell Biol. 225(1), e202504053 (2026). https://doi.org/10.1083/jcb.202504053

[2] R. Kelley, et al. Towards community-driven visual proteomics with large-scale cryo-electron tomography of Chlamydomonas reinhardtii. Mol. Cell (2025). https://doi.org/10.1016/j.molcel.2025.04.022

[3] L. Lamm, et al. MemBrain v2: an end-to-end tool for the analysis of membranes in cryo-electron tomography. Nat. Methods (2026). https://doi.org/10.1038/s41592-026-03178-8


Setup#

Set paths to the input segmentation and tomogram MRC files and to the output directory. All pipeline outputs (CSVs, optional MRC volumes) will be written under output_path.

[1]:
%load_ext autoreload
%autoreload 2
[ ]:
import os
from cryocat.analysis import memthick
[3]:
segmentation_path = "inputs/2140_z150to400_segmented.mrc"
tomo_path         = "inputs/2140_z150to400.mrc"
output_path        = "outputs"
os.makedirs(output_path, exist_ok=True)

Parameters#

Step 1: Surface extraction#

Parameter

Default

Description

smooth_sigma_segmentation

None

Gaussian sigma (voxels) applied to the segmentation before running marching cubes. Smoothing blurs the binary mask into a soft field. None skips smoothing. Typical values: 0.5–2.5.

step_size_marching_cubes

1

Marching-cubes stride. 1 produces the densest mesh (one vertex per voxel surface)

surface_separation_mode

"planar"

How the two bilayer leaflets are separated — see table below. Pass a dict (e.g. {"ER": "planar", "OMM": "closed"}) to use different modes per membrane.

refine_normals

True

Run neighbor-based normal averaging within each separated surface after the global orientation step. Strongly recommended: it minimizes systematic errors that the MST-based global orientation leaves on curved membranes.

radius_hit

3.0

Voxel-radius neighborhood used for normal refinement. Larger values average over a wider area (smoother normals, less sensitive to local mesh noise).

snap_vertices_to_boundary

False

Round marching-cubes vertices to integer voxels and discard any that are not on the segmentation boundary. Use when you want to run step 2 on confirmed boundary voxels only, e.g. for a segmentation-geometry-only analysis without intensity profiles. Do not use when extracting intensity profiles from the tomogram — sub-voxel vertex positions give better-centred profile sampling.

save_split_surface_meshes

False

Save each bilayer leaflet as a .ply mesh file. Useful for visualising the surface split (MeshLab, ParaView, Plotly).

save_vertices_mrc

False

Save vertex positions as a binary MRC volume (voxel = 1 at each vertex position). Useful for checking whether vertices cover the segmentation surface.

surface_separation_mode — choosing the right mode#

Mode

When to use

How it works

"planar"

Flat or slightly curved membranes: nuclear envelope, ER, plasma membrane patches

PCA on all vertex coordinates. Vertices are split by which side of the membrane’s principal plane they fall on. Fast and robust for planar geometry; fails for closed surfaces where both leaflets span the same plane.

"closed"

Closed organelle membranes: mitochondria (especially OMM), vesicles, lysosomes

Connected-component labeling on the mesh. The outer connected component becomes surface 2, the inner becomes surface 1. Requires a complete, watertight mesh.

Stage 2: Geometric matching#

Parameter

Default

Description

max_distance_nm

8.0

Hard cap (nm) on the source–target distance. Pairs farther apart are never considered. This value is also reused in step 3 as the maximum allowed inflection-point thickness — set it to the largest physically plausible membrane thickness for your sample.

max_angle

10.0

Half-angle (degrees) of the normal-aligned search cone. Candidate targets whose lateral deviation from the source normal exceeds this angle are rejected. Larger values allow more oblique matches (useful for curved membranes); smaller values enforce stricter collinearity.

direction

"1to2"

Which surface seeds the ray casting. "1to2" iterates over surface-1 points and finds their nearest surface-2 partner inside the cone; "2to1" reverses the roles. The number of matched pairs and their distribution can differ slightly between directions.

use_gpu

False

Use the CUDA kernel when a compatible GPU is available. Automatically falls back to the CPU KDTree path if CUDA is not available.

pixel_size_nm

None

Voxel size in nm. Overrides the value read from the MRC header. Required when the segmentation was saved without voxel-size metadata (header reports 0 Å).

Stage 3: Intensity profiles#

Parameter

Default

Description

profile_half_width_nm

5.5

Half-width of each sampled 1-D intensity profile on both sides of the matched-pair midpoint (nm). The full profile spans 2 × profile_half_width_nm. Should be large enough to capture both outward maxima beyond the leaflet minima, but not so large that unrelated density leaks into the profile. Typical values: 5.0–7.0 nm.

max_distance_nm

8.0

Rows whose inflection-point thickness (or minima-to-minima distance for minima_only rows) exceeds this value are excluded from the exported *_thickness.csv. Should match the geometric matching cap.

analyzer

IntensityProfileAnalyzer() detection parameter object (see table above).


IntensityProfileAnalyzer — settings for analyzing the extracted intensity profiles#

The IntensityProfileAnalyzer object holds all parameters for step 3 (intensity profile-based search for minima, outward maxima, and profile inflection points). Create it once and pass it to run_full_pipeline or analyse_intensity_profiles.

Parameter

Default

Description

smooth_sigma_intensity_profiles

0.5

Gaussian smoothing applied along the profile axis before peak detection. Increase if profiles are noisy; decrease (or set to 0) to preserve fine structure.

extrema_prominence_threshold

0.1

Minimum prominence for minima and maxima found by find_peaks. Lower values detect shallower features; higher values suppress noise peaks.

minima_search_nm

(3.0, 4.0)

Search window applied symmetrically around each matched segmentation surface point when looking for the bilayer minima. The pipeline first tries the narrower window; if no valid minimum pair is found, it retries with the wider window.

anchor_search_nm

4.0

Half-width (nm) of the outward search window beyond each identified minimum, within which the pipeline searches for an outward intensity maximum. This maximum marks where the density returns to baseline outside the bilayer and anchors the inflection-point calculation. See below for what happens when no maximum is found.

n_jobs

-1

Worker count for parallel profile processing. -1 = all available cores; 1 = serial.

What happens when an outward maximum is not found on one side.
If no maximum is detected within anchor_search_nm of a bilayer minimum, the pipeline borrows the good side’s geometry: the distance from that side’s minimum to its outward maximum (the “span”) is mirrored to predict where the missing anchor should be. The inflection point is then computed on the predicted slope segment, exactly as it would be for a real maximum. Profiles resolved this way are labeled max_anchor or anchor_max in the detection_mode column, depending on which side was mirrored.
If no outward maximum exists on either side, no inflection-point thickness is calculated; instead, only the distance between the two leaflet minima is reported (minima_only mode, membrane_thickness_nm = NaN).
[4]:
membrane_labels = {
    "ER": 1,
    "IMM": 2,
    "OMM": 3,
}

surface_separation_mode = {
    "ER": "planar",
    "IMM": "closed",   # less clear whether to go with open or close, as there will be flipping of the surfaces on the self-folding cristae
    "OMM": "closed",   # closed is important to get a meaningful separation of the two leaflets
}

analyzer = memthick.IntensityProfileAnalyzer(
    smooth_sigma_intensity_profiles=0.5,  # empirical, avoid over-smoothing
    minima_search_nm=(3.0, 4.0),
    anchor_search_nm=4.0,
    n_jobs=-1,
)

Run full pipeline#

Runs all three steps (surface extraction → geometric matching → intensity profile analysis) in a single call. Use this for a fresh run from a new segmentation. To re-enter the pipeline at a specific step, see the individual step sections below.

[5]:
results = memthick.run_full_pipeline(
    segmentation_map=segmentation_path,
    tomogram_map=tomo_path,
    output_path=output_path,
    membrane_labels=membrane_labels,
    # -- Step 1: surface extraction --
    smooth_sigma_segmentation=2.0,    # to reduce "step-artifacts" on the surface due to the discrete nature of the segmentation
    surface_separation_mode=surface_separation_mode,
    refine_normals=True,
    radius_hit=3.0,
    # -- Step 2: geometric point matching --
    max_distance_nm=8.0,
    max_angle=10.0,
    direction="1to2",
    use_gpu=False,                    # set to True if you have a GPU
    # -- Step 3: intensity profiles analysis --
    extract_intensity_profiles=True,
    profile_half_width_nm=5.5,
    analyzer=analyzer,
    # -- File saving options --
    save_split_surface_meshes=True,   # to visualize surface splitting (will produce a .ply mesh file)
    save_vertices_mrc=True,           # to visualize vertices positions (rounds vertices to the nearest integer)
    save_thickness_mrc=True,          # voxel values correspond to thickness (aggregated from vertices that match back to this voxel)
)
2026-06-09 07:43:17,036 - Starting full membrane thickness analysis pipeline for inputs/2140_z150to400_segmented.mrc
2026-06-09 07:43:17,037 - Will attempt finding intensity profile features using tomogram: inputs/2140_z150to400.mrc
2026-06-09 07:43:17,037 - Step 1: Processing membrane segmentation
2026-06-09 07:43:17,639 -
Processing ER (label 1)
2026-06-09 07:43:17,730 - Extracting surface points (mesh vertices) with step size 1 from segmentation after smoothing with sigma 2.0...
Applying Gaussian smoothing (sigma=2.0)...
Smoothed field range: [0.000, 0.997]
Running marching cubes (level=0.5, step_size=1)...
2026-06-09 07:43:31,025 - Extracted 477614 mesh vertices
2026-06-09 07:43:52,824 - Separating surfaces (mode='planar')...
2026-06-09 07:43:52,847 - Successfully separated membrane surfaces:
2026-06-09 07:43:52,848 - Surface 1: 242035 points
2026-06-09 07:43:52,848 - Surface 2: 235579 points
2026-06-09 07:43:52,849 - Refining normals within surface 1...
Refining normals (radius=3.0, batch=2000, iter=1, mask=242035/477614)
Iteration 1/1
100%|██████████| 122/122 [00:17<00:00,  7.10it/s]
2026-06-09 07:44:10,255 - Refining normals within surface 2...
Normal refinement complete.
Refining normals (radius=3.0, batch=2000, iter=1, mask=235579/477614)
Iteration 1/1
100%|██████████| 118/118 [00:16<00:00,  7.08it/s]
Normal refinement complete.
2026-06-09 07:44:30,610 - Saved split mesh: outputs/2140_z150to400_segmented_ER_surface1.ply
Saved mesh to outputs/2140_z150to400_segmented_ER_surface1.ply
  Vertices: 242,035
  Faces: 479,188
2026-06-09 07:44:33,973 - Saved split mesh: outputs/2140_z150to400_segmented_ER_surface2.ply
2026-06-09 07:44:33,975 -
Saving outputs for ER:
2026-06-09 07:44:33,977 - Vertices: 477614, Surface 1: 242035, Surface 2: 235579
Saved mesh to outputs/2140_z150to400_segmented_ER_surface2.ply
  Vertices: 235,579
  Faces: 466,171
2026-06-09 07:44:35,128 - Saved MRC to outputs/2140_z150to400_segmented_ER_vertices.mrc
2026-06-09 07:44:39,506 - Saved CSV with 477614 vertices to outputs/2140_z150to400_segmented_ER_vertices_normals.csv
2026-06-09 07:44:39,507 -
Processing IMM (label 2)
2026-06-09 07:44:39,613 - Extracting surface points (mesh vertices) with step size 1 from segmentation after smoothing with sigma 2.0...
Applying Gaussian smoothing (sigma=2.0)...
Smoothed field range: [0.000, 0.992]
Running marching cubes (level=0.5, step_size=1)...
2026-06-09 07:44:52,862 - Extracted 1857969 mesh vertices
2026-06-09 07:46:17,337 - Separating surfaces (mode='closed')...
2026-06-09 07:46:17,481 - Successfully separated membrane surfaces:
2026-06-09 07:46:17,482 - Surface 1: 903196 points
2026-06-09 07:46:17,484 - Surface 2: 954773 points
2026-06-09 07:46:17,484 - Refining normals within surface 1...
Refining normals (radius=3.0, batch=2000, iter=1, mask=903196/1857969)
Iteration 1/1
100%|██████████| 452/452 [01:04<00:00,  7.04it/s]
2026-06-09 07:47:22,611 - Refining normals within surface 2...
Normal refinement complete.
Refining normals (radius=3.0, batch=2000, iter=1, mask=954773/1857969)
Iteration 1/1
100%|██████████| 478/478 [01:07<00:00,  7.05it/s]
Normal refinement complete.
2026-06-09 07:48:44,329 - Saved split mesh: outputs/2140_z150to400_segmented_IMM_surface1.ply
Saved mesh to outputs/2140_z150to400_segmented_IMM_surface1.ply
  Vertices: 903,196
  Faces: 1,787,839
2026-06-09 07:48:57,135 - Saved split mesh: outputs/2140_z150to400_segmented_IMM_surface2.ply
2026-06-09 07:48:57,141 -
Saving outputs for IMM:
2026-06-09 07:48:57,143 - Vertices: 1857969, Surface 1: 903196, Surface 2: 954773
Saved mesh to outputs/2140_z150to400_segmented_IMM_surface2.ply
  Vertices: 954,773
  Faces: 1,890,722
2026-06-09 07:48:58,353 - Saved MRC to outputs/2140_z150to400_segmented_IMM_vertices.mrc
2026-06-09 07:49:15,477 - Saved CSV with 1857969 vertices to outputs/2140_z150to400_segmented_IMM_vertices_normals.csv
2026-06-09 07:49:15,480 -
Processing OMM (label 3)
2026-06-09 07:49:15,584 - Extracting surface points (mesh vertices) with step size 1 from segmentation after smoothing with sigma 2.0...
Applying Gaussian smoothing (sigma=2.0)...
Smoothed field range: [0.000, 0.984]
Running marching cubes (level=0.5, step_size=1)...
2026-06-09 07:49:29,030 - Extracted 1255650 mesh vertices
2026-06-09 07:50:26,452 - Separating surfaces (mode='closed')...
2026-06-09 07:50:26,548 - Successfully separated membrane surfaces:
2026-06-09 07:50:26,549 - Surface 1: 617884 points
2026-06-09 07:50:26,550 - Surface 2: 637766 points
2026-06-09 07:50:26,551 - Refining normals within surface 1...
Refining normals (radius=3.0, batch=2000, iter=1, mask=617884/1255650)
Iteration 1/1
100%|██████████| 309/309 [00:43<00:00,  7.08it/s]
2026-06-09 07:51:10,827 - Refining normals within surface 2...
Normal refinement complete.
Refining normals (radius=3.0, batch=2000, iter=1, mask=637766/1255650)
Iteration 1/1
100%|██████████| 319/319 [00:45<00:00,  7.04it/s]
Normal refinement complete.
2026-06-09 07:52:05,538 - Saved split mesh: outputs/2140_z150to400_segmented_OMM_surface1.ply
Saved mesh to outputs/2140_z150to400_segmented_OMM_surface1.ply
  Vertices: 617,884
  Faces: 1,231,092
2026-06-09 07:52:14,356 - Saved split mesh: outputs/2140_z150to400_segmented_OMM_surface2.ply
2026-06-09 07:52:14,361 -
Saving outputs for OMM:
2026-06-09 07:52:14,362 - Vertices: 1255650, Surface 1: 617884, Surface 2: 637766
Saved mesh to outputs/2140_z150to400_segmented_OMM_surface2.ply
  Vertices: 637,766
  Faces: 1,270,737
2026-06-09 07:52:15,512 - Saved MRC to outputs/2140_z150to400_segmented_OMM_vertices.mrc
2026-06-09 07:52:27,013 - Saved CSV with 1255650 vertices to outputs/2140_z150to400_segmented_OMM_vertices_normals.csv
2026-06-09 07:52:27,027 -
Step 2: Matching points between surfaces
2026-06-09 07:52:27,028 -
Processing surface matches for ER
2026-06-09 07:52:27,231 - Loading data from outputs/2140_z150to400_segmented_ER_vertices_normals.csv...
2026-06-09 07:52:27,737 - Starting surface matching (1to2)...
2026-06-09 07:52:27,737 - Using all available CPU threads (numba default)
2026-06-09 07:52:27,738 - Matching surface 1 points to surface 2...
2026-06-09 07:52:27,738 - Starting CPU surface matching with 477614 points...
2026-06-09 07:52:27,739 - Source points: 242035, target points: 235579, max match distance: 8.00 nm (10.20 vox)
2026-06-09 07:52:27,740 - Max angle: 10.0 degrees
2026-06-09 07:52:27,747 - Building KD-tree for target points...
2026-06-09 07:52:27,835 - Querying KD-tree in batches of 200000 (242035 source points)...
2026-06-09 07:52:45,370 - KD-tree neighborhood search completed in 17.54 seconds
2026-06-09 07:52:45,371 - Found 1990999 potential matches across all source points
2026-06-09 07:52:45,372 - Resolving one-to-one surface matches...
2026-06-09 07:52:46,964 - Resolved 211259 one-to-one surface matches
2026-06-09 07:52:47,207 - Processing completed in 19.47 seconds
2026-06-09 07:52:47,228 - Statistics saved to outputs/2140_z150to400_segmented_ER_matched_points_stats.txt
2026-06-09 07:52:47,228 -
Creating matched-points CSV with valid one-to-one assignments...
2026-06-09 07:52:47,300 - Saving 211259 matched point pairs to outputs/2140_z150to400_segmented_ER_matched_points.csv
2026-06-09 07:52:49,757 - Matched-point distance summary:
2026-06-09 07:52:49,758 -   Mean: 5.813 nm (7.414 vox)
2026-06-09 07:52:49,758 -   Median: 5.880 nm (7.500 vox)
2026-06-09 07:52:49,759 -   Range: 0.331-7.990 nm (0.422-10.191 vox)
2026-06-09 07:52:49,759 - Surface matching complete
2026-06-09 07:52:49,763 - Point matching for ER completed successfully
2026-06-09 07:52:49,763 -
Step 3: Extracting tomogram profiles and resolving boundaries for ER
2026-06-09 07:52:49,764 - Validating segmentation and tomogram dimensions and pixel size compatibility...
2026-06-09 07:52:50,005 - ✓ Segmentation and tomogram are compatible in dimensions and pixel size
2026-06-09 07:52:50,306 - === Extracting intensity profiles ===
2026-06-09 07:52:50,306 - Tomogram MRC: 2140_z150to400.mrc
2026-06-09 07:52:50,308 -           path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/2140_z150to400.mrc
2026-06-09 07:52:50,308 - Segmentation MRC: 2140_z150to400_segmented.mrc
2026-06-09 07:52:50,309 -             path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/2140_z150to400_segmented.mrc
2026-06-09 07:52:50,309 - Matched-point CSV: 2140_z150to400_segmented_ER_matched_points.csv
2026-06-09 07:52:50,310 -              path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/2140_z150to400_segmented_ER_matched_points.csv
2026-06-09 07:52:50,310 -              rows: 211,259; usable coordinate pairs (non-NaN): 211,259
2026-06-09 07:52:50,376 - Z-score normalizing tomogram before profile sampling...
2026-06-09 07:53:02,536 -
Finding profile minima and inflection points...
2026-06-09 07:54:22,532 - Applying max-distance filter to resolved profiles (8.000 nm)...
2026-06-09 07:54:22,533 - === Intensity profiles summary ===
2026-06-09 07:54:22,534 - Number of intensity profiles resolved: 176,730/211,259 (83.7%)
2026-06-09 07:54:22,534 - Number of profiles after max-distance filter (8.000 nm): 172,110/176,730 (97.4% of resolved; 81.5% overall)
2026-06-09 07:54:22,535 - Number of profiles with thickness calculated as distances between inflection points: 148,728
2026-06-09 07:54:22,535 - Number of profiles where only minima were detected (no inflection points): 23,382
2026-06-09 07:54:22,536 - === Profiles with missing outward maxima ===
2026-06-09 07:54:22,536 - Profiles lacking an outward-maximum on one or both leaflets: 95,764 / 211,259 sampled pairs (45.3%)
2026-06-09 07:54:22,536 - One maximum missing → opposite-side minima-to-maximum distances mirrored: 72,030
2026-06-09 07:54:22,537 - Both maxima missing → no inflection-point distances calculated: 23,734
2026-06-09 07:54:22,537 - === Distances between minima (all profiles with two detected minima) ===
2026-06-09 07:54:22,537 - Distances between minima mean: 3.941 nm (5.027 vox)
2026-06-09 07:54:22,538 - Distances between minima median: 3.826 nm (4.880 vox)
2026-06-09 07:54:22,539 - Distances between minima range: 0.917-9.900 nm (1.169-12.628 vox)
2026-06-09 07:54:22,540 - === Membrane thickness from inflection points ===
2026-06-09 07:54:22,540 - Inflection-point distances mean: 5.781 nm (7.374 vox)
2026-06-09 07:54:22,541 - Inflection-point distances median: 5.671 nm (7.234 vox)
2026-06-09 07:54:22,541 - Inflection-point distances range: 2.391-8.000 nm (3.050-10.204 vox)
2026-06-09 07:54:22,542 - === Distance shift from matched segmentation points to inflection points ===
2026-06-09 07:54:22,542 - Distance shift mean: -0.108 nm (-0.138 vox)
2026-06-09 07:54:22,543 - Distance shift median: -0.238 nm (-0.304 vox)
2026-06-09 07:54:22,543 -
Saving results to: outputs
2026-06-09 07:54:33,864 - Saved thickness table: outputs/2140_z150to400_segmented_ER_thickness.csv
2026-06-09 07:54:58,350 - Saved intensity profiles: outputs/2140_z150to400_segmented_ER_int_profiles.pkl
2026-06-09 07:54:58,353 - Saved statistics: outputs/2140_z150to400_segmented_ER_boundary_stats.txt
2026-06-09 07:54:58,501 - Successfully analysed intensity profiles for ER
2026-06-09 07:54:58,501 -
Step 4: Discretizing per-voxel median thickness volumes for ER
2026-06-09 07:55:00,584 - Discretizing 148728 rows to integer voxels (23382 non-finite dropped)
2026-06-09 07:55:00,652 - Leaflet 1: 107922 unique voxels; Leaflet 2: 106664 unique voxels
2026-06-09 07:55:02,386 - Saved Surface 1 volume: outputs/2140_z150to400_segmented_ER_surface1_membrane_thickness_discretized_nm.mrc
2026-06-09 07:55:02,958 - Saved Surface 1 discretized CSV: outputs/2140_z150to400_segmented_ER_surface1_membrane_thickness_discretized_nm.csv
2026-06-09 07:55:04,474 - Saved Surface 2 volume: outputs/2140_z150to400_segmented_ER_surface2_membrane_thickness_discretized_nm.mrc
2026-06-09 07:55:05,025 - Saved Surface 2 discretized CSV: outputs/2140_z150to400_segmented_ER_surface2_membrane_thickness_discretized_nm.csv
2026-06-09 07:55:05,062 - Surface volumes saved for ER
2026-06-09 07:55:05,063 -
Processing surface matches for IMM
2026-06-09 07:55:05,208 - Loading data from outputs/2140_z150to400_segmented_IMM_vertices_normals.csv...
2026-06-09 07:55:07,126 - Starting surface matching (1to2)...
2026-06-09 07:55:07,127 - Using all available CPU threads (numba default)
2026-06-09 07:55:07,127 - Matching surface 1 points to surface 2...
2026-06-09 07:55:07,127 - Starting CPU surface matching with 1857969 points...
2026-06-09 07:55:07,130 - Source points: 903196, target points: 954773, max match distance: 8.00 nm (10.20 vox)
2026-06-09 07:55:07,130 - Max angle: 10.0 degrees
2026-06-09 07:55:07,158 - Building KD-tree for target points...
2026-06-09 07:55:07,548 - Querying KD-tree in batches of 200000 (903196 source points)...
2026-06-09 07:56:28,613 - KD-tree neighborhood search completed in 81.06 seconds
2026-06-09 07:56:28,614 - Found 5490559 potential matches across all source points
2026-06-09 07:56:28,615 - Resolving one-to-one surface matches...
2026-06-09 07:56:34,884 - Resolved 809465 one-to-one surface matches
2026-06-09 07:56:35,896 - Processing completed in 88.77 seconds
2026-06-09 07:56:35,964 - Statistics saved to outputs/2140_z150to400_segmented_IMM_matched_points_stats.txt
2026-06-09 07:56:35,965 -
Creating matched-points CSV with valid one-to-one assignments...
2026-06-09 07:56:36,216 - Saving 809465 matched point pairs to outputs/2140_z150to400_segmented_IMM_matched_points.csv
2026-06-09 07:56:45,954 - Matched-point distance summary:
2026-06-09 07:56:45,954 -   Mean: 5.037 nm (6.424 vox)
2026-06-09 07:56:45,954 -   Median: 5.072 nm (6.470 vox)
2026-06-09 07:56:45,955 -   Range: 0.143-7.998 nm (0.183-10.202 vox)
2026-06-09 07:56:45,955 - Surface matching complete
2026-06-09 07:56:45,969 - Point matching for IMM completed successfully
2026-06-09 07:56:45,969 -
Step 3: Extracting tomogram profiles and resolving boundaries for IMM
2026-06-09 07:56:45,969 - Validating segmentation and tomogram dimensions and pixel size compatibility...
2026-06-09 07:56:46,276 - ✓ Segmentation and tomogram are compatible in dimensions and pixel size
2026-06-09 07:56:47,413 - === Extracting intensity profiles ===
2026-06-09 07:56:47,414 - Tomogram MRC: 2140_z150to400.mrc
2026-06-09 07:56:47,415 -           path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/2140_z150to400.mrc
2026-06-09 07:56:47,415 - Segmentation MRC: 2140_z150to400_segmented.mrc
2026-06-09 07:56:47,416 -             path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/2140_z150to400_segmented.mrc
2026-06-09 07:56:47,417 - Matched-point CSV: 2140_z150to400_segmented_IMM_matched_points.csv
2026-06-09 07:56:47,417 -              path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/2140_z150to400_segmented_IMM_matched_points.csv
2026-06-09 07:56:47,418 -              rows: 809,465; usable coordinate pairs (non-NaN): 809,465
2026-06-09 07:56:47,486 - Z-score normalizing tomogram before profile sampling...
2026-06-09 07:57:13,593 -
Finding profile minima and inflection points...
2026-06-09 08:02:10,609 - Applying max-distance filter to resolved profiles (8.000 nm)...
2026-06-09 08:02:10,613 - === Intensity profiles summary ===
2026-06-09 08:02:10,614 - Number of intensity profiles resolved: 691,240/809,465 (85.4%)
2026-06-09 08:02:10,614 - Number of profiles after max-distance filter (8.000 nm): 678,128/691,240 (98.1% of resolved; 83.8% overall)
2026-06-09 08:02:10,615 - Number of profiles with thickness calculated as distances between inflection points: 594,155
2026-06-09 08:02:10,615 - Number of profiles where only minima were detected (no inflection points): 83,973
2026-06-09 08:02:10,615 - === Profiles with missing outward maxima ===
2026-06-09 08:02:10,616 - Profiles lacking an outward-maximum on one or both leaflets: 320,889 / 809,465 sampled pairs (39.6%)
2026-06-09 08:02:10,616 - One maximum missing → opposite-side minima-to-maximum distances mirrored: 235,877
2026-06-09 08:02:10,616 - Both maxima missing → no inflection-point distances calculated: 85,012
2026-06-09 08:02:10,617 - === Distances between minima (all profiles with two detected minima) ===
2026-06-09 08:02:10,617 - Distances between minima mean: 3.703 nm (4.723 vox)
2026-06-09 08:02:10,618 - Distances between minima median: 3.667 nm (4.677 vox)
2026-06-09 08:02:10,618 - Distances between minima range: 1.048-10.043 nm (1.336-12.811 vox)
2026-06-09 08:02:10,619 - === Membrane thickness from inflection points ===
2026-06-09 08:02:10,620 - Inflection-point distances mean: 5.544 nm (7.072 vox)
2026-06-09 08:02:10,621 - Inflection-point distances median: 5.371 nm (6.851 vox)
2026-06-09 08:02:10,621 - Inflection-point distances range: 2.095-8.000 nm (2.672-10.204 vox)
2026-06-09 08:02:10,622 - === Distance shift from matched segmentation points to inflection points ===
2026-06-09 08:02:10,622 - Distance shift mean: 0.431 nm (0.550 vox)
2026-06-09 08:02:10,622 - Distance shift median: 0.237 nm (0.302 vox)
2026-06-09 08:02:10,623 -
Saving results to: outputs
2026-06-09 08:02:58,117 - Saved thickness table: outputs/2140_z150to400_segmented_IMM_thickness.csv
2026-06-09 08:04:56,192 - Saved intensity profiles: outputs/2140_z150to400_segmented_IMM_int_profiles.pkl
2026-06-09 08:04:56,208 - Saved statistics: outputs/2140_z150to400_segmented_IMM_boundary_stats.txt
2026-06-09 08:04:57,373 - Successfully analysed intensity profiles for IMM
2026-06-09 08:04:57,374 -
Step 4: Discretizing per-voxel median thickness volumes for IMM
2026-06-09 08:05:05,848 - Discretizing 594155 rows to integer voxels (83973 non-finite dropped)
2026-06-09 08:05:06,136 - Leaflet 1: 440609 unique voxels; Leaflet 2: 439982 unique voxels
2026-06-09 08:05:08,282 - Saved Surface 1 volume: outputs/2140_z150to400_segmented_IMM_surface1_membrane_thickness_discretized_nm.mrc
2026-06-09 08:05:10,612 - Saved Surface 1 discretized CSV: outputs/2140_z150to400_segmented_IMM_surface1_membrane_thickness_discretized_nm.csv
2026-06-09 08:05:12,358 - Saved Surface 2 volume: outputs/2140_z150to400_segmented_IMM_surface2_membrane_thickness_discretized_nm.mrc
2026-06-09 08:05:14,771 - Saved Surface 2 discretized CSV: outputs/2140_z150to400_segmented_IMM_surface2_membrane_thickness_discretized_nm.csv
2026-06-09 08:05:14,879 - Surface volumes saved for IMM
2026-06-09 08:05:14,880 -
Processing surface matches for OMM
2026-06-09 08:05:15,035 - Loading data from outputs/2140_z150to400_segmented_OMM_vertices_normals.csv...
2026-06-09 08:05:16,339 - Starting surface matching (1to2)...
2026-06-09 08:05:16,340 - Using all available CPU threads (numba default)
2026-06-09 08:05:16,340 - Matching surface 1 points to surface 2...
2026-06-09 08:05:16,341 - Starting CPU surface matching with 1255650 points...
2026-06-09 08:05:16,342 - Source points: 617884, target points: 637766, max match distance: 8.00 nm (10.20 vox)
2026-06-09 08:05:16,343 - Max angle: 10.0 degrees
2026-06-09 08:05:16,361 - Building KD-tree for target points...
2026-06-09 08:05:16,612 - Querying KD-tree in batches of 200000 (617884 source points)...
2026-06-09 08:06:14,928 - KD-tree neighborhood search completed in 58.32 seconds
2026-06-09 08:06:14,931 - Found 3478677 potential matches across all source points
2026-06-09 08:06:14,931 - Resolving one-to-one surface matches...
2026-06-09 08:06:18,488 - Resolved 580064 one-to-one surface matches
2026-06-09 08:06:18,999 - Processing completed in 62.66 seconds
2026-06-09 08:06:19,049 - Statistics saved to outputs/2140_z150to400_segmented_OMM_matched_points_stats.txt
2026-06-09 08:06:19,050 -
Creating matched-points CSV with valid one-to-one assignments...
2026-06-09 08:06:19,222 - Saving 580064 matched point pairs to outputs/2140_z150to400_segmented_OMM_matched_points.csv
2026-06-09 08:06:26,149 - Matched-point distance summary:
2026-06-09 08:06:26,150 -   Mean: 4.874 nm (6.217 vox)
2026-06-09 08:06:26,150 -   Median: 4.878 nm (6.222 vox)
2026-06-09 08:06:26,151 -   Range: 0.174-7.958 nm (0.222-10.150 vox)
2026-06-09 08:06:26,152 - Surface matching complete
2026-06-09 08:06:26,159 - Point matching for OMM completed successfully
2026-06-09 08:06:26,160 -
Step 3: Extracting tomogram profiles and resolving boundaries for OMM
2026-06-09 08:06:26,160 - Validating segmentation and tomogram dimensions and pixel size compatibility...
2026-06-09 08:06:26,472 - ✓ Segmentation and tomogram are compatible in dimensions and pixel size
2026-06-09 08:06:27,273 - === Extracting intensity profiles ===
2026-06-09 08:06:27,273 - Tomogram MRC: 2140_z150to400.mrc
2026-06-09 08:06:27,274 -           path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/2140_z150to400.mrc
2026-06-09 08:06:27,275 - Segmentation MRC: 2140_z150to400_segmented.mrc
2026-06-09 08:06:27,276 -             path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/2140_z150to400_segmented.mrc
2026-06-09 08:06:27,276 - Matched-point CSV: 2140_z150to400_segmented_OMM_matched_points.csv
2026-06-09 08:06:27,277 -              path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/2140_z150to400_segmented_OMM_matched_points.csv
2026-06-09 08:06:27,278 -              rows: 580,064; usable coordinate pairs (non-NaN): 580,064
2026-06-09 08:06:27,345 - Z-score normalizing tomogram before profile sampling...
2026-06-09 08:06:49,017 -
Finding profile minima and inflection points...
2026-06-09 08:10:25,822 - Applying max-distance filter to resolved profiles (8.000 nm)...
2026-06-09 08:10:25,827 - === Intensity profiles summary ===
2026-06-09 08:10:25,828 - Number of intensity profiles resolved: 502,070/580,064 (86.6%)
2026-06-09 08:10:25,828 - Number of profiles after max-distance filter (8.000 nm): 495,947/502,070 (98.8% of resolved; 85.5% overall)
2026-06-09 08:10:25,829 - Number of profiles with thickness calculated as distances between inflection points: 433,282
2026-06-09 08:10:25,829 - Number of profiles where only minima were detected (no inflection points): 62,665
2026-06-09 08:10:25,830 - === Profiles with missing outward maxima ===
2026-06-09 08:10:25,830 - Profiles lacking an outward-maximum on one or both leaflets: 242,091 / 580,064 sampled pairs (41.7%)
2026-06-09 08:10:25,830 - One maximum missing → opposite-side minima-to-maximum distances mirrored: 179,135
2026-06-09 08:10:25,831 - Both maxima missing → no inflection-point distances calculated: 62,956
2026-06-09 08:10:25,831 - === Distances between minima (all profiles with two detected minima) ===
2026-06-09 08:10:25,832 - Distances between minima mean: 3.678 nm (4.691 vox)
2026-06-09 08:10:25,832 - Distances between minima median: 3.667 nm (4.677 vox)
2026-06-09 08:10:25,833 - Distances between minima range: 1.000-9.900 nm (1.276-12.628 vox)
2026-06-09 08:10:25,833 - === Membrane thickness from inflection points ===
2026-06-09 08:10:25,833 - Inflection-point distances mean: 5.530 nm (7.054 vox)
2026-06-09 08:10:25,834 - Inflection-point distances median: 5.278 nm (6.732 vox)
2026-06-09 08:10:25,834 - Inflection-point distances range: 2.200-8.000 nm (2.806-10.204 vox)
2026-06-09 08:10:25,834 - === Distance shift from matched segmentation points to inflection points ===
2026-06-09 08:10:25,835 - Distance shift mean: 0.605 nm (0.771 vox)
2026-06-09 08:10:25,835 - Distance shift median: 0.364 nm (0.465 vox)
2026-06-09 08:10:25,836 -
Saving results to: outputs
2026-06-09 08:10:59,862 - Saved thickness table: outputs/2140_z150to400_segmented_OMM_thickness.csv
2026-06-09 08:12:21,725 - Saved intensity profiles: outputs/2140_z150to400_segmented_OMM_int_profiles.pkl
2026-06-09 08:12:21,740 - Saved statistics: outputs/2140_z150to400_segmented_OMM_boundary_stats.txt
2026-06-09 08:12:22,687 - Successfully analysed intensity profiles for OMM
2026-06-09 08:12:22,687 -
Step 4: Discretizing per-voxel median thickness volumes for OMM
2026-06-09 08:12:28,897 - Discretizing 433282 rows to integer voxels (62665 non-finite dropped)
2026-06-09 08:12:29,120 - Leaflet 1: 314590 unique voxels; Leaflet 2: 320058 unique voxels
2026-06-09 08:12:31,708 - Saved Surface 1 volume: outputs/2140_z150to400_segmented_OMM_surface1_membrane_thickness_discretized_nm.mrc
2026-06-09 08:12:33,458 - Saved Surface 1 discretized CSV: outputs/2140_z150to400_segmented_OMM_surface1_membrane_thickness_discretized_nm.csv
2026-06-09 08:12:35,353 - Saved Surface 2 volume: outputs/2140_z150to400_segmented_OMM_surface2_membrane_thickness_discretized_nm.mrc
2026-06-09 08:12:37,118 - Saved Surface 2 discretized CSV: outputs/2140_z150to400_segmented_OMM_surface2_membrane_thickness_discretized_nm.csv
2026-06-09 08:12:37,212 - Surface volumes saved for OMM
2026-06-09 08:12:37,213 -
Full pipeline completed!
2026-06-09 08:12:37,213 - ER: ✓ Completed - 172110 profiles after filtering
2026-06-09 08:12:37,214 - IMM: ✓ Completed - 678128 profiles after filtering
2026-06-09 08:12:37,214 - OMM: ✓ Completed - 495947 profiles after filtering

Run step 1: surface extraction#

Extract surface meshes from each membrane label and write *_vertices_normals.csv. Run this step when you want to:

  • Inspect the extracted surfaces before committing to a full pipeline run.

  • Try different surface_separation_mode or smooth_sigma_segmentation values.

  • Run steps 2 and 3 separately afterwards (pass the output CSVs to match_points).

Output per membrane: one CSV with columns x_voxel, y_voxel, z_voxel (float, sub-voxel marching-cubes coordinates), normal_x/y/z, surface1, surface2.

snap_vertices_to_boundary=False is the recommended setting when you intend to extract intensity profiles in step 3. Sub-voxel vertex positions center the sampled line profiles more accurately between the two leaflets. Set it to True only if you are using the segmentation geometry as a direct measure of thickness (no tomogram intensity values).

[6]:
# Returns {membrane_name: path_to_vertices_normals_csv}
vertices_csvs = memthick.process_membrane_segmentation(
    segmentation_map=segmentation_path,
    output_path=output_path,
    membrane_labels=membrane_labels,
    smooth_sigma_segmentation=2.0,
    surface_separation_mode=surface_separation_mode,
    refine_normals=True,
    radius_hit=3.0,
    snap_vertices_to_boundary=False, # set to False if you are extracting intensity profiles from the tomogram; set to True if you are using the segmentation as a pure measure of thickness
    save_split_surface_meshes=False, # to visualize surface splitting (will produce a .ply mesh file)
    save_vertices_mrc=False, # to visualize vertices positions (rounds vertices to the nearest integer)
)
print(vertices_csvs)
2026-06-09 08:18:45,089 -
Processing ER (label 1)
2026-06-09 08:18:45,182 - Extracting surface points (mesh vertices) with step size 1 from segmentation after smoothing with sigma 2.0...
Applying Gaussian smoothing (sigma=2.0)...
Smoothed field range: [0.000, 0.997]
Running marching cubes (level=0.5, step_size=1)...
2026-06-09 08:18:58,754 - Extracted 477614 mesh vertices
2026-06-09 08:19:26,707 - Separating surfaces (mode='planar')...
2026-06-09 08:19:26,738 - Successfully separated membrane surfaces:
2026-06-09 08:19:26,739 - Surface 1: 242035 points
2026-06-09 08:19:26,740 - Surface 2: 235579 points
2026-06-09 08:19:26,741 - Refining normals within surface 1...
Refining normals (radius=3.0, batch=2000, iter=1, mask=242035/477614)
Iteration 1/1
100%|██████████| 122/122 [00:29<00:00,  4.08it/s]
2026-06-09 08:19:57,001 - Refining normals within surface 2...
Normal refinement complete.
Refining normals (radius=3.0, batch=2000, iter=1, mask=235579/477614)
Iteration 1/1
100%|██████████| 118/118 [00:16<00:00,  6.96it/s]
2026-06-09 08:20:14,185 -
Saving outputs for ER:
2026-06-09 08:20:14,187 - Vertices: 477614, Surface 1: 242035, Surface 2: 235579
Normal refinement complete.
2026-06-09 08:20:18,636 - Saved CSV with 477614 vertices to outputs/2140_z150to400_segmented_ER_vertices_normals.csv
2026-06-09 08:20:18,637 -
Processing IMM (label 2)
2026-06-09 08:20:18,920 - Extracting surface points (mesh vertices) with step size 1 from segmentation after smoothing with sigma 2.0...
Applying Gaussian smoothing (sigma=2.0)...
Smoothed field range: [0.000, 0.992]
Running marching cubes (level=0.5, step_size=1)...
2026-06-09 08:20:33,871 - Extracted 1857969 mesh vertices
2026-06-09 08:22:40,427 - Separating surfaces (mode='closed')...
2026-06-09 08:22:40,593 - Successfully separated membrane surfaces:
2026-06-09 08:22:40,594 - Surface 1: 903196 points
2026-06-09 08:22:40,595 - Surface 2: 954773 points
2026-06-09 08:22:40,596 - Refining normals within surface 1...
Refining normals (radius=3.0, batch=2000, iter=1, mask=903196/1857969)
Iteration 1/1
100%|██████████| 452/452 [01:08<00:00,  6.57it/s]
2026-06-09 08:23:50,330 - Refining normals within surface 2...
Normal refinement complete.
Refining normals (radius=3.0, batch=2000, iter=1, mask=954773/1857969)
Iteration 1/1
100%|██████████| 478/478 [01:49<00:00,  4.35it/s]
2026-06-09 08:25:41,284 -
Saving outputs for IMM:
2026-06-09 08:25:41,287 - Vertices: 1857969, Surface 1: 903196, Surface 2: 954773
Normal refinement complete.
2026-06-09 08:25:59,556 - Saved CSV with 1857969 vertices to outputs/2140_z150to400_segmented_IMM_vertices_normals.csv
2026-06-09 08:25:59,562 -
Processing OMM (label 3)
2026-06-09 08:25:59,855 - Extracting surface points (mesh vertices) with step size 1 from segmentation after smoothing with sigma 2.0...
Applying Gaussian smoothing (sigma=2.0)...
Smoothed field range: [0.000, 0.984]
Running marching cubes (level=0.5, step_size=1)...
2026-06-09 08:26:14,315 - Extracted 1255650 mesh vertices
2026-06-09 08:27:37,837 - Separating surfaces (mode='closed')...
2026-06-09 08:27:37,943 - Successfully separated membrane surfaces:
2026-06-09 08:27:37,944 - Surface 1: 617884 points
2026-06-09 08:27:37,945 - Surface 2: 637766 points
2026-06-09 08:27:37,945 - Refining normals within surface 1...
Refining normals (radius=3.0, batch=2000, iter=1, mask=617884/1255650)
Iteration 1/1
100%|██████████| 309/309 [00:44<00:00,  6.99it/s]
2026-06-09 08:28:22,764 - Refining normals within surface 2...
Normal refinement complete.
Refining normals (radius=3.0, batch=2000, iter=1, mask=637766/1255650)
Iteration 1/1
100%|██████████| 319/319 [00:45<00:00,  7.00it/s]
2026-06-09 08:29:08,953 -
Saving outputs for OMM:
2026-06-09 08:29:08,954 - Vertices: 1255650, Surface 1: 617884, Surface 2: 637766
Normal refinement complete.
2026-06-09 08:29:20,704 - Saved CSV with 1255650 vertices to outputs/2140_z150to400_segmented_OMM_vertices_normals.csv
{'ER': 'outputs/2140_z150to400_segmented_ER_vertices_normals.csv', 'IMM': 'outputs/2140_z150to400_segmented_IMM_vertices_normals.csv', 'OMM': 'outputs/2140_z150to400_segmented_OMM_vertices_normals.csv'}

Run step 2: surface point matching#

Use this when you want to re-run geometric matching with different parameters — e.g. a wider distance cap or a different cone angle — without repeating the meshing step.

Input: *_vertices_normals.csv from step 1.
Output per membrane: *_matched_points.csv + *_matched_points_stats.txt.
The matched-points CSV contains one row per successfully paired point on surface 1, with the coordinates of both the surface-1 and surface-2 points (x1_voxel … z2_voxel) and the Euclidean distance between them (match_distance_nm).
This distance is a geometric quantity derived purely from the segmentation — it is the distance along the normal between the two surface meshes.
``match_distance_nm`` vs ``membrane_thickness_nm``.
match_distance_nm (step 2 output) is the distance between the two geometrically matched segmentation surface points — it describes the segmentation, not the underlying biology.
membrane_thickness_nm (step 3 output) is the distance between the inflection points of the tomogram intensity profiles and is measured from the tomographic densities.
[7]:
# Point to the CSV(s) produced by step 1
vertices_csvs = {
    "ER": f"{output_path}/2140_z150to400_segmented_ER_vertices_normals.csv",
    "IMM": f"{output_path}/2140_z150to400_segmented_IMM_vertices_normals.csv",
    "OMM": f"{output_path}/2140_z150to400_segmented_OMM_vertices_normals.csv",
}

for membrane_name, csv in vertices_csvs.items():
    matched_csv, stats_file = memthick.match_points(
        segmentation_map=segmentation_path,
        input_csv=csv,
        output_path=output_path,
        max_distance_nm=8.0,
        max_angle=10.0,
        direction="1to2",
        use_gpu=False,
        surface_separation_mode=surface_separation_mode[membrane_name], # not actively used but written in the log file
    )
    print(f"{membrane_name}: {matched_csv}")
2026-06-09 08:30:38,034 - Loading data from outputs/2140_z150to400_segmented_ER_vertices_normals.csv...
2026-06-09 08:30:38,553 - Starting surface matching (1to2)...
2026-06-09 08:30:38,554 - Using all available CPU threads (numba default)
2026-06-09 08:30:38,554 - Matching surface 1 points to surface 2...
2026-06-09 08:30:38,555 - Starting CPU surface matching with 477614 points...
2026-06-09 08:30:38,556 - Source points: 242035, target points: 235579, max match distance: 8.00 nm (10.20 vox)
2026-06-09 08:30:38,556 - Max angle: 10.0 degrees
2026-06-09 08:30:38,564 - Building KD-tree for target points...
2026-06-09 08:30:38,654 - Querying KD-tree in batches of 200000 (242035 source points)...
2026-06-09 08:30:58,935 - KD-tree neighborhood search completed in 20.28 seconds
2026-06-09 08:30:58,938 - Found 1990999 potential matches across all source points
2026-06-09 08:30:58,938 - Resolving one-to-one surface matches...
2026-06-09 08:31:00,769 - Resolved 211259 one-to-one surface matches
2026-06-09 08:31:01,092 - Processing completed in 22.54 seconds
2026-06-09 08:31:01,115 - Statistics saved to outputs/2140_z150to400_segmented_ER_vertices_normals_matched_points_stats.txt
2026-06-09 08:31:01,115 -
Creating matched-points CSV with valid one-to-one assignments...
2026-06-09 08:31:01,183 - Saving 211259 matched point pairs to outputs/2140_z150to400_segmented_ER_vertices_normals_matched_points.csv
2026-06-09 08:31:03,697 - Matched-point distance summary:
2026-06-09 08:31:03,697 -   Mean: 5.813 nm (7.414 vox)
2026-06-09 08:31:03,698 -   Median: 5.880 nm (7.500 vox)
2026-06-09 08:31:03,698 -   Range: 0.331-7.990 nm (0.422-10.191 vox)
2026-06-09 08:31:03,699 - Surface matching complete
ER: outputs/2140_z150to400_segmented_ER_vertices_normals_matched_points.csv
2026-06-09 08:31:03,924 - Loading data from outputs/2140_z150to400_segmented_IMM_vertices_normals.csv...
2026-06-09 08:31:06,002 - Starting surface matching (1to2)...
2026-06-09 08:31:06,002 - Using all available CPU threads (numba default)
2026-06-09 08:31:06,003 - Matching surface 1 points to surface 2...
2026-06-09 08:31:06,003 - Starting CPU surface matching with 1857969 points...
2026-06-09 08:31:06,005 - Source points: 903196, target points: 954773, max match distance: 8.00 nm (10.20 vox)
2026-06-09 08:31:06,006 - Max angle: 10.0 degrees
2026-06-09 08:31:06,035 - Building KD-tree for target points...
2026-06-09 08:31:06,431 - Querying KD-tree in batches of 200000 (903196 source points)...
2026-06-09 08:33:14,847 - KD-tree neighborhood search completed in 128.41 seconds
2026-06-09 08:33:14,857 - Found 5490559 potential matches across all source points
2026-06-09 08:33:14,858 - Resolving one-to-one surface matches...
2026-06-09 08:33:23,779 - Resolved 809465 one-to-one surface matches
2026-06-09 08:33:25,157 - Processing completed in 139.15 seconds
2026-06-09 08:33:25,235 - Statistics saved to outputs/2140_z150to400_segmented_IMM_vertices_normals_matched_points_stats.txt
2026-06-09 08:33:25,237 -
Creating matched-points CSV with valid one-to-one assignments...
2026-06-09 08:33:25,492 - Saving 809465 matched point pairs to outputs/2140_z150to400_segmented_IMM_vertices_normals_matched_points.csv
2026-06-09 08:33:35,237 - Matched-point distance summary:
2026-06-09 08:33:35,238 -   Mean: 5.037 nm (6.424 vox)
2026-06-09 08:33:35,238 -   Median: 5.072 nm (6.470 vox)
2026-06-09 08:33:35,238 -   Range: 0.143-7.998 nm (0.183-10.202 vox)
2026-06-09 08:33:35,239 - Surface matching complete
IMM: outputs/2140_z150to400_segmented_IMM_vertices_normals_matched_points.csv
2026-06-09 08:33:35,508 - Loading data from outputs/2140_z150to400_segmented_OMM_vertices_normals.csv...
2026-06-09 08:33:36,859 - Starting surface matching (1to2)...
2026-06-09 08:33:36,860 - Using all available CPU threads (numba default)
2026-06-09 08:33:36,860 - Matching surface 1 points to surface 2...
2026-06-09 08:33:36,861 - Starting CPU surface matching with 1255650 points...
2026-06-09 08:33:36,863 - Source points: 617884, target points: 637766, max match distance: 8.00 nm (10.20 vox)
2026-06-09 08:33:36,863 - Max angle: 10.0 degrees
2026-06-09 08:33:36,884 - Building KD-tree for target points...
2026-06-09 08:33:37,182 - Querying KD-tree in batches of 200000 (617884 source points)...
2026-06-09 08:34:51,899 - KD-tree neighborhood search completed in 74.72 seconds
2026-06-09 08:34:51,903 - Found 3478677 potential matches across all source points
2026-06-09 08:34:51,903 - Resolving one-to-one surface matches...
2026-06-09 08:34:56,599 - Resolved 580064 one-to-one surface matches
2026-06-09 08:34:57,346 - Processing completed in 80.49 seconds
2026-06-09 08:34:57,397 - Statistics saved to outputs/2140_z150to400_segmented_OMM_vertices_normals_matched_points_stats.txt
2026-06-09 08:34:57,398 -
Creating matched-points CSV with valid one-to-one assignments...
2026-06-09 08:34:57,575 - Saving 580064 matched point pairs to outputs/2140_z150to400_segmented_OMM_vertices_normals_matched_points.csv
2026-06-09 08:35:04,690 - Matched-point distance summary:
2026-06-09 08:35:04,691 -   Mean: 4.874 nm (6.217 vox)
2026-06-09 08:35:04,691 -   Median: 4.878 nm (6.222 vox)
2026-06-09 08:35:04,692 -   Range: 0.174-7.958 nm (0.222-10.150 vox)
2026-06-09 08:35:04,692 - Surface matching complete
OMM: outputs/2140_z150to400_segmented_OMM_vertices_normals_matched_points.csv

(Optional) per-surface MRC volumes from segmentation surface point matching#

Rasterizes the matched-point distances (purely based on segmentation surfaces) onto two per-surface MRC volumes and CSVs — one for each bilayer surface. Use this when you want a spatial map of match_distance_nm (segmentation thickness) to visually overlay on the tomogram.

Each MRC voxel receives the median match_distance_nm (segmentation thickness)across all vertices that round to that integer position. Voxels with no matching vertex are zero.

Key parameters for ``save_thickness_volume_mrc``:

Parameter

Description

thickness_col

Column to rasterize. Use "match_distance_nm" for step-2 geometry output.

coord_suffix

Coordinate column suffix. Use "voxel" to read x1_voxel / x2_voxel (step-2 CSV format).

[9]:
# Aggreagates vertices that match back to the same voxel into a single voxel value
matched_csvs = {
    "ER": f"{output_path}/2140_z150to400_segmented_ER_matched_points.csv",
    "IMM": f"{output_path}/2140_z150to400_segmented_IMM_matched_points.csv",
    "OMM": f"{output_path}/2140_z150to400_segmented_OMM_matched_points.csv",
}

for membrane_name, csv in matched_csvs.items():
    saved = memthick.save_thickness_volume_mrc(
        thickness_csv=csv,
        segmentation_map=segmentation_path,
        output_path=output_path,
        thickness_col="match_distance_nm",
        coord_suffix="voxel",
    )
    print(f"{membrane_name}: {saved}")
Discretizing 211259 rows to integer voxels (0 non-finite dropped)
Leaflet 1: 120317 unique voxels; Leaflet 2: 115755 unique voxels
Saved Surface 1 volume: outputs/2140_z150to400_segmented_ER_surface1_match_distance_discretized_nm.mrc
Saved Surface 1 discretized CSV: outputs/2140_z150to400_segmented_ER_surface1_match_distance_discretized_nm.csv
Saved Surface 2 volume: outputs/2140_z150to400_segmented_ER_surface2_match_distance_discretized_nm.mrc
Saved Surface 2 discretized CSV: outputs/2140_z150to400_segmented_ER_surface2_match_distance_discretized_nm.csv
ER: {'surface1_mrc': PosixPath('outputs/2140_z150to400_segmented_ER_surface1_match_distance_discretized_nm.mrc'), 'surface1_csv': PosixPath('outputs/2140_z150to400_segmented_ER_surface1_match_distance_discretized_nm.csv'), 'surface2_mrc': PosixPath('outputs/2140_z150to400_segmented_ER_surface2_match_distance_discretized_nm.mrc'), 'surface2_csv': PosixPath('outputs/2140_z150to400_segmented_ER_surface2_match_distance_discretized_nm.csv')}
Discretizing 809465 rows to integer voxels (0 non-finite dropped)
Leaflet 1: 492919 unique voxels; Leaflet 2: 498705 unique voxels
Saved Surface 1 volume: outputs/2140_z150to400_segmented_IMM_surface1_match_distance_discretized_nm.mrc
Saved Surface 1 discretized CSV: outputs/2140_z150to400_segmented_IMM_surface1_match_distance_discretized_nm.csv
Saved Surface 2 volume: outputs/2140_z150to400_segmented_IMM_surface2_match_distance_discretized_nm.mrc
Saved Surface 2 discretized CSV: outputs/2140_z150to400_segmented_IMM_surface2_match_distance_discretized_nm.csv
IMM: {'surface1_mrc': PosixPath('outputs/2140_z150to400_segmented_IMM_surface1_match_distance_discretized_nm.mrc'), 'surface1_csv': PosixPath('outputs/2140_z150to400_segmented_IMM_surface1_match_distance_discretized_nm.csv'), 'surface2_mrc': PosixPath('outputs/2140_z150to400_segmented_IMM_surface2_match_distance_discretized_nm.mrc'), 'surface2_csv': PosixPath('outputs/2140_z150to400_segmented_IMM_surface2_match_distance_discretized_nm.csv')}
Discretizing 580064 rows to integer voxels (0 non-finite dropped)
Leaflet 1: 350739 unique voxels; Leaflet 2: 356510 unique voxels
Saved Surface 1 volume: outputs/2140_z150to400_segmented_OMM_surface1_match_distance_discretized_nm.mrc
Saved Surface 1 discretized CSV: outputs/2140_z150to400_segmented_OMM_surface1_match_distance_discretized_nm.csv
Saved Surface 2 volume: outputs/2140_z150to400_segmented_OMM_surface2_match_distance_discretized_nm.mrc
Saved Surface 2 discretized CSV: outputs/2140_z150to400_segmented_OMM_surface2_match_distance_discretized_nm.csv
OMM: {'surface1_mrc': PosixPath('outputs/2140_z150to400_segmented_OMM_surface1_match_distance_discretized_nm.mrc'), 'surface1_csv': PosixPath('outputs/2140_z150to400_segmented_OMM_surface1_match_distance_discretized_nm.csv'), 'surface2_mrc': PosixPath('outputs/2140_z150to400_segmented_OMM_surface2_match_distance_discretized_nm.mrc'), 'surface2_csv': PosixPath('outputs/2140_z150to400_segmented_OMM_surface2_match_distance_discretized_nm.csv')}

Run step 3: intensity profile analysis#

Use this when you want to iterate on IntensityProfileAnalyzer settings — e.g. adjusting profile smoothing, minima search windows, or the outward maximum search range — without re-running meshing or geometric matching.

Input: *_matched_points.csv from step 2 + the original tomogram MRC.
Output per membrane: *_thickness.csv, *_int_profiles.pkl, *_boundary_stats.txt.

For each matched pair, the pipeline:

  1. Extracts a short 1-D intensity profile from the tomogram along the vector connecting the two matched points, extended by profile_half_width_nm on each side of the midpoint.

  2. Detects the two bilayer minima (phosphate headgroup regions) and the central maximum (hydrophobic core) within search windows anchored on the matched segmentation surface points.

  3. Searches outward from each minimum for a maximum (close to where the intensity returns to baseline). The inflection point on the steepest slope between each minimum and its outward maximum defines the membrane boundary.

  4. Applies a final max_distance_nm cap: rows whose measured inflection-point thickness exceeds this value are excluded from the exported CSV.

See Membrane thickness: analysis for a detailed description of the output columns, detection modes (max_max, max_anchor, minima_only), and how to interpret the intensity profile plots.

[10]:
# Point to the CSV(s) produced by step 2
matched_csvs = {
    "ER": f"{output_path}/2140_z150to400_segmented_ER_matched_points.csv",
    "IMM": f"{output_path}/2140_z150to400_segmented_IMM_matched_points.csv",
    "OMM": f"{output_path}/2140_z150to400_segmented_OMM_matched_points.csv",
}

for membrane_name, csv in matched_csvs.items():
    intensity_results = memthick.analyse_intensity_profiles(
        thickness_csv=csv,
        tomogram_map=tomo_path,
        segmentation_path=segmentation_path,
        membrane_label=membrane_name,
        output_path=output_path,
        profile_half_width_nm=5.5,
        max_distance_nm=8.0,
        analyzer=analyzer,
    )
    print(f"{membrane_name}: {intensity_results.get('saved_files', {})}")
2026-06-09 08:37:49,153 - Writing pipeline diagnostics to 2140_z150to400_segmented_ER_2140_z150to400_analysis.log (under /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs)
2026-06-09 08:37:49,484 - === Extracting intensity profiles ===
2026-06-09 08:37:49,485 - Tomogram MRC: 2140_z150to400.mrc
2026-06-09 08:37:49,486 -           path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/2140_z150to400.mrc
2026-06-09 08:37:49,487 - Segmentation MRC: 2140_z150to400_segmented.mrc
2026-06-09 08:37:49,488 -             path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/2140_z150to400_segmented.mrc
2026-06-09 08:37:49,488 - Matched-point CSV: 2140_z150to400_segmented_ER_matched_points.csv
2026-06-09 08:37:49,489 -              path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/2140_z150to400_segmented_ER_matched_points.csv
2026-06-09 08:37:49,489 -              rows: 211,259; usable coordinate pairs (non-NaN): 211,259
2026-06-09 08:37:49,604 - Z-score normalizing tomogram before profile sampling...
2026-06-09 08:38:04,602 -
Finding profile minima and inflection points...
2026-06-09 08:40:07,374 - Applying max-distance filter to resolved profiles (8.000 nm)...
2026-06-09 08:40:07,378 - === Intensity profiles summary ===
2026-06-09 08:40:07,378 - Number of intensity profiles resolved: 176,730/211,259 (83.7%)
2026-06-09 08:40:07,379 - Number of profiles after max-distance filter (8.000 nm): 172,110/176,730 (97.4% of resolved; 81.5% overall)
2026-06-09 08:40:07,379 - Number of profiles with thickness calculated as distances between inflection points: 148,728
2026-06-09 08:40:07,380 - Number of profiles where only minima were detected (no inflection points): 23,382
2026-06-09 08:40:07,380 - === Profiles with missing outward maxima ===
2026-06-09 08:40:07,380 - Profiles lacking an outward-maximum on one or both leaflets: 95,764 / 211,259 sampled pairs (45.3%)
2026-06-09 08:40:07,381 - One maximum missing → opposite-side minima-to-maximum distances mirrored: 72,030
2026-06-09 08:40:07,381 - Both maxima missing → no inflection-point distances calculated: 23,734
2026-06-09 08:40:07,381 - === Distances between minima (all profiles with two detected minima) ===
2026-06-09 08:40:07,382 - Distances between minima mean: 3.941 nm (5.027 vox)
2026-06-09 08:40:07,382 - Distances between minima median: 3.826 nm (4.880 vox)
2026-06-09 08:40:07,383 - Distances between minima range: 0.917-9.900 nm (1.169-12.628 vox)
2026-06-09 08:40:07,383 - === Membrane thickness from inflection points ===
2026-06-09 08:40:07,384 - Inflection-point distances mean: 5.781 nm (7.374 vox)
2026-06-09 08:40:07,384 - Inflection-point distances median: 5.671 nm (7.234 vox)
2026-06-09 08:40:07,385 - Inflection-point distances range: 2.391-8.000 nm (3.050-10.204 vox)
2026-06-09 08:40:07,386 - === Distance shift from matched segmentation points to inflection points ===
2026-06-09 08:40:07,386 - Distance shift mean: -0.108 nm (-0.138 vox)
2026-06-09 08:40:07,386 - Distance shift median: -0.238 nm (-0.304 vox)
2026-06-09 08:40:07,387 -
Saving results to: outputs
2026-06-09 08:40:19,417 - Saved thickness table: outputs/2140_z150to400_segmented_ER_thickness.csv
2026-06-09 08:40:54,744 - Saved intensity profiles: outputs/2140_z150to400_segmented_ER_int_profiles.pkl
2026-06-09 08:40:54,757 - Saved statistics: outputs/2140_z150to400_segmented_ER_boundary_stats.txt
2026-06-09 08:40:55,169 - Writing pipeline diagnostics to 2140_z150to400_segmented_IMM_2140_z150to400_analysis.log (under /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs)
ER: {'thickness_csv': PosixPath('outputs/2140_z150to400_segmented_ER_thickness.csv'), 'int_profiles': PosixPath('outputs/2140_z150to400_segmented_ER_int_profiles.pkl'), 'statistics': PosixPath('outputs/2140_z150to400_segmented_ER_boundary_stats.txt')}
2026-06-09 08:40:56,368 - === Extracting intensity profiles ===
2026-06-09 08:40:56,369 - Tomogram MRC: 2140_z150to400.mrc
2026-06-09 08:40:56,370 -           path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/2140_z150to400.mrc
2026-06-09 08:40:56,370 - Segmentation MRC: 2140_z150to400_segmented.mrc
2026-06-09 08:40:56,371 -             path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/2140_z150to400_segmented.mrc
2026-06-09 08:40:56,372 - Matched-point CSV: 2140_z150to400_segmented_IMM_matched_points.csv
2026-06-09 08:40:56,372 -              path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/2140_z150to400_segmented_IMM_matched_points.csv
2026-06-09 08:40:56,373 -              rows: 809,465; usable coordinate pairs (non-NaN): 809,465
2026-06-09 08:40:56,490 - Z-score normalizing tomogram before profile sampling...
2026-06-09 08:41:23,306 -
Finding profile minima and inflection points...
2026-06-09 08:47:23,501 - Applying max-distance filter to resolved profiles (8.000 nm)...
2026-06-09 08:47:23,528 - === Intensity profiles summary ===
2026-06-09 08:47:23,530 - Number of intensity profiles resolved: 691,240/809,465 (85.4%)
2026-06-09 08:47:23,530 - Number of profiles after max-distance filter (8.000 nm): 678,128/691,240 (98.1% of resolved; 83.8% overall)
2026-06-09 08:47:23,531 - Number of profiles with thickness calculated as distances between inflection points: 594,155
2026-06-09 08:47:23,532 - Number of profiles where only minima were detected (no inflection points): 83,973
2026-06-09 08:47:23,532 - === Profiles with missing outward maxima ===
2026-06-09 08:47:23,533 - Profiles lacking an outward-maximum on one or both leaflets: 320,889 / 809,465 sampled pairs (39.6%)
2026-06-09 08:47:23,533 - One maximum missing → opposite-side minima-to-maximum distances mirrored: 235,877
2026-06-09 08:47:23,534 - Both maxima missing → no inflection-point distances calculated: 85,012
2026-06-09 08:47:23,534 - === Distances between minima (all profiles with two detected minima) ===
2026-06-09 08:47:23,534 - Distances between minima mean: 3.703 nm (4.723 vox)
2026-06-09 08:47:23,535 - Distances between minima median: 3.667 nm (4.677 vox)
2026-06-09 08:47:23,535 - Distances between minima range: 1.048-10.043 nm (1.336-12.811 vox)
2026-06-09 08:47:23,536 - === Membrane thickness from inflection points ===
2026-06-09 08:47:23,536 - Inflection-point distances mean: 5.544 nm (7.072 vox)
2026-06-09 08:47:23,537 - Inflection-point distances median: 5.371 nm (6.851 vox)
2026-06-09 08:47:23,537 - Inflection-point distances range: 2.095-8.000 nm (2.672-10.204 vox)
2026-06-09 08:47:23,537 - === Distance shift from matched segmentation points to inflection points ===
2026-06-09 08:47:23,538 - Distance shift mean: 0.431 nm (0.550 vox)
2026-06-09 08:47:23,538 - Distance shift median: 0.237 nm (0.302 vox)
2026-06-09 08:47:23,538 -
Saving results to: outputs
2026-06-09 08:48:10,807 - Saved thickness table: outputs/2140_z150to400_segmented_IMM_thickness.csv
2026-06-09 08:50:18,859 - Saved intensity profiles: outputs/2140_z150to400_segmented_IMM_int_profiles.pkl
2026-06-09 08:50:18,870 - Saved statistics: outputs/2140_z150to400_segmented_IMM_boundary_stats.txt
2026-06-09 08:50:25,758 - Writing pipeline diagnostics to 2140_z150to400_segmented_OMM_2140_z150to400_analysis.log (under /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs)
IMM: {'thickness_csv': PosixPath('outputs/2140_z150to400_segmented_IMM_thickness.csv'), 'int_profiles': PosixPath('outputs/2140_z150to400_segmented_IMM_int_profiles.pkl'), 'statistics': PosixPath('outputs/2140_z150to400_segmented_IMM_boundary_stats.txt')}
2026-06-09 08:50:26,734 - === Extracting intensity profiles ===
2026-06-09 08:50:26,735 - Tomogram MRC: 2140_z150to400.mrc
2026-06-09 08:50:26,736 -           path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/2140_z150to400.mrc
2026-06-09 08:50:26,736 - Segmentation MRC: 2140_z150to400_segmented.mrc
2026-06-09 08:50:26,737 -             path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/2140_z150to400_segmented.mrc
2026-06-09 08:50:26,737 - Matched-point CSV: 2140_z150to400_segmented_OMM_matched_points.csv
2026-06-09 08:50:26,738 -              path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/2140_z150to400_segmented_OMM_matched_points.csv
2026-06-09 08:50:26,738 -              rows: 580,064; usable coordinate pairs (non-NaN): 580,064
2026-06-09 08:50:26,976 - Z-score normalizing tomogram before profile sampling...
2026-06-09 08:50:50,355 -
Finding profile minima and inflection points...
2026-06-09 08:55:53,442 - Applying max-distance filter to resolved profiles (8.000 nm)...
2026-06-09 08:55:53,454 - === Intensity profiles summary ===
2026-06-09 08:55:53,455 - Number of intensity profiles resolved: 502,070/580,064 (86.6%)
2026-06-09 08:55:53,455 - Number of profiles after max-distance filter (8.000 nm): 495,947/502,070 (98.8% of resolved; 85.5% overall)
2026-06-09 08:55:53,455 - Number of profiles with thickness calculated as distances between inflection points: 433,282
2026-06-09 08:55:53,456 - Number of profiles where only minima were detected (no inflection points): 62,665
2026-06-09 08:55:53,456 - === Profiles with missing outward maxima ===
2026-06-09 08:55:53,456 - Profiles lacking an outward-maximum on one or both leaflets: 242,091 / 580,064 sampled pairs (41.7%)
2026-06-09 08:55:53,457 - One maximum missing → opposite-side minima-to-maximum distances mirrored: 179,135
2026-06-09 08:55:53,457 - Both maxima missing → no inflection-point distances calculated: 62,956
2026-06-09 08:55:53,457 - === Distances between minima (all profiles with two detected minima) ===
2026-06-09 08:55:53,458 - Distances between minima mean: 3.678 nm (4.691 vox)
2026-06-09 08:55:53,459 - Distances between minima median: 3.667 nm (4.677 vox)
2026-06-09 08:55:53,460 - Distances between minima range: 1.000-9.900 nm (1.276-12.628 vox)
2026-06-09 08:55:53,460 - === Membrane thickness from inflection points ===
2026-06-09 08:55:53,462 - Inflection-point distances mean: 5.530 nm (7.054 vox)
2026-06-09 08:55:53,462 - Inflection-point distances median: 5.278 nm (6.732 vox)
2026-06-09 08:55:53,463 - Inflection-point distances range: 2.200-8.000 nm (2.806-10.204 vox)
2026-06-09 08:55:53,463 - === Distance shift from matched segmentation points to inflection points ===
2026-06-09 08:55:53,464 - Distance shift mean: 0.605 nm (0.771 vox)
2026-06-09 08:55:53,464 - Distance shift median: 0.364 nm (0.465 vox)
2026-06-09 08:55:53,464 -
Saving results to: outputs
2026-06-09 08:56:27,582 - Saved thickness table: outputs/2140_z150to400_segmented_OMM_thickness.csv
2026-06-09 08:58:18,688 - Saved intensity profiles: outputs/2140_z150to400_segmented_OMM_int_profiles.pkl
2026-06-09 08:58:18,703 - Saved statistics: outputs/2140_z150to400_segmented_OMM_boundary_stats.txt
OMM: {'thickness_csv': PosixPath('outputs/2140_z150to400_segmented_OMM_thickness.csv'), 'int_profiles': PosixPath('outputs/2140_z150to400_segmented_OMM_int_profiles.pkl'), 'statistics': PosixPath('outputs/2140_z150to400_segmented_OMM_boundary_stats.txt')}

(Optional) per-surface MRC volumes from full thickness analysis#

Rasterizes inflection-point-based membrane thickness onto two per-surface MRC volumes and CSVs. Run this after analyse_intensity_profiles has written a *_thickness.csv.

Compared to the step-2 geometry MRC above, this uses membrane_thickness_nm (inflection-point distances from the tomogram) rather than match_distance_nm (segmentation-surface distances). The coordinate columns also differ: the step-3 CSV uses corrected coordinates (x1_corr_voxel, x2_corr_voxel, …) that reflect the inflection-point positions rather than the original matched surface points.

Key parameters for ``save_thickness_volume_mrc``:

Parameter

Default

Description

thickness_col

"membrane_thickness_nm"

Column to rasterize. minima_only rows have NaN here and are automatically skipped.

coord_suffix

"corr_voxel"

Coordinate column suffix. Matches x1_corr_voxel / x2_corr_voxel in *_thickness.csv.

[13]:
thickness_csvs = {
    "ER": f"{output_path}/2140_z150to400_segmented_ER_thickness.csv",
    "IMM": f"{output_path}/2140_z150to400_segmented_IMM_thickness.csv",
    "OMM": f"{output_path}/2140_z150to400_segmented_OMM_thickness.csv",
}

for membrane_name, csv in thickness_csvs.items():
    saved = memthick.save_thickness_volume_mrc(
        thickness_csv=csv,
        segmentation_map=segmentation_path,
        output_path=output_path,
        coord_suffix="corr_voxel",              # default, matches *_thickness.csv
        thickness_col="membrane_thickness_nm"  # default
    )
    print(f"{membrane_name}: {saved}")
Discretizing 148728 rows to integer voxels (23382 non-finite dropped)
Leaflet 1: 107922 unique voxels; Leaflet 2: 106664 unique voxels
Saved Surface 1 volume: outputs/2140_z150to400_segmented_ER_surface1_membrane_thickness_discretized_nm.mrc
Saved Surface 1 discretized CSV: outputs/2140_z150to400_segmented_ER_surface1_membrane_thickness_discretized_nm.csv
Saved Surface 2 volume: outputs/2140_z150to400_segmented_ER_surface2_membrane_thickness_discretized_nm.mrc
Saved Surface 2 discretized CSV: outputs/2140_z150to400_segmented_ER_surface2_membrane_thickness_discretized_nm.csv
ER: {'surface1_mrc': PosixPath('outputs/2140_z150to400_segmented_ER_surface1_membrane_thickness_discretized_nm.mrc'), 'surface1_csv': PosixPath('outputs/2140_z150to400_segmented_ER_surface1_membrane_thickness_discretized_nm.csv'), 'surface2_mrc': PosixPath('outputs/2140_z150to400_segmented_ER_surface2_membrane_thickness_discretized_nm.mrc'), 'surface2_csv': PosixPath('outputs/2140_z150to400_segmented_ER_surface2_membrane_thickness_discretized_nm.csv')}
Discretizing 594155 rows to integer voxels (83973 non-finite dropped)
Leaflet 1: 440609 unique voxels; Leaflet 2: 439982 unique voxels
Saved Surface 1 volume: outputs/2140_z150to400_segmented_IMM_surface1_membrane_thickness_discretized_nm.mrc
Saved Surface 1 discretized CSV: outputs/2140_z150to400_segmented_IMM_surface1_membrane_thickness_discretized_nm.csv
Saved Surface 2 volume: outputs/2140_z150to400_segmented_IMM_surface2_membrane_thickness_discretized_nm.mrc
Saved Surface 2 discretized CSV: outputs/2140_z150to400_segmented_IMM_surface2_membrane_thickness_discretized_nm.csv
IMM: {'surface1_mrc': PosixPath('outputs/2140_z150to400_segmented_IMM_surface1_membrane_thickness_discretized_nm.mrc'), 'surface1_csv': PosixPath('outputs/2140_z150to400_segmented_IMM_surface1_membrane_thickness_discretized_nm.csv'), 'surface2_mrc': PosixPath('outputs/2140_z150to400_segmented_IMM_surface2_membrane_thickness_discretized_nm.mrc'), 'surface2_csv': PosixPath('outputs/2140_z150to400_segmented_IMM_surface2_membrane_thickness_discretized_nm.csv')}
Discretizing 433282 rows to integer voxels (62665 non-finite dropped)
Leaflet 1: 314590 unique voxels; Leaflet 2: 320058 unique voxels
Saved Surface 1 volume: outputs/2140_z150to400_segmented_OMM_surface1_membrane_thickness_discretized_nm.mrc
Saved Surface 1 discretized CSV: outputs/2140_z150to400_segmented_OMM_surface1_membrane_thickness_discretized_nm.csv
Saved Surface 2 volume: outputs/2140_z150to400_segmented_OMM_surface2_membrane_thickness_discretized_nm.mrc
Saved Surface 2 discretized CSV: outputs/2140_z150to400_segmented_OMM_surface2_membrane_thickness_discretized_nm.csv
OMM: {'surface1_mrc': PosixPath('outputs/2140_z150to400_segmented_OMM_surface1_membrane_thickness_discretized_nm.mrc'), 'surface1_csv': PosixPath('outputs/2140_z150to400_segmented_OMM_surface1_membrane_thickness_discretized_nm.csv'), 'surface2_mrc': PosixPath('outputs/2140_z150to400_segmented_OMM_surface2_membrane_thickness_discretized_nm.mrc'), 'surface2_csv': PosixPath('outputs/2140_z150to400_segmented_OMM_surface2_membrane_thickness_discretized_nm.csv')}