pyfsviz 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyfsviz/__init__.py +26 -0
- pyfsviz/__main__.py +14 -0
- pyfsviz/_internal/__init__.py +0 -0
- pyfsviz/_internal/cli.py +59 -0
- pyfsviz/_internal/debug.py +110 -0
- pyfsviz/_internal/html/individual.html +120 -0
- pyfsviz/_internal/mni305.cor.mgz +0 -0
- pyfsviz/_internal/mni305.cor.nii.gz +0 -0
- pyfsviz/freesurfer.py +616 -0
- pyfsviz/py.typed +0 -0
- pyfsviz/reports.py +48 -0
- pyfsviz-0.1.1.dist-info/METADATA +57 -0
- pyfsviz-0.1.1.dist-info/RECORD +16 -0
- pyfsviz-0.1.1.dist-info/WHEEL +4 -0
- pyfsviz-0.1.1.dist-info/entry_points.txt +5 -0
- pyfsviz-0.1.1.dist-info/licenses/LICENSE +21 -0
pyfsviz/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""pyFSViz package.
|
|
2
|
+
|
|
3
|
+
Python tools for FreeSurfer visualization and QA
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pyfsviz import freesurfer, reports
|
|
9
|
+
from pyfsviz._internal.cli import get_parser, main
|
|
10
|
+
from pyfsviz.freesurfer import (
|
|
11
|
+
FreeSurfer,
|
|
12
|
+
get_freesurfer_colormap,
|
|
13
|
+
)
|
|
14
|
+
from pyfsviz.reports import (
|
|
15
|
+
Template,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__: list[str] = [
|
|
19
|
+
"FreeSurfer",
|
|
20
|
+
"Template",
|
|
21
|
+
"freesurfer",
|
|
22
|
+
"get_freesurfer_colormap",
|
|
23
|
+
"get_parser",
|
|
24
|
+
"main",
|
|
25
|
+
"reports",
|
|
26
|
+
]
|
pyfsviz/__main__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Entry-point module, in case you use `python -m pyfsviz`.
|
|
2
|
+
|
|
3
|
+
Why does this file exist, and why `__main__`? For more info, read:
|
|
4
|
+
|
|
5
|
+
- https://www.python.org/dev/peps/pep-0338/
|
|
6
|
+
- https://docs.python.org/3/using/cmdline.html#cmdoption-m
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from pyfsviz._internal.cli import main
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
sys.exit(main(sys.argv[1:]))
|
|
File without changes
|
pyfsviz/_internal/cli.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Why does this file exist, and why not put this in `__main__`?
|
|
2
|
+
#
|
|
3
|
+
# You might be tempted to import things from `__main__` later,
|
|
4
|
+
# but that will cause problems: the code will get executed twice:
|
|
5
|
+
#
|
|
6
|
+
# - When you run `python -m pyfsviz` python will execute
|
|
7
|
+
# `__main__.py` as a script. That means there won't be any
|
|
8
|
+
# `pyfsviz.__main__` in `sys.modules`.
|
|
9
|
+
# - When you import `__main__` it will get executed again (as a module) because
|
|
10
|
+
# there's no `pyfsviz.__main__` in `sys.modules`.
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from pyfsviz._internal import debug
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _DebugInfo(argparse.Action):
|
|
22
|
+
def __init__(self, nargs: int | str | None = 0, **kwargs: Any) -> None:
|
|
23
|
+
super().__init__(nargs=nargs, **kwargs)
|
|
24
|
+
|
|
25
|
+
def __call__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
|
|
26
|
+
debug._print_debug_info()
|
|
27
|
+
sys.exit(0)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_parser() -> argparse.ArgumentParser:
|
|
31
|
+
"""Return the CLI argument parser.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
An argparse parser.
|
|
36
|
+
"""
|
|
37
|
+
parser = argparse.ArgumentParser(prog="pyfsviz")
|
|
38
|
+
parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {debug._get_version()}")
|
|
39
|
+
parser.add_argument("--debug-info", action=_DebugInfo, help="Print debug information.")
|
|
40
|
+
return parser
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main(args: list[str] | None = None) -> int:
|
|
44
|
+
"""Run the main program.
|
|
45
|
+
|
|
46
|
+
This function is executed when you type `pyfsviz` or `python -m pyfsviz`.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
args: Arguments passed from the command line.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
An exit code.
|
|
55
|
+
"""
|
|
56
|
+
parser = get_parser()
|
|
57
|
+
opts = parser.parse_args(args=args)
|
|
58
|
+
print(opts)
|
|
59
|
+
return 0
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from importlib import metadata
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class _Variable:
|
|
12
|
+
"""Dataclass describing an environment variable."""
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
"""Variable name."""
|
|
16
|
+
value: str
|
|
17
|
+
"""Variable value."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class _Package:
|
|
22
|
+
"""Dataclass describing a Python package."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
"""Package name."""
|
|
26
|
+
version: str
|
|
27
|
+
"""Package version."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class _Environment:
|
|
32
|
+
"""Dataclass to store environment information."""
|
|
33
|
+
|
|
34
|
+
interpreter_name: str
|
|
35
|
+
"""Python interpreter name."""
|
|
36
|
+
interpreter_version: str
|
|
37
|
+
"""Python interpreter version."""
|
|
38
|
+
interpreter_path: str
|
|
39
|
+
"""Path to Python executable."""
|
|
40
|
+
platform: str
|
|
41
|
+
"""Operating System."""
|
|
42
|
+
packages: list[_Package]
|
|
43
|
+
"""Installed packages."""
|
|
44
|
+
variables: list[_Variable]
|
|
45
|
+
"""Environment variables."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _interpreter_name_version() -> tuple[str, str]:
|
|
49
|
+
if hasattr(sys, "implementation"):
|
|
50
|
+
impl = sys.implementation.version
|
|
51
|
+
version = f"{impl.major}.{impl.minor}.{impl.micro}"
|
|
52
|
+
kind = impl.releaselevel
|
|
53
|
+
if kind != "final":
|
|
54
|
+
version += kind[0] + str(impl.serial)
|
|
55
|
+
return sys.implementation.name, version
|
|
56
|
+
return "", "0.0.0"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_version(dist: str = "pyfsviz") -> str:
|
|
60
|
+
"""Get version of the given distribution.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
dist: A distribution name.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
A version number.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
return metadata.version(dist)
|
|
72
|
+
except metadata.PackageNotFoundError:
|
|
73
|
+
return "0.0.0"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _get_debug_info() -> _Environment:
|
|
77
|
+
"""Get debug/environment information.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
Environment information.
|
|
82
|
+
"""
|
|
83
|
+
py_name, py_version = _interpreter_name_version()
|
|
84
|
+
packages = ["pyfsviz"]
|
|
85
|
+
variables = ["PYTHONPATH", *[var for var in os.environ if var.startswith("PYFSVIZ")]]
|
|
86
|
+
return _Environment(
|
|
87
|
+
interpreter_name=py_name,
|
|
88
|
+
interpreter_version=py_version,
|
|
89
|
+
interpreter_path=sys.executable,
|
|
90
|
+
platform=platform.platform(),
|
|
91
|
+
variables=[_Variable(var, val) for var in variables if (val := os.getenv(var))], # ty: ignore[invalid-argument-type]
|
|
92
|
+
packages=[_Package(pkg, _get_version(pkg)) for pkg in packages],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _print_debug_info() -> None:
|
|
97
|
+
"""Print debug/environment information."""
|
|
98
|
+
info = _get_debug_info()
|
|
99
|
+
print(f"- __System__: {info.platform}")
|
|
100
|
+
print(f"- __Python__: {info.interpreter_name} {info.interpreter_version} ({info.interpreter_path})")
|
|
101
|
+
print("- __Environment variables__:")
|
|
102
|
+
for var in info.variables:
|
|
103
|
+
print(f" - `{var.name}`: `{var.value}`")
|
|
104
|
+
print("- __Installed packages__:")
|
|
105
|
+
for pkg in info.packages:
|
|
106
|
+
print(f" - `{pkg.name}` v{pkg.version}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
_print_debug_info()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8" ?>
|
|
2
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
3
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
6
|
+
<meta name="generator" content="MRIQC" />
|
|
7
|
+
<title>FreeSurfer: Individual Report</title>
|
|
8
|
+
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
|
9
|
+
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
|
10
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
|
11
|
+
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
|
|
12
|
+
|
|
13
|
+
<style type="text/css">
|
|
14
|
+
body {
|
|
15
|
+
font-family: helvetica;
|
|
16
|
+
padding: 50px 10px 10px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
div.warning p.admonition-title, .code .error {
|
|
20
|
+
color: red ;
|
|
21
|
+
font-weight: bold ;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
span.problematic {
|
|
25
|
+
color: red;
|
|
26
|
+
font-weight: bold ;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
p.label {
|
|
30
|
+
white-space: nowrap }
|
|
31
|
+
|
|
32
|
+
span.section-subtitle {
|
|
33
|
+
/* font-size relative to parent (h1..h6 element) */
|
|
34
|
+
font-size: 80% }
|
|
35
|
+
|
|
36
|
+
div.embeded-report {
|
|
37
|
+
width: 100%;
|
|
38
|
+
page-break-before:always;
|
|
39
|
+
padding-top: 20px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
div.embeded-report svg { width: 100%; }
|
|
43
|
+
|
|
44
|
+
div#accordionOther {
|
|
45
|
+
margin: 0 20px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.add-padding {
|
|
49
|
+
padding-top: 15px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#report-qi2-fitting {
|
|
53
|
+
max-width: 450px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
</style>
|
|
57
|
+
</head>
|
|
58
|
+
<body>
|
|
59
|
+
<div class="document">
|
|
60
|
+
|
|
61
|
+
<nav class="navbar fixed-top navbar-expand-lg navbar-light bg-light">
|
|
62
|
+
<div class="collapse navbar-collapse">
|
|
63
|
+
<ul class="navbar-nav">
|
|
64
|
+
<li class="nav-item"><a class="nav-link" href="#summary">Summary</a></li>
|
|
65
|
+
<li class="nav-item"><a class="nav-link" href="#tlrc">Talairach Registration</a></li>
|
|
66
|
+
<li class="nav-item"><a class="nav-link" href="#aseg">Aparc+Aseg Parcellations</a></li>
|
|
67
|
+
<li class="nav-item"><a class="nav-link" href="#surf">Surfaces</a></li>
|
|
68
|
+
</ul>
|
|
69
|
+
</div>
|
|
70
|
+
</nav>
|
|
71
|
+
<noscript>
|
|
72
|
+
<h1 class="text-danger"> The navigation menu uses Javascript. Without it this report might not work as expected </h1>
|
|
73
|
+
</noscript>
|
|
74
|
+
|
|
75
|
+
<h1 class="mt-5 mb-5">FreeSurfer: Individual Report</h1>
|
|
76
|
+
|
|
77
|
+
<div class="card mt-3" style="width: 480pt;">
|
|
78
|
+
<h2 id="summary" class="card-header">Summary</h2>
|
|
79
|
+
<div class="card-body">
|
|
80
|
+
<ul class="simple">
|
|
81
|
+
<li>Date and time: {{ timestamp }}.</li>
|
|
82
|
+
</ul>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<h2 id="tlrc" class="mt-5 mb-2">Talairach Registration</h2>
|
|
87
|
+
{% for tag in tlrc %}
|
|
88
|
+
<div class="card mt-2">
|
|
89
|
+
<div id="report-tlrc" class="card-body">
|
|
90
|
+
{{ tag }}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
{% endfor %}
|
|
94
|
+
|
|
95
|
+
<h2 id="aseg" class="mt-5 mb-2">Aparc+Aseg Parcellations</h2>
|
|
96
|
+
{% for tag in aseg %}
|
|
97
|
+
<div class="card mt-2">
|
|
98
|
+
<div id="report-aseg" class="card-body">
|
|
99
|
+
{{ tag }}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
{% endfor %}
|
|
103
|
+
|
|
104
|
+
<h2 id="surf" class="mt-5 mb-2">Surfaces</h2>
|
|
105
|
+
{% for label, tag in surf %}
|
|
106
|
+
<div class="card mt-2">
|
|
107
|
+
<h4 id="surf" class="mt-5 mb-2">{{ label }}</h4>
|
|
108
|
+
<div id="report-{{ label }}" class="card-body">
|
|
109
|
+
{{ tag }}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
{% endfor %}
|
|
113
|
+
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
</body>
|
|
120
|
+
</html>
|
|
Binary file
|
|
Binary file
|
pyfsviz/freesurfer.py
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
"""FreeSurfer data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
from importlib_resources import files
|
|
13
|
+
from matplotlib import colors
|
|
14
|
+
from matplotlib import pyplot as plt
|
|
15
|
+
from nilearn import plotting
|
|
16
|
+
from nipype.interfaces.freesurfer import MRIConvert
|
|
17
|
+
from nipype.interfaces.fsl import FLIRT
|
|
18
|
+
from nireports.interfaces.reporting.base import SimpleBeforeAfterRPT
|
|
19
|
+
|
|
20
|
+
from pyfsviz.reports import Template
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_freesurfer_colormap(freesurfer_home: Path | str) -> colors.ListedColormap:
|
|
24
|
+
"""Generate matplotlib colormap from FreeSurfer LUT.
|
|
25
|
+
|
|
26
|
+
Code from:
|
|
27
|
+
https://github.com/Deep-MI/qatools-python/blob/freesurfer-module-releases/qatoolspython/createScreenshots.py
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
freesurfer_home : path or str representing a path to a directory
|
|
32
|
+
Path corresponding to FREESURFER_HOME env var.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
colormap : matplotlib.colors.ListedColormap
|
|
37
|
+
A matplotlib compatible FreeSurfer colormap.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
freesurfer_home = Path(freesurfer_home) if isinstance(freesurfer_home, str) else freesurfer_home
|
|
41
|
+
lut = pd.read_csv(
|
|
42
|
+
freesurfer_home / "FreeSurferColorLUT.txt",
|
|
43
|
+
sep=r"\s+",
|
|
44
|
+
comment="#",
|
|
45
|
+
header=None,
|
|
46
|
+
skipinitialspace=True,
|
|
47
|
+
skip_blank_lines=True,
|
|
48
|
+
)
|
|
49
|
+
lut = np.array(lut)
|
|
50
|
+
lut_tab = np.array(lut[:, (2, 3, 4, 5)] / 255, dtype="float32")
|
|
51
|
+
lut_tab[:, 3] = 1
|
|
52
|
+
|
|
53
|
+
return colors.ListedColormap(lut_tab)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FreeSurfer:
|
|
57
|
+
"""Base class for FreeSurfer data."""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
freesurfer_home: str | None = None,
|
|
62
|
+
subjects_dir: str | None = None,
|
|
63
|
+
log_level: str = "INFO",
|
|
64
|
+
):
|
|
65
|
+
"""Initialize the FreeSurfer data.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
freesurfer_home : str representing a path to a directory
|
|
70
|
+
Path corresponding to FREESURFER_HOME env var.
|
|
71
|
+
subjects_dir : str representing a path to a directory
|
|
72
|
+
Path corresponding to SUBJECTS_DIR env var.
|
|
73
|
+
log_level : str
|
|
74
|
+
Logging level (e.g., "INFO", "DEBUG", "WARNING").
|
|
75
|
+
Default is "INFO".
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
None
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
# Set up logger
|
|
83
|
+
logging.basicConfig(level=getattr(logging, log_level.upper()))
|
|
84
|
+
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
|
85
|
+
"""Logger for the FreeSurfer class."""
|
|
86
|
+
|
|
87
|
+
if freesurfer_home is None:
|
|
88
|
+
self.freesurfer_home = Path(os.environ.get("FREESURFER_HOME") or "")
|
|
89
|
+
"""Path to the FreeSurfer home directory."""
|
|
90
|
+
else:
|
|
91
|
+
self.freesurfer_home = Path(freesurfer_home)
|
|
92
|
+
"""Path to the FreeSurfer home directory."""
|
|
93
|
+
if not self.freesurfer_home.exists():
|
|
94
|
+
raise FileNotFoundError(f"FREESURFER_HOME not found: {self.freesurfer_home}")
|
|
95
|
+
if self.freesurfer_home is None:
|
|
96
|
+
raise ValueError("FREESURFER_HOME must be set")
|
|
97
|
+
|
|
98
|
+
if subjects_dir is None:
|
|
99
|
+
self.subjects_dir = Path(os.environ.get("SUBJECTS_DIR") or "")
|
|
100
|
+
"""Path to the subjects directory."""
|
|
101
|
+
else:
|
|
102
|
+
self.subjects_dir = Path(subjects_dir)
|
|
103
|
+
"""Path to the subjects directory."""
|
|
104
|
+
if not self.subjects_dir.exists():
|
|
105
|
+
raise FileNotFoundError(f"SUBJECTS_DIR not found: {self.subjects_dir}")
|
|
106
|
+
"""Path to the subjects directory."""
|
|
107
|
+
self._mni_nii = files("pyfsviz._internal") / "mni305.cor.nii.gz"
|
|
108
|
+
"""Path to the MNI template NIfTI file."""
|
|
109
|
+
self._mni_mgz = files("pyfsviz._internal") / "mni305.cor.mgz"
|
|
110
|
+
"""Path to the MNI template MGH file."""
|
|
111
|
+
|
|
112
|
+
def get_colormap(self) -> colors.ListedColormap:
|
|
113
|
+
"""Return the colormap for the FreeSurfer data."""
|
|
114
|
+
return get_freesurfer_colormap(self.freesurfer_home)
|
|
115
|
+
|
|
116
|
+
def get_subjects(self) -> list[str]:
|
|
117
|
+
"""Return the subjects in the subjects directory."""
|
|
118
|
+
return [
|
|
119
|
+
subject.name
|
|
120
|
+
for subject in self.subjects_dir.iterdir()
|
|
121
|
+
if subject.is_dir() and (subject / "mri" / "transforms" / "talairach.lta").exists()
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
def check_recon_all(self, subject: str) -> bool:
|
|
125
|
+
"""Verify that the subject's FreeSurfer recon finished successfully."""
|
|
126
|
+
recon_file = self.subjects_dir / subject / "scripts" / "recon-all.log"
|
|
127
|
+
|
|
128
|
+
with open(recon_file, encoding="utf-8") as f:
|
|
129
|
+
line = f.readlines()[-1]
|
|
130
|
+
return "finished without error" in line
|
|
131
|
+
|
|
132
|
+
def gen_tlrc_data(self, subject: str, output_dir: str) -> None:
|
|
133
|
+
"""Generate inverse talairach data for report generation.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
output_dir : str
|
|
138
|
+
Path for intermediate file output.
|
|
139
|
+
|
|
140
|
+
Examples
|
|
141
|
+
--------
|
|
142
|
+
>>> from pyfsviz.freesurfer import FreeSurfer
|
|
143
|
+
>>> fs_dir = FreeSurfer(
|
|
144
|
+
... freesurfer_home="/opt/freesurfer",
|
|
145
|
+
... subjects_dir="/opt/data",
|
|
146
|
+
... subject="sub-001",
|
|
147
|
+
... )
|
|
148
|
+
>>> fs_dir.gen_tlrc_data("sub-001", Path("/opt/data/sub-001/mri/transforms"))
|
|
149
|
+
"""
|
|
150
|
+
# get inverse transform
|
|
151
|
+
lta_file = self.subjects_dir / subject / "mri" / "transforms" / "talairach.xfm.lta"
|
|
152
|
+
xfm = np.genfromtxt(lta_file, skip_header=5, max_rows=4)
|
|
153
|
+
inverse_xfm = np.linalg.inv(xfm)
|
|
154
|
+
np.savetxt(
|
|
155
|
+
f"{output_dir}/inv.xfm",
|
|
156
|
+
inverse_xfm,
|
|
157
|
+
fmt="%0.8f",
|
|
158
|
+
delimiter=" ",
|
|
159
|
+
newline="\n",
|
|
160
|
+
encoding="utf-8",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# convert subject original T1 to nifti (for FSL)
|
|
164
|
+
convert = MRIConvert(
|
|
165
|
+
in_file=self.subjects_dir / subject / "mri" / "orig.mgz",
|
|
166
|
+
out_file=f"{output_dir}/orig.nii.gz",
|
|
167
|
+
out_type="niigz",
|
|
168
|
+
)
|
|
169
|
+
convert.run()
|
|
170
|
+
|
|
171
|
+
# use FSL to convert template file to subject original space
|
|
172
|
+
flirt = FLIRT(
|
|
173
|
+
in_file=self._mni_nii,
|
|
174
|
+
reference=f"{output_dir}/orig.nii.gz",
|
|
175
|
+
out_file=f"{output_dir}/mni2orig.nii.gz",
|
|
176
|
+
in_matrix_file=f"{output_dir}/inv.xfm",
|
|
177
|
+
apply_xfm=True,
|
|
178
|
+
out_matrix_file=f"{output_dir}/out.mat",
|
|
179
|
+
)
|
|
180
|
+
flirt.run()
|
|
181
|
+
|
|
182
|
+
def gen_tlrc_report(
|
|
183
|
+
self,
|
|
184
|
+
subject: str,
|
|
185
|
+
output_dir: str,
|
|
186
|
+
tlrc_dir: str | None = None,
|
|
187
|
+
*,
|
|
188
|
+
gen_data: bool = True,
|
|
189
|
+
) -> Path:
|
|
190
|
+
"""Generate a before and after report of Talairach registration. (Will also run file generation if needed).
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
subject : str
|
|
195
|
+
Subject ID.
|
|
196
|
+
output_dir : str
|
|
197
|
+
Path to SVG output.
|
|
198
|
+
gen_data : bool
|
|
199
|
+
Generate inverse Talairach data, by default True
|
|
200
|
+
tlrc_dir : str | None
|
|
201
|
+
Path to output of `gen_tlrc_data`. Default is the subject's mri/transforms directory.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
Path:
|
|
206
|
+
SVG file generated from the niworkflows SimpleBeforeAfterRPT
|
|
207
|
+
|
|
208
|
+
Examples
|
|
209
|
+
--------
|
|
210
|
+
>>> from pyfsviz.freesurfer import FreeSurfer
|
|
211
|
+
>>> fs_dir = FreeSurfer(
|
|
212
|
+
... freesurfer_home="/opt/freesurfer",
|
|
213
|
+
... subjects_dir="/opt/data",
|
|
214
|
+
... subject="sub-001",
|
|
215
|
+
... )
|
|
216
|
+
>>> report = fs_dir.gen_tlrc_report(
|
|
217
|
+
... "sub-001", Path("/opt/data/sub-001/mri/transforms")
|
|
218
|
+
... )
|
|
219
|
+
"""
|
|
220
|
+
if tlrc_dir is None:
|
|
221
|
+
tlrc_dir = f"{self.subjects_dir}/{subject}/mri/transforms"
|
|
222
|
+
|
|
223
|
+
mri_dir = f"{self.subjects_dir}/{subject}/mri"
|
|
224
|
+
|
|
225
|
+
if gen_data:
|
|
226
|
+
self.gen_tlrc_data(subject, tlrc_dir)
|
|
227
|
+
|
|
228
|
+
# use white matter segmentation to compare registrations
|
|
229
|
+
report = SimpleBeforeAfterRPT(
|
|
230
|
+
before=f"{mri_dir}/orig.mgz",
|
|
231
|
+
after=f"{tlrc_dir}/mni2orig.nii.gz",
|
|
232
|
+
wm_seg=f"{mri_dir}/wm.mgz",
|
|
233
|
+
before_label="Subject Orig",
|
|
234
|
+
after_label="Template",
|
|
235
|
+
out_report=f"{output_dir}/tlrc.svg",
|
|
236
|
+
)
|
|
237
|
+
result = report.run()
|
|
238
|
+
return result.outputs.out_report
|
|
239
|
+
|
|
240
|
+
def gen_aparcaseg_plots(self, subject: str, output_dir: str, num_imgs: int = 10) -> list[Path]:
|
|
241
|
+
"""Generate parcellation images (aparc & aseg).
|
|
242
|
+
|
|
243
|
+
Parameters
|
|
244
|
+
----------
|
|
245
|
+
output_dir : str
|
|
246
|
+
Path to output directory.
|
|
247
|
+
num_imgs : int
|
|
248
|
+
Number of images/slices to make.
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
list
|
|
253
|
+
List of SVG image paths
|
|
254
|
+
|
|
255
|
+
Examples
|
|
256
|
+
--------
|
|
257
|
+
>>> from pyfsviz.freesurfer import FreeSurfer
|
|
258
|
+
>>> fs_dir = FreeSurfer(
|
|
259
|
+
... freesurfer_home="/opt/freesurfer",
|
|
260
|
+
... subjects_dir="/opt/data",
|
|
261
|
+
... subject="sub-001",
|
|
262
|
+
... )
|
|
263
|
+
>>> images = fs_dir.gen_aparcaseg_plots(
|
|
264
|
+
... "sub-001", Path("/opt/data/sub-001/mri/transforms")
|
|
265
|
+
... )
|
|
266
|
+
"""
|
|
267
|
+
mri_dir = f"{self.subjects_dir}/{subject}/mri"
|
|
268
|
+
cmap = self.get_colormap()
|
|
269
|
+
|
|
270
|
+
# get parcellation and segmentation images
|
|
271
|
+
plotting.plot_roi(
|
|
272
|
+
f"{mri_dir}/aparc+aseg.mgz",
|
|
273
|
+
f"{mri_dir}/T1.mgz",
|
|
274
|
+
cmap=cmap,
|
|
275
|
+
display_mode="mosaic",
|
|
276
|
+
dim=-1,
|
|
277
|
+
cut_coords=num_imgs,
|
|
278
|
+
alpha=0.5,
|
|
279
|
+
output_file=f"{output_dir}/aseg.svg",
|
|
280
|
+
)
|
|
281
|
+
display = plotting.plot_anat(
|
|
282
|
+
f"{mri_dir}/brainmask.mgz",
|
|
283
|
+
display_mode="mosaic",
|
|
284
|
+
cut_coords=num_imgs,
|
|
285
|
+
dim=-1,
|
|
286
|
+
)
|
|
287
|
+
display.add_contours(
|
|
288
|
+
f"{mri_dir}/lh.ribbon.mgz",
|
|
289
|
+
colors="b",
|
|
290
|
+
linewidths=0.5,
|
|
291
|
+
levels=[0.5],
|
|
292
|
+
)
|
|
293
|
+
display.add_contours(
|
|
294
|
+
f"{mri_dir}/rh.ribbon.mgz",
|
|
295
|
+
colors="r",
|
|
296
|
+
linewidths=0.5,
|
|
297
|
+
levels=[0.5],
|
|
298
|
+
)
|
|
299
|
+
display.savefig(f"{output_dir}/aparc.svg")
|
|
300
|
+
display.close()
|
|
301
|
+
|
|
302
|
+
return [Path(f"{output_dir}/aseg.svg"), Path(f"{output_dir}/aparc.svg")]
|
|
303
|
+
|
|
304
|
+
def gen_surf_plots(self, subject: str, output_dir: str) -> list[Path]:
|
|
305
|
+
"""Generate pial, inflated, and sulcal images from various viewpoints.
|
|
306
|
+
|
|
307
|
+
Parameters
|
|
308
|
+
----------
|
|
309
|
+
output_dir : str
|
|
310
|
+
Surface plot output directory.
|
|
311
|
+
|
|
312
|
+
Returns
|
|
313
|
+
-------
|
|
314
|
+
list[Path]:
|
|
315
|
+
List of generated SVG images
|
|
316
|
+
|
|
317
|
+
Examples
|
|
318
|
+
--------
|
|
319
|
+
>>> from pyfsviz.freesurfer import FreeSurfer
|
|
320
|
+
>>> fs_dir = FreeSurfer(
|
|
321
|
+
... freesurfer_home="/opt/freesurfer",
|
|
322
|
+
... subjects_dir="/opt/data",
|
|
323
|
+
... subject="sub-001",
|
|
324
|
+
... )
|
|
325
|
+
>>> images = fs_dir.gen_surf_plots("sub-001", Path("/opt/data/sub-001/surf"))
|
|
326
|
+
"""
|
|
327
|
+
surf_dir = f"{self.subjects_dir}/{subject}/surf"
|
|
328
|
+
label_dir = f"{self.subjects_dir}/{subject}/label"
|
|
329
|
+
cmap = self.get_colormap()
|
|
330
|
+
|
|
331
|
+
hemis = {"lh": "left", "rh": "right"}
|
|
332
|
+
for key, val in hemis.items():
|
|
333
|
+
pial = f"{surf_dir}/{key}.pial"
|
|
334
|
+
inflated = f"{surf_dir}/{key}.inflated"
|
|
335
|
+
sulc = f"{surf_dir}/{key}.sulc"
|
|
336
|
+
white = f"{surf_dir}/{key}.white"
|
|
337
|
+
annot = f"{label_dir}/{key}.aparc.annot"
|
|
338
|
+
|
|
339
|
+
label_files = {pial: "pial", inflated: "infl", white: "white"}
|
|
340
|
+
|
|
341
|
+
for surf, label in label_files.items():
|
|
342
|
+
fig, axs = plt.subplots(2, 3, subplot_kw={"projection": "3d"})
|
|
343
|
+
plotting.plot_surf_roi(
|
|
344
|
+
surf,
|
|
345
|
+
annot,
|
|
346
|
+
hemi=val,
|
|
347
|
+
view="lateral",
|
|
348
|
+
bg_map=sulc,
|
|
349
|
+
bg_on_data=True,
|
|
350
|
+
darkness=1,
|
|
351
|
+
cmap=cmap,
|
|
352
|
+
axes=axs[0, 0],
|
|
353
|
+
figure=fig,
|
|
354
|
+
)
|
|
355
|
+
plotting.plot_surf_roi(
|
|
356
|
+
surf,
|
|
357
|
+
annot,
|
|
358
|
+
hemi=val,
|
|
359
|
+
view="medial",
|
|
360
|
+
bg_map=sulc,
|
|
361
|
+
bg_on_data=True,
|
|
362
|
+
darkness=1,
|
|
363
|
+
cmap=cmap,
|
|
364
|
+
axes=axs[0, 1],
|
|
365
|
+
figure=fig,
|
|
366
|
+
)
|
|
367
|
+
plotting.plot_surf_roi(
|
|
368
|
+
surf,
|
|
369
|
+
annot,
|
|
370
|
+
hemi=val,
|
|
371
|
+
view="dorsal",
|
|
372
|
+
bg_map=sulc,
|
|
373
|
+
bg_on_data=True,
|
|
374
|
+
darkness=1,
|
|
375
|
+
cmap=cmap,
|
|
376
|
+
axes=axs[0, 2],
|
|
377
|
+
figure=fig,
|
|
378
|
+
)
|
|
379
|
+
plotting.plot_surf_roi(
|
|
380
|
+
surf,
|
|
381
|
+
annot,
|
|
382
|
+
hemi=val,
|
|
383
|
+
view="ventral",
|
|
384
|
+
bg_map=sulc,
|
|
385
|
+
bg_on_data=True,
|
|
386
|
+
darkness=1,
|
|
387
|
+
cmap=cmap,
|
|
388
|
+
axes=axs[1, 0],
|
|
389
|
+
figure=fig,
|
|
390
|
+
)
|
|
391
|
+
plotting.plot_surf_roi(
|
|
392
|
+
surf,
|
|
393
|
+
annot,
|
|
394
|
+
hemi=val,
|
|
395
|
+
view="anterior",
|
|
396
|
+
bg_map=sulc,
|
|
397
|
+
bg_on_data=True,
|
|
398
|
+
darkness=1,
|
|
399
|
+
cmap=cmap,
|
|
400
|
+
axes=axs[1, 1],
|
|
401
|
+
figure=fig,
|
|
402
|
+
)
|
|
403
|
+
plotting.plot_surf_roi(
|
|
404
|
+
surf,
|
|
405
|
+
annot,
|
|
406
|
+
hemi=val,
|
|
407
|
+
view="posterior",
|
|
408
|
+
bg_map=sulc,
|
|
409
|
+
bg_on_data=True,
|
|
410
|
+
darkness=1,
|
|
411
|
+
cmap=cmap,
|
|
412
|
+
axes=axs[1, 2],
|
|
413
|
+
figure=fig,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
plt.savefig(f"{output_dir}/{key}_{label}.svg", dpi=300, format="svg")
|
|
417
|
+
plt.close()
|
|
418
|
+
|
|
419
|
+
return sorted(Path(output_dir).glob("*svg"))
|
|
420
|
+
|
|
421
|
+
def gen_html_report(
|
|
422
|
+
self,
|
|
423
|
+
subject: str,
|
|
424
|
+
output_dir: str,
|
|
425
|
+
img_out: str | None = None,
|
|
426
|
+
template: str | None = None,
|
|
427
|
+
) -> Path:
|
|
428
|
+
"""Generate html report with FreeSurfer images.
|
|
429
|
+
|
|
430
|
+
Parameters
|
|
431
|
+
----------
|
|
432
|
+
subject : str
|
|
433
|
+
Subject ID.
|
|
434
|
+
output_dir : str
|
|
435
|
+
HTML file name
|
|
436
|
+
img_out : str | None
|
|
437
|
+
Location where SVG images are saved, default is subject's data directory.
|
|
438
|
+
template : str | None
|
|
439
|
+
HTML template to use. Default is local freesurfer.html.
|
|
440
|
+
|
|
441
|
+
Returns
|
|
442
|
+
-------
|
|
443
|
+
Path:
|
|
444
|
+
Path to html file.
|
|
445
|
+
|
|
446
|
+
Examples
|
|
447
|
+
--------
|
|
448
|
+
>>> from pyfsviz.freesurfer import FreeSurfer
|
|
449
|
+
>>> fs_dir = FreeSurfer(
|
|
450
|
+
... freesurfer_home="/opt/freesurfer",
|
|
451
|
+
... subjects_dir="/opt/data",
|
|
452
|
+
... subject="sub-001",
|
|
453
|
+
... )
|
|
454
|
+
>>> report = fs_dir.gen_html_report(out_name="sub-001.html", output_dir=".")
|
|
455
|
+
"""
|
|
456
|
+
if template is None:
|
|
457
|
+
template = files("pyfsviz._internal.html") / "individual.html"
|
|
458
|
+
if img_out is None:
|
|
459
|
+
image_list = list((self.subjects_dir / subject).glob("*/*svg"))
|
|
460
|
+
else:
|
|
461
|
+
image_list = list(Path(img_out).glob("*svg"))
|
|
462
|
+
|
|
463
|
+
tlrc = []
|
|
464
|
+
aseg = []
|
|
465
|
+
surf = []
|
|
466
|
+
|
|
467
|
+
for img in image_list:
|
|
468
|
+
with open(img, encoding="utf-8") as img_file:
|
|
469
|
+
img_data = img_file.read()
|
|
470
|
+
|
|
471
|
+
if "tlrc" in img.name:
|
|
472
|
+
tlrc.append(img_data)
|
|
473
|
+
elif "aseg" in img.name or "aparc" in img.name:
|
|
474
|
+
aseg.append(img_data)
|
|
475
|
+
else:
|
|
476
|
+
labels = {
|
|
477
|
+
"lh_pial": "LH Pial",
|
|
478
|
+
"rh_pial": "RH Pial",
|
|
479
|
+
"lh_infl": "LH Inflated",
|
|
480
|
+
"rh_infl": "RH Inflated",
|
|
481
|
+
"lh_white": "LH White Matter",
|
|
482
|
+
"rh_white": "RH White Matter",
|
|
483
|
+
}
|
|
484
|
+
surf_tuple = (labels[img.stem], img_data)
|
|
485
|
+
surf.append(surf_tuple)
|
|
486
|
+
|
|
487
|
+
_config = {
|
|
488
|
+
"timestamp": datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y-%m-%d, %H:%M"),
|
|
489
|
+
"subject": subject,
|
|
490
|
+
"tlrc": tlrc,
|
|
491
|
+
"aseg": aseg,
|
|
492
|
+
"surf": surf,
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
tpl = Template(str(template))
|
|
496
|
+
tpl.generate_conf(_config, f"{output_dir}/{subject}.html")
|
|
497
|
+
|
|
498
|
+
return Path(output_dir) / f"{subject}.html"
|
|
499
|
+
|
|
500
|
+
def gen_batch_reports(
|
|
501
|
+
self,
|
|
502
|
+
output_dir: str | Path,
|
|
503
|
+
subjects: list[str] | None = None,
|
|
504
|
+
template: str | None = None,
|
|
505
|
+
*,
|
|
506
|
+
gen_images: bool = True,
|
|
507
|
+
skip_failed: bool = True,
|
|
508
|
+
) -> dict[str, Path | Exception]:
|
|
509
|
+
"""Generate HTML reports with images for multiple subjects.
|
|
510
|
+
|
|
511
|
+
This method first generates all required images (TLRC, aparc+aseg, surfaces)
|
|
512
|
+
and then creates HTML reports for each subject.
|
|
513
|
+
|
|
514
|
+
Parameters
|
|
515
|
+
----------
|
|
516
|
+
output_dir : str or Path
|
|
517
|
+
Directory where HTML reports will be saved.
|
|
518
|
+
subjects : list[str] or None
|
|
519
|
+
List of subject IDs to process. If None, processes all subjects
|
|
520
|
+
in the subjects directory.
|
|
521
|
+
template : str or None
|
|
522
|
+
HTML template to use. Default is local individual.html.
|
|
523
|
+
gen_images : bool
|
|
524
|
+
Generate images for each subject. Default is True.
|
|
525
|
+
skip_failed : bool
|
|
526
|
+
If True, continues processing other subjects if one fails.
|
|
527
|
+
If False, raises exception on first failure.
|
|
528
|
+
|
|
529
|
+
Returns
|
|
530
|
+
-------
|
|
531
|
+
dict[str, Path | Exception]
|
|
532
|
+
Dictionary mapping subject IDs to either the generated HTML file
|
|
533
|
+
path or the exception that occurred during processing.
|
|
534
|
+
|
|
535
|
+
Examples
|
|
536
|
+
--------
|
|
537
|
+
>>> from pyfsviz.freesurfer import FreeSurfer
|
|
538
|
+
>>> fs = FreeSurfer()
|
|
539
|
+
>>> results = fs.gen_batch_reports("reports/", log_level="INFO")
|
|
540
|
+
>>> for subject, result in results.items():
|
|
541
|
+
... if isinstance(result, Path):
|
|
542
|
+
... print(f"Generated report for {subject}: {result}")
|
|
543
|
+
... else:
|
|
544
|
+
... print(f"Failed to generate report for {subject}: {result}")
|
|
545
|
+
"""
|
|
546
|
+
output_dir = Path(output_dir)
|
|
547
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
548
|
+
|
|
549
|
+
if subjects is None:
|
|
550
|
+
subjects = self.get_subjects()
|
|
551
|
+
|
|
552
|
+
self.logger.info(f"Generating reports with images for {len(subjects)} subjects...")
|
|
553
|
+
self.logger.info(f"Output directory: {output_dir}")
|
|
554
|
+
|
|
555
|
+
results: dict[str, Path | Exception] = {}
|
|
556
|
+
|
|
557
|
+
for i, subject in enumerate(subjects, 1):
|
|
558
|
+
self.logger.info(f"[{i}/{len(subjects)}] Processing subject: {subject}")
|
|
559
|
+
|
|
560
|
+
try:
|
|
561
|
+
# Check if recon-all completed successfully
|
|
562
|
+
if not self.check_recon_all(subject):
|
|
563
|
+
self.logger.warning(f"Subject {subject} recon-all did not complete successfully")
|
|
564
|
+
|
|
565
|
+
# Create subject-specific output directory for images
|
|
566
|
+
subject_output_dir = output_dir / subject
|
|
567
|
+
subject_output_dir.mkdir(parents=True, exist_ok=True)
|
|
568
|
+
|
|
569
|
+
# Generate images
|
|
570
|
+
self.logger.info(f" Generating images for {subject}...")
|
|
571
|
+
|
|
572
|
+
if gen_images:
|
|
573
|
+
# Generate TLRC data and report
|
|
574
|
+
tlrc_dir = subject_output_dir / "tlrc"
|
|
575
|
+
tlrc_dir.mkdir(parents=True, exist_ok=True)
|
|
576
|
+
self.gen_tlrc_data(subject, str(tlrc_dir))
|
|
577
|
+
self.gen_tlrc_report(subject, str(tlrc_dir))
|
|
578
|
+
|
|
579
|
+
# Generate aparc+aseg plots
|
|
580
|
+
aparc_dir = subject_output_dir / "aparcaseg"
|
|
581
|
+
aparc_dir.mkdir(parents=True, exist_ok=True)
|
|
582
|
+
self.gen_aparcaseg_plots(subject, str(aparc_dir))
|
|
583
|
+
|
|
584
|
+
# Generate surface plots
|
|
585
|
+
surf_dir = subject_output_dir / "surfaces"
|
|
586
|
+
surf_dir.mkdir(parents=True, exist_ok=True)
|
|
587
|
+
self.gen_surf_plots(subject, str(surf_dir))
|
|
588
|
+
|
|
589
|
+
# Generate HTML report using all generated images
|
|
590
|
+
html_file = self.gen_html_report(
|
|
591
|
+
subject=subject,
|
|
592
|
+
output_dir=str(output_dir),
|
|
593
|
+
img_out=str(subject_output_dir),
|
|
594
|
+
template=template,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
results[subject] = html_file
|
|
598
|
+
|
|
599
|
+
self.logger.info(f" ✓ Generated report with images: {html_file}")
|
|
600
|
+
|
|
601
|
+
except Exception as e:
|
|
602
|
+
error_msg = f"Failed to generate report with images for {subject}: {e!s}"
|
|
603
|
+
results[subject] = e
|
|
604
|
+
|
|
605
|
+
self.logger.error(f" ✗ {error_msg}") # noqa: TRY400
|
|
606
|
+
|
|
607
|
+
if not skip_failed:
|
|
608
|
+
raise e # noqa: TRY201 # pylint: disable=try-except-raise
|
|
609
|
+
|
|
610
|
+
successful = sum(1 for result in results.values() if isinstance(result, Path))
|
|
611
|
+
failed = len(results) - successful
|
|
612
|
+
self.logger.info("\nBatch report generation with images completed:")
|
|
613
|
+
self.logger.info(f" Successful: {successful}")
|
|
614
|
+
self.logger.info(f" Failed: {failed}")
|
|
615
|
+
|
|
616
|
+
return results
|
pyfsviz/py.typed
ADDED
|
File without changes
|
pyfsviz/reports.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Copyright 2025 Lezlie España <www.github.com/l-espana>
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
"""Utility class for generating a config file from a jinja template.
|
|
17
|
+
|
|
18
|
+
https://github.com/oesteban/endofday/blob/f2e79c625d648ef45b08cc1f11fd0bd84342d604/endofday/core/template.py.
|
|
19
|
+
|
|
20
|
+
Along with other report-related functions.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import jinja2
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Template:
|
|
28
|
+
"""Simplified jinja2 template class from oesteban."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, template_str: str) -> None:
|
|
31
|
+
self._template_str = template_str
|
|
32
|
+
self._env = jinja2.Environment(
|
|
33
|
+
loader=jinja2.FileSystemLoader(searchpath="/"),
|
|
34
|
+
trim_blocks=True,
|
|
35
|
+
lstrip_blocks=True,
|
|
36
|
+
autoescape=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def compile(self, configs: dict) -> str:
|
|
40
|
+
"""Generate a string with the replacements."""
|
|
41
|
+
template = self._env.get_template(self._template_str)
|
|
42
|
+
return template.render(configs)
|
|
43
|
+
|
|
44
|
+
def generate_conf(self, configs: dict, path: str) -> None:
|
|
45
|
+
"""Save the outcome after replacement on the template to file."""
|
|
46
|
+
output = self.compile(configs)
|
|
47
|
+
with open(path, "w+", encoding="utf-8") as output_file:
|
|
48
|
+
output_file.write(output)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyfsviz
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python tools for FreeSurfer visualization and QA
|
|
5
|
+
Author-Email: Lezlie Espana <lespana@mcw.edu>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Documentation
|
|
19
|
+
Classifier: Topic :: Software Development
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Project-URL: Homepage, https://mcw-meier-lab.github.io/pyFSViz
|
|
23
|
+
Project-URL: Documentation, https://mcw-meier-lab.github.io/pyFSViz
|
|
24
|
+
Project-URL: Changelog, https://mcw-meier-lab.github.io/pyFSViz/changelog
|
|
25
|
+
Project-URL: Repository, https://github.com/mcw-meier-lab/pyFSViz
|
|
26
|
+
Project-URL: Issues, https://github.com/mcw-meier-lab/pyFSViz/issues
|
|
27
|
+
Project-URL: Discussions, https://github.com/mcw-meier-lab/pyFSViz/discussions
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Requires-Dist: importlib-resources>=6.5.2
|
|
30
|
+
Requires-Dist: matplotlib>=3.9.4
|
|
31
|
+
Requires-Dist: nilearn>=0.12.1
|
|
32
|
+
Requires-Dist: nipype>=1.10.0
|
|
33
|
+
Requires-Dist: nireports>=25.0.1
|
|
34
|
+
Requires-Dist: numpy>=2.0.2
|
|
35
|
+
Requires-Dist: pandas>=2.3.3
|
|
36
|
+
Requires-Dist: plotly>=6.3.1
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# pyFSViz
|
|
40
|
+
|
|
41
|
+
[](https://github.com/mcw-meier-lab/pyFSViz/actions?query=workflow%3Aci)
|
|
42
|
+
[](https://mcw-meier-lab.github.io/pyFSViz/)
|
|
43
|
+
[](https://pypi.org/project/pyfsviz/)
|
|
44
|
+
|
|
45
|
+
Python tools for FreeSurfer visualization and QA
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install pyfsviz
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
With [`uv`](https://docs.astral.sh/uv/):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv tool install pyfsviz
|
|
57
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
pyfsviz-0.1.1.dist-info/METADATA,sha256=pqEjcd4Wso3eiKAW9IOayReb83p_bXaGWS25WVCPai4,2119
|
|
2
|
+
pyfsviz-0.1.1.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
|
|
3
|
+
pyfsviz-0.1.1.dist-info/entry_points.txt,sha256=cre399MbfrIUVQUwxNKa1ur6ne0s-CkD6_tRL80fKgE,71
|
|
4
|
+
pyfsviz-0.1.1.dist-info/licenses/LICENSE,sha256=Ey0-DGBtdr06UFzAOHz4YEyp8SSCHHkanQy5T2b7Vlc,1070
|
|
5
|
+
pyfsviz/__init__.py,sha256=w7v2Fen3VPhL2hKJ2Mdvhgybcn1REPnHkNL9SJCEZng,482
|
|
6
|
+
pyfsviz/__main__.py,sha256=h_atneBM9iWXrAHRSGS20eaPSCKpWP50_2MBMnXO6wo,349
|
|
7
|
+
pyfsviz/_internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
pyfsviz/_internal/cli.py,sha256=Hs-pypt6Mgg2373TLqCrCZH-6Eh3xxDGqWxtJ0hM8_g,1699
|
|
9
|
+
pyfsviz/_internal/debug.py,sha256=FaBMWd86h58Rl05q4wwGkt7XSnZJYIAeUdJt3z4OByQ,2882
|
|
10
|
+
pyfsviz/_internal/html/individual.html,sha256=-QJkMm-0pV_cEUHy3ziTzMohe1Kq-fI9NHfp4bv7QAw,3567
|
|
11
|
+
pyfsviz/_internal/mni305.cor.mgz,sha256=U1i2rb8Y1cQrJysZvc3aGo8osdXOS3GdU56G810XN-k,3285041
|
|
12
|
+
pyfsviz/_internal/mni305.cor.nii.gz,sha256=M9Li-7fXl0RoZyF3Re8HQnsNLrz-hgWC8-4YfHma6Yo,3295839
|
|
13
|
+
pyfsviz/freesurfer.py,sha256=hhGgeEMKJUSJersRKBZPmmbcwbMd7mzXgp9aUmTXQLc,21116
|
|
14
|
+
pyfsviz/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
pyfsviz/reports.py,sha256=uJh0mjNCtORWZDrMYarqCgM1Eva-iYhjd2yGPXhKEvs,1703
|
|
16
|
+
pyfsviz-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 mcw-meier-lab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|