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#
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 |
|---|---|---|
|
Deep-learning denoising (cryoCARE) |
|
|
Weighted back-projection |
|
|
WBP with Wiener-like CTF deconvolution |
|
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:
Step 1: surface extraction — run once on the segmentation, independent of the tomogram.
Step 2: surface point matching — run once, pairing points on opposing segmentation surfaces based on geometry alone.
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 |
|---|---|---|
|
1 |
|
|
2 |
|
Steps 1 & 2 (geometry)#
Parameter |
Value |
Notes |
|---|---|---|
|
|
Gaussian smoothing of the segmentation before marching cubes meshing |
|
|
Neighbour-based normal averaging |
|
|
Neighbourhood radius for normal refinement (vox) |
|
|
Hard cap on matched-point distance |
|
|
Cone half-angle for matching |
|
|
Sub-voxel coordinates for accurate profile centering |
Step 3 (intensity profiles)#
Parameter |
Value |
Notes |
|---|---|---|
|
|
Half-width of sampled profile on each side of the midpoint |
|
|
Rows exceeding this inflection thickness are excluded from the CSV |
|
|
Gaussian smoothing along the profile axis |
|
|
Search window radii around matched surface points |
|
|
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 |
|---|---|
|
|
|
|
|
|
[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 lowermembrane_thickness_nmvalues 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 |
|
|---|---|---|
|
Both outward maxima found as real peaks |
✓ |
|
One real max, other side inferred by mirroring |
✓ |
|
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]:
[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]:
[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]:
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]:
[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]:
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]:
[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]:
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
IMM all profiles— WBP
IMM all profiles— WBP_deconv
[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
OMM all profiles— WBP
OMM all profiles— WBP_deconv
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
IMM two outward maxima profiles— WBP
IMM two outward maxima profiles— WBP_deconv
[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
OMM two outward maxima profiles— WBP
OMM two outward maxima profiles— WBP_deconv
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
IMM mirrored max profiles— WBP
IMM mirrored max profiles— WBP_deconv
[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
OMM mirrored max profiles— WBP
OMM mirrored max profiles— WBP_deconv