Skip to content
Draft
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
21 changes: 18 additions & 3 deletions .github/workflows/check_pull_request_title.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
name: "Check PR title"
name: Check PR title

on:
pull_request:
types: [edited, opened, synchronize, reopened]

permissions:
pull-requests: read
statuses: write # Required to update commit status (e.g. pass/fail)

jobs:
pr-title-check:
runs-on: ubuntu-latest
Expand All @@ -14,12 +19,22 @@ jobs:

- uses: naveenk1223/action-pr-title@master
with:
# Valid titles: "Do something"
# ^ Start of string
# [A-Z] First character must be an uppercase ASCII letter
# [a-zA-Z]* Followed by zero or more ASCII letters
# (?<![^s]s) Negative lookbehind: disallow a single 's' at the end of the first word
# ( .+)+ At least one space and one or more characters (requires more words)
# [^.] Final character must not be a period
# $ End of string
regex: '^[A-Z][a-zA-Z]*(?<![^s]s)( .+)+[^.]$'
# Valid titles:
# - "Do something"
# - "Address something"
# Invalid title:
# - "do something"
# - "Do something."
# - "Does something"
# - "Do"
regex: "^[A-Z][a-zA-Z]*(?<!s)( .+)+[^.]$" # Thanks, ChatGPT
# - "Addresses something"
min_length: 10
max_length: 72
4 changes: 4 additions & 0 deletions .github/workflows/code_quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
schedule:
- cron: "0 4 * * *"

permissions:
contents: read
statuses: write

env:
FORCE_COLOR: 1

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
push:
workflow_dispatch:

permissions:
pull-requests: read

jobs:
build:
name: Build the package
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
schedule:
- cron: "0 4 * * *"

permissions:
contents: read
statuses: write

env:
FORCE_COLOR: 1

Expand Down
14 changes: 7 additions & 7 deletions src/torchio/datasets/ixi.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def _check_exists(root, modalities):
return exists

@staticmethod
def _get_subjects_list(root, modalities):
def _get_subjects_list(root: Path, modalities: Sequence[str]) -> list[Subject]:
# The number of files for each modality is not the same
# E.g. 581 for T1, 578 for T2
# Let's just use the first modality as reference for now
Expand Down Expand Up @@ -137,7 +137,7 @@ def _get_subjects_list(root, modalities):
subjects.append(Subject(**images_dict))
return subjects

def _download(self, root, modalities):
def _download(self, root: Path, modalities: Sequence[str]) -> None:
"""Download the IXI data if it does not exist already."""
for modality in modalities:
modality_dir = root / modality
Expand Down Expand Up @@ -195,7 +195,7 @@ def __init__(
super().__init__(subjects_list, transform=transform, **kwargs)

@staticmethod
def _get_subjects_list(root):
def _get_subjects_list(root: Path) -> list[Subject]:
image_paths = sglob(root / 'image', '*.nii.gz')
label_paths = sglob(root / 'label', '*.nii.gz')
if not (image_paths and label_paths):
Expand All @@ -214,7 +214,7 @@ def _get_subjects_list(root):
subjects.append(Subject(**subject_dict))
return subjects

def _download(self, root):
def _download(self, root: Path) -> None:
"""Download the tiny IXI data if it doesn't exist already."""
if root.is_dir(): # assume it's been downloaded
print('Root directory for IXITiny found:', root) # noqa: T201
Expand All @@ -234,9 +234,9 @@ def _download(self, root):
shutil.rmtree(ixi_tiny_dir)


def sglob(directory, pattern):
return sorted(Path(directory).glob(pattern))
def sglob(directory: Path, pattern: str) -> list[Path]:
return sorted(directory.glob(pattern))


def get_subject_id(path):
def get_subject_id(path: Path) -> str:
return '-'.join(path.name.split('-')[:-1])
83 changes: 53 additions & 30 deletions src/torchio/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
from .types import TypeNumber
from .types import TypePath

ITK_SNAP = 'ITK-SNAP'
SLICER = 'Slicer'


def to_tuple(
value: Any,
Expand Down Expand Up @@ -344,44 +347,64 @@ def add_images_from_batch(
def guess_external_viewer() -> Path | None:
"""Guess the path to an executable that could be used to visualize images.

Currently, it looks for 1) ITK-SNAP and 2) 3D Slicer. Implemented
for macOS and Windows.
It looks for 1) ITK-SNAP and 2) 3D Slicer.
"""
if 'SITK_SHOW_COMMAND' in os.environ:
return Path(os.environ['SITK_SHOW_COMMAND'])
platform = sys.platform
itk = 'ITK-SNAP'
slicer = 'Slicer'
if platform == 'darwin':

if (platform := sys.platform) == 'darwin':
return _guess_macos_viewer()
elif platform == 'win32':
return _guess_windows_viewer()
elif 'linux' in platform:
return _guess_linux_viewer()
else:
return None


def _guess_macos_viewer() -> Path | None:
def _get_app_path(app_name: str) -> Path:
app_path = '/Applications/{}.app/Contents/MacOS/{}'
itk_snap_path = Path(app_path.format(2 * (itk,)))
return Path(app_path.format(2 * (app_name,)))

if (itk_snap_path := _get_app_path(ITK_SNAP)).is_file():
return itk_snap_path
elif (slicer_path := _get_app_path(SLICER)).is_file():
return slicer_path
else:
return None


def _guess_windows_viewer() -> Path | None:
def _get_app_path(app_dirs: list[Path], bin_name: str) -> Path:
app_dir = app_dirs[-1]
app_path = app_dir / bin_name
if app_path.is_file():
return app_path

program_files_dir = Path(os.environ['ProgramW6432'])
itk_snap_dirs = list(program_files_dir.glob(f'{ITK_SNAP}*'))
slicer_dirs = list(program_files_dir.glob(f'{SLICER}*'))

if itk_snap_dirs:
itk_snap_path = _get_app_path(itk_snap_dirs, 'bin/itk-snap.exe')
if itk_snap_path.is_file():
return itk_snap_path
slicer_path = Path(app_path.format(2 * (slicer,)))
elif slicer_dirs:
slicer_path = _get_app_path(slicer_dirs, 'slicer.exe')
if slicer_path.is_file():
return slicer_path
elif platform == 'win32':
program_files_dir = Path(os.environ['ProgramW6432'])
itk_snap_dirs = list(program_files_dir.glob('ITK-SNAP*'))
if itk_snap_dirs:
itk_snap_dir = itk_snap_dirs[-1]
itk_snap_path = itk_snap_dir / 'bin/itk-snap.exe'
if itk_snap_path.is_file():
return itk_snap_path
slicer_dirs = list(program_files_dir.glob('Slicer*'))
if slicer_dirs:
slicer_dir = slicer_dirs[-1]
slicer_path = slicer_dir / 'slicer.exe'
if slicer_path.is_file():
return slicer_path
elif 'linux' in platform:
itk_snap_which = shutil.which('itksnap')
if itk_snap_which is not None:
return Path(itk_snap_which)
slicer_which = shutil.which('Slicer')
if slicer_which is not None:
return Path(slicer_which)
return None # for mypy
else:
return None


def _guess_linux_viewer() -> Path | None:
if (itk_snap_which := shutil.which('itksnap')) is not None:
return Path(itk_snap_which)
elif (slicer_which := shutil.which('Slicer')) is not None:
return Path(slicer_which)
else:
return None


def parse_spatial_shape(shape):
Expand Down
Loading