Effect of tomogram preprocessing on membrane thickness measurements#

This tutorial presumes familiarity with running the pipeline (Membrane thickness: run) and with analysing its outputs (Membrane thickness: analysis).


Table of contents#

  1. Introduction

  2. Setup

  3. Parameters

  4. Step 1: surface extraction

  5. Step 2: surface point matching

  6. Step 3: intensity profile analysis

  7. Load results

  8. Boundary mode summary

  9. Thickness distributions: IMM vs OMM

  10. IMM: comparison across tomograms

  11. OMM: comparison across tomograms

  12. Intensity profile summaries

  13. Profiles: two outward maxima

  14. Profiles: mirrored maximum


Introduction#

Cryo-electron tomograms can be reconstructed in different ways, producing volumes with different noise levels, contrast, and potential artefacts. This tutorial examines how that choice of tomographic preprocessing affects the membrane thickness values measured by cryocat.analysis.memthick.

The example tomogram is a (large) HEK cell mitochondrion with highly curved and self-folding membranes: the inner mitochondrial membrane (IMM) and the outer mitochondrial membrane (OMM).

Label

Method

File

cryoCARE

Deep-learning denoising (cryoCARE)

TS_0198_cryoCARE_200to400.mrc

WBP

Weighted back-projection

TS_0198_AT_WBP_200to400.mrc

WBP_deconv

WBP with Wiener-like CTF deconvolution

TS_0198_AT_WBP_200to400_deconv.mrc

All three tomograms were reconstructed from the same tilt series with identical alignment parameters (AreTomo2) and share the same physical dimensions and voxel size. The only difference is the reconstruction and post-processing applied.

cryoCARE uses self-supervised (Noise2Noise-based) deep-learning denoising on paired even/odd frame reconstructions, yielding the highest signal-to-noise ratio of the three.

WBP is a standard filtered back-projection reconstruction and has a comparatively low signal-to-noise ratio.

WBP + deconvolution applies a Wiener-like filter to a WBP volume to enhance contrast and recover attenuated spatial frequencies.


Analysis steps#

Because the tomograms are geometrically identical, we use the same segmentation (generated by MemBrain-seg on the cryoCARE tomogram) for the surface extraction and point matching steps. The pipeline is therefore structured as follows:

  1. Step 1: surface extraction — run once on the segmentation, independent of the tomogram.

  2. Step 2: surface point matching — run once, pairing points on opposing segmentation surfaces based on geometry alone.

  3. Step 3: intensity profile analysis — run separately for each tomogram, sampling profiles through the same surface point pairs but through different density volumes. Outputs are written to separate subdirectories (outputs/cryoCARE/, outputs/WBP/, outputs/WBP_deconv/) to prevent overwriting.


Setup#

[1]:
%load_ext autoreload
%autoreload 2
[ ]:
from pathlib import Path
import memthick_analyze_plot as maple
from cryocat.analysis import memthick

from IPython.display import Image
[3]:
# Input files
segmentation_path = "inputs/TS_0198_200to400_segmented.mrc"
seg_base = "TS_0198_200to400_segmented"

tomo_paths = {
    "cryoCARE":   "inputs/TS_0198_cryoCARE_200to400.mrc",
    "WBP":        "inputs/TS_0198_AT_WBP_200to400.mrc",
    "WBP_deconv": "inputs/TS_0198_AT_WBP_200to400_deconv.mrc",
}
tomo_names = list(tomo_paths.keys())
membrane_names = ["IMM", "OMM"]

# Output directories
output_path = Path("outputs")
output_path.mkdir(exist_ok=True)
for name in tomo_names:
    (output_path / name).mkdir(exist_ok=True)

# Plots directory
plots_path = Path("plots")
plots_path.mkdir(exist_ok=True)

# Helper: path to a per-membrane, per-tomo thickness CSV
def thickness_csv(tomo: str, membrane: str) -> Path:
    return output_path / tomo / f"{seg_base}_{membrane}_vertices_normals_thickness.csv"

# Helper: path to a per-membrane, per-tomo intensity profile pickle
def int_profiles_pkl(tomo: str, membrane: str) -> Path:
    return output_path / tomo / f"{seg_base}_{membrane}_vertices_normals_int_profiles.pkl"

print("Input files:")
print(f"  Segmentation: {segmentation_path}")
for label, p in tomo_paths.items():
    status = "OK" if Path(p).exists() else "MISSING"
    print(f"  {label}: {p}  [{status}]")

Input files:
  Segmentation: inputs/TS_0198_200to400_segmented.mrc
  cryoCARE: inputs/TS_0198_cryoCARE_200to400.mrc  [OK]
  WBP: inputs/TS_0198_AT_WBP_200to400.mrc  [OK]
  WBP_deconv: inputs/TS_0198_AT_WBP_200to400_deconv.mrc  [OK]

Parameters#

Membrane labels#

Label

Segmentation value

Surface separation mode

IMM

1

"closed" — inner mitochondrial membrane

OMM

2

"closed" — outer mitochondrial membrane

Steps 1 & 2 (geometry)#

Parameter

Value

Notes

smooth_sigma_segmentation

2.0

Gaussian smoothing of the segmentation before marching cubes meshing

refine_normals

True

Neighbour-based normal averaging

radius_hit

3.0

Neighbourhood radius for normal refinement (vox)

max_distance_nm

8.0

Hard cap on matched-point distance

max_angle

10.0

Cone half-angle for matching

snap_vertices_to_boundary

False

Sub-voxel coordinates for accurate profile centering

Step 3 (intensity profiles)#

Parameter

Value

Notes

profile_half_width_nm

5.5

Half-width of sampled profile on each side of the midpoint

max_distance_nm

8.0

Rows exceeding this inflection thickness are excluded from the CSV

smooth_sigma_intensity_profiles

0.5

Gaussian smoothing along the profile axis

minima_search_nm

(3.0, 4.0)

Search window radii around matched surface points

anchor_search_nm

4.0

Half-width of outward maximum search window

These parameters are identical across all three tomograms.

[4]:
membrane_labels = {"IMM": 1, "OMM": 2}
surface_separation_mode = {"IMM": "closed", "OMM": "closed"}

analyzer = memthick.IntensityProfileAnalyzer(
    smooth_sigma_intensity_profiles=0.5,
    minima_search_nm=(3.0, 4.0),
    anchor_search_nm=4.0,
    n_jobs=-1,
)

Step 1: surface extraction#

Generates a marching-cubes mesh from the segmentation, refines normals, and splits the mesh into two surfaces (inner and outer leaflet). Run once — output is the same for all three tomograms.

Output: outputs/{seg_base}_{membrane}_vertices_normals.csv

[5]:
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,
    save_split_surface_meshes=True,
    save_vertices_mrc=False,
)
print(vertices_csvs)
2026-06-09 10:03:44,949 -
Processing IMM (label 1)
2026-06-09 10:03:45,116 - 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, 1.000]
Running marching cubes (level=0.5, step_size=1)...
2026-06-09 10:03:58,583 - Extracted 4798720 mesh vertices
2026-06-09 10:07:47,015 - Separating surfaces (mode='closed')...
2026-06-09 10:07:47,430 - Successfully separated membrane surfaces:
2026-06-09 10:07:47,432 - Surface 1: 2367089 points
2026-06-09 10:07:47,434 - Surface 2: 2431631 points
2026-06-09 10:07:47,435 - Refining normals within surface 1...
Refining normals (radius=3.0, batch=2000, iter=1, mask=2367089/4798720)
Iteration 1/1
100%|██████████| 1184/1184 [02:48<00:00,  7.05it/s]
2026-06-09 10:10:37,959 - Refining normals within surface 2...
Normal refinement complete.
Refining normals (radius=3.0, batch=2000, iter=1, mask=2431631/4798720)
Iteration 1/1
100%|██████████| 1216/1216 [02:53<00:00,  7.02it/s]
Normal refinement complete.
2026-06-09 10:14:07,756 - Saved split mesh: outputs/TS_0198_200to400_segmented_IMM_surface1.ply
Saved mesh to outputs/TS_0198_200to400_segmented_IMM_surface1.ply
  Vertices: 2,367,089
  Faces: 4,652,081
2026-06-09 10:14:41,377 - Saved split mesh: outputs/TS_0198_200to400_segmented_IMM_surface2.ply
2026-06-09 10:14:41,393 -
Saving outputs for IMM:
2026-06-09 10:14:41,398 - Vertices: 4798720, Surface 1: 2367089, Surface 2: 2431631
Saved mesh to outputs/TS_0198_200to400_segmented_IMM_surface2.ply
  Vertices: 2,431,631
  Faces: 4,779,898
2026-06-09 10:15:28,543 - Saved CSV with 4798720 vertices to outputs/TS_0198_200to400_segmented_IMM_vertices_normals.csv
2026-06-09 10:15:28,574 -
Processing OMM (label 2)
2026-06-09 10:15:28,687 - 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.999]
Running marching cubes (level=0.5, step_size=1)...
2026-06-09 10:15:45,746 - Extracted 705785 mesh vertices
2026-06-09 10:16:17,542 - Separating surfaces (mode='closed')...
2026-06-09 10:16:17,601 - Successfully separated membrane surfaces:
2026-06-09 10:16:17,602 - Surface 1: 353075 points
2026-06-09 10:16:17,603 - Surface 2: 352710 points
2026-06-09 10:16:17,603 - Refining normals within surface 1...
Refining normals (radius=3.0, batch=2000, iter=1, mask=353075/705785)
Iteration 1/1
100%|██████████| 177/177 [00:25<00:00,  7.04it/s]
2026-06-09 10:16:43,072 - Refining normals within surface 2...
Normal refinement complete.
Refining normals (radius=3.0, batch=2000, iter=1, mask=352710/705785)
Iteration 1/1
100%|██████████| 177/177 [00:25<00:00,  6.98it/s]
Normal refinement complete.
2026-06-09 10:17:13,767 - Saved split mesh: outputs/TS_0198_200to400_segmented_OMM_surface1.ply
Saved mesh to outputs/TS_0198_200to400_segmented_OMM_surface1.ply
  Vertices: 353,075
  Faces: 701,106
2026-06-09 10:17:18,852 - Saved split mesh: outputs/TS_0198_200to400_segmented_OMM_surface2.ply
2026-06-09 10:17:18,855 -
Saving outputs for OMM:
2026-06-09 10:17:18,857 - Vertices: 705785, Surface 1: 353075, Surface 2: 352710
Saved mesh to outputs/TS_0198_200to400_segmented_OMM_surface2.ply
  Vertices: 352,710
  Faces: 700,032
2026-06-09 10:17:25,983 - Saved CSV with 705785 vertices to outputs/TS_0198_200to400_segmented_OMM_vertices_normals.csv
{'IMM': 'outputs/TS_0198_200to400_segmented_IMM_vertices_normals.csv', 'OMM': 'outputs/TS_0198_200to400_segmented_OMM_vertices_normals.csv'}

Step 2: surface point matching#

Pairs each surface-1 vertex with its nearest surface-2 partner inside a normal-cone search window. The output CSV contains one row per matched pair and records the Euclidean distance between the segmentation surface points (match_distance_nm). Run once — the same matched pairs are used for all three tomograms.

``match_distance_nm`` vs ``membrane_thickness_nm``.
match_distance_nm (step 2) is the Euclidean distance between the two matched vertices on the segmentation surfaces — it is a property of the segmentation.
membrane_thickness_nm (step 3) is the distance between the inflection points of the tomogram intensity profiles — it is measured from the actual densities.

Output: outputs/{seg_base}_{membrane}_matched_points.csv

[6]:
matched_csvs = {}
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,
    )
    matched_csvs[membrane_name] = matched_csv
    print(f"{membrane_name}: {matched_csv}")
2026-06-09 10:21:00,215 - Loading data from outputs/TS_0198_200to400_segmented_IMM_vertices_normals.csv...
2026-06-09 10:21:05,254 - Starting surface matching (1to2)...
2026-06-09 10:21:05,255 - Using all available CPU threads (numba default)
2026-06-09 10:21:05,255 - Matching surface 1 points to surface 2...
2026-06-09 10:21:05,256 - Starting CPU surface matching with 4798720 points...
2026-06-09 10:21:05,260 - Source points: 2367089, target points: 2431631, max match distance: 8.00 nm (10.15 vox)
2026-06-09 10:21:05,261 - Max angle: 10.0 degrees
2026-06-09 10:21:05,342 - Building KD-tree for target points...
2026-06-09 10:21:06,399 - Querying KD-tree in batches of 200000 (2367089 source points)...
2026-06-09 10:24:04,259 - KD-tree neighborhood search completed in 177.86 seconds
2026-06-09 10:24:04,260 - Found 21151487 potential matches across all source points
2026-06-09 10:24:04,261 - Resolving one-to-one surface matches...
2026-06-09 10:24:37,496 - Resolved 1995505 one-to-one surface matches
2026-06-09 10:24:41,864 - Processing completed in 216.61 seconds
2026-06-09 10:24:42,053 - Statistics saved to outputs/TS_0198_200to400_segmented_IMM_vertices_normals_matched_points_stats.txt
2026-06-09 10:24:42,054 -
Creating matched-points CSV with valid one-to-one assignments...
2026-06-09 10:24:42,756 - Saving 1995505 matched point pairs to outputs/TS_0198_200to400_segmented_IMM_vertices_normals_matched_points.csv
2026-06-09 10:25:06,416 - Matched-point distance summary:
2026-06-09 10:25:06,417 -   Mean: 6.196 nm (7.859 vox)
2026-06-09 10:25:06,418 -   Median: 6.310 nm (8.003 vox)
2026-06-09 10:25:06,418 -   Range: 0.109-8.000 nm (0.138-10.147 vox)
2026-06-09 10:25:06,419 - Surface matching complete
2026-06-09 10:25:06,631 - Loading data from outputs/TS_0198_200to400_segmented_OMM_vertices_normals.csv...
IMM: outputs/TS_0198_200to400_segmented_IMM_vertices_normals_matched_points.csv
2026-06-09 10:25:07,385 - Starting surface matching (1to2)...
2026-06-09 10:25:07,385 - Using all available CPU threads (numba default)
2026-06-09 10:25:07,386 - Matching surface 1 points to surface 2...
2026-06-09 10:25:07,386 - Starting CPU surface matching with 705785 points...
2026-06-09 10:25:07,388 - Source points: 353075, target points: 352710, max match distance: 8.00 nm (10.15 vox)
2026-06-09 10:25:07,389 - Max angle: 10.0 degrees
2026-06-09 10:25:07,399 - Building KD-tree for target points...
2026-06-09 10:25:07,535 - Querying KD-tree in batches of 200000 (353075 source points)...
2026-06-09 10:25:35,536 - KD-tree neighborhood search completed in 28.00 seconds
2026-06-09 10:25:35,537 - Found 2258663 potential matches across all source points
2026-06-09 10:25:35,538 - Resolving one-to-one surface matches...
2026-06-09 10:25:37,799 - Resolved 316175 one-to-one surface matches
2026-06-09 10:25:38,194 - Processing completed in 30.81 seconds
2026-06-09 10:25:38,221 - Statistics saved to outputs/TS_0198_200to400_segmented_OMM_vertices_normals_matched_points_stats.txt
2026-06-09 10:25:38,222 -
Creating matched-points CSV with valid one-to-one assignments...
2026-06-09 10:25:38,333 - Saving 316175 matched point pairs to outputs/TS_0198_200to400_segmented_OMM_vertices_normals_matched_points.csv
2026-06-09 10:25:42,084 - Matched-point distance summary:
2026-06-09 10:25:42,084 -   Mean: 5.357 nm (6.795 vox)
2026-06-09 10:25:42,084 -   Median: 5.368 nm (6.808 vox)
2026-06-09 10:25:42,085 -   Range: 1.422-7.999 nm (1.803-10.146 vox)
2026-06-09 10:25:42,086 - Surface matching complete
OMM: outputs/TS_0198_200to400_segmented_OMM_vertices_normals_matched_points.csv

Step 3: intensity profile analysis#

For each matched point pair, the pipeline: 1. Samples a 1-D intensity profile from the tomogram along the normal direction, centred at the pair midpoint and extending profile_half_width_nm on each side. 2. Detects the two bilayer minima (phosphate headgroups) and the central maximum (hydrophobic core). 3. Searches outward from each minimum for an intensity maximum (the density returning to baseline). The inflection point on the slope between each minimum and its outward maximum defines the membrane boundary. 4. Rows whose inflection-point thickness exceeds max_distance_nm are excluded from the exported CSV.

This step is run separately for each tomogram. Outputs go to per-tomogram subdirectories:

Tomogram

Output directory

cryoCARE

outputs/cryoCARE/

WBP

outputs/WBP/

WBP_deconv

outputs/WBP_deconv/

[7]:
for tomo_name, tomo_path in tomo_paths.items():
    tomo_out = output_path / tomo_name
    print(f"\n{'═'*60}")
    print(f"  Tomogram: {tomo_name}")
    print(f"{'═'*60}")
    for membrane_name, matched_csv in matched_csvs.items():
        print(f"\n  Membrane: {membrane_name}")
        memthick.analyse_intensity_profiles(
            thickness_csv=matched_csv,
            tomogram_map=tomo_path,
            segmentation_path=segmentation_path,
            membrane_label=membrane_name,
            output_path=tomo_out,
            profile_half_width_nm=5.5,
            max_distance_nm=8.0,
            analyzer=analyzer,
        )
2026-06-09 10:34:06,486 - Writing pipeline diagnostics to TS_0198_200to400_segmented_IMM_TS_0198_cryoCARE_200to400_analysis.log (under /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/cryoCARE)

════════════════════════════════════════════════════════════
  Tomogram: cryoCARE
════════════════════════════════════════════════════════════

  Membrane: IMM
2026-06-09 10:34:09,562 - === Extracting intensity profiles ===
2026-06-09 10:34:09,563 - Tomogram MRC: TS_0198_cryoCARE_200to400.mrc
2026-06-09 10:34:09,564 -           path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/TS_0198_cryoCARE_200to400.mrc
2026-06-09 10:34:09,564 - Segmentation MRC: TS_0198_200to400_segmented.mrc
2026-06-09 10:34:09,565 -             path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/TS_0198_200to400_segmented.mrc
2026-06-09 10:34:09,565 - Matched-point CSV: TS_0198_200to400_segmented_IMM_vertices_normals_matched_points.csv
2026-06-09 10:34:09,566 -              path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/TS_0198_200to400_segmented_IMM_vertices_normals_matched_points.csv
2026-06-09 10:34:09,566 -              rows: 1,995,505; usable coordinate pairs (non-NaN): 1,995,505
2026-06-09 10:34:09,988 - Z-score normalizing tomogram before profile sampling...
2026-06-09 10:34:58,323 -
Finding profile minima and inflection points...
2026-06-09 10:49:25,640 - Applying max-distance filter to resolved profiles (8.000 nm)...
2026-06-09 10:49:25,703 - === Intensity profiles summary ===
2026-06-09 10:49:25,706 - Number of intensity profiles resolved: 1,822,813/1,995,505 (91.3%)
2026-06-09 10:49:25,706 - Number of profiles after max-distance filter (8.000 nm): 1,787,756/1,822,813 (98.1% of resolved; 89.6% overall)
2026-06-09 10:49:25,707 - Number of profiles with thickness calculated as distances between inflection points: 1,542,047
2026-06-09 10:49:25,707 - Number of profiles where only minima were detected (no inflection points): 245,709
2026-06-09 10:49:25,708 - === Profiles with missing outward maxima ===
2026-06-09 10:49:25,709 - Profiles lacking an outward-maximum on one or both leaflets: 890,060 / 1,995,505 sampled pairs (44.6%)
2026-06-09 10:49:25,709 - One maximum missing → opposite-side minima-to-maximum distances mirrored: 641,842
2026-06-09 10:49:25,709 - Both maxima missing → no inflection-point distances calculated: 248,218
2026-06-09 10:49:25,710 - === Distances between minima (all profiles with two detected minima) ===
2026-06-09 10:49:25,710 - Distances between minima mean: 4.080 nm (5.176 vox)
2026-06-09 10:49:25,711 - Distances between minima median: 4.000 nm (5.074 vox)
2026-06-09 10:49:25,711 - Distances between minima range: 1.000-10.000 nm (1.268-12.684 vox)
2026-06-09 10:49:25,711 - === Membrane thickness from inflection points ===
2026-06-09 10:49:25,712 - Inflection-point distances mean: 6.080 nm (7.712 vox)
2026-06-09 10:49:25,712 - Inflection-point distances median: 6.025 nm (7.642 vox)
2026-06-09 10:49:25,712 - Inflection-point distances range: 2.095-8.000 nm (2.658-10.147 vox)
2026-06-09 10:49:25,713 - === Distance shift from matched segmentation points to inflection points ===
2026-06-09 10:49:25,713 - Distance shift mean: -0.204 nm (-0.259 vox)
2026-06-09 10:49:25,713 - Distance shift median: -0.308 nm (-0.391 vox)
2026-06-09 10:49:25,714 -
Saving results to: outputs/cryoCARE
2026-06-09 10:51:29,984 - Saved thickness table: outputs/cryoCARE/TS_0198_200to400_segmented_IMM_vertices_normals_thickness.csv
2026-06-09 10:58:00,463 - Saved intensity profiles: outputs/cryoCARE/TS_0198_200to400_segmented_IMM_vertices_normals_int_profiles.pkl
2026-06-09 10:58:00,482 - Saved statistics: outputs/cryoCARE/TS_0198_200to400_segmented_IMM_vertices_normals_boundary_stats.txt
2026-06-09 10:58:28,151 - Writing pipeline diagnostics to TS_0198_200to400_segmented_OMM_TS_0198_cryoCARE_200to400_analysis.log (under /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/cryoCARE)

  Membrane: OMM
2026-06-09 10:58:28,618 - === Extracting intensity profiles ===
2026-06-09 10:58:28,618 - Tomogram MRC: TS_0198_cryoCARE_200to400.mrc
2026-06-09 10:58:28,619 -           path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/TS_0198_cryoCARE_200to400.mrc
2026-06-09 10:58:28,620 - Segmentation MRC: TS_0198_200to400_segmented.mrc
2026-06-09 10:58:28,620 -             path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/TS_0198_200to400_segmented.mrc
2026-06-09 10:58:28,621 - Matched-point CSV: TS_0198_200to400_segmented_OMM_vertices_normals_matched_points.csv
2026-06-09 10:58:28,622 -              path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/TS_0198_200to400_segmented_OMM_vertices_normals_matched_points.csv
2026-06-09 10:58:28,622 -              rows: 316,175; usable coordinate pairs (non-NaN): 316,175
2026-06-09 10:58:28,956 - Z-score normalizing tomogram before profile sampling...
2026-06-09 10:58:39,513 -
Finding profile minima and inflection points...
2026-06-09 11:00:37,050 - Applying max-distance filter to resolved profiles (8.000 nm)...
2026-06-09 11:00:37,051 - === Intensity profiles summary ===
2026-06-09 11:00:37,051 - Number of intensity profiles resolved: 270,228/316,175 (85.5%)
2026-06-09 11:00:37,052 - Number of profiles after max-distance filter (8.000 nm): 267,934/270,228 (99.2% of resolved; 84.7% overall)
2026-06-09 11:00:37,052 - Number of profiles with thickness calculated as distances between inflection points: 247,322
2026-06-09 11:00:37,053 - Number of profiles where only minima were detected (no inflection points): 20,612
2026-06-09 11:00:37,053 - === Profiles with missing outward maxima ===
2026-06-09 11:00:37,053 - Profiles lacking an outward-maximum on one or both leaflets: 88,699 / 316,175 sampled pairs (28.1%)
2026-06-09 11:00:37,054 - One maximum missing → opposite-side minima-to-maximum distances mirrored: 68,056
2026-06-09 11:00:37,054 - Both maxima missing → no inflection-point distances calculated: 20,643
2026-06-09 11:00:37,054 - === Distances between minima (all profiles with two detected minima) ===
2026-06-09 11:00:37,055 - Distances between minima mean: 3.697 nm (4.690 vox)
2026-06-09 11:00:37,055 - Distances between minima median: 3.667 nm (4.651 vox)
2026-06-09 11:00:37,055 - Distances between minima range: 1.048-9.429 nm (1.329-11.959 vox)
2026-06-09 11:00:37,055 - === Membrane thickness from inflection points ===
2026-06-09 11:00:37,056 - Inflection-point distances mean: 5.753 nm (7.297 vox)
2026-06-09 11:00:37,056 - Inflection-point distances median: 5.698 nm (7.227 vox)
2026-06-09 11:00:37,057 - Inflection-point distances range: 2.500-8.000 nm (3.171-10.147 vox)
2026-06-09 11:00:37,057 - === Distance shift from matched segmentation points to inflection points ===
2026-06-09 11:00:37,058 - Distance shift mean: 0.314 nm (0.398 vox)
2026-06-09 11:00:37,058 - Distance shift median: 0.199 nm (0.252 vox)
2026-06-09 11:00:37,058 -
Saving results to: outputs/cryoCARE
2026-06-09 11:00:54,760 - Saved thickness table: outputs/cryoCARE/TS_0198_200to400_segmented_OMM_vertices_normals_thickness.csv
2026-06-09 11:01:32,525 - Saved intensity profiles: outputs/cryoCARE/TS_0198_200to400_segmented_OMM_vertices_normals_int_profiles.pkl
2026-06-09 11:01:32,529 - Saved statistics: outputs/cryoCARE/TS_0198_200to400_segmented_OMM_vertices_normals_boundary_stats.txt
2026-06-09 11:01:33,508 - Writing pipeline diagnostics to TS_0198_200to400_segmented_IMM_TS_0198_AT_WBP_200to400_analysis.log (under /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/WBP)

════════════════════════════════════════════════════════════
  Tomogram: WBP
════════════════════════════════════════════════════════════

  Membrane: IMM
2026-06-09 11:01:36,505 - === Extracting intensity profiles ===
2026-06-09 11:01:36,505 - Tomogram MRC: TS_0198_AT_WBP_200to400.mrc
2026-06-09 11:01:36,506 -           path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/TS_0198_AT_WBP_200to400.mrc
2026-06-09 11:01:36,507 - Segmentation MRC: TS_0198_200to400_segmented.mrc
2026-06-09 11:01:36,507 -             path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/TS_0198_200to400_segmented.mrc
2026-06-09 11:01:36,508 - Matched-point CSV: TS_0198_200to400_segmented_IMM_vertices_normals_matched_points.csv
2026-06-09 11:01:36,509 -              path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/TS_0198_200to400_segmented_IMM_vertices_normals_matched_points.csv
2026-06-09 11:01:36,509 -              rows: 1,995,505; usable coordinate pairs (non-NaN): 1,995,505
2026-06-09 11:01:36,831 - Z-score normalizing tomogram before profile sampling...
2026-06-09 11:02:23,290 -
Finding profile minima and inflection points...
2026-06-09 11:17:34,534 - Applying max-distance filter to resolved profiles (8.000 nm)...
2026-06-09 11:17:34,599 - === Intensity profiles summary ===
2026-06-09 11:17:34,601 - Number of intensity profiles resolved: 1,906,004/1,995,505 (95.5%)
2026-06-09 11:17:34,602 - Number of profiles after max-distance filter (8.000 nm): 1,861,192/1,906,004 (97.6% of resolved; 93.3% overall)
2026-06-09 11:17:34,602 - Number of profiles with thickness calculated as distances between inflection points: 1,742,993
2026-06-09 11:17:34,602 - Number of profiles where only minima were detected (no inflection points): 118,199
2026-06-09 11:17:34,603 - === Profiles with missing outward maxima ===
2026-06-09 11:17:34,603 - Profiles lacking an outward-maximum on one or both leaflets: 887,332 / 1,995,505 sampled pairs (44.5%)
2026-06-09 11:17:34,603 - One maximum missing → opposite-side minima-to-maximum distances mirrored: 768,637
2026-06-09 11:17:34,604 - Both maxima missing → no inflection-point distances calculated: 118,695
2026-06-09 11:17:34,604 - === Distances between minima (all profiles with two detected minima) ===
2026-06-09 11:17:34,605 - Distances between minima mean: 3.539 nm (4.489 vox)
2026-06-09 11:17:34,605 - Distances between minima median: 3.500 nm (4.439 vox)
2026-06-09 11:17:34,606 - Distances between minima range: 0.917-10.083 nm (1.163-12.790 vox)
2026-06-09 11:17:34,606 - === Membrane thickness from inflection points ===
2026-06-09 11:17:34,607 - Inflection-point distances mean: 5.354 nm (6.790 vox)
2026-06-09 11:17:34,607 - Inflection-point distances median: 5.418 nm (6.872 vox)
2026-06-09 11:17:34,607 - Inflection-point distances range: 1.435-8.000 nm (1.820-10.147 vox)
2026-06-09 11:17:34,608 - === Distance shift from matched segmentation points to inflection points ===
2026-06-09 11:17:34,608 - Distance shift mean: -0.858 nm (-1.088 vox)
2026-06-09 11:17:34,609 - Distance shift median: -0.818 nm (-1.037 vox)
2026-06-09 11:17:34,610 -
Saving results to: outputs/WBP
2026-06-09 11:19:49,253 - Saved thickness table: outputs/WBP/TS_0198_200to400_segmented_IMM_vertices_normals_thickness.csv
2026-06-09 11:26:56,898 - Saved intensity profiles: outputs/WBP/TS_0198_200to400_segmented_IMM_vertices_normals_int_profiles.pkl
2026-06-09 11:26:56,913 - Saved statistics: outputs/WBP/TS_0198_200to400_segmented_IMM_vertices_normals_boundary_stats.txt
2026-06-09 11:27:25,419 - Writing pipeline diagnostics to TS_0198_200to400_segmented_OMM_TS_0198_AT_WBP_200to400_analysis.log (under /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/WBP)

  Membrane: OMM
2026-06-09 11:27:25,891 - === Extracting intensity profiles ===
2026-06-09 11:27:25,892 - Tomogram MRC: TS_0198_AT_WBP_200to400.mrc
2026-06-09 11:27:25,893 -           path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/TS_0198_AT_WBP_200to400.mrc
2026-06-09 11:27:25,893 - Segmentation MRC: TS_0198_200to400_segmented.mrc
2026-06-09 11:27:25,894 -             path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/TS_0198_200to400_segmented.mrc
2026-06-09 11:27:25,894 - Matched-point CSV: TS_0198_200to400_segmented_OMM_vertices_normals_matched_points.csv
2026-06-09 11:27:25,895 -              path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/TS_0198_200to400_segmented_OMM_vertices_normals_matched_points.csv
2026-06-09 11:27:25,895 -              rows: 316,175; usable coordinate pairs (non-NaN): 316,175
2026-06-09 11:27:26,217 - Z-score normalizing tomogram before profile sampling...
2026-06-09 11:27:36,762 -
Finding profile minima and inflection points...
2026-06-09 11:29:46,578 - Applying max-distance filter to resolved profiles (8.000 nm)...
2026-06-09 11:29:46,579 - === Intensity profiles summary ===
2026-06-09 11:29:46,580 - Number of intensity profiles resolved: 299,809/316,175 (94.8%)
2026-06-09 11:29:46,580 - Number of profiles after max-distance filter (8.000 nm): 295,219/299,809 (98.5% of resolved; 93.4% overall)
2026-06-09 11:29:46,580 - Number of profiles with thickness calculated as distances between inflection points: 274,884
2026-06-09 11:29:46,581 - Number of profiles where only minima were detected (no inflection points): 20,335
2026-06-09 11:29:46,581 - === Profiles with missing outward maxima ===
2026-06-09 11:29:46,582 - Profiles lacking an outward-maximum on one or both leaflets: 131,141 / 316,175 sampled pairs (41.5%)
2026-06-09 11:29:46,582 - One maximum missing → opposite-side minima-to-maximum distances mirrored: 110,790
2026-06-09 11:29:46,583 - Both maxima missing → no inflection-point distances calculated: 20,351
2026-06-09 11:29:46,584 - === Distances between minima (all profiles with two detected minima) ===
2026-06-09 11:29:46,584 - Distances between minima mean: 3.348 nm (4.247 vox)
2026-06-09 11:29:46,585 - Distances between minima median: 3.143 nm (3.986 vox)
2026-06-09 11:29:46,586 - Distances between minima range: 1.000-9.429 nm (1.268-11.959 vox)
2026-06-09 11:29:46,586 - === Membrane thickness from inflection points ===
2026-06-09 11:29:46,586 - Inflection-point distances mean: 5.236 nm (6.641 vox)
2026-06-09 11:29:46,587 - Inflection-point distances median: 5.238 nm (6.644 vox)
2026-06-09 11:29:46,587 - Inflection-point distances range: 1.571-8.000 nm (1.993-10.147 vox)
2026-06-09 11:29:46,588 - === Distance shift from matched segmentation points to inflection points ===
2026-06-09 11:29:46,588 - Distance shift mean: -0.132 nm (-0.168 vox)
2026-06-09 11:29:46,588 - Distance shift median: -0.133 nm (-0.169 vox)
2026-06-09 11:29:46,588 -
Saving results to: outputs/WBP
2026-06-09 11:30:06,499 - Saved thickness table: outputs/WBP/TS_0198_200to400_segmented_OMM_vertices_normals_thickness.csv
2026-06-09 11:30:49,079 - Saved intensity profiles: outputs/WBP/TS_0198_200to400_segmented_OMM_vertices_normals_int_profiles.pkl
2026-06-09 11:30:49,081 - Saved statistics: outputs/WBP/TS_0198_200to400_segmented_OMM_vertices_normals_boundary_stats.txt
2026-06-09 11:30:50,232 - Writing pipeline diagnostics to TS_0198_200to400_segmented_IMM_TS_0198_AT_WBP_200to400_deconv_analysis.log (under /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/WBP_deconv)

════════════════════════════════════════════════════════════
  Tomogram: WBP_deconv
════════════════════════════════════════════════════════════

  Membrane: IMM
2026-06-09 11:30:53,575 - === Extracting intensity profiles ===
2026-06-09 11:30:53,576 - Tomogram MRC: TS_0198_AT_WBP_200to400_deconv.mrc
2026-06-09 11:30:53,576 -           path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/TS_0198_AT_WBP_200to400_deconv.mrc
2026-06-09 11:30:53,577 - Segmentation MRC: TS_0198_200to400_segmented.mrc
2026-06-09 11:30:53,578 -             path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/TS_0198_200to400_segmented.mrc
2026-06-09 11:30:53,578 - Matched-point CSV: TS_0198_200to400_segmented_IMM_vertices_normals_matched_points.csv
2026-06-09 11:30:53,579 -              path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/TS_0198_200to400_segmented_IMM_vertices_normals_matched_points.csv
2026-06-09 11:30:53,579 -              rows: 1,995,505; usable coordinate pairs (non-NaN): 1,995,505
2026-06-09 11:30:53,933 - Z-score normalizing tomogram before profile sampling...
2026-06-09 11:31:51,956 -
Finding profile minima and inflection points...
2026-06-09 11:45:50,259 - Applying max-distance filter to resolved profiles (8.000 nm)...
2026-06-09 11:45:50,323 - === Intensity profiles summary ===
2026-06-09 11:45:50,324 - Number of intensity profiles resolved: 1,308,750/1,995,505 (65.6%)
2026-06-09 11:45:50,325 - Number of profiles after max-distance filter (8.000 nm): 1,283,561/1,308,750 (98.1% of resolved; 64.3% overall)
2026-06-09 11:45:50,325 - Number of profiles with thickness calculated as distances between inflection points: 695,354
2026-06-09 11:45:50,327 - Number of profiles where only minima were detected (no inflection points): 588,207
2026-06-09 11:45:50,327 - === Profiles with missing outward maxima ===
2026-06-09 11:45:50,328 - Profiles lacking an outward-maximum on one or both leaflets: 1,149,937 / 1,995,505 sampled pairs (57.6%)
2026-06-09 11:45:50,328 - One maximum missing → opposite-side minima-to-maximum distances mirrored: 559,018
2026-06-09 11:45:50,329 - Both maxima missing → no inflection-point distances calculated: 590,919
2026-06-09 11:45:50,330 - === Distances between minima (all profiles with two detected minima) ===
2026-06-09 11:45:50,331 - Distances between minima mean: 3.962 nm (5.025 vox)
2026-06-09 11:45:50,331 - Distances between minima median: 3.826 nm (4.853 vox)
2026-06-09 11:45:50,332 - Distances between minima range: 1.435-10.043 nm (1.820-12.739 vox)
2026-06-09 11:45:50,332 - === Membrane thickness from inflection points ===
2026-06-09 11:45:50,333 - Inflection-point distances mean: 6.072 nm (7.701 vox)
2026-06-09 11:45:50,333 - Inflection-point distances median: 6.011 nm (7.625 vox)
2026-06-09 11:45:50,333 - Inflection-point distances range: 3.143-8.000 nm (3.986-10.147 vox)
2026-06-09 11:45:50,334 - === Distance shift from matched segmentation points to inflection points ===
2026-06-09 11:45:50,334 - Distance shift mean: -0.190 nm (-0.241 vox)
2026-06-09 11:45:50,335 - Distance shift median: -0.273 nm (-0.346 vox)
2026-06-09 11:45:50,336 -
Saving results to: outputs/WBP_deconv
2026-06-09 11:47:52,343 - Saved thickness table: outputs/WBP_deconv/TS_0198_200to400_segmented_IMM_vertices_normals_thickness.csv
2026-06-09 11:51:56,393 - Saved intensity profiles: outputs/WBP_deconv/TS_0198_200to400_segmented_IMM_vertices_normals_int_profiles.pkl
2026-06-09 11:51:56,405 - Saved statistics: outputs/WBP_deconv/TS_0198_200to400_segmented_IMM_vertices_normals_boundary_stats.txt
2026-06-09 11:52:15,288 - Writing pipeline diagnostics to TS_0198_200to400_segmented_OMM_TS_0198_AT_WBP_200to400_deconv_analysis.log (under /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/WBP_deconv)

  Membrane: OMM
2026-06-09 11:52:15,759 - === Extracting intensity profiles ===
2026-06-09 11:52:15,759 - Tomogram MRC: TS_0198_AT_WBP_200to400_deconv.mrc
2026-06-09 11:52:15,760 -           path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/TS_0198_AT_WBP_200to400_deconv.mrc
2026-06-09 11:52:15,760 - Segmentation MRC: TS_0198_200to400_segmented.mrc
2026-06-09 11:52:15,761 -             path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/inputs/TS_0198_200to400_segmented.mrc
2026-06-09 11:52:15,761 - Matched-point CSV: TS_0198_200to400_segmented_OMM_vertices_normals_matched_points.csv
2026-06-09 11:52:15,763 -              path: /Users/desislavaglushkova/code/local_tests/cryocat_refactor/example_notebooks/memthick/outputs/TS_0198_200to400_segmented_OMM_vertices_normals_matched_points.csv
2026-06-09 11:52:15,763 -              rows: 316,175; usable coordinate pairs (non-NaN): 316,175
2026-06-09 11:52:16,094 - Z-score normalizing tomogram before profile sampling...
2026-06-09 11:52:26,564 -
Finding profile minima and inflection points...
2026-06-09 11:54:14,039 - Applying max-distance filter to resolved profiles (8.000 nm)...
2026-06-09 11:54:14,041 - === Intensity profiles summary ===
2026-06-09 11:54:14,041 - Number of intensity profiles resolved: 144,306/316,175 (45.6%)
2026-06-09 11:54:14,042 - Number of profiles after max-distance filter (8.000 nm): 142,256/144,306 (98.6% of resolved; 45.0% overall)
2026-06-09 11:54:14,042 - Number of profiles with thickness calculated as distances between inflection points: 82,230
2026-06-09 11:54:14,043 - Number of profiles where only minima were detected (no inflection points): 60,026
2026-06-09 11:54:14,043 - === Profiles with missing outward maxima ===
2026-06-09 11:54:14,043 - Profiles lacking an outward-maximum on one or both leaflets: 121,523 / 316,175 sampled pairs (38.4%)
2026-06-09 11:54:14,044 - One maximum missing → opposite-side minima-to-maximum distances mirrored: 61,425
2026-06-09 11:54:14,044 - Both maxima missing → no inflection-point distances calculated: 60,098
2026-06-09 11:54:14,045 - === Distances between minima (all profiles with two detected minima) ===
2026-06-09 11:54:14,045 - Distances between minima mean: 3.689 nm (4.680 vox)
2026-06-09 11:54:14,046 - Distances between minima median: 3.500 nm (4.439 vox)
2026-06-09 11:54:14,046 - Distances between minima range: 1.500-9.900 nm (1.903-12.557 vox)
2026-06-09 11:54:14,046 - === Membrane thickness from inflection points ===
2026-06-09 11:54:14,047 - Inflection-point distances mean: 5.855 nm (7.427 vox)
2026-06-09 11:54:14,047 - Inflection-point distances median: 5.762 nm (7.308 vox)
2026-06-09 11:54:14,047 - Inflection-point distances range: 3.143-8.000 nm (3.986-10.147 vox)
2026-06-09 11:54:14,048 - === Distance shift from matched segmentation points to inflection points ===
2026-06-09 11:54:14,048 - Distance shift mean: 0.367 nm (0.466 vox)
2026-06-09 11:54:14,048 - Distance shift median: 0.241 nm (0.305 vox)
2026-06-09 11:54:14,049 -
Saving results to: outputs/WBP_deconv
2026-06-09 11:54:23,184 - Saved thickness table: outputs/WBP_deconv/TS_0198_200to400_segmented_OMM_vertices_normals_thickness.csv
2026-06-09 11:54:42,659 - Saved intensity profiles: outputs/WBP_deconv/TS_0198_200to400_segmented_OMM_vertices_normals_int_profiles.pkl
2026-06-09 11:54:42,662 - Saved statistics: outputs/WBP_deconv/TS_0198_200to400_segmented_OMM_vertices_normals_boundary_stats.txt

Load results#

load_membrane_data reads the thickness CSV and, when auto_discover_related_files=True, automatically locates the matching *_int_profiles.pkl in the same folder.

analyze_membrane_thickness computes summary statistics (mean, median, std, percentiles).
analyze_intensity_profiles summarises the extracted profiles without re-reading the tomogram.
What we generally observe across the three tomograms:
- WBP_deconv resolves substantially fewer profiles than cryoCARE or WBP. Deconvolution can distort the profile shape enough that the minima or outward maxima are no longer reliably detected. Probably not the best idea to use as input tomogram. - WBP has a noticeably lower signal-to-noise ratio (SNR) than the other two methods. The mean intensity profiles look noisier, and because individual profiles are noisy, the inflection-point detection tends to place the membrane boundaries closer to the minima — resulting in systematically lower membrane_thickness_nm values compared to cryoCARE. The min–min distance plots are a useful here, as they are a bit less sensitive to the outward maximum detection.
- cryoCARE-denoised has profiles with higher SNR and quite consistent inflection-point detection (notice that it agrees well with the segmentation boundaries in most cases).
[8]:
membrane_data     = {t: {} for t in tomo_names}
thickness_results = {t: {} for t in tomo_names}
profile_results   = {t: {} for t in tomo_names}

for tomo in tomo_names:
    print(f"\n{'═'*60}")
    print(f"  {tomo}")
    print(f"{'═'*60}")
    for m in membrane_names:
        csv = thickness_csv(tomo, m)
        if not csv.exists():
            print(f"  {m}: CSV not found — skipping ({csv})")
            continue

        md = maple.load_membrane_data(
            thickness_csv=csv,
            auto_discover_related_files=True,
        )
        tr = maple.analyze_membrane_thickness(data=md)
        pr = maple.analyze_intensity_profiles(data=md)

        membrane_data[tomo][m]     = md
        thickness_results[tomo][m] = tr
        profile_results[tomo][m]   = pr

        s = tr.statistics
        n_fin = s.get("count_finite", s["count"])
        print(f"\n  {m} — {len(md.intensity_profiles):,} profiles")
        print(f"    Thickness (nm)  n_finite={n_fin:,}")
        print(f"      mean ± std : {s['mean']:.3f} ± {s['std']:.3f}")
        print(f"      median     : {s['median']:.3f}")
        print(f"      IQR        : {s['q25']:.3f} – {s['q75']:.3f}")

════════════════════════════════════════════════════════════
  cryoCARE
════════════════════════════════════════════════════════════
Loaded 1787756 intensity profiles from outputs/cryoCARE/TS_0198_200to400_segmented_IMM_vertices_normals_int_profiles.pkl
Analyzing 1787756 intensity profiles...

  IMM — 1,787,756 profiles
    Thickness (nm)  n_finite=1,542,047
      mean ± std : 6.080 ± 0.623
      median     : 6.025
      IQR        : 5.720 – 6.417
Loaded 267934 intensity profiles from outputs/cryoCARE/TS_0198_200to400_segmented_OMM_vertices_normals_int_profiles.pkl
Analyzing 267934 intensity profiles...

  OMM — 267,934 profiles
    Thickness (nm)  n_finite=247,322
      mean ± std : 5.753 ± 0.640
      median     : 5.698
      IQR        : 5.321 – 6.050

════════════════════════════════════════════════════════════
  WBP
════════════════════════════════════════════════════════════
Loaded 1861192 intensity profiles from outputs/WBP/TS_0198_200to400_segmented_IMM_vertices_normals_int_profiles.pkl
Analyzing 1861192 intensity profiles...

  IMM — 1,861,192 profiles
    Thickness (nm)  n_finite=1,742,993
      mean ± std : 5.354 ± 1.206
      median     : 5.418
      IQR        : 4.500 – 6.217
Loaded 295219 intensity profiles from outputs/WBP/TS_0198_200to400_segmented_OMM_vertices_normals_int_profiles.pkl
Analyzing 295219 intensity profiles...

  OMM — 295,219 profiles
    Thickness (nm)  n_finite=274,884
      mean ± std : 5.236 ± 1.196
      median     : 5.238
      IQR        : 4.400 – 6.052

════════════════════════════════════════════════════════════
  WBP_deconv
════════════════════════════════════════════════════════════
Loaded 1283561 intensity profiles from outputs/WBP_deconv/TS_0198_200to400_segmented_IMM_vertices_normals_int_profiles.pkl
Analyzing 1283561 intensity profiles...

  IMM — 1,283,561 profiles
    Thickness (nm)  n_finite=695,354
      mean ± std : 6.072 ± 0.743
      median     : 6.011
      IQR        : 5.549 – 6.537
Loaded 142256 intensity profiles from outputs/WBP_deconv/TS_0198_200to400_segmented_OMM_vertices_normals_int_profiles.pkl
Analyzing 142256 intensity profiles...

  OMM — 142,256 profiles
    Thickness (nm)  n_finite=82,230
      mean ± std : 5.855 ± 0.779
      median     : 5.762
      IQR        : 5.309 – 6.289

Boundary mode summary#

The detection_mode column records how each membrane boundary was detected:

Mode

Description

membrane_thickness_nm

max_max

Both outward maxima found as real peaks

✓

max_anchor / anchor_max

One real max, other side inferred by mirroring

✓

minima_only

No outward maxima found; only min–min distance reported

NaN

[9]:
print(f"{'Tomo':<12} {'Membrane':<9} {'Inflection points':>16} {'max_max':>16} {'max_anchor':>16} {'minima_only':>16} {'Resolved':>16}")
print("-" * 120)
for tomo in tomo_names:
    for m in membrane_names:
        if m not in membrane_data[tomo]:
            continue
        info = maple.return_boundary_info(membrane_data[tomo][m])
        n_tot = info["n_measurements"]
        n_inf = info["n_finite_inflection_thickness_nm"]
        n_mm  = info["by_detection_mode"].get("max_max", 0)
        n_ma  = info["by_detection_mode"].get("max_anchor", 0)
        n_mo  = info["by_detection_mode"].get("minima_only", 0)
        n_res = info["n_resolved"]
        pct   = lambda n: f"{n / n_tot:.1%}" if n_tot > 0 else "?"
        print(
            f"{tomo:<14} {m:<8} "
            f"{n_inf:>8,} ({pct(n_inf)}) "
            f"{n_mm:>6,} ({pct(n_mm)}) "
            f"{n_ma:>6,} ({pct(n_ma)}) "
            f"{n_mo:>6,} ({pct(n_mo)}) "
            f"{n_res:>6,}"
        )
Tomo         Membrane  Inflection points          max_max       max_anchor      minima_only         Resolved
------------------------------------------------------------------------------------------------------------------------
cryoCARE       IMM      1,542,047 (86.3%) 933,928 (52.2%) 608,119 (34.0%) 245,709 (13.7%) 1,787,756
cryoCARE       OMM       247,322 (92.3%) 181,695 (67.8%) 65,627 (24.5%) 20,612 (7.7%) 267,934
WBP            IMM      1,742,993 (93.6%) 1,059,038 (56.9%) 683,955 (36.7%) 118,199 (6.4%) 1,861,192
WBP            OMM       274,884 (93.1%) 176,193 (59.7%) 98,691 (33.4%) 20,335 (6.9%) 295,219
WBP_deconv     IMM       695,354 (54.2%) 158,897 (12.4%) 536,457 (41.8%) 588,207 (45.8%) 1,283,561
WBP_deconv     OMM        82,230 (57.8%) 22,794 (16.0%) 59,436 (41.8%) 60,026 (42.2%) 142,256

Thickness distributions: IMM vs OMM#

Notice that irrespective of the exact membrane thickness values, IMM is consistently thicker than OMM across the three tomograms.

[10]:
thickness_range_nm = (3.0, 8.0)

for tomo in tomo_names:
    print(f"\n{'═'*60}")
    print(f"  {tomo}")
    print(f"{'═'*60}")

    results_list = [
        thickness_results[tomo][m]
        for m in membrane_names
        if m in thickness_results[tomo]
    ]
    names_list = [m for m in membrane_names if m in thickness_results[tomo]]
    if not results_list:
        continue
    fig = maple.plot_thickness_distribution(
        results_list,
        membrane_names=names_list,
        thickness_range_nm=thickness_range_nm,
        histogram_bins=50,
        density_normalization=True,
        show_mean_lines=True,
        show_statistics=True,
        opacity=0.6,
        plot_title=f"{tomo} — IMM vs OMM membrane thickness",
    )
    #fig.show()
    #fname = plots_path / f"{tomo}_IMM_vs_OMM_thickness.html"
    fname = plots_path / f"{tomo}_IMM_vs_OMM_thickness.png"
    #fig.write_html(str(fname), include_plotlyjs="cdn")
    fig.write_image(str(fname), width=850, height=600)


════════════════════════════════════════════════════════════
  cryoCARE
════════════════════════════════════════════════════════════

Statistics for IMM:
  Mean:   6.08
  Std:    0.62
  Median: 6.02
  Count:  1,541,945

Statistics for OMM:
  Mean:   5.75
  Std:    0.64
  Median: 5.70
  Count:  247,308

════════════════════════════════════════════════════════════
  WBP
════════════════════════════════════════════════════════════

Statistics for IMM:
  Mean:   5.42
  Std:    1.14
  Median: 5.50
  Count:  1,696,906

Statistics for OMM:
  Mean:   5.30
  Std:    1.14
  Median: 5.24
  Count:  268,169

════════════════════════════════════════════════════════════
  WBP_deconv
════════════════════════════════════════════════════════════

Statistics for IMM:
  Mean:   6.07
  Std:    0.74
  Median: 6.01
  Count:  695,354

Statistics for OMM:
  Mean:   5.86
  Std:    0.78
  Median: 5.76
  Count:  82,230
[11]:
#IFrame(str(plots_path / f"{tomo_names[0]}_IMM_vs_OMM_thickness.html"), width=850, height=600)
Image(str(plots_path / f"{tomo_names[0]}_IMM_vs_OMM_thickness.png"), width=850, height=600)
[11]:
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_19_0.png
[12]:
#IFrame(str(plots_path / f"{tomo_names[1]}_IMM_vs_OMM_thickness.html"), width=850, height=600)
Image(str(plots_path / f"{tomo_names[1]}_IMM_vs_OMM_thickness.png"), width=850, height=600)
[12]:
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_20_0.png
[13]:
#IFrame(str(plots_path / f"{tomo_names[2]}_IMM_vs_OMM_thickness.html"), width=850, height=600)
Image(str(plots_path / f"{tomo_names[2]}_IMM_vs_OMM_thickness.png"), width=850, height=600)
[13]:
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_21_0.png

IMM: comparison across tomograms#

[14]:
# IMM membrane thickness based on inflection points
fig = maple.plot_thickness_distribution(
    [thickness_results[t]["IMM"] for t in tomo_names if "IMM" in thickness_results[t]],
    membrane_names=[t for t in tomo_names if "IMM" in thickness_results[t]],
    thickness_range_nm=thickness_range_nm,
    histogram_bins=50,
    density_normalization=True,
    show_mean_lines=True,
    show_statistics=True,
    opacity=0.6,
    plot_title="IMM — membrane thickness across preprocessing methods",
)
#fig.show()
#fname = plots_path / "IMM_thickness_across_tomos.html"
fname = plots_path / "IMM_thickness_across_tomos.png"
#fig.write_html(str(fname), include_plotlyjs="cdn")
fig.write_image(str(fname), width=850, height=600)

Statistics for cryoCARE:
  Mean:   6.08
  Std:    0.62
  Median: 6.02
  Count:  1,541,945

Statistics for WBP:
  Mean:   5.42
  Std:    1.14
  Median: 5.50
  Count:  1,696,906

Statistics for WBP_deconv:
  Mean:   6.07
  Std:    0.74
  Median: 6.01
  Count:  695,354
[15]:
#IFrame(str(fname), width=850, height=600)
Image(str(fname), width=850, height=600)
[15]:
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_24_0.png
[16]:
# IMM min–min distances
first_tomo = next(t for t in tomo_names if "IMM" in profile_results[t])
ps_imm = profile_results[first_tomo]["IMM"].parameters.get("pixel_size_nm")

fig = maple.plot_min_to_min_distribution(
    [thickness_results[t]["IMM"] for t in tomo_names if "IMM" in thickness_results[t]],
    membrane_names=[t for t in tomo_names if "IMM" in thickness_results[t]],
    minima_separation_range_nm=(2.0, 6.0),
    histogram_bins=30,
    density_normalization=True,
    show_mean_lines=True,
    show_statistics=True,
    opacity=0.6,
    pixel_size_nm=ps_imm,
    plot_title="IMM — min–min distance across preprocessing methods",
)
#fig.show()
#fname = plots_path / "IMM_min_min_across_tomos.html"
fname = plots_path / "IMM_min_min_across_tomos.png"
#fig.write_html(str(fname), include_plotlyjs="cdn")
fig.write_image(str(fname), width=850, height=600)

Statistics for cryoCARE:
  Mean:   3.99
  Std:    0.57
  Median: 4.00
  Count:  1,747,962

Statistics for WBP:
  Mean:   3.58
  Std:    0.88
  Median: 3.50
  Count:  1,713,134

Statistics for WBP_deconv:
  Mean:   3.82
  Std:    0.72
  Median: 3.83
  Count:  1,233,162
[17]:
#IFrame(str(fname), width=850, height=600)
Image(str(fname), width=850, height=600)
[17]:
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_26_0.png

OMM: comparison across tomograms#

[18]:
# OMM membrane thickness based on inflection points
fig = maple.plot_thickness_distribution(
    [thickness_results[t]["OMM"] for t in tomo_names if "OMM" in thickness_results[t]],
    membrane_names=[t for t in tomo_names if "OMM" in thickness_results[t]],
    thickness_range_nm=thickness_range_nm,
    histogram_bins=50,
    density_normalization=True,
    show_mean_lines=True,
    show_statistics=True,
    opacity=0.6,
    plot_title="OMM — membrane thickness across preprocessing methods",
)
#fig.show()
fname = plots_path / "OMM_thickness_across_tomos.png"
#fig.write_html(str(fname), include_plotlyjs="cdn")
fig.write_image(str(fname), width=850, height=600)

Statistics for cryoCARE:
  Mean:   5.75
  Std:    0.64
  Median: 5.70
  Count:  247,308

Statistics for WBP:
  Mean:   5.30
  Std:    1.14
  Median: 5.24
  Count:  268,169

Statistics for WBP_deconv:
  Mean:   5.86
  Std:    0.78
  Median: 5.76
  Count:  82,230
[19]:
#IFrame(str(fname), width=850, height=600)
Image(str(fname), width=850, height=600)
[19]:
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_29_0.png
[20]:
# OMM min–min distances
first_tomo_omm = next(t for t in tomo_names if "OMM" in profile_results[t])
ps_omm = profile_results[first_tomo_omm]["OMM"].parameters.get("pixel_size_nm")

fig = maple.plot_min_to_min_distribution(
    [thickness_results[t]["OMM"] for t in tomo_names if "OMM" in thickness_results[t]],
    membrane_names=[t for t in tomo_names if "OMM" in thickness_results[t]],
    minima_separation_range_nm=(2.0, 6.0),
    histogram_bins=30,
    density_normalization=True,
    show_mean_lines=True,
    show_statistics=True,
    opacity=0.6,
    pixel_size_nm=ps_omm,
    plot_title="OMM — min–min distance across preprocessing methods",
)
#fig.show()
fname = plots_path / "OMM_min_min_across_tomos.png"
#fig.write_html(str(fname), include_plotlyjs="cdn")
fig.write_image(str(fname), width=850, height=600)

Statistics for cryoCARE:
  Mean:   3.66
  Std:    0.62
  Median: 3.67
  Count:  265,082

Statistics for WBP:
  Mean:   3.40
  Std:    0.90
  Median: 3.14
  Count:  274,371

Statistics for WBP_deconv:
  Mean:   3.60
  Std:    0.80
  Median: 3.50
  Count:  138,869
[21]:
#IFrame(str(fname), width=850, height=600)
Image(str(fname), width=850, height=600)
[21]:
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_31_0.png

Intensity profile summaries#

plot_intensity_profile_summary stacks all stored profiles, interpolates them onto a common distance axis, and shows the mean intensity with a 25th–75th percentile band.

Vertical markers: - Gray dashed — mean positions of the geometrically matched segmentation surface points (mean match_distance_nm). - Green / red dashed — mean left and right inflection points (mean membrane_thickness_nm). - Dark grey solid — mean geometric midpoint between the two minima.

[22]:
for membrane in membrane_names:
    for tomo in tomo_names:
        if membrane not in membrane_data[tomo]:
            continue
        fig = maple.plot_intensity_profile_summary(
            membrane_data[tomo][membrane],
            membrane_names=[f"{membrane} ({tomo})"],
            show_segmentation_boundary_markers=True,
            show_segmentation_boundary_distributions=True,
            show_inflection_point_markers=True,
            show_inflection_point_distributions=True,
            show_minima_midpoint=True,
            plot_title=f"{membrane} — mean intensity profiles ({tomo})",
        )
        #fig.show()
        #fname = plots_path / f"{membrane}_{tomo}_profiles_all.html"
        fname = plots_path / f"{membrane}_{tomo}_profiles_all.png"
        #fig.write_html(str(fname), include_plotlyjs="cdn")
        fig.write_image(str(fname), width=850, height=600)
[23]:
# IMM
for tomo in tomo_names:
    print(f"IMM all profiles— {tomo}")
    display(Image(str(plots_path / f"IMM_{tomo}_profiles_all.png"), width=850, height=550))
IMM all profiles— cryoCARE
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_34_1.png
IMM all profiles— WBP
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_34_3.png
IMM all profiles— WBP_deconv
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_34_5.png
[21]:
# OMM
for tomo in tomo_names:
    print(f"OMM all profiles— {tomo}")
    #display(IFrame(str(plots_path / f"OMM_{tomo}_profiles_all.html"), width=850, height=550))
    display(Image(str(plots_path / f"OMM_{tomo}_profiles_all.png"), width=850, height=550))
OMM all profiles— cryoCARE
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_35_1.png
OMM all profiles— WBP
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_35_3.png
OMM all profiles— WBP_deconv
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_35_5.png

Profiles: two outward maxima#

Filtering to thickness_regime="max_max" keeps only profiles where both outward maxima were detected as genuine peaks. Both inflection-point boundaries are independently determined, making this the highest-confidence subset.

[24]:
for membrane in membrane_names:
    for tomo in tomo_names:
        if membrane not in membrane_data[tomo]:
            continue
        try:
            fig = maple.plot_intensity_profile_summary(
                membrane_data[tomo][membrane],
                membrane_names=[f"{membrane} ({tomo})"],
                thickness_regime="max_max",
                show_segmentation_boundary_markers=True,
                show_segmentation_boundary_distributions=True,
                show_inflection_point_markers=True,
                show_inflection_point_distributions=True,
                show_minima_midpoint=True,
                plot_title=f"{membrane} — two outward maxima profiles ({tomo})",
            )
            #fname = plots_path / f"{membrane}_{tomo}_profiles_max_max.html"
            fname = plots_path / f"{membrane}_{tomo}_profiles_max_max.png"
            #fig.write_html(str(fname), include_plotlyjs="cdn")
            fig.write_image(str(fname), width=850, height=600)
            #fig.show()
        except ValueError as e:
            print(f"  {tomo}: skipped — {e}")
[25]:
# IMM
for tomo in tomo_names:
    fname = plots_path / f"IMM_{tomo}_profiles_max_max.png"
    if fname.exists():
        print(f"IMM two outward maxima profiles— {tomo}")
        display(Image(str(fname), width=850, height=550))
IMM two outward maxima profiles— cryoCARE
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_38_1.png
IMM two outward maxima profiles— WBP
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_38_3.png
IMM two outward maxima profiles— WBP_deconv
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_38_5.png
[26]:
# OMM
for tomo in tomo_names:
    fname = plots_path / f"OMM_{tomo}_profiles_max_max.png"
    if fname.exists():
        print(f"OMM two outward maxima profiles— {tomo}")
        display(Image(str(fname), width=850, height=550))
OMM two outward maxima profiles— cryoCARE
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_39_1.png
OMM two outward maxima profiles— WBP
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_39_3.png
OMM two outward maxima profiles— WBP_deconv
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_39_5.png

Profiles: mirrored maximum#

Filtering to thickness_regime="max_anchor" keeps profiles where one boundary was found from a real outward maximum and the other was inferred by mirroring the good side’s max-to-min span. See the IntensityProfileAnalyzer documentation in Membrane thickness: run for details on how the mirroring works.

[27]:
for membrane in membrane_names:
    for tomo in tomo_names:
        if membrane not in membrane_data[tomo]:
            continue
        try:
            fig = maple.plot_intensity_profile_summary(
                membrane_data[tomo][membrane],
                membrane_names=[f"{membrane} ({tomo})"],
                thickness_regime="max_anchor",
                show_segmentation_boundary_markers=True,
                show_segmentation_boundary_distributions=True,
                show_inflection_point_markers=True,
                show_inflection_point_distributions=True,
                show_minima_midpoint=True,
                plot_title=f"{membrane} — mirrored max profiles ({tomo})",
            )
            #fname = plots_path / f"{membrane}_{tomo}_profiles_max_anchor.html"
            fname = plots_path / f"{membrane}_{tomo}_profiles_max_anchor.png"
            #fig.write_html(str(fname), include_plotlyjs="cdn")
            fig.write_image(str(fname), width=850, height=600)
            #fig.show()
        except ValueError as e:
            print(f"  {tomo}: skipped — {e}")
[28]:
# IMM
for tomo in tomo_names:
    fname = plots_path / f"IMM_{tomo}_profiles_max_anchor.png"
    if fname.exists():
        print(f"IMM mirrored max profiles— {tomo}")
        display(Image(str(fname), width=850, height=550))
IMM mirrored max profiles— cryoCARE
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_42_1.png
IMM mirrored max profiles— WBP
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_42_3.png
IMM mirrored max profiles— WBP_deconv
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_42_5.png
[29]:
# OMM
for tomo in tomo_names:
    fname = plots_path / f"OMM_{tomo}_profiles_max_anchor.png"
    if fname.exists():
        print(f"OMM mirrored max profiles— {tomo}")
        display(Image(str(fname), width=850, height=550))
OMM mirrored max profiles— cryoCARE
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_43_1.png
OMM mirrored max profiles— WBP
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_43_3.png
OMM mirrored max profiles— WBP_deconv
../../_images/tutorials_membrane_thickness_memthick_tomo_preprocessing_43_5.png