mspdetect 0.1.0__tar.gz

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.
Files changed (56) hide show
  1. mspdetect-0.1.0/.github/workflows/release.yml +56 -0
  2. mspdetect-0.1.0/.github/workflows/testpypi.yml +59 -0
  3. mspdetect-0.1.0/.github/workflows/tests.yml +32 -0
  4. mspdetect-0.1.0/.gitignore +136 -0
  5. mspdetect-0.1.0/.pre-commit-config.yaml +43 -0
  6. mspdetect-0.1.0/.readthedocs.yml +28 -0
  7. mspdetect-0.1.0/CHANGELOG.md +39 -0
  8. mspdetect-0.1.0/CITATION.cff +22 -0
  9. mspdetect-0.1.0/LICENSE +21 -0
  10. mspdetect-0.1.0/PKG-INFO +122 -0
  11. mspdetect-0.1.0/README.md +85 -0
  12. mspdetect-0.1.0/docs/source/api/batch.rst +16 -0
  13. mspdetect-0.1.0/docs/source/api/data.rst +16 -0
  14. mspdetect-0.1.0/docs/source/api/detection.rst +23 -0
  15. mspdetect-0.1.0/docs/source/api/metrics.rst +6 -0
  16. mspdetect-0.1.0/docs/source/api/visualization.rst +6 -0
  17. mspdetect-0.1.0/docs/source/api.rst +11 -0
  18. mspdetect-0.1.0/docs/source/cli.rst +36 -0
  19. mspdetect-0.1.0/docs/source/conf.py +77 -0
  20. mspdetect-0.1.0/docs/source/configuration.rst +6 -0
  21. mspdetect-0.1.0/docs/source/conventions.rst +119 -0
  22. mspdetect-0.1.0/docs/source/index.rst +26 -0
  23. mspdetect-0.1.0/docs/source/visualization.rst +34 -0
  24. mspdetect-0.1.0/examples/config.example.yaml +40 -0
  25. mspdetect-0.1.0/pyproject.toml +163 -0
  26. mspdetect-0.1.0/setup.cfg +4 -0
  27. mspdetect-0.1.0/src/mspdetect/__init__.py +51 -0
  28. mspdetect-0.1.0/src/mspdetect/_version.py +24 -0
  29. mspdetect-0.1.0/src/mspdetect/batch.py +284 -0
  30. mspdetect-0.1.0/src/mspdetect/cli.py +58 -0
  31. mspdetect-0.1.0/src/mspdetect/config.py +256 -0
  32. mspdetect-0.1.0/src/mspdetect/detect.py +197 -0
  33. mspdetect-0.1.0/src/mspdetect/io.py +174 -0
  34. mspdetect-0.1.0/src/mspdetect/metrics.py +162 -0
  35. mspdetect-0.1.0/src/mspdetect/optimize.py +175 -0
  36. mspdetect-0.1.0/src/mspdetect/plane.py +155 -0
  37. mspdetect-0.1.0/src/mspdetect/preprocessing.py +181 -0
  38. mspdetect-0.1.0/src/mspdetect/results.py +229 -0
  39. mspdetect-0.1.0/src/mspdetect/viz.py +507 -0
  40. mspdetect-0.1.0/src/mspdetect.egg-info/PKG-INFO +122 -0
  41. mspdetect-0.1.0/src/mspdetect.egg-info/SOURCES.txt +54 -0
  42. mspdetect-0.1.0/src/mspdetect.egg-info/dependency_links.txt +1 -0
  43. mspdetect-0.1.0/src/mspdetect.egg-info/entry_points.txt +2 -0
  44. mspdetect-0.1.0/src/mspdetect.egg-info/requires.txt +24 -0
  45. mspdetect-0.1.0/src/mspdetect.egg-info/scm_file_list.json +49 -0
  46. mspdetect-0.1.0/src/mspdetect.egg-info/scm_version.json +8 -0
  47. mspdetect-0.1.0/src/mspdetect.egg-info/top_level.txt +1 -0
  48. mspdetect-0.1.0/tests/conftest.py +103 -0
  49. mspdetect-0.1.0/tests/test_cli.py +132 -0
  50. mspdetect-0.1.0/tests/test_config_results.py +97 -0
  51. mspdetect-0.1.0/tests/test_detect.py +88 -0
  52. mspdetect-0.1.0/tests/test_metrics.py +97 -0
  53. mspdetect-0.1.0/tests/test_plane.py +52 -0
  54. mspdetect-0.1.0/tests/test_preprocessing.py +76 -0
  55. mspdetect-0.1.0/tests/test_viz.py +75 -0
  56. mspdetect-0.1.0/uv.lock +2585 -0
@@ -0,0 +1,56 @@
1
+ name: Build
2
+
3
+ on:
4
+ release:
5
+ types: [ created ]
6
+
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ build:
11
+ name: Build package from source
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ persist-credentials: false
18
+ fetch-depth: 0
19
+ - name: Install Python 3
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: '3.10'
23
+ - name: Install build tools
24
+ run: |
25
+ python3 -m pip install build --user
26
+ - name: Build package
27
+ run: |
28
+ python3 -m build
29
+ - name: Upload to CI runner
30
+ uses: actions/upload-artifact@v4
31
+ with:
32
+ name: built-package
33
+ path: dist/
34
+
35
+ pypi-publish:
36
+ name: Publish built package on PyPI
37
+ runs-on: ubuntu-latest
38
+ needs:
39
+ - build
40
+
41
+ # Specifying a GitHub environment is optional, but strongly encouraged
42
+ environment:
43
+ name: pypi
44
+ url: https://pypi.org/p/mspdetect
45
+ permissions:
46
+ # IMPORTANT: this permission is mandatory for Trusted Publishing
47
+ id-token: write
48
+ steps:
49
+ # retrieve your distributions here
50
+ - name: Download from CI runner
51
+ uses: actions/download-artifact@v4
52
+ with:
53
+ name: built-package
54
+ path: dist/
55
+ - name: Publish on PyPI
56
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,59 @@
1
+ name: Test Build
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ build:
12
+ name: Build package from source
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with:
18
+ persist-credentials: false
19
+ fetch-depth: 0
20
+ - name: Install Python 3
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: '3.10'
24
+ - name: Install build tools
25
+ run: |
26
+ python3 -m pip install build --user
27
+ - name: Build package
28
+ run: |
29
+ python3 -m build
30
+ - name: Upload to CI runner
31
+ uses: actions/upload-artifact@v4
32
+ with:
33
+ name: built-package
34
+ path: dist/
35
+
36
+ testpypi-publish:
37
+ name: Publish built package on TestPyPI
38
+ runs-on: ubuntu-latest
39
+ needs:
40
+ - build
41
+
42
+ # Specifying a GitHub environment is optional, but strongly encouraged
43
+ environment:
44
+ name: testpypi
45
+ url: https://test.pypi.org/p/mspdetect
46
+ permissions:
47
+ # IMPORTANT: this permission is mandatory for Trusted Publishing
48
+ id-token: write
49
+ steps:
50
+ # retrieve your distributions here
51
+ - name: Download from CI runner
52
+ uses: actions/download-artifact@v4
53
+ with:
54
+ name: built-package
55
+ path: dist/
56
+ - name: Publish on PyPI
57
+ uses: pypa/gh-action-pypi-publish@release/v1
58
+ with:
59
+ repository-url: https://test.pypi.org/legacy/
@@ -0,0 +1,32 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ pull-requests: write # to post the coverage comment on PRs
13
+ contents: write # to store coverage data/badge on a branch
14
+ strategy:
15
+ matrix:
16
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0 # setuptools_scm needs the tags
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+ - run: pip install -e ".[dev, viz]"
25
+ - run: ruff check src tests
26
+ - run: ruff format --check src tests
27
+ - run: pytest --cov=mspdetect
28
+ - name: Coverage comment
29
+ if: matrix.python-version == '3.12' # report once, not four times
30
+ uses: py-cov-action/python-coverage-comment-action@v3
31
+ with:
32
+ GITHUB_TOKEN: ${{ github.token }}
@@ -0,0 +1,136 @@
1
+ # VS Code
2
+ .vscode/*
3
+ .history/*
4
+
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ pip-wheel-metadata/
28
+ share/python-wheels/
29
+ *.egg-info/
30
+ .installed.cfg
31
+ *.egg
32
+ MANIFEST
33
+ **/_version.py
34
+
35
+ # PyInstaller
36
+ # Usually these files are written by a python script from a template
37
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
38
+ *.manifest
39
+ *.spec
40
+
41
+ # Installer logs
42
+ pip-log.txt
43
+ pip-delete-this-directory.txt
44
+
45
+ # Unit test / coverage reports
46
+ htmlcov/
47
+ .tox/
48
+ .nox/
49
+ .coverage
50
+ .coverage.*
51
+ .cache
52
+ nosetests.xml
53
+ coverage.xml
54
+ *.cover
55
+ *.py,cover
56
+ .hypothesis/
57
+ .pytest_cache/
58
+
59
+ # Translations
60
+ *.mo
61
+ *.pot
62
+
63
+ # Django stuff:
64
+ *.log
65
+ local_settings.py
66
+ db.sqlite3
67
+ db.sqlite3-journal
68
+ static/
69
+
70
+ # Flask stuff:
71
+ instance/
72
+ .webassets-cache
73
+
74
+ # Scrapy stuff:
75
+ .scrapy
76
+
77
+ # documentation
78
+ docs/_build/
79
+ docs/build/
80
+ pdocs/
81
+ pdoc3/
82
+ docs/jupyter_execute
83
+
84
+ # PyBuilder
85
+ target/
86
+
87
+ # Jupyter Notebook
88
+ .ipynb_checkpoints
89
+
90
+ # IPython
91
+ profile_default/
92
+ ipython_config.py
93
+
94
+ # pyenv
95
+ .python-version
96
+
97
+ # pipenv
98
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
99
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
100
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
101
+ # install all needed dependencies.
102
+ #Pipfile.lock
103
+
104
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
105
+ __pypackages__/
106
+
107
+ # Environments
108
+ .env
109
+ .venv
110
+ env/
111
+ venv/
112
+ ENV/
113
+ env.bak/
114
+ venv.bak/
115
+ .test
116
+
117
+ # Spyder project settings
118
+ .spyderproject
119
+ .spyproject
120
+
121
+ # Rope project settings
122
+ .ropeproject
123
+
124
+ # mkdocs documentation
125
+ /site
126
+
127
+ # mypy
128
+ .mypy_cache/
129
+ .dmypy.json
130
+ dmypy.json
131
+
132
+ # Pyre type checker
133
+ .pyre/
134
+
135
+ # MacOs stuff
136
+ .DS_Store
@@ -0,0 +1,43 @@
1
+ default_install_hook_types: [pre-commit, commit-msg]
2
+
3
+ repos:
4
+ - repo: https://github.com/pre-commit/pre-commit-hooks
5
+ rev: v5.0.0
6
+ hooks:
7
+ - id: trailing-whitespace
8
+ - id: end-of-file-fixer
9
+ - id: check-merge-conflict
10
+ - id: check-toml
11
+ - id: check-yaml
12
+ - id: check-json
13
+ - repo: https://github.com/astral-sh/ruff-pre-commit
14
+ rev: v0.12.1
15
+ hooks:
16
+ - id: ruff
17
+ args: [ --fix ]
18
+ - id: ruff-format
19
+ - repo: https://github.com/kynan/nbstripout
20
+ rev: 0.8.1
21
+ hooks:
22
+ - id: nbstripout
23
+ - repo: https://github.com/compilerla/conventional-pre-commit
24
+ rev: v4.2.0
25
+ hooks:
26
+ - id: conventional-pre-commit
27
+ stages: [commit-msg]
28
+ args:
29
+ - build # changes of the build system or dependencies
30
+ - change # commit alters the implementation of an existing feature
31
+ - chore # technical or maintenance task not related to feature or user story
32
+ - ci # edits to the continuous integration scripts/configuration
33
+ - deprecate # a feature or functionality will be deprecated
34
+ - docs # add, update of revise the documentation
35
+ - feat # a new feature was implemented (bump MINOR version)
36
+ - fix # an issue or bug has been fixed (bump PATCH version)
37
+ - perf # performance improvements that do not alter existing behaviour
38
+ - refactor # update shuffles code around but does not alter functionality
39
+ - remove # a feature or functionality is removed
40
+ - style # source code is improved w.r.t. its code quality
41
+ - test # commits enhance or add to the test suite
42
+ - merge # merge one branch into another. Should be ignored by git-chglog
43
+ - revert # revert previous commit(s). Should be ignored by git-chglog
@@ -0,0 +1,28 @@
1
+ # .readthedocs.yml
2
+ # Read the Docs configuration file
3
+ # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4
+
5
+ # Required
6
+ version: 2
7
+
8
+ build:
9
+ os: ubuntu-24.04
10
+ tools:
11
+ python: "3.12"
12
+ jobs:
13
+ post_checkout:
14
+ - git fetch --unshallow || true
15
+
16
+ # Build documentation in the docs/ directory with Sphinx
17
+ sphinx:
18
+ builder: html
19
+ configuration: docs/source/conf.py
20
+ fail_on_warning: false
21
+
22
+ # tell RTD to install the package with the docs optional requirements
23
+ python:
24
+ install:
25
+ - method: pip
26
+ path: .
27
+ extra_requirements:
28
+ - docs
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.1.0] - 2026-10-26
6
+
7
+ ### Documentation
8
+
9
+ - Add documentation.
10
+ - Create changelog.
11
+
12
+ ### Miscellaneous Tasks
13
+
14
+ - Update repository URLs after transfer to lycosystem.
15
+ - Add github workflows for publishing.
16
+
17
+ ### Refactor
18
+
19
+ - Restructure mspdetect package.
20
+
21
+ ### Bug Fixes
22
+
23
+ - Fix imports and logging.
24
+ - Fix bug in widget to correctly visualise sagittal cropping.
25
+ - Fix memory issues.
26
+ - Fix visualisation issues.
27
+ - Adjust optimisation.
28
+
29
+ ### Features
30
+
31
+ - Add MSP detection algorithm.
32
+ - Add volume computation of structures.
33
+ - Add more options for cropping.
34
+ - Add possibility to specify patient list.
35
+ - Add possibility to crop shoulders for msp detection.
36
+ - Add configs for checking parameter input.
37
+
38
+
39
+ [0.1.0]: https://github.com/lycosystem/mspdetect/releases/tag/0.1.0
@@ -0,0 +1,22 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite the article below."
3
+ title: "mspdetect: mid-sagittal plane detection in head and neck planning CTs"
4
+ type: software
5
+ license: MIT
6
+ repository-code: "https://github.com/lycosystem/mspdetect"
7
+ authors:
8
+ - family-names: Keller
9
+ given-names: Loris
10
+ - family-names: Pérez Haas
11
+ given-names: Yoel
12
+ - family-names: Bührer
13
+ given-names: Noemi
14
+ # TODO: fill in once the article is published
15
+ preferred-citation:
16
+ type: article
17
+ title: "TODO: article title"
18
+ authors:
19
+ - family-names: Bührer
20
+ given-names: Noemi
21
+ journal: "TODO"
22
+ year: 2026
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Loris Keller, Yoel Pérez Haas, Noemi Bührer
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.
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: mspdetect
3
+ Version: 0.1.0
4
+ Summary: Automatic mid-sagittal plane detection in head and neck planning CTs.
5
+ Author: Loris Keller, Yoel Pérez Haas, Noemi Bührer
6
+ License-Expression: MIT
7
+ Project-URL: source, https://github.com/lycosystem/mspdetect
8
+ Project-URL: documentation, https://mspdetect.readthedocs.io
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: numpy>=1.24
16
+ Requires-Dist: scipy>=1.10
17
+ Requires-Dist: nibabel>=5.0
18
+ Requires-Dist: pydantic>=2.0
19
+ Requires-Dist: pyyaml>=6.0
20
+ Requires-Dist: tqdm>=4.60
21
+ Provides-Extra: viz
22
+ Requires-Dist: matplotlib>=3.7; extra == "viz"
23
+ Requires-Dist: ipywidgets>=8.0; extra == "viz"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8.0; extra == "dev"
26
+ Requires-Dist: pytest-cov; extra == "dev"
27
+ Requires-Dist: ruff>=0.12; extra == "dev"
28
+ Requires-Dist: pre-commit; extra == "dev"
29
+ Requires-Dist: git-cliff; extra == "dev"
30
+ Provides-Extra: docs
31
+ Requires-Dist: sphinx; extra == "docs"
32
+ Requires-Dist: sphinx-book-theme; extra == "docs"
33
+ Requires-Dist: myst-parser; extra == "docs"
34
+ Requires-Dist: autodoc_pydantic; extra == "docs"
35
+ Requires-Dist: sphinxcontrib-programoutput; extra == "docs"
36
+ Dynamic: license-file
37
+
38
+ # mspdetect
39
+
40
+ Automatic detection of the **mid-sagittal plane (MSP)** in head and neck planning CTs, and
41
+ metrics of structures relative to it (left/right volumes, laterality and distance of the
42
+ tumour to the midline).
43
+
44
+ The plane is found by maximising the left-right symmetry of the bone: bone voxels are mirrored
45
+ across a candidate plane and compared with the CT intensity at the mirrored position.
46
+
47
+ > **Research software.** mspdetect is not a medical device and must not be used for clinical
48
+ > decisions. Always check results visually (see [Visualization](https://mspdetect.readthedocs.io/en/stable/visualization.html)).
49
+
50
+ ## Installation
51
+
52
+ Requires Python ≥ 3.10.
53
+
54
+ ```bash
55
+ pip install "mspdetect[viz] @ git+https://github.com/lycosystem/mspdetect"
56
+ ```
57
+
58
+ `[viz]` adds plotting (matplotlib, ipywidgets) and can be omitted.
59
+
60
+ ## Usage
61
+
62
+ ### Python
63
+
64
+ ```python
65
+ import mspdetect as msp
66
+
67
+ case = msp.load_case(
68
+ "data/patient01",
69
+ image="CT",
70
+ structures={"GTVp": "ROI__[GTVp]", "Body": "ROI__[EXTERNAL]", "Parotid_L": "ROI__[Parotid_L]"},
71
+ )
72
+ result = msp.detect_msp(case) # default settings, see DetectionConfig
73
+ print(result.plane) # normal and offset in world coordinates
74
+
75
+ metrics = msp.compute_metrics(case, result.plane, structures=["Parotid_L"], target="GTVp")
76
+ msp.save_result(result, "results/patient01") # msp.json + details.npz
77
+ ```
78
+
79
+ Settings are passed as a `DetectionConfig`, e.g.
80
+ `msp.DetectionConfig(crop={"reference": None})` to use the whole scan.
81
+
82
+ ### Command line
83
+
84
+ ```bash
85
+ mspdetect check-config config.yaml # validate and print all settings (incl. defaults)
86
+ mspdetect run config.yaml # detect the plane of all patients
87
+ mspdetect metrics config.yaml # compute metrics from the detected planes
88
+ ```
89
+
90
+ See the [command line documentation](https://mspdetect.readthedocs.io/en/stable/cli.html) for the behaviour of the commands and all configuration options.
91
+
92
+ ## Inputs and outputs
93
+
94
+ Input are NIfTI files containing the CT in Hounsfield units and, optionally, binary structure masks of the Body, GTVp, or other structures. All volumes are reoriented to RAS+ on loading.
95
+
96
+ The outputs contain the detected plane, objective values along other information such as the package version, settings, etc. Planes are stored in **world coordinates**.
97
+
98
+ When calculating metrics on the detected planes, for each specified structure (`metrics.structures`) a handful of information related to the detected MSP will be determined, e.g. the side, the distance to the MSP, lateral extent, etc.
99
+
100
+ See the [documentation](https://mspdetect.readthedocs.io/en/stable/conventions.html) for further details.
101
+
102
+
103
+ ## Method
104
+
105
+ 1. **Crop** a slab of slices around the reference structure (fixed margins or a fixed-size
106
+ window), optionally extended to the mandible and cropped laterally to exclude the shoulders.
107
+ 2. **Bone**: voxels inside `Body` with HU in `hu_range` (default 300–2800).
108
+ 3. **Grid search** over the normal's azimuth and polar angle, for planes through the bone's
109
+ centre of mass, scored with the sum of squared differences between each bone voxel and the
110
+ (cubic-interpolated) CT at its mirror position. A robust Huber threshold δ is estimated
111
+ from the residuals (1.345 · 1.4826 · MAD).
112
+ 4. **Optimisation** of the Huber loss with BFGS, starting from the best grid plane and using
113
+ the δ of that plane. With `n_starts > 1`, further starts from the next-best grid planes are
114
+ run, each with the δ of its own grid plane. Since these starts minimise different Huber
115
+ losses, the plane with the lowest final quadratic loss (independent of δ) is returned as
116
+ the best, and `first_start_agrees` reports whether the first start reached the same plane
117
+ (within 0.5° and 0.5 mm).
118
+
119
+
120
+ ## Citation
121
+
122
+ If you use mspdetect, please cite the article in [`CITATION.cff`](https://github.com/lycosystem/mspdetect/blob/main/CITATION.cff).
@@ -0,0 +1,85 @@
1
+ # mspdetect
2
+
3
+ Automatic detection of the **mid-sagittal plane (MSP)** in head and neck planning CTs, and
4
+ metrics of structures relative to it (left/right volumes, laterality and distance of the
5
+ tumour to the midline).
6
+
7
+ The plane is found by maximising the left-right symmetry of the bone: bone voxels are mirrored
8
+ across a candidate plane and compared with the CT intensity at the mirrored position.
9
+
10
+ > **Research software.** mspdetect is not a medical device and must not be used for clinical
11
+ > decisions. Always check results visually (see [Visualization](https://mspdetect.readthedocs.io/en/stable/visualization.html)).
12
+
13
+ ## Installation
14
+
15
+ Requires Python ≥ 3.10.
16
+
17
+ ```bash
18
+ pip install "mspdetect[viz] @ git+https://github.com/lycosystem/mspdetect"
19
+ ```
20
+
21
+ `[viz]` adds plotting (matplotlib, ipywidgets) and can be omitted.
22
+
23
+ ## Usage
24
+
25
+ ### Python
26
+
27
+ ```python
28
+ import mspdetect as msp
29
+
30
+ case = msp.load_case(
31
+ "data/patient01",
32
+ image="CT",
33
+ structures={"GTVp": "ROI__[GTVp]", "Body": "ROI__[EXTERNAL]", "Parotid_L": "ROI__[Parotid_L]"},
34
+ )
35
+ result = msp.detect_msp(case) # default settings, see DetectionConfig
36
+ print(result.plane) # normal and offset in world coordinates
37
+
38
+ metrics = msp.compute_metrics(case, result.plane, structures=["Parotid_L"], target="GTVp")
39
+ msp.save_result(result, "results/patient01") # msp.json + details.npz
40
+ ```
41
+
42
+ Settings are passed as a `DetectionConfig`, e.g.
43
+ `msp.DetectionConfig(crop={"reference": None})` to use the whole scan.
44
+
45
+ ### Command line
46
+
47
+ ```bash
48
+ mspdetect check-config config.yaml # validate and print all settings (incl. defaults)
49
+ mspdetect run config.yaml # detect the plane of all patients
50
+ mspdetect metrics config.yaml # compute metrics from the detected planes
51
+ ```
52
+
53
+ See the [command line documentation](https://mspdetect.readthedocs.io/en/stable/cli.html) for the behaviour of the commands and all configuration options.
54
+
55
+ ## Inputs and outputs
56
+
57
+ Input are NIfTI files containing the CT in Hounsfield units and, optionally, binary structure masks of the Body, GTVp, or other structures. All volumes are reoriented to RAS+ on loading.
58
+
59
+ The outputs contain the detected plane, objective values along other information such as the package version, settings, etc. Planes are stored in **world coordinates**.
60
+
61
+ When calculating metrics on the detected planes, for each specified structure (`metrics.structures`) a handful of information related to the detected MSP will be determined, e.g. the side, the distance to the MSP, lateral extent, etc.
62
+
63
+ See the [documentation](https://mspdetect.readthedocs.io/en/stable/conventions.html) for further details.
64
+
65
+
66
+ ## Method
67
+
68
+ 1. **Crop** a slab of slices around the reference structure (fixed margins or a fixed-size
69
+ window), optionally extended to the mandible and cropped laterally to exclude the shoulders.
70
+ 2. **Bone**: voxels inside `Body` with HU in `hu_range` (default 300–2800).
71
+ 3. **Grid search** over the normal's azimuth and polar angle, for planes through the bone's
72
+ centre of mass, scored with the sum of squared differences between each bone voxel and the
73
+ (cubic-interpolated) CT at its mirror position. A robust Huber threshold δ is estimated
74
+ from the residuals (1.345 · 1.4826 · MAD).
75
+ 4. **Optimisation** of the Huber loss with BFGS, starting from the best grid plane and using
76
+ the δ of that plane. With `n_starts > 1`, further starts from the next-best grid planes are
77
+ run, each with the δ of its own grid plane. Since these starts minimise different Huber
78
+ losses, the plane with the lowest final quadratic loss (independent of δ) is returned as
79
+ the best, and `first_start_agrees` reports whether the first start reached the same plane
80
+ (within 0.5° and 0.5 mm).
81
+
82
+
83
+ ## Citation
84
+
85
+ If you use mspdetect, please cite the article in [`CITATION.cff`](https://github.com/lycosystem/mspdetect/blob/main/CITATION.cff).
@@ -0,0 +1,16 @@
1
+ Batch
2
+ ======
3
+
4
+ Batch processing
5
+ -----------------
6
+
7
+ .. automodule:: mspdetect.batch
8
+ :members:
9
+ :show-inheritance:
10
+
11
+ Results storage
12
+ ----------------
13
+
14
+ .. automodule:: mspdetect.results
15
+ :members:
16
+ :show-inheritance:
@@ -0,0 +1,16 @@
1
+ Data loading and preprocessing
2
+ ===============================
3
+
4
+ Data Loading
5
+ -------------
6
+
7
+ .. automodule:: mspdetect.io
8
+ :members:
9
+ :show-inheritance:
10
+
11
+ Preprocessing
12
+ --------------
13
+
14
+ .. automodule:: mspdetect.preprocessing
15
+ :members:
16
+ :show-inheritance: