Skip to content
Snippets Groups Projects
Commit fc0f7050 authored by Paul McCarthy's avatar Paul McCarthy :mountain_bicyclist:
Browse files

Merge branch 'enh/flip' into 'main'

Enh/flip

See merge request fsl/fslpy!403
parents 13d55095 ce22afcd
No related branches found
No related tags found
No related merge requests found
Pipeline #19860 failed
...@@ -96,9 +96,9 @@ variables: ...@@ -96,9 +96,9 @@ variables:
- branches@fsl/fslpy - branches@fsl/fslpy
.only_master: &only_master .only_main: &only_main
only: only:
- master@fsl/fslpy - main@fsl/fslpy
.only_release_branches: &only_release_branches .only_release_branches: &only_release_branches
...@@ -210,7 +210,7 @@ style: ...@@ -210,7 +210,7 @@ style:
############# #############
# I would like to have separate doc deploys for # I would like to have separate doc deploys for
# both the master and latest release branches, # both the main and latest release branches,
# but this is awkward with gitlab pages. So # but this is awkward with gitlab pages. So
# currently the most recently executed pages # currently the most recently executed pages
# job is the one that gets deployed. # job is the one that gets deployed.
......
...@@ -6,6 +6,14 @@ order. ...@@ -6,6 +6,14 @@ order.
-------------------------- --------------------------
Added
^^^^^
* New :func:`.affine.flip` function, for applying a flip/inversion to the
axes of an affine transformation (!403).
Changed Changed
^^^^^^^ ^^^^^^^
......
...@@ -41,11 +41,10 @@ Dependencies ...@@ -41,11 +41,10 @@ Dependencies
All of the core dependencies of ``fslpy`` are listed in the All of the core dependencies of ``fslpy`` are listed in the
`requirements.txt <requirements.txt>`_ file. `pyproject.toml <pyproject.toml>`_ file.
Some extra dependencies are listed in Some optional dependencies (labelled ``extra`` in ``pyproject.toml``) provide
`requirements-extra.txt <requirements-extra.txt>`_ addditional functionality:
which provide addditional functionality:
- ``wxPython``: The `fsl.utils.idle <fsl/utils/idle.py>`_ module has - ``wxPython``: The `fsl.utils.idle <fsl/utils/idle.py>`_ module has
functionality to schedule functions on the ``wx`` idle loop. functionality to schedule functions on the ``wx`` idle loop.
...@@ -70,13 +69,13 @@ specific platform:: ...@@ -70,13 +69,13 @@ specific platform::
Once wxPython has been installed, you can type the following to install the Once wxPython has been installed, you can type the following to install the
rest of the extra dependencies:: remaining optional dependencies::
pip install fslpy[extras] pip install "fslpy[extra]"
Dependencies for testing and documentation are listed in the Dependencies for testing and documentation are also listed in ``pyproject.toml``,
`requirements-dev.txt <requirements-dev.txt>`_ file. and are respectively labelled as ``test`` and ``doc``.
Non-Python dependencies Non-Python dependencies
...@@ -104,10 +103,10 @@ https://open.win.ox.ac.uk/pages/fsl/fslpy/. ...@@ -104,10 +103,10 @@ https://open.win.ox.ac.uk/pages/fsl/fslpy/.
``fslpy`` is documented using `sphinx <http://http://sphinx-doc.org/>`_. You ``fslpy`` is documented using `sphinx <http://http://sphinx-doc.org/>`_. You
can build the API documentation by running:: can build the API documentation by running::
pip install -r requirements-dev.txt pip install ".[doc]"
python setup.py doc sphinx-build doc html
The HTML documentation will be generated and saved in the ``doc/html/`` The HTML documentation will be generated and saved in the ``html/``
directory. directory.
...@@ -116,11 +115,15 @@ Tests ...@@ -116,11 +115,15 @@ Tests
Run the test suite via:: Run the test suite via::
pip install -r requirements-dev.txt pip install ".[test]"
python setup.py test pytest
A test report will be generated at ``report.html``, and a code coverage report
will be generated in ``htmlcov/``. Some tests will only pass if the test environment meets certain criteria -
refer to the ``tool.pytest.init_options`` section of
[``pyproject.toml``](pyproject.toml) for a list of [pytest
marks](https://docs.pytest.org/en/7.1.x/example/markers.html) which can be
selectively enabled or disabled.
Contributing Contributing
......
...@@ -569,10 +569,10 @@ def parseArgs(args): ...@@ -569,10 +569,10 @@ def parseArgs(args):
usages = { usages = {
'main' : 'usage: atlasq [-h] command [options]', 'main' : 'usage: atlasq [-h] command [options]',
'ohi' : textwrap.dedent(""" 'ohi' : textwrap.dedent("""
usage: atlasq ohi -h usage: atlasquery -h
atlasq ohi --dumpatlases atlasquery --dumpatlases
atlasq ohi -a atlas -c X,Y,Z atlasquery -a atlas -c X,Y,Z
atlasq ohi -a atlas -m mask atlasquery -a atlas -m mask
""").strip(), """).strip(),
'list' : 'usage: atlasq list [-e]', 'list' : 'usage: atlasq list [-e]',
'summary' : 'usage: atlasq summary atlas', 'summary' : 'usage: atlasq summary atlas',
......
...@@ -147,6 +147,24 @@ def test_normalise(seed): ...@@ -147,6 +147,24 @@ def test_normalise(seed):
assert np.all(pars) assert np.all(pars)
def test_flip():
shape = [5, 5, 5]
xform = np.diag([2, 2, 2, 1])
xform[:3, 3] = [-4.5, -4.5, -4.5]
allaxes = list(it.chain(
it.combinations([0, 1, 2], 1),
it.combinations([0, 1, 2], 2),
it.combinations([0, 1, 2], 3)))
for axes in allaxes:
flipped = affine.flip(shape, xform, *axes)
flo, fhi = affine.axisBounds(shape, flipped, boundary=None)
assert np.all(np.isclose(flo, [-5, -5, -5]))
assert np.all(np.isclose(fhi, [ 5, 5, 5]))
def test_scaleOffsetXform(): def test_scaleOffsetXform():
# Test numerically # Test numerically
...@@ -209,6 +227,17 @@ def test_scaleOffsetXform(): ...@@ -209,6 +227,17 @@ def test_scaleOffsetXform():
result = affine.scaleOffsetXform(1, offset) result = affine.scaleOffsetXform(1, offset)
assert np.all(np.isclose(result, expected)) assert np.all(np.isclose(result, expected))
# both args are optional
assert np.all(np.isclose(
affine.scaleOffsetXform(), np.eye(4)))
assert np.all(np.isclose(
affine.scaleOffsetXform([1, 2, 3]), np.diag([1, 2, 3, 1])))
exp = np.eye(4)
exp[:3, 3] = [1, 2, 3]
assert np.all(np.isclose(
affine.scaleOffsetXform(None, [1, 2, 3]), exp))
def test_compose_and_decompose(): def test_compose_and_decompose():
......
...@@ -13,6 +13,7 @@ transformations. The following functions are available: ...@@ -13,6 +13,7 @@ transformations. The following functions are available:
transform transform
scaleOffsetXform scaleOffsetXform
invert invert
flip
concat concat
compose compose
decompose decompose
...@@ -79,7 +80,41 @@ def normalise(vec): ...@@ -79,7 +80,41 @@ def normalise(vec):
return n return n
def scaleOffsetXform(scales, offsets): def flip(shape, xform, *axes):
"""Applies a flip/inversion to an affine transform along specified axes.
:arg shape: The ``(x, y, z)`` shape of the data.
:arg xform: Transformation matrix which transforms voxel coordinates
to a world coordinate system.
:arg axes: Indices of axes to flip
"""
if len(axes) == 0:
return xform
axes = sorted(set(axes))
if any(a not in (0, 1, 2) for a in axes):
raise ValueError(f'Invalid axis: {axes}')
los, his = axisBounds(shape, xform, axes, boundary=None)
scales = [1, 1, 1]
pre = [0, 0, 0]
post = [0, 0, 0]
for ax, lo, hi in zip(axes, los, his):
mid = lo + (hi - lo) / 2
scales[ax] = -1
pre[ ax] = -mid
post[ ax] = mid
pre = scaleOffsetXform(None, pre)
post = scaleOffsetXform(None, post)
scales = scaleOffsetXform(scales)
return concat(post, scales, pre, xform)
def scaleOffsetXform(scales=None, offsets=None):
"""Creates and returns an affine transformation matrix which encodes """Creates and returns an affine transformation matrix which encodes
the specified scale(s) and offset(s). the specified scale(s) and offset(s).
...@@ -95,6 +130,9 @@ def scaleOffsetXform(scales, offsets): ...@@ -95,6 +130,9 @@ def scaleOffsetXform(scales, offsets):
:returns: A ``numpy.float32`` array of size :math:`4 \\times 4`. :returns: A ``numpy.float32`` array of size :math:`4 \\times 4`.
""" """
if scales is None: scales = [1, 1, 1]
if offsets is None: offsets = [0, 0, 0]
oktypes = (abc.Sequence, np.ndarray) oktypes = (abc.Sequence, np.ndarray)
if not isinstance(scales, oktypes): scales = [scales] if not isinstance(scales, oktypes): scales = [scales]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment