Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a ProjectionModel specification #20

Merged
merged 7 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

`cryotypes` defines a set of super-simple, extensible data structures for the fundamental types of cryoEM data and their relative metadata:
- `PoseSet`: a set of particle poses, compatible with 2D and 3D data
- `ProjectionModel`: a set of parameters for a projection model (tilt-series alignments)
- `Tomogram`: a 3D image
- `Micrograph`: a 2D image

Expand Down Expand Up @@ -49,6 +50,43 @@ Particle orientations are stored as
[`scipy.spatial.transform.Rotation`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.html) objects.
These transformations should rotate the basis vectors of a reference such that they are correctly oriented in a tomogram.

## `ProjectionModel`
A `ProjectionModel` is a [pandas `DataFrame`](https://pandas.pydata.org/docs/) with specific column
headings for the parameters of a projection model. Together, this information constitues a 'tilt-series alignment'.

| Heading | Python name | Semantics |
|:----------------|:--------------|:--------------------------------------------------|
| `rotation_x` | ROTATION_X | specimen rotation around x-axis |
| `rotation_y` | ROTATION_Y | specimen rotation around y-axis |
| `rotation_z` | ROTATION_Z | specimen rotation around z-axis |
| `dx` | SHIFT_X | specimen shift in x-dimension of the camera plane |
| `dy` | SHIFT_Y | particle shift in y-dimension of the camera plane |
| `experiment_id` | EXPERIMENT_ID | identifier for micrograph/tilt-series |
| `pixel_spacing` | PIXEL_SPACING | isotropic pixel/voxel spacing for shifts |
| `source` | SOURCE | reference to the file from which data came |

In the microsope reference frame, the z-axis is the beam direction.
Extrinsic rotation of the tomogram around the x-axis, the y-axis, then the z-axis by
`rotation_x`, `rotation_y`, `rotation_z` followed by projection along the z-axis (beam direction)
then shifting the 2D image in the camera plane by `dx` and `dy` produces the experimental projection
image.

A utility function is also provided for generating projection matrices from these data.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What transformation are these proj matrices describing exactly?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

position in tomogram -> position in tilt image, one matrix per tilt image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in, if you apply this to the real object and project along z, you get the tilt image? I would add a small note for this for clarity!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, if by z you mean the beam axis :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note added

These projection matrices can be used to calculate a 2D position in a tilt-image from a 3D position
in the tomogram.

```python
from cryotypes.projectionmodel import projection_model_to_projection_matrices

projection_matrices = projection_model_to_projection_matrices(
df=projection_model, # ProjectionModel dataframe
tilt_image_center=(1919, 1355), # tilt-image rotation center (xy)
tomogram_dimensions=(3838, 3710, 2000) # dimensions of tomogram (xyz)
)
```
Comment on lines +78 to +86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should provide the same for particles (if one wants to work with affines rather than pos + rot).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

possible but not sure of the benefit there, will open an issue to discuss something re this though


**Note:** these projection matrices are only valid for positions in a tomogram of the dimensions
provided in this function and must be recalculated for different tomogram dimensions.

## `Tomogram`
A `Tomogram` is an object that follows a specific [python `Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol) for tomogram data. The protocol specifies the following attributes:
Expand Down
9 changes: 9 additions & 0 deletions cryotypes/projectionmodel/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from ._data_labels import ProjectionModelDataLabels
from ._typing import ProjectionModel
from ._utils import projection_model_to_projection_matrices

__all__ = [
"ProjectionModelDataLabels",
"ProjectionModel",
"projection_model_to_projection_matrices"
]
15 changes: 15 additions & 0 deletions cryotypes/projectionmodel/_data_labels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class ProjectionModelDataLabels:
ROTATION_X = "rotation_x"
ROTATION_Y = "rotation_y"
ROTATION_Z = "rotation_z"
ROTATION = [ROTATION_X, ROTATION_Y, ROTATION_Z]
Comment on lines +2 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we maybe for consistency use actual Rotation objects internally?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree we want consistency but don't think a Rotation is simpler for this use case where a primary use case could be checking how much things have shifted from expected stage tilt angles -> I don't want to force understanding rotation decomposition on someone in that situation

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can always provide utility functions... I mean, this still requires all the convention stuff from euler angles, so at least the Rotation moves the burden to the creation of the object, instead of both creation and utilisation. I just think consistency here would be nice to keep; and like with poses, it's just more convenient to work with single values rather than multiple things.

Either way, we should at least clarify in which order rotations happen, if they are intrinsic/extrinsicm, yada yada.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note added explaining rotations!


SHIFT_X = "dx"
SHIFT_Y = "dy"
SHIFT = [SHIFT_X, SHIFT_Y]

EXPERIMENT_ID = "experiment_id"

PIXEL_SPACING = "pixel_spacing"

SOURCE = "source"
116 changes: 116 additions & 0 deletions cryotypes/projectionmodel/_transformations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import numpy as np
import einops


def Rx(theta: np.ndarray) -> np.ndarray:
"""Generate 4x4 matrices which rotate vectors around the X-axis by the angle `theta`.

The rotation angle `theta` is expected to be in degrees.
Application of these matrices is performed by left-multiplying xyzw column vectors.

Parameters
----------
theta: np.ndarray
An `(n, )` array of rotation angles in degrees.

Returns
-------
matrices: np.ndarray
An `(n, 4, 4)` array of matrices which left-multiply xyzw column vectors.
"""
theta = np.asarray(theta).reshape(-1)
c = np.cos(np.deg2rad(theta))
s = np.sin(np.deg2rad(theta))
matrices = einops.repeat(
np.eye(4), 'i j -> n i j', n=len(theta)
)
matrices[:, 1, 1] = c
matrices[:, 1, 2] = -s
matrices[:, 2, 1] = s
matrices[:, 2, 2] = c
return matrices


def Ry(theta: np.ndarray) -> np.ndarray:
"""Generate 4x4 matrices which rotate vectors around the Y-axis by the angle `theta`.

The rotation angle `theta` is expected to be in degrees.
Application of these matrices is performed by left-multiplying xyzw column vectors.

Parameters
----------
theta: np.ndarray
An `(n, )` array of rotation angles in degrees.

Returns
-------
matrices: np.ndarray
An `(n, 4, 4)` array of matrices which left-multiply xyzw column vectors."""
theta = np.asarray(theta).reshape(-1)
c = np.cos(np.deg2rad(theta))
s = np.sin(np.deg2rad(theta))
matrices = einops.repeat(np.eye(4), 'i j -> n i j', n=len(theta))
matrices[:, 0, 0] = c
matrices[:, 0, 2] = s
matrices[:, 2, 0] = -s
matrices[:, 2, 2] = c
return matrices


def Rz(theta: float) -> np.ndarray:
"""Generate 4x4 matrices which rotate vectors around the Z-axis by the angle `theta`.

The rotation angle `theta` is expected to be in degrees.
Application of these matrices is performed by left-multiplying xyzw column vectors.

Parameters
----------
theta: np.ndarray
An `(n, )` array of rotation angles in degrees.

Returns
-------
matrices: np.ndarray
An `(n, 4, 4)` array of matrices which left-multiply xyzw column vectors."""
theta = np.asarray(theta).reshape(-1)
c = np.cos(np.deg2rad(theta))
s = np.sin(np.deg2rad(theta))
matrices = einops.repeat(
np.eye(4), 'i j -> n i j', n=len(theta)
)
matrices[:, 0, 0] = c
matrices[:, 0, 1] = -s
matrices[:, 1, 0] = s
matrices[:, 1, 1] = c
return np.squeeze(matrices)


def S(shifts: np.ndarray) -> np.ndarray:
"""Generate 4x4 matrices for 2D (xy) or 3D (xyz) shifts.

Application of these matrices is performed by left-multiplying xyzw column vectors.

Parameters
----------
shifts: np.ndarray
An `(n, 2)` or `(n, 3)` array of xy(z) shifts.

Returns
-------
matrices: np.ndarray
An `(n, 4, 4)` array of
"""
shifts = np.asarray(shifts, dtype=float)
if shifts.shape[-1] == 2:
shifts = _promote_2d_to_3d(shifts)
shifts = np.array(shifts).reshape((-1, 3))
matrices = einops.repeat(np.eye(4), 'i j -> n i j', n=shifts.shape[0])
matrices[:, 0:3, 3] = shifts
return np.squeeze(matrices)


def _promote_2d_to_3d(shifts: np.ndarray) -> np.ndarray:
"""Promote 2D vectors to 3D with zeros in the last dimension."""
shifts = np.asarray(shifts).reshape(-1, 2)
shifts = np.c_[shifts, np.zeros(len(shifts))]
return np.squeeze(shifts)
4 changes: 4 additions & 0 deletions cryotypes/projectionmodel/_typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import pandas as pd
from typing_extensions import TypeAlias

ProjectionModel: TypeAlias = pd.DataFrame
42 changes: 42 additions & 0 deletions cryotypes/projectionmodel/_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from typing import Tuple

import numpy as np

from ._typing import ProjectionModel
from ._data_labels import ProjectionModelDataLabels as PMDL
from ._transformations import Rx, Ry, Rz, S


def projection_model_to_projection_matrices(
df: ProjectionModel,
tilt_image_center: Tuple[int, int],
tomogram_dimensions: Tuple[int, int, int]
) -> np.ndarray:
"""Calculate 4x4 projection matrices from a ProjectionModel dataframe.

The resulting 4x4 projection matrices left-multiply xyzw column vectors containing any position
in the tomogram to produce the projected position (xyzw) in the camera plane.


Parameters
----------
df: ProjectionModel
A pandas DataFrame adhering to the `ProjectionModel` specification.
tilt_image_center: Tuple[int, int]
Rotation center in the 2D tilt-images, ordered xy.
tomogram_dimensions: Tuple[int, int, int]
dimensions of the tomogram, ordered xyz.

Returns
-------
projection_matrices: np.ndarray
An `(n, 4, 4)` array of projection matrices which left-multiply xyzw column vectors.
"""
specimen_center = np.array(tomogram_dimensions) // 2
s0 = S(-specimen_center)
r0 = Rx(df[PMDL.ROTATION_X])
r1 = Ry(df[PMDL.ROTATION_Y])
r2 = Rz(df[PMDL.ROTATION_Z])
s1 = S(df[PMDL.SHIFT])
s2 = S(tilt_image_center)
return s2 @ s1 @ r2 @ r1 @ r0 @ s0
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ install_requires =
pandas
scipy
typing_extensions
einops
python_requires = >=3.7
setup_requires =
setuptools_scm
Expand Down