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
17 changes: 17 additions & 0 deletions localizer/src/core/common/PathUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ namespace Common::path {

namespace {

#ifdef _WIN32
bool containsNonAscii(const std::string& value) {
return std::any_of(value.begin(), value.end(), [](unsigned char ch) {
return ch > 0x7F;
});
}
#endif

std::string normalizeDirectory(const std::filesystem::path& directory) {
if (directory.empty()) {
return {};
Expand Down Expand Up @@ -103,6 +111,15 @@ std::string toUtf8(const std::filesystem::path& value) {

std::string legacyFileName(const std::filesystem::path& value) {
#ifdef _WIN32
const std::string utf8Path = toUtf8(value);
// ITK selects an ImageIO from the filename extension. Windows 8.3 paths
// collapse a compound NIfTI extension such as .nii.gz to .GZ, so keep
// ordinary ASCII paths intact and only fall back to a short path when it
// is needed for a non-ASCII filename.
if (!containsNonAscii(utf8Path)) {
return utf8Path;
}

if (const std::wstring shortPath = shortPathForExistingPath(value); !shortPath.empty()) {
return wideToUtf8(shortPath);
}
Expand Down
10 changes: 8 additions & 2 deletions python/dcccpy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ dcccpy.centiloid("amyloid.nii", suvr=True)
dcccpy.centaurz("tau.nii", report_detailed_regions=True)
dcccpy.fillstates("fdg.nii", tracer="fdg")
dcccpy.normalize("pet.nii", iterative=True)
dcccpy.pet_motion_correct(
"dynamic_pet.nii.gz",
"averaged_pet.nii.gz",
save_corrected_dynamic="corrected.nii.gz",
motion_output="motion.tsv",
)
dcccpy.run(["centiloid", "--input", "a.nii", "--output", "b.nii"])
```

Expand Down Expand Up @@ -142,15 +148,15 @@ the calculation again.
Release wheels should vendor the matching `DCCCcore` runtime tree before build:

```bash
python scripts/vendor_dccccore.py --version 4.2.4 --release-platform ubuntu-latest-x64
python scripts/vendor_dccccore.py --version 4.3.0 --release-platform ubuntu-latest-x64
python -m build --wheel
```

The Linux ARM64 runtime package uses the corresponding release asset:

```bash
cd python/dcccpy-linux-arm64-runtime
python scripts/vendor_dccccore.py --version 4.2.4 --release-platform ubuntu-latest-arm64
python scripts/vendor_dccccore.py --version 4.3.0 --release-platform ubuntu-latest-arm64
python -m build --wheel
```

Expand Down
2 changes: 1 addition & 1 deletion python/dcccpy/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "dcccpy"
version = "0.1.6"
version = "0.1.7"
description = "Python wrapper for the DCCCcore PET biomarker command-line tool"
readme = "README.md"
requires-python = ">=3.9"
Expand Down
2 changes: 1 addition & 1 deletion python/dcccpy/scripts/vendor_dccccore.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@


REPO = "tctco/DCCCSlicer"
DEFAULT_VERSION = "4.2.4"
DEFAULT_VERSION = "4.3.0"
DEFAULT_RELEASE_PLATFORM = "ubuntu-latest-x64"
DEFAULT_VENDOR_PLATFORM = "linux-x86_64"

Expand Down
2 changes: 2 additions & 0 deletions python/dcccpy/src/dcccpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
fillstates,
normalize,
parse_metrics,
pet_motion_correct,
rigid,
run,
suvr,
Expand All @@ -36,6 +37,7 @@
"fillstates",
"normalize",
"parse_metrics",
"pet_motion_correct",
"rigid",
"run",
"suvr",
Expand Down
39 changes: 39 additions & 0 deletions python/dcccpy/src/dcccpy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,42 @@ def rigid(
**kwargs: object,
) -> DCCCResult:
return _spatial_command("rigid", input, output, **kwargs)


def pet_motion_correct(
input: object,
output: str | os.PathLike[str] | None = None,
*,
save_corrected_dynamic: str | os.PathLike[str] | None = None,
motion_output: str | os.PathLike[str] | None = None,
check: bool = True,
executable: str | os.PathLike[str] | None = None,
) -> DCCCResult:
"""Rigidly align a multi-frame PET image and return its averaged 3D output."""

input_path, actual_output, temp_dir = _prepare_io(input, output)
args = [
"pet-motion-correct",
"--input",
input_path,
"--output",
os.fspath(actual_output),
]
if save_corrected_dynamic is not None:
corrected_path = _resolve_user_path(save_corrected_dynamic)
args.extend(["--save-corrected-dynamic", os.fspath(corrected_path)])
if motion_output is not None:
motion_path = _resolve_user_path(motion_output)
args.extend(["--motion-output", os.fspath(motion_path)])

result = run(args, check=check, executable=executable, output=actual_output)
return DCCCResult(
args=result.args,
returncode=result.returncode,
stdout=result.stdout,
stderr=result.stderr,
output=actual_output,
temp_dir=temp_dir,
executable=result.executable,
metrics=result.metrics,
)
2 changes: 1 addition & 1 deletion python/dcccpy/src/dcccpy/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from typing import Iterable


DCCCCORE_VERSION = "4.2.4"
DCCCCORE_VERSION = "4.3.0"
RELEASE_REPO = "tctco/DCCCSlicer"


Expand Down
57 changes: 54 additions & 3 deletions python/dcccpy/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@
import pytest

import dcccpy
from dcccpy.runtime import DCCCcoreNotFoundError, dccccore_path, find_existing_dccccore, release_platform
from dcccpy.runtime import (
DCCCCORE_VERSION,
DCCCcoreNotFoundError,
dccccore_path,
find_existing_dccccore,
release_platform,
release_url,
)


def make_fake_dccccore(tmp_path: Path) -> Path:
Expand Down Expand Up @@ -135,15 +142,15 @@ def test_dccccore_path_can_disable_auto_download(tmp_path: Path, monkeypatch: py


def test_dccccore_path_auto_downloads_to_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
asset_root = tmp_path / "asset" / "DCCCcore-4.2.4-ubuntu-latest-x64"
asset_root = tmp_path / "asset" / "DCCCcore-4.3.0-ubuntu-latest-x64"
asset_root.mkdir(parents=True)
exe = asset_root / "DCCCcore"
exe.write_text("#!/usr/bin/env sh\nprintf 'fake dccccore\\n'\n")
exe.chmod(0o755)

archive = tmp_path / "DCCCcore.zip"
with zipfile.ZipFile(archive, "w") as zf:
zf.write(exe, "DCCCcore-4.2.4-ubuntu-latest-x64/DCCCcore")
zf.write(exe, "DCCCcore-4.3.0-ubuntu-latest-x64/DCCCcore")

monkeypatch.delenv("DCCCPY_DCCCCORE", raising=False)
monkeypatch.setenv("DCCCPY_AUTO_DOWNLOAD", "1")
Expand Down Expand Up @@ -179,6 +186,13 @@ def test_linux_arm64_release_platform() -> None:
assert release_platform("linux-arm64") == "ubuntu-latest-arm64"


def test_default_release_targets_dccccore_430() -> None:
assert DCCCCORE_VERSION == "4.3.0"
assert release_url(platform_name="ubuntu-latest-x64").endswith(
"/releases/download/v4.3.0/DCCCcore-4.3.0-ubuntu-latest-x64.zip"
)


def test_find_existing_dccccore_checks_linux_arm64_runtime_package(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
Expand Down Expand Up @@ -211,6 +225,43 @@ def test_cli_forwards_raw_args(tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
assert "Metric: Centiloid" in capsys.readouterr().out


def test_pet_motion_correct_forwards_optional_outputs(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
fake_args = tmp_path / "args.txt"
fake_exe = make_fake_dccccore(tmp_path)
input_path = tmp_path / "dynamic.nii.gz"
output_path = tmp_path / "average.nii.gz"
corrected_path = tmp_path / "corrected.nii.gz"
motion_path = tmp_path / "motion.tsv"
input_path.write_text("fake dynamic PET")
monkeypatch.setenv("DCCCPY_DCCCCORE", str(fake_exe))
monkeypatch.setenv("DCCCPY_FAKE_ARGS", str(fake_args))

result = dcccpy.pet_motion_correct(
input_path,
output_path,
save_corrected_dynamic=corrected_path,
motion_output=motion_path,
)

assert result.returncode == 0
assert result.output == output_path
assert output_path.exists()
assert fake_args.read_text().splitlines() == [
"pet-motion-correct",
"--input",
str(input_path),
"--output",
str(output_path),
"--save-corrected-dynamic",
str(corrected_path),
"--motion-output",
str(motion_path),
]


def test_run_adds_macos_security_hint_for_blocked_runtime(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
Expand Down
Loading