Skip to content
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
14 changes: 7 additions & 7 deletions docs/advanced/python-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ print(fa.shape) # (Y, X), values in [0, 1]

---

## Helix and Transverse Angles
## Helical and Intrusion Angles

```python
import numpy as np
import matplotlib.pyplot as plt
from cardiotensor import compute_helix_and_transverse_angles
from cardiotensor import compute_helical_and_intrusion_angles
from cardiotensor.orientation import rotate_vectors_to_new_axis

# Use one slice of the eigenvector field computed above
Expand All @@ -76,17 +76,17 @@ center_point = (
z_index,
)

helix_angle, intrusion_angle = compute_helix_and_transverse_angles(
helical_angle, intrusion_angle = compute_helical_and_intrusion_angles(
rotated_vectors,
center_point,
)
# Both arrays shape: (Y, X), values in degrees
# The second output corresponds to the transverse/intrusion companion angle map
# The second output corresponds to the intrusion angle map

fig, axes = plt.subplots(1, 2, figsize=(10, 4), constrained_layout=True)

im0 = axes[0].imshow(helix_angle, cmap="viridis", vmin=-90, vmax=90)
axes[0].set_title("Helix angle")
im0 = axes[0].imshow(helical_angle, cmap="viridis", vmin=-90, vmax=90)
axes[0].set_title("Helical angle")
axes[0].axis("off")
fig.colorbar(im0, ax=axes[0], label="degrees")

Expand All @@ -113,7 +113,7 @@ from cardiotensor import (
calculate_structure_tensor,
compute_azimuth_and_elevation,
compute_fraction_anisotropy,
compute_helix_and_transverse_angles,
compute_helical_and_intrusion_angles,
# Tractography
generate_streamlines_from_vector_field,
generate_streamlines_from_params,
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ The `./examples/` directory contains:
3. Outputs will be saved in the `./output` directory with the following structure:
```
./output
├── HA # Helix angle results
├── HA # Helical angle results
├── IA # Intrusion angle results
├── FA # Fractional anisotropy results
└── eigen_vec # 3rd Eigenvectors
Expand Down
31 changes: 26 additions & 5 deletions docs/getting-technical/angles.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Angle Definitions

This page explains how helix and intrusion angles are calculated from the 3D eigenvector field derived by `cardiotensor`.
This page explains how helical and intrusion angles are calculated from the 3D eigenvector field derived by `cardiotensor`.

By default, Cardiotensor reports **unprojected 3D angles**. The primary eigenvector is not first flattened onto a 2D plane before the angle is measured, because projection discards one component of the vector and can bias the resulting angle. Projection-based angles are available only for legacy comparison.

## Coordinate System

Expand All @@ -10,12 +12,17 @@ A transformation to a cylindrical coordinate system is defined for each voxel ba
- **Circumferential (θ)**: tangential around the ventricle
- **Longitudinal (z)**: base to apex direction

To compute local fiber angles consistently, all eigenvectors are first rotated into this cylindrical coordinate frame. This alignment is performed using Rodrigues' rotation formula, which computes the minimal-angle rotation that maps the global reference axis (here the z-axis) onto the local longitudinal axis at each point. This allows a robust comparison of orientations across the myocardium.
To compute local fiber angles consistently, all eigenvectors are first rotated into this cylindrical coordinate frame. This alignment is performed using the Rodrigues rotation formula, which computes the minimal-angle rotation that maps the global reference axis (here the z-axis) onto the local longitudinal axis at each point. This allows a robust comparison of orientations across the myocardium.

## Helical Angle (HA)

The helical angle is defined as the angle between the primary myocyte-axis eigenvector \\( \vec{v}_1 \\) and the local circumferential plane.

## Helix Angle (HA)
In local cylindrical coordinates, with radial component \\(R\\), circumferential component \\(C\\), and longitudinal component \\(L\\), the unprojected helical angle is:

The helix angle is defined as the angle between the third eigenvector \\( \vec{v}_3 \\) (smallest eigenvalue direction) and the **local circumferential plane**.
\\[
\mathrm{HA} = \arctan2\left(L, \sqrt{R^2 + C^2}\right)
\\]

It captures the transmural variation of fiber orientation from epicardium to endocardium.

Expand All @@ -26,10 +33,24 @@ Typical pattern:

## Intrusion Angle (IA)

The intrusion angle is the angle between \\( \vec{v}_3 \\) and the **tangential plane** (longitudinal + circumferential).
The intrusion angle is the angle between the primary myocyte-axis eigenvector \\( \vec{v}_1 \\) and the **tangential plane** (longitudinal + circumferential).

Using the same local components, the unprojected intrusion angle is:

\\[
\mathrm{IA} = \arctan2\left(R, \sqrt{C^2 + L^2}\right)
\\]

It captures radial deviation of fiber aggregates and can help identify wall thickening or microstructural disruptions.

## Projection Bias

Conventional projected angles are computed after removing one vector component, for example \\(\arctan2(L, C)\\) for projected helical angle or \\(\arctan2(R, C)\\) for projected intrusion angle. These projected quantities can differ from the true 3D orientation when the discarded component is large.

Set `PROJECTED_ANGLES = True` only when you need legacy projected `HA_projected` and `IA_projected` maps for comparison with literature.

This convention follows the projection-error discussion in Agger et al., "Anatomically correct assessment of the orientation of the cardiomyocytes using diffusion tensor imaging", *NMR in Biomedicine* (2020), https://doi.org/10.1002/nbm.4205.

## Angle Ranges

Both angles are reported in degrees:
Expand Down
4 changes: 2 additions & 2 deletions docs/getting-technical/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ Eigenvector maps are saved in the shape `(3, Z, Y, X)`:
- 3 = x, y, z vector components
- Each voxel contains the orientation of the 3rd eigenvector (principal myocyte axis)

These vector fields are used for computing helix/intrusion angles, streamlines, and for visualization.
These vector fields are used for computing helical/intrusion angles, streamlines, and for visualization.


## Units of measurement

- Lengths are expressed in **pixels** (or voxels for 3D).
- Angles (helix, intrusion) are in **degrees**.
- Angles (helical, intrusion) are in **degrees**.
- Fractional anisotropy is **dimensionless**, ranging from 0 (isotropic) to 1 (highly anisotropic).


Expand Down
2 changes: 1 addition & 1 deletion docs/getting-technical/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This section provides a deeper understanding of how `cardiotensor` works, includ

- [Structure Tensor](./structure_tensor.md): Mathematical background on the structure tensor and eigen decomposition

- [Angle Definitions](./angles.md): Detailed explanation of helix and intrusion angles
- [Angle Definitions](./angles.md): Detailed explanation of helical and intrusion angles

- [Fractional Anisotropy](./fractional_anisotropy.md)

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-technical/structure_tensor.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Each structure tensor $S$ is decomposed into eigenvalues $\lambda_1 \leq \lambda
## Output and Interpretation

* The third eigenvector $\vec{v}_1$ is stored as the local myocyte orientation.
* Helix and intrusion angles are computed from $\vec{v}_1$ after transforming it into cylindrical coordinates.
* Helical and intrusion angles are computed from $\vec{v}_1$ after transforming it into cylindrical coordinates.
* Fractional Anisotropy (FA) is computed using the three eigenvalues (see [FA section](./fractional_anisotropy.md)).

---
Expand Down
4 changes: 2 additions & 2 deletions docs/getting-technical/tractography.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Streamlines shorter than `min_length_pts` (default: 10 points) are discarded. Yo
The streamline results are saved in `.trk` format as:

* `streamlines`: a list of arrays, each of shape (N, 3), where N is the number of points in that streamline
* `ha_values`: sampled HA (helix angle) values along each point
* `ha_values`: sampled HA (helical angle) values along each point

These can be loaded in Python or exported to `.vtk` for 3D visualization in ParaView.

Expand All @@ -74,7 +74,7 @@ cardio-generate config.conf --seeds 20000 --bin 2 --step 0.5 --fa-threshold 0.15

* Streamlines are computed from the 3rd eigenvector of the structure tensor, corresponding to the myocyte axis.
* FA is computed once from the structure tensor and optionally downsampled for speed.
* Helix angle (HA) is sampled at each point along the streamline and saved for further analysis.
* Helical angle (HA) is sampled at each point along the streamline and saved for further analysis.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Cardiotensor provides powerful features for analyzing 3D cardiac imaging data, f
Compute myocyte orientation from a 3D volume using a configuration file.

- `cardio-tensor`
Computes structure tensor, helix/transverse angle, FA, and eigenvectors.
Computes structure tensor, helical/intrusion angle, FA, and eigenvectors.

- `cardio-tensor-slurm`
Submits chunked `cardio-tensor` runs as SLURM array jobs.
Expand Down
6 changes: 4 additions & 2 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ REVERSE = False
WRITE_ANGLES = True

# Choose which angle pair to compute:
# ha_ia → Helix Angle / Intrusion Angle (standard myocardial architecture)
# ha_ia → Helical Angle / Intrusion Angle (standard myocardial architecture)
# az_el → Azimuth / Elevation (generic vector orientation in 3D)
ANGLE_MODE = ha_ia

Expand Down Expand Up @@ -145,7 +145,9 @@ OUTPUT_TYPE = 8bit
---

### `[ANGLE CALCULATION]`
- **`WRITE_ANGLES`**: Save helix and intrusion angles and fractional anisotropy values.
- **`WRITE_ANGLES`**: Save helical and intrusion angles and fractional anisotropy values.
- **`ANGLE_MODE`**: Choose `ha_ia` for cardiac HA/IA or `az_el` for generic azimuth/elevation maps.
- **`PROJECTED_ANGLES`**: In `ha_ia` mode, keep `False` for unprojected 3D HA/IA. Set `True` only to write legacy projected `HA_projected`/`IA_projected` comparison maps.
- **`AXIS_POINTS`**: List of 3D points `[X, Y, Z]` along the ventricle axis. Typically, the first and last points correspond to the mitral valve and apex. Intermediate points help create a curved axis via interpolation.

---
Expand Down
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ To process the entire volume:
3. The results will be saved in the `./output` directory with the following structure:
```
./output
├── HA # Helix angle results
├── HA # Helical angle results
├── IA # Intrusion angle results
└── FA # Fractional anisotropy results
```
Expand Down
58 changes: 29 additions & 29 deletions examples/parameters_example.conf
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ REVERSE = False
WRITE_ANGLES = True

# Choose which angle pair to compute:
# ha_ia → Helix Angle / Intrusion Angle (standard myocardial architecture)
# ha_ia → Helical Angle / Intrusion Angle (standard myocardial architecture)
# az_el → Azimuth / Elevation (generic vector orientation in 3D)
ANGLE_MODE = ha_ia

Expand All @@ -60,38 +60,38 @@ ANGLE_MODE = ha_ia
AXIS_POINTS = [104,110,116], [41,87,210], [68,95,162]


[TEST]
# Enable test mode:
# - True: Process and plot only a single slice for testing.
# - False: Perform the full processing on the entire volume.
TEST = True

# Specify the slice number to process when test mode is enabled.
N_SLICE_TEST = 155

# Overlay the in-plane vector field as a quiver plot on the grayscale test slice.
# - True: show the arrows
# - False: keep only the grayscale slice and angle/FA panels
SHOW_QUIVER = True


[OUTPUT]
[TEST]
# Enable test mode:
# - True: Process and plot only a single slice for testing.
# - False: Perform the full processing on the entire volume.
TEST = True
# Specify the slice number to process when test mode is enabled.
N_SLICE_TEST = 155
# Overlay the in-plane vector field as a quiver plot on the grayscale test slice.
# - True: show the arrows
# - False: keep only the grayscale slice and angle/FA panels
SHOW_QUIVER = True
[OUTPUT]
# Path to the folder where the results will be saved
OUTPUT_PATH =./output

# Output file format for the results (e.g., jp2 or tif).
# Default format is jp2
OUTPUT_FORMAT = tif

# Type of pixel values in the output file:
# - "8bit" for grayscale 8-bit images
# - "rgb" for 3-channel color images
OUTPUT_TYPE = 8bit

# Optional colormaps for RGB output and test plots.
# Use any Matplotlib colormap name, or helix_angle for the CardioTensor map.
# COLORMAP applies to both angles unless COLORMAP_ANGLE1 or COLORMAP_ANGLE2 is set.
# For ANGLE_MODE = az_el, defaults are helix_angle for AZ and viridis for EL.
# COLORMAP =
# COLORMAP_ANGLE1 =
# COLORMAP_ANGLE2 =
# Type of pixel values in the output file:
# - "8bit" for grayscale 8-bit images
# - "rgb" for 3-channel color images
OUTPUT_TYPE = 8bit
# Optional colormaps for RGB output and test plots.
# Use any Matplotlib colormap name, or helix_angle for the CardioTensor map.
# COLORMAP applies to both angles unless COLORMAP_ANGLE1 or COLORMAP_ANGLE2 is set.
# For ANGLE_MODE = az_el, defaults are helix_angle for AZ and viridis for EL.
# COLORMAP =
# COLORMAP_ANGLE1 =
# COLORMAP_ANGLE2 =
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dependencies = [
"opencv-python-headless",
"pandas",
"psutil",
"pyvista",
"scikit-image",
"structure-tensor>=0.3.0",
"tifffile",
Expand Down Expand Up @@ -77,6 +78,7 @@ git = [
cardio-analysis = "cardiotensor.scripts.gui_analysis_tool:script"
cardio-analysis-histo = "cardiotensor.scripts.analysis_histograms:script"
cardio-analysis-streamlines = "cardiotensor.scripts.analysis_streamlines:script"
cardio-create-movie = "cardiotensor.scripts.create_movie:script"
cardio-generate-streamlines = "cardiotensor.scripts.generate_streamlines:script"
cardio-tensor = "cardiotensor.scripts.compute_orientation:script"
cardio-tensor-slurm = "cardiotensor.scripts.slurm_launcher:script"
Expand Down
4 changes: 2 additions & 2 deletions src/cardiotensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
calculate_structure_tensor,
compute_azimuth_and_elevation,
compute_fraction_anisotropy,
compute_helix_and_transverse_angles,
compute_helical_and_intrusion_angles,
compute_orientation,
)
from cardiotensor.tractography import (
Expand All @@ -72,7 +72,7 @@
"calculate_structure_tensor",
"compute_azimuth_and_elevation",
"compute_fraction_anisotropy",
"compute_helix_and_transverse_angles",
"compute_helical_and_intrusion_angles",
# Tractography
"generate_streamlines_from_vector_field",
"generate_streamlines_from_params",
Expand Down
2 changes: 1 addition & 1 deletion src/cardiotensor/analysis/gui_analysis_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def plot_label_and_limits(mode: str):
if m == "FA":
return "Fractional Anisotropy", 0.0, 1.0
if m in {"HA", "IA", "EL"}:
name = {"HA": "Helix Angle", "IA": "Intrusion Angle", "EL": "Elevation Angle"}[
name = {"HA": "Helical Angle", "IA": "Intrusion Angle", "EL": "Elevation Angle"}[
m
]
return f"{name} (°)", -90.0, 90.0
Expand Down
4 changes: 2 additions & 2 deletions src/cardiotensor/orientation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
calculate_structure_tensor,
compute_azimuth_and_elevation,
compute_fraction_anisotropy,
compute_helix_and_transverse_angles,
compute_helical_and_intrusion_angles,
orient_vectors_z_positive,
rotate_vectors_to_new_axis,
)
Expand All @@ -16,7 +16,7 @@
"calculate_structure_tensor",
"compute_azimuth_and_elevation",
"compute_fraction_anisotropy",
"compute_helix_and_transverse_angles",
"compute_helical_and_intrusion_angles",
"compute_orientation",
"orient_vectors_z_positive",
"rotate_vectors_to_new_axis",
Expand Down
Loading
Loading