diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 553ddbf26782d250f14bb1e9d98ee8b41744de60..5738c126c7c3aedc3f931d29aeb7c8fa91540af8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -96,9 +96,9 @@ variables:
     - branches@fsl/fslpy
 
 
-.only_master: &only_master
+.only_main: &only_main
   only:
-    - master@fsl/fslpy
+    - main@fsl/fslpy
 
 
 .only_release_branches: &only_release_branches
@@ -210,7 +210,7 @@ style:
 #############
 
 # 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
 # currently the most recently executed pages
 # job is the one that gets deployed.
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index e2fd66a75e6ca97652f15f2280b5ebc7121d8642..c95570f231ee6a1a098251438b200abbc208bc3f 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -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
 ^^^^^^^
 
diff --git a/README.rst b/README.rst
index 352946137bbe145b49870d572ca7b6c6325671d6..4eafb12b92e44ca4b191259461576a40cc20857f 100644
--- a/README.rst
+++ b/README.rst
@@ -41,11 +41,10 @@ Dependencies
 
 
 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
-`requirements-extra.txt <requirements-extra.txt>`_
-which provide addditional functionality:
+Some optional dependencies (labelled ``extra`` in ``pyproject.toml``) provide
+addditional functionality:
 
 - ``wxPython``: The `fsl.utils.idle <fsl/utils/idle.py>`_ module has
   functionality  to schedule functions on the ``wx`` idle loop.
@@ -70,13 +69,13 @@ specific platform::
 
 
 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
-`requirements-dev.txt <requirements-dev.txt>`_ file.
+Dependencies for testing and documentation are also listed in ``pyproject.toml``,
+and are respectively labelled as ``test`` and ``doc``.
 
 
 Non-Python dependencies
@@ -104,10 +103,10 @@ https://open.win.ox.ac.uk/pages/fsl/fslpy/.
 ``fslpy`` is documented using `sphinx <http://http://sphinx-doc.org/>`_. You
 can build the API documentation by running::
 
-    pip install -r requirements-dev.txt
-    python setup.py doc
+    pip install ".[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.
 
 
@@ -116,11 +115,15 @@ Tests
 
 Run the test suite via::
 
-    pip install -r requirements-dev.txt
-    python setup.py test
+    pip install ".[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
diff --git a/fsl/scripts/atlasq.py b/fsl/scripts/atlasq.py
index 01ed67b4c373247b65e4571b14dba2d6a57f16ea..b0ef32a949f8e127f8544442b87fe8cb29980ceb 100644
--- a/fsl/scripts/atlasq.py
+++ b/fsl/scripts/atlasq.py
@@ -569,10 +569,10 @@ def parseArgs(args):
     usages = {
         'main'    : 'usage: atlasq [-h] command [options]',
         'ohi'     : textwrap.dedent("""
-                     usage: atlasq ohi -h
-                            atlasq ohi --dumpatlases
-                            atlasq ohi -a atlas -c X,Y,Z
-                            atlasq ohi -a atlas -m mask
+                     usage: atlasquery -h
+                            atlasquery --dumpatlases
+                            atlasquery -a atlas -c X,Y,Z
+                            atlasquery -a atlas -m mask
         """).strip(),
         'list'    : 'usage: atlasq list [-e]',
         'summary' : 'usage: atlasq summary atlas',
diff --git a/fsl/tests/test_transform/test_affine.py b/fsl/tests/test_transform/test_affine.py
index 1512109af6c10a3047f2a4174d7d897bdc2ab3bd..100b5db6daaa1aaa9e703b4eb5d1391e4375acbd 100644
--- a/fsl/tests/test_transform/test_affine.py
+++ b/fsl/tests/test_transform/test_affine.py
@@ -147,6 +147,24 @@ def test_normalise(seed):
     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():
 
     # Test numerically
@@ -209,6 +227,17 @@ def test_scaleOffsetXform():
         result   = affine.scaleOffsetXform(1, offset)
         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():
 
diff --git a/fsl/transform/affine.py b/fsl/transform/affine.py
index 83ee70b77e724f3f2684810cc3299d4c6416e239..fe8efd35e4faed848e1a9099d297db13166ce6d3 100644
--- a/fsl/transform/affine.py
+++ b/fsl/transform/affine.py
@@ -13,6 +13,7 @@ transformations. The following functions are available:
    transform
    scaleOffsetXform
    invert
+   flip
    concat
    compose
    decompose
@@ -79,7 +80,41 @@ def normalise(vec):
     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
     the specified scale(s) and offset(s).
 
@@ -95,6 +130,9 @@ def scaleOffsetXform(scales, offsets):
     :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)
 
     if not isinstance(scales,  oktypes): scales  = [scales]