Skip to content

Commit 5474c0c

Browse files
committed
Add animate CLI subcommand
1 parent 9433193 commit 5474c0c

5 files changed

Lines changed: 102 additions & 2 deletions

File tree

docs/how-to/tta.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ restored = tio.apply_inverse_transform(data)
8383
| `Affine` || Uses the inverse affine matrix |
8484
| `ElasticDeformation` || Negates the sampled displacement field |
8585
| `Spatial` || Inverts resampling, affine, and elastic parts together |
86+
| `Normalize` || Reverses the linear rescaling |
87+
| `Standardize` || Multiplies by std and adds mean |
8688
| `Noise` || Skipped silently when ``ignore_intensity=True`` |
8789

8890
Non-invertible transforms are **skipped with a warning** (not

docs/how-to/visualization.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,31 @@ The data is flipped and transposed as needed so these directions are
206206
always in the same screen positions. The axis labels show which tensor
207207
axis (I, J, K) maps to each direction, making it easy to relate what
208208
you see to the underlying data layout.
209+
210+
## Animated GIFs and videos
211+
212+
You can export an animation sweeping through slices along any
213+
anatomical direction:
214+
215+
<!-- pytest-codeblocks:skip -->
216+
```python
217+
image.to_gif("brain.gif", seconds=5, direction="I")
218+
image.to_video("brain.mp4", seconds=5, direction="S")
219+
```
220+
221+
The ``direction`` parameter accepts ``"I"`` (inferior), ``"S"``
222+
(superior), ``"A"`` (anterior), ``"P"`` (posterior), ``"R"`` (right),
223+
or ``"L"`` (left). The image is automatically reoriented so slices
224+
appear in the correct anatomical view.
225+
226+
From the command line:
227+
228+
```bash
229+
torchio animate brain.nii.gz brain.gif
230+
torchio animate brain.nii.gz brain.mp4 --seconds 10 --direction S
231+
```
232+
233+
!!! note "Optional dependencies"
234+
GIFs require ``Pillow`` (included in the ``[plot]`` extra).
235+
Videos require ``ffmpeg-python`` (``pip install torchio[video]``)
236+
and a working ``ffmpeg`` installation.

docs/reference/cli.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ TorchIO includes a `torchio` command with several subcommands.
55
## Usage
66

77
```
8-
torchio [plot|info|convert|transform|cache] [options]
8+
torchio [plot|animate|info|convert|transform|cache] [options]
99
```
1010

1111
## Reference
@@ -18,6 +18,11 @@ Help text for each subcommand is generated from the source code.
1818
show_root_heading: true
1919
heading_level: 3
2020

21+
::: torchio.cli.Animate
22+
options:
23+
show_root_heading: true
24+
heading_level: 3
25+
2126
::: torchio.cli.Info
2227
options:
2328
show_root_heading: true

src/torchio/cli.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,53 @@ def run(self) -> None:
4848
)
4949

5050

51+
@dataclass
52+
class Animate:
53+
"""Create an animated GIF or MP4 sweeping through slices.
54+
55+
The output format is inferred from the file extension:
56+
``.gif`` produces an animated GIF, ``.mp4`` produces a video.
57+
58+
Examples::
59+
60+
torchio animate brain.nii.gz brain.gif
61+
torchio animate brain.nii.gz brain.mp4 --seconds 10 --direction S
62+
"""
63+
64+
path: Annotated[Path, tyro.conf.Positional]
65+
"""Path to the input image."""
66+
67+
output: Annotated[Path, tyro.conf.Positional]
68+
"""Output path (.gif or .mp4)."""
69+
70+
seconds: float = 5.0
71+
"""Duration of the animation in seconds."""
72+
73+
direction: str = "I"
74+
"""Anatomical sweep direction (I, S, A, P, R, or L)."""
75+
76+
def run(self) -> None:
77+
image = tio.ScalarImage(self.path)
78+
suffix = self.output.suffix.lower()
79+
if suffix == ".gif":
80+
image.to_gif(
81+
self.output,
82+
seconds=self.seconds,
83+
direction=self.direction,
84+
)
85+
elif suffix == ".mp4":
86+
image.to_video(
87+
self.output,
88+
seconds=self.seconds,
89+
direction=self.direction,
90+
)
91+
else:
92+
msg = f"Unsupported output format {self.output.suffix!r}. Use .gif or .mp4."
93+
print(msg, file=sys.stderr)
94+
sys.exit(1)
95+
print(f"Created {self.output}")
96+
97+
5198
@dataclass
5299
class Info:
53100
"""Print image metadata to stdout."""
@@ -162,7 +209,7 @@ def run(self) -> None:
162209
self.command.run()
163210

164211

165-
Command = Union[Plot, Info, Convert, Transform, Cache] # noqa: UP007 (tyro needs Union)
212+
Command = Union[Plot, Animate, Info, Convert, Transform, Cache] # noqa: UP007 (tyro needs Union)
166213

167214

168215
# ---------------------------------------------------------------------------

tests/test_cli.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import numpy as np
1414
import pytest
1515

16+
from torchio.cli import Animate
1617
from torchio.cli import Cache
1718
from torchio.cli import Convert
1819
from torchio.cli import Dir
@@ -90,3 +91,20 @@ def test_plot_to_file(self, nii_path: Path, tmp_path: Path) -> None:
9091
Plot(path=nii_path, output=output).run()
9192
assert output.exists()
9293
assert output.stat().st_size > 0
94+
95+
96+
class TestAnimate:
97+
def test_animate_gif(self, nii_path: Path, tmp_path: Path) -> None:
98+
output = tmp_path / "anim.gif"
99+
Animate(path=nii_path, output=output, seconds=1.0, direction="I").run()
100+
assert output.exists()
101+
assert output.stat().st_size > 0
102+
103+
def test_animate_unsupported_format(
104+
self,
105+
nii_path: Path,
106+
tmp_path: Path,
107+
) -> None:
108+
output = tmp_path / "bad.avi"
109+
with pytest.raises(SystemExit):
110+
Animate(path=nii_path, output=output).run()

0 commit comments

Comments
 (0)