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
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
name: Test on ${{ matrix.os }} with Python ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
fail-fast: true
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest, macos-latest]
Expand Down
18 changes: 15 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,39 @@ license = "MIT"
license-files = ["LICENSE"]

dependencies = [
"click",
"tqdm",
"numpy",
"rich-argparse",
"nibabel",
"pandas"]

[project.optional-dependencies]
extra = []
show = [
"textual-image", # Used to show images in terminal
"pillow", # Used to handle image data
"matplotlib" # Used for colormaps
]
napari = [
"napari[all]", # Used for interactive visualization
]
test = [
"pytest",
"pytest-cov",

]
pypi = [
"build" # Used for building wheels and uploading to pypi
]
all = ["mri-toolkit[extra,test,pypi]"]
all = ["mri-toolkit[extra,test,pypi,show,napari]"]


[project.urls]
Homepage = "https://github.com/scientificcomputing/mri-toolkit.git"


[project.scripts]
mritk = "mritk.cli:main"

[tool.mypy]
ignore_missing_imports = true # Does not show errors when importing untyped libraries
files = [ # Folder to which files that should be checked by mypy
Expand Down
2 changes: 1 addition & 1 deletion src/mritk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
meta = metadata("mri-toolkit")
__version__ = meta["Version"]
__author__ = meta["Author-email"]
__license__ = meta["License"]
__license__ = meta["license-expression"]
__email__ = meta["Author-email"]
__program_name__ = meta["Name"]

Expand Down
81 changes: 78 additions & 3 deletions src/mritk/cli.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,107 @@
import logging
from importlib.metadata import metadata
from pathlib import Path
import argparse
from typing import Sequence, Optional

from . import download_data
from rich_argparse import RichHelpFormatter

from . import download_data, info, statistics, show, napari


def version_info():
from rich.console import Console
from rich.table import Table
from rich import box
import sys
import nibabel as nib
import numpy as np

console = Console()

meta = metadata("mri-toolkit")
toolkit_version = meta["Version"]
python_version = sys.version.split()[0]

table = Table(
title="MRI Toolkit Environment",
box=box.ROUNDED, # Nice rounded corners
show_lines=True, # Separator lines between rows
header_style="bold magenta",
)

table.add_column("Package", style="cyan", no_wrap=True)
table.add_column("Version", style="green", justify="right")

table.add_row("mri-toolkit", toolkit_version)
table.add_row("Python", python_version)
table.add_row("Nibabel", nib.__version__)
table.add_row("Numpy", np.__version__)

console.print(table)


def setup_parser():
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser = argparse.ArgumentParser(formatter_class=RichHelpFormatter)
parser.add_argument("--version", action="store_true")

subparsers = parser.add_subparsers(dest="command")

# Download test data parser
download_parser = subparsers.add_parser("download-test-data", help="Download test data")
download_parser = subparsers.add_parser(
"download-test-data", help="Download test data", formatter_class=parser.formatter_class
)
download_parser.add_argument("outdir", type=Path, help="Output directory to download test data")

info_parser = subparsers.add_parser(
"info", help="Display information about a file", formatter_class=parser.formatter_class
)
info_parser.add_argument("file", type=Path, help="File to display information about")

info_parser.add_argument(
"--json", action="store_true", help="Output information in JSON format"
)

stats_parser = subparsers.add_parser(
"stats", help="Compute MRI statistics", formatter_class=parser.formatter_class
)
statistics.cli.add_arguments(stats_parser)

show_parser = subparsers.add_parser(
"show", help="Show MRI data in a terminal", formatter_class=parser.formatter_class
)
show.add_arguments(show_parser)

napari_parser = subparsers.add_parser(
"napari", help="Show MRI data using napari", formatter_class=parser.formatter_class
)
napari.add_arguments(napari_parser)

return parser


def dispatch(parser: argparse.ArgumentParser, argv: Optional[Sequence[str]] = None) -> int:
args = vars(parser.parse_args(argv))

if args.pop("version"):
version_info()
return 0
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
command = args.pop("command")
logger = logging.getLogger(__name__)
try:
if command == "download-test-data":
outdir = args.pop("outdir")
download_data.download_test_data(outdir)
elif command == "info":
file = args.pop("file")
info.nifty_info(file, json_output=args.pop("json"))
elif command == "stats":
statistics.cli.dispatch(args)
elif command == "show":
show.dispatch(args)
elif command == "napari":
napari.dispatch(args)
else:
logger.error(f"Unknown command {command}")
parser.print_help()
Expand Down
2 changes: 1 addition & 1 deletion src/mritk/data/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

def load_mri_data(
path: Path | str,
dtype: type,
dtype: type = np.float64,
orient: bool = True,
) -> MRIData:
suffix_regex = re.compile(r".+(?P<suffix>(\.nii(\.gz|)|\.mg(z|h)))")
Expand Down
91 changes: 91 additions & 0 deletions src/mritk/info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import json
import typing
from pathlib import Path
import numpy as np
import nibabel as nib
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich import box


def custom_json(obj):
if isinstance(obj, np.ndarray):
return obj.tolist()
elif np.isscalar(obj):
return float(obj)
else:
return str(obj)


def nifty_info(filename: Path, json_output: bool = False) -> dict[str, typing.Any]:
console = Console()

# 1. Load the NIfTI file

img = nib.load(str(filename))
header = img.header
affine = img.affine

# --- Part A: Extracting Dimensions & Resolution ---
img_shape = img.shape
zooms = header.get_zooms()
data_type = header.get_data_dtype()

data = {
"filename": str(filename),
"shape": img_shape,
"voxel_size_mm": zooms,
"data_type": data_type,
"affine": affine,
}

if json_output:
print(json.dumps(data, default=custom_json, indent=4))
return data

# Create a nice header panel
console.print(
Panel(
f"[bold blue]NIfTI File Analysis[/bold blue]\n[green]{filename}[/green]", expand=False
)
)

# Create a table for Basic Info
info_table = Table(
title="Basic Information",
box=box.SIMPLE_HEAVY,
show_header=True,
header_style="bold magenta",
)
info_table.add_column("Property", style="cyan")
info_table.add_column("Value", style="white")

# Format the tuples/lists as strings for the table
shape_str = ", ".join(map(str, img_shape))
zoom_str = ", ".join([f"{z:.2f}" for z in zooms])

info_table.add_row("Shape (x, y, z)", f"({shape_str})")
info_table.add_row("Voxel Size (mm)", f"({zoom_str})")
info_table.add_row("Data Type", str(data_type))

console.print(info_table)

# --- Part B: The Affine Matrix ---

console.print("\n[bold]Affine Transformation Matrix[/bold] (Voxel → World)", style="yellow")

# Create a specific table for the matrix to align numbers nicely
matrix_table = Table(show_header=False, box=box.ROUNDED, border_style="dim")

# Add 4 columns for the 4x4 matrix
for _ in range(4):
matrix_table.add_column(justify="right", style="green")

for row in affine:
# Format numbers to 4 decimal places for cleanliness
matrix_table.add_row(*[f"{val: .4f}" for val in row])

console.print(matrix_table)

return data
Loading
Loading