11. Electron Tomography#

11.1. Introduction#

Single-particle analysis (see Section 10) achieves near-atomic resolution by averaging thousands of identical copies of a purified molecule. But much of biology is irreducibly unique: the arrangement of organelles in a single cell, the architecture of a synaptic vesicle cluster, the organisation of chromatin in a nucleus. These structures cannot be averaged across many copies because each instance is different. Electron tomography (cryo-ET) addresses this by reconstructing a three-dimensional volume from a single specimen through a series of images collected at different tilt angles.

The reconstruction problem in tomography — recovering a 3D volume from a set of 2D projections — is one of the oldest in imaging science, predating cryo-EM entirely. The mathematical basis (the Radon transform and its inverse) was established by Radon in 1917. The same principles underlie medical CT scanning. In electron tomography, however, the dose constraints of radiation-sensitive biological specimens make the problem fundamentally harder: the total electron dose must be spread over all tilt angles, limiting the signal per image to much less than in a single cryo-EM micrograph.

In the below video, we explain why a unique object requires a different approach than single-particle analysis, how a tilt series is assembled into a sinogram and back projected into a three-dimensional reconstruction, and how tilt range, tilt increment, the missing wedge and weighted back projection determine the quality of that reconstruction.



11.2. Tilt Series Acquisition#

A tilt series is collected by systematically tilting the specimen stage in the TEM to angles ranging from approximately \(-60°\) to \(+60°\) in steps of \(2°–3°\), collecting a micrograph at each tilt. This yields 40–60 images per tilt series. The total electron dose is distributed across all tilts — typically 1–3 e⁻/Ų per tilt image — so that the cumulative dose stays within ~100 e⁻/Ų total.

Several practical challenges distinguish cryo-ET from SPA data collection:

Dose fractionation: To stay within radiation damage limits, each tilt image receives only a fraction of the total dose. Images are therefore even noisier than individual SPA frames.

Mechanical hysteresis: Tilting the stage causes small translations and defocus changes that must be corrected during alignment of the tilt series.

Cosine-law thickening: At high tilt angles, the effective specimen thickness seen by the beam increases as \(1/\cos\theta\). At \(60°\), the effective thickness is doubled, increasing inelastic scattering and reducing image quality. Most practical tilt series are therefore limited to \(\pm 60°\)\(70°\).

The interactive below demonstrates the key data acquisition step: what the electron beam “sees” as the specimen is tilted. We use a 2D test image containing three objects representative of common cellular features — a ring (cross-section of a microtubule or other tube), a line (a membrane sheet), and a dot (a globular protein complex). As the tilt angle changes, the projection profile (what the detector records as a 1D line sum) changes dramatically.

Interactive element

Click Live Code to activate, expand the Show code toggle, and click ▶ to run. Drag the tilt angle slider and observe how the 1D projection profile changes as the specimen tilts. Notice that the ring and the dot are easily distinguished at some angles but merged at others.

Hide code cell source

import io
import numpy as np
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt
from scipy.ndimage import rotate as nd_rotate
from ipywidgets import IntSlider, VBox, Output
from IPython.display import display, Image, clear_output

def _tomo_test(n=100):
    y, x = np.mgrid[0:n, 0:n].astype(float)
    img = np.zeros((n, n))
    cx1, cy1 = n*0.30, n*0.32
    R1 = np.sqrt((x - cx1)**2 + (y - cy1)**2)
    img += 0.9 * ((R1 > n*0.11) & (R1 < n*0.18)).astype(float)
    cy2 = n*0.67
    img += 0.85 * (np.abs(y - cy2) < n*0.025).astype(float)
    cx3, cy3 = n*0.70, n*0.36
    R3 = np.sqrt((x - cx3)**2 + (y - cy3)**2)
    img += 0.75 * (R3 < n*0.07).astype(float)
    return np.clip(img, 0, 1)

_tilt_img = _tomo_test(100)

def draw_tilt(angle_deg=0):
    rotated = nd_rotate(_tilt_img, -angle_deg, reshape=False, order=1)
    proj = rotated.sum(axis=0)
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.5),
                                    gridspec_kw={'width_ratios': [1, 1]})
    ax1.imshow(rotated, cmap='gray')
    for yp in range(0, 100, 7):
        ax1.axhline(yp, color='#4fc3f7', alpha=0.15, linewidth=0.7)
    ax1.set_title(f"Specimen at tilt {angle_deg:+d}°\n(horizontal lines = electron beam paths)", fontsize=9)
    ax1.axis('off')
    ax2.plot(proj, color='#e74c3c', linewidth=2)
    ax2.fill_between(range(100), proj, alpha=0.3, color='#e74c3c')
    ax2.set_xlabel("Detector position (pixels)", fontsize=9)
    ax2.set_ylabel("Integrated electron intensity", fontsize=9)
    ax2.set_title(f"1D projection profile at {angle_deg:+d}°", fontsize=9)
    ax2.set_ylim(0, proj.max()*1.3); ax2.grid(alpha=0.3)
    fig.tight_layout()
    buf = io.BytesIO(); fig.savefig(buf, format='png', dpi=96); buf.seek(0)
    display(Image(data=buf.read())); plt.close(fig)

tilt_sl = IntSlider(value=0, min=-75, max=75, step=3,
                    description="Tilt angle (°)",
                    style={"description_width": "120px"},
                    layout={"width": "420px"})
out_tilt = Output()
def update_tilt(_=None):
    with out_tilt:
        clear_output(wait=True)
        draw_tilt(tilt_sl.value)
tilt_sl.observe(update_tilt, names='value')
display(VBox([tilt_sl, out_tilt]))
update_tilt()

11.3. The Projection Theorem and the Fourier Slice Theorem#

The mathematical foundation of tomographic reconstruction is the projection theorem (also called the central slice theorem or Fourier slice theorem). It states [1]:

The 1D Fourier transform of a projection of a 2D object (or equivalently, the 2D Fourier transform of a projection of a 3D object) is equal to a central slice through the 2D (or 3D) Fourier transform of that object, perpendicular to the projection direction.

Formally, let \(f(x, y)\) be a 2D density function and \(P_\theta(s)\) be its 1D projection at angle \(\theta\):

(11.1)#\[ P_\theta(s) = \int_{-\infty}^{\infty} f(s\cos\theta - t\sin\theta,\; s\sin\theta + t\cos\theta)\, dt \]

The 1D Fourier transform of \(P_\theta\) satisfies:

(11.2)#\[ \hat{P}_\theta(\nu) = \hat{F}(\nu\cos\theta,\; \nu\sin\theta) \]

where \(\hat{F}\) is the 2D Fourier transform of \(f\). This is the Fourier slice theorem: every projection fills in one central line through 2D Fourier space. Collecting projections from many different angles fills in 2D Fourier space, and an inverse 2D Fourier transform then recovers the 2D density.

The interactive demonstration below makes this theorem concrete. For each tilt angle, you can see:

  1. The 2D test image tilted to that angle, and the resulting 1D projection profile

  2. The 2D Fourier transform of the image, with the corresponding line (central slice) highlighted in red

  3. A direct comparison showing that the magnitude of the FT of the projection matches the extracted slice from the 2D FT — the mathematical statement of the theorem

Interactive element

Click Live Code to activate, expand the Show code toggle, and click ▶ to run. Drag the angle slider and watch the red line rotate through the 2D Fourier space. The last panel verifies the theorem: the FT of the 1D projection (red) should match the amplitude extracted along that slice of the 2D FT (blue dashed).

Hide code cell source

import io
import numpy as np
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt
from scipy.ndimage import rotate as nd_rotate, map_coordinates
from ipywidgets import IntSlider, VBox, Output
from IPython.display import display, Image, clear_output

def _tomo_t(n=80):
    y, x = np.mgrid[0:n, 0:n].astype(float)
    img = np.zeros((n, n))
    cx1, cy1 = n*0.30, n*0.32
    R1 = np.sqrt((x - cx1)**2 + (y - cy1)**2)
    img += 0.9 * ((R1 > n*0.11) & (R1 < n*0.18)).astype(float)
    cy2 = n*0.67
    img += 0.85 * (np.abs(y - cy2) < n*0.025).astype(float)
    cx3, cy3 = n*0.70, n*0.36
    R3 = np.sqrt((x - cx3)**2 + (y - cy3)**2)
    img += 0.75 * (R3 < n*0.07).astype(float)
    return np.clip(img, 0, 1)

_fst_i = _tomo_t(80)
_nf = 80
_fst_FT2 = np.abs(np.fft.fftshift(np.fft.fft2(_fst_i)))
_fst_log2 = np.log1p(_fst_FT2)

def draw_fst(angle_deg=0):
    n = _nf; cx = cy = n // 2
    theta = np.deg2rad(angle_deg)
    rot = nd_rotate(_fst_i, -angle_deg, reshape=False, order=1)
    proj = rot.sum(axis=0)
    proj_ft = np.fft.fftshift(np.abs(np.fft.fft(proj)))
    t = np.linspace(-n//2, n//2, n)
    kx = (cx + t * np.cos(theta)).clip(0, n-1)
    ky = (cy + t * np.sin(theta)).clip(0, n-1)
    slice_mag = map_coordinates(_fst_FT2, [ky, kx], order=1, mode='constant')

    fig, axes = plt.subplots(1, 4, figsize=(16, 4.2))

    axes[0].imshow(_fst_i, cmap='gray'); axes[0].axis('off')
    axes[0].set_title("Test image\n(ring, line, dot)", fontsize=9)

    axes[1].imshow(rot, cmap='gray'); axes[1].axis('off')
    r_half = n // 2
    axes[1].axhline(r_half, color='red', alpha=0.6, linewidth=1.5, linestyle='--')
    axes[1].set_title(f"Image at {angle_deg}°\n(red: projection direction)", fontsize=9)

    axes[2].imshow(_fst_log2, cmap='inferno')
    r = n//2 - 3
    axes[2].plot([cx - r*np.cos(theta), cx + r*np.cos(theta)],
                 [cy - r*np.sin(theta), cy + r*np.sin(theta)],
                 'r-', linewidth=2.5)
    axes[2].axis('off')
    axes[2].set_title(f"2D Fourier space\n(red = slice at {angle_deg}°)", fontsize=9)

    p = proj_ft / (proj_ft.max() + 1e-10)
    s = slice_mag / (slice_mag.max() + 1e-10)
    axes[3].plot(p, 'r-', linewidth=1.8, label='|FT(projection)|')
    axes[3].plot(s, 'b--', linewidth=1.8, label='|slice through 2D FT|')
    axes[3].legend(fontsize=8); axes[3].set_yticks([])
    axes[3].set_xlabel("Frequency (a.u.)", fontsize=8)
    axes[3].set_title("Fourier Slice Theorem\n(curves should match)", fontsize=9)
    axes[3].grid(alpha=0.3)

    fig.suptitle(f"Fourier Slice Theorem — angle {angle_deg}°", fontsize=10)
    fig.tight_layout()
    buf = io.BytesIO(); fig.savefig(buf, format='png', dpi=96); buf.seek(0)
    display(Image(data=buf.read())); plt.close(fig)

fst_sl = IntSlider(value=0, min=0, max=175, step=5,
                   description="Tilt angle (°)",
                   style={"description_width": "120px"},
                   layout={"width": "420px"})
out_fst = Output()
def update_fst(_=None):
    with out_fst:
        clear_output(wait=True)
        draw_fst(fst_sl.value)
fst_sl.observe(update_fst, names='value')
display(VBox([fst_sl, out_fst]))
update_fst()

11.4. Reconstruction: Backprojection#

Given a set of 1D projections (the tilt series), how do we recover the 2D image? The simplest approach is direct backprojection: for each projection, “smear” the 1D signal back along the projection direction into a 2D slab, then accumulate all such slabs:

(11.3)#\[ f_\text{BP}(x, y) = \int_0^\pi P_\theta(x\cos\theta + y\sin\theta)\, d\theta \]

While intuitively appealing, direct backprojection overweights low spatial frequencies, producing reconstructions with a characteristic blurriness. This is because each projection contributes a 2D “smear” that is one pixel wide but extends across the entire image — the same low-frequency information is back-projected repeatedly, while high-frequency detail is added only sparsely.

Fig. 11.1 shows the reconstructed volume from filtered backprojection as a function of the number of projection views: with only 3 views the reconstruction is severely star-artefacted; by 50–100 views the overall shape is faithfully recovered, though some residual blurring remains due to the limited tilt range (missing wedge).

../_images/teddy_filtered_reconstruction.png

Fig. 11.1 Effect of the number of projection views on filtered backprojection reconstruction quality. From left to right: the original 3D phantom and reconstructions from 3, 10, 20, 50, and 100 evenly spaced views. With few views the reconstruction is dominated by star artefacts; structural detail emerges progressively as more views are added, though angular sampling still limits the achievable resolution.#

The interactive below shows the same test image (ring, line, dot) reconstructed from a tilt series at different tilt ranges and tilt increments. It also shows the Fourier-space view: the Fourier Slice Theorem tells us exactly which frequencies are sampled by the available projections.

Interactive element

Click Live Code to activate, expand the Show code toggle, and click ▶ to run. Adjust both the maximum tilt angle and the tilt increment to see how each parameter affects reconstruction quality and Fourier space coverage. The Fourier space panel shows which frequencies are sampled (bright lines) and which are missing (dark regions).

Hide code cell source

import io
import numpy as np
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt
from scipy.ndimage import rotate as nd_rotate
from ipywidgets import IntSlider, VBox, Output
from IPython.display import display, Image, clear_output

def _tomo_t2(n=80):
    y, x = np.mgrid[0:n, 0:n].astype(float)
    img = np.zeros((n, n))
    cx1, cy1 = n*0.30, n*0.32
    R1 = np.sqrt((x - cx1)**2 + (y - cy1)**2)
    img += 0.9 * ((R1 > n*0.11) & (R1 < n*0.18)).astype(float)
    cy2 = n*0.67
    img += 0.85 * (np.abs(y - cy2) < n*0.025).astype(float)
    cx3, cy3 = n*0.70, n*0.36
    R3 = np.sqrt((x - cx3)**2 + (y - cy3)**2)
    img += 0.75 * (R3 < n*0.07).astype(float)
    return np.clip(img, 0, 1)

_bp_img = _tomo_t2(80)
_n_bp = 80
_all_angles = np.arange(-90, 91, 1)
_all_sino = np.zeros((len(_all_angles), _n_bp))
for _i, _a in enumerate(_all_angles):
    _all_sino[_i] = nd_rotate(_bp_img, -_a, reshape=False, order=1).sum(axis=0)

def _bp_recon(sino, angles):
    n = _n_bp; recon = np.zeros((n, n))
    yy, xx = (np.mgrid[0:n, 0:n] - n//2).astype(float)
    for proj, ang in zip(sino, angles):
        a = np.deg2rad(ang)
        px = (xx*np.cos(a) + yy*np.sin(a) + n//2).clip(0, n-1)
        lo = px.astype(int); hi = np.minimum(lo+1, n-1); f = px - lo
        recon += proj[lo]*(1-f) + proj[hi]*f
    return recon / max(len(angles), 1)

def draw_bp(tilt_inc=3, max_tilt=60):
    angs = np.arange(-max_tilt, max_tilt+1, tilt_inc)
    sel = np.isin(_all_angles, angs)
    sino = _all_sino[sel]; angs_use = _all_angles[sel]
    rec  = _bp_recon(sino, angs_use)
    ft   = np.log1p(np.abs(np.fft.fftshift(np.fft.fft2(rec))))

    fig, axes = plt.subplots(1, 4, figsize=(16, 4.2))
    axes[0].imshow(_bp_img, cmap='gray'); axes[0].axis('off')
    axes[0].set_title("Original", fontsize=9)
    axes[1].imshow(sino, cmap='gray', aspect='auto'); axes[1].axis('off')
    axes[1].set_title(f"Sinogram ({len(angs_use)} projections)", fontsize=9)
    axes[2].imshow(rec, cmap='gray'); axes[2].axis('off')
    axes[2].set_title(f"Backprojection\n(Δθ={tilt_inc}°, ±{max_tilt}°)", fontsize=9)
    axes[3].imshow(ft, cmap='inferno'); axes[3].axis('off')
    axes[3].set_title("Fourier space of BP\n(coverage visible as lines)", fontsize=9)

    fig.tight_layout()
    buf = io.BytesIO(); fig.savefig(buf, format='png', dpi=96); buf.seek(0)
    display(Image(data=buf.read())); plt.close(fig)

style = {"description_width": "140px"}
inc_sl  = IntSlider(value=3,  min=1, max=15, step=1,
                    description="Tilt increment (°)", style=style, layout={"width":"420px"})
tmax_sl = IntSlider(value=60, min=20, max=90, step=5,
                    description="Max tilt (°)", style=style, layout={"width":"420px"})
out_bp = Output()
def update_bp(_=None):
    with out_bp:
        clear_output(wait=True)
        draw_bp(inc_sl.value, tmax_sl.value)
for s in [inc_sl, tmax_sl]:
    s.observe(update_bp, names='value')
display(VBox([inc_sl, tmax_sl, out_bp]))
update_bp()

11.5. Filtered Backprojection#

Direct backprojection produces blurry reconstructions because Fourier space is oversampled near the origin. As more projections are added, low-frequency components are reinforced more than high-frequency components, which are sparse in the collection of central slices.

Filtered backprojection (FBP) corrects this by applying a ramp filter \(|\nu|\) to each 1D projection before backprojecting. In Fourier space, multiplying by \(|\nu|\) compensates exactly for the radial oversampling density, yielding a reconstruction with uniform frequency weighting:

(11.4)#\[ f(\mathbf{r}) = \int_0^\pi \mathcal{F}^{-1}\!\left[ |\nu| \cdot \hat{P}_\theta(\nu) \right](s)\, d\theta \]

In practice the ramp filter is windowed (e.g., with a Hann or Ram-Lak window) to suppress high-frequency noise amplification. The filtered projection in real space is then backprojected normally.

The comparison below shows direct backprojection vs. filtered backprojection from the same tilt series, with the interactive slider controlling the maximum tilt range.

Interactive element

Click Live Code to activate, expand the Show code toggle, and click ▶ to run. Drag the max-tilt slider to see the effect of the missing wedge on both reconstruction methods, and how FBP sharper than direct BP even when the tilt range is limited.

Hide code cell source

import io
import numpy as np
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt
from scipy.ndimage import rotate as nd_rotate
from ipywidgets import IntSlider, VBox, Output
from IPython.display import display, Image, clear_output

def _tomo_t3(n=80):
    y, x = np.mgrid[0:n, 0:n].astype(float)
    img = np.zeros((n, n))
    cx1, cy1 = n*0.30, n*0.32
    R1 = np.sqrt((x - cx1)**2 + (y - cy1)**2)
    img += 0.9 * ((R1 > n*0.11) & (R1 < n*0.18)).astype(float)
    cy2 = n*0.67
    img += 0.85 * (np.abs(y - cy2) < n*0.025).astype(float)
    cx3, cy3 = n*0.70, n*0.36
    R3 = np.sqrt((x - cx3)**2 + (y - cy3)**2)
    img += 0.75 * (R3 < n*0.07).astype(float)
    return np.clip(img, 0, 1)

_fbp_img = _tomo_t3(80)
_n_fbp = 80
_fbp_all_angles = np.arange(-90, 91, 3)
_fbp_all_sino = np.zeros((len(_fbp_all_angles), _n_fbp))
for _i, _a in enumerate(_fbp_all_angles):
    _fbp_all_sino[_i] = nd_rotate(_fbp_img, -_a, reshape=False, order=1).sum(axis=0)

def _bp2(sino, angles):
    n = _n_fbp; recon = np.zeros((n, n))
    yy, xx = (np.mgrid[0:n, 0:n] - n//2).astype(float)
    for proj, ang in zip(sino, angles):
        a = np.deg2rad(ang)
        px = (xx*np.cos(a) + yy*np.sin(a) + n//2).clip(0, n-1)
        lo = px.astype(int); hi = np.minimum(lo+1, n-1); f = px - lo
        recon += proj[lo]*(1-f) + proj[hi]*f
    return recon / max(len(angles), 1)

def _fbp2(sino, angles):
    freqs = np.fft.rfftfreq(_n_fbp); ramp = np.abs(freqs); ramp[0] = 0
    fs = np.real(np.fft.irfft(np.fft.rfft(sino, axis=1)*ramp[None,:], n=_n_fbp, axis=1))
    return _bp2(fs, angles)

def draw_fbp(max_tilt=60):
    sel = np.abs(_fbp_all_angles) <= max_tilt
    angs = _fbp_all_angles[sel]; sino = _fbp_all_sino[sel]
    bp  = _bp2(sino, angs)
    rec = _fbp2(sino, angs)
    n = _n_fbp; cx = cy = n//2

    fig, axes = plt.subplots(1, 4, figsize=(16, 4.2))
    axes[0].imshow(_fbp_img, cmap='gray'); axes[0].axis('off')
    axes[0].set_title("Original", fontsize=9)
    axes[1].imshow(bp, cmap='gray'); axes[1].axis('off')
    axes[1].set_title(f"Direct BP\n{max_tilt}°, {sel.sum()} tilts)", fontsize=9)
    axes[2].imshow(np.clip(rec, 0, None), cmap='gray'); axes[2].axis('off')
    axes[2].set_title(f"Filtered BP (FBP)\n{max_tilt}°, ramp filter)", fontsize=9)

    ft = np.log1p(np.abs(np.fft.fftshift(np.fft.fft2(rec))))
    axes[3].imshow(ft, cmap='inferno'); axes[3].axis('off')
    if max_tilt < 90:
        r = n//2 - 3
        phi = np.deg2rad(max_tilt)
        for sgn in [1, -1]:
            axes[3].plot([cx - r*np.cos(phi), cx + r*np.cos(phi)],
                         [cy + sgn*r*np.sin(phi), cy - sgn*r*np.sin(phi)],
                         'r-', linewidth=2)
    axes[3].set_title(f"FT of FBP\n(red = missing wedge boundary)", fontsize=9)

    fig.suptitle(f"FBP reconstruction — tilt range ±{max_tilt}°", fontsize=10)
    fig.tight_layout()
    buf = io.BytesIO(); fig.savefig(buf, format='png', dpi=96); buf.seek(0)
    display(Image(data=buf.read())); plt.close(fig)

tmax_sl2 = IntSlider(value=60, min=15, max=90, step=5,
                     description="Max tilt (°)",
                     style={"description_width": "110px"},
                     layout={"width": "400px"})
out_fbp = Output()
def update_fbp(_=None):
    with out_fbp:
        clear_output(wait=True)
        draw_fbp(tmax_sl2.value)
tmax_sl2.observe(update_fbp, names='value')
display(VBox([tmax_sl2, out_fbp]))
update_fbp()

11.6. The Missing Wedge#

A critical limitation of single-axis tilt series tomography is the missing wedge: because tilt angles are limited to \(\pm 60°\)\(70°\), the central region of 3D Fourier space perpendicular to the tilt axis is never sampled. Fourier components within this missing wedge cannot be reconstructed directly. The consequences in real space are:

  1. Elongation artifact: features are elongated along the beam direction (the axis perpendicular to the specimen plane).

  2. Reduced axial resolution: the effective resolution along the beam direction is typically 2–3× worse than in the plane.

  3. Anisotropic artifact: objects oriented perpendicular to the tilt axis (e.g., membranes parallel to the grid) are more faithfully reconstructed than objects parallel to it.

Consider the three features in our test image:

  • The ring (tube cross-section): the missing wedge rounds the sharp ring walls facing the tilt axis direction while preserving the walls facing perpendicular — making the ring appear elongated.

  • The horizontal line (membrane): strongly suppressed when the membrane is perpendicular to the sampled directions — the missing wedge creates a characteristic “ghost” pair of lines.

  • The dot (protein): becomes an elongated ellipse along the missing-wedge direction.

The missing wedge can be partially mitigated by using a dual-axis tilt series (collecting tilt series around two perpendicular axes), which replaces the missing wedge with a smaller missing pyramid. Alternatively, regularised reconstruction algorithms can impose constraints (non-negativity, sparsity) that partially fill in the missing information.

11.7. The Crowther Criterion#

How finely must the tilt series be sampled to fill Fourier space up to a desired resolution? The answer is given by the Crowther criterion.

Consider a 2D image of diameter \(D\) pixels (or thickness \(D\) Å). Each 1D projection fills in one central line of the 2D Fourier transform. The width of that line in Fourier space is \(1/D\) (the inverse of the object size — finer objects need finer sampling). The circumference of the circle at resolution \(r\) (in Fourier space, \(k = 1/r\)) is \(2\pi/r\).

To fill this circumference with lines of width \(1/D\) and avoid gaps, we need at least:

(11.5)#\[ m \geq \frac{2\pi/r}{1/D} = \frac{2\pi D}{r} \quad \Longrightarrow \quad \Delta\theta \leq \frac{r}{D} \]

where \(\Delta\theta\) is the tilt increment in radians. This is the Crowther criterion: the number of tilt angles required to achieve resolution \(r\) from a specimen of diameter \(D\) scales as \(D/r\).

Practical consequences: For a specimen of thickness \(D = 50\) nm = 500 Å and a target resolution of \(r = 5\) nm = 50 Å: $\( m \geq \frac{500\,\text{Å}}{50\,\text{Å}} = 10 \text{ tilts minimum} \)\( But to reach \)r = 2\( nm resolution, \)m\geq 250$ tilts are needed — far more than the 40–60 tilts possible within the radiation dose budget. Dose constraints therefore fundamentally limit the achievable resolution in cryo-ET; this is why subtomogram averaging (combining many copies of the same structure) is needed to reach sub-nanometre resolution.

../_images/915fb36f8e2b1549beb73b8c3fea3ed9c1723af5eda65f89b6b0a04e5b3845d5.png

The right panel shows that for a typical 50 nm specimen, achieving 5 Å resolution would require ~300 tilts — far beyond what dose constraints allow. This fundamental tension between dose budget and Crowther sampling is a key driver for subtomogram averaging and denoising approaches in cryo-ET.

11.8. Iterative Reconstruction Methods#

Filtered backprojection is fast but treats each tilt image independently. Iterative methods improve reconstruction quality by treating it as an optimisation problem: find the 2D (or 3D) density that best explains all observed projections simultaneously.

SIRT (Simultaneous Iterative Reconstruction Technique): At each iteration, compute forward projections of the current estimate, compare with measured projections, backproject the residuals, and update the estimate. SIRT is more tolerant of the missing wedge and reduces the “star artifact” visible in FBP reconstructions.

Compressed sensing (CS) reconstruction: Biological structures are often sparse in an appropriate basis (e.g., sparse gradient). CS adds a regularisation term that promotes sparsity, enabling reasonable reconstruction quality even with very few tilt angles.

Deep learning methods: Neural networks can be trained to predict missing information or denoise tomograms. Self-supervised approaches exploit the even/odd frames of tilt movies to denoise each tilt image without requiring clean reference data.

11.9. Subtomogram Averaging#

Electron tomography preserves the 3D context of molecules in their native environment, but individual protein molecules within a tomogram are so small (\(\sim\)10–20 nm) and the SNR so low that single tomograms cannot reveal atomic detail. Subtomogram averaging (STA) combines the contextual information of tomography with the averaging power of SPA:

  1. Template matching: A reference volume is cross-correlated with the tomogram in 3D to locate all candidate positions and orientations of a given complex.

  2. Subtomogram extraction: Small 3D cubes (“subtomograms”) are extracted around each detected location.

  3. Alignment and averaging: Subtomograms are aligned to each other using 3D cross-correlation and averaged, suppressing noise by \(\sqrt{N}\) as in SPA.

  4. Classification: 3D classification can sort subtomograms by conformation, revealing structural heterogeneity.

STA achieves resolutions of 3–8 Å for large, abundant complexes in intact cells. The unique advantage over SPA is that the positions and orientations of the averaged copies are known in their cellular context — the distance distributions and lattice arrangements of molecular machines can be mapped directly.

11.10. Tilt Series Alignment and CTF Correction#

Before reconstruction, the tilt series images must be aligned to a common coordinate frame. Standard alignment uses fiducial markers — gold nanoparticles mixed into the sample before freezing, visible as high-contrast blobs in every tilt image. The 3D coordinates of the fiducials are refined by minimising reprojection errors across all tilts.

Fiducial-free alignment using cross-correlation of image patches is increasingly common and works well for data collected with good stage stability.

CTF correction in cryo-ET is more complex than in SPA because the defocus varies across a tilted specimen: a point at height \(h\) above the grid centre has defocus \(\Delta f + h \sin\theta\). CTF correction must therefore be applied locally, either per-tilt or per-sub-volume.

11.11. In-Situ Cryo-ET: Seeing Molecules in their Cellular Context#

The most powerful application of cryo-ET is imaging biological structures directly within the cell. In-situ cryo-ET can visualise organelles, cytoskeletal networks, membrane contacts, and macromolecular machines in the intact cellular environment, without the artefacts introduced by biochemical purification.

The main technical challenge is specimen thickness: whole mammalian cells (\(5\)\(20\) µm thick) are far too thick for TEM. Solutions include:

  • Cryo-focused ion beam (cryo-FIB) milling: a focused ion beam sculpts a thin lamella (\(< 200\) nm) through the vitrified cell, exposing a thin slice suitable for TEM.

  • Cryo-sections: thin sections of high-pressure-frozen material, analogous to ultra-thin sectioning for conventional EM.

Cryo-FIB-SEM milling combined with cryo-ET and STA now routinely achieves sub-nanometre resolution of in-situ structures, making it possible to map entire interactomes within a cell volume.

11.12. References#

[1]

Avinash C. Kak and Malcolm Slaney. Principles of Computerized Tomographic Imaging. Society for Industrial and Applied Mathematics, Philadelphia, PA, 2001. doi:10.1137/1.9780898719277.