Skip to content

Commit

Permalink
Merge pull request #1054 from mantidproject/1052_operations_docs
Browse files Browse the repository at this point in the history
User friendly docs for operations
  • Loading branch information
samtygier-stfc authored Jul 21, 2021
2 parents 87f0218 + e71f420 commit 308202f
Show file tree
Hide file tree
Showing 30 changed files with 250 additions and 111 deletions.
7 changes: 7 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Copyright (C) 2021 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later

import sys
import os

# -*- coding: utf-8 -*-
#
# MantidImaging documentation build configuration file, created by
Expand Down Expand Up @@ -45,6 +48,10 @@
'sphinx_multiversion',
]

# Add custom extensions
sys.path.append(os.path.abspath("./ext"))
extensions.append("operations_user_doc")

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

Expand Down
97 changes: 97 additions & 0 deletions docs/ext/operations_user_doc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Copyright (C) 2021 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later

from typing import List
import inspect

from docutils import nodes
from docutils.nodes import Node
from docutils.statemachine import ViewList
from docutils.parsers.rst import Directive
from sphinx.util.nodes import nested_parse_with_titles
"""Custom extension to add nicely formatted documentation for the operations.
Use:
.. operations_user_doc::
in a documentation rst file to generate.
"""


def make_heading(s: str, char: str) -> List[str]:
return [s, len(s) * char, ""]


def split_lines(s: str) -> List[str]:
s = s.replace("\n\n", "DOUBLE_NEW_LINE")
s = s.replace("\n", " ")
s = s.replace("DOUBLE_NEW_LINE", "\n\n")
return s.split("\n")


PARAM_SKIP_LIST = ["images", "cores", "chunksize", "progress"]


def get_params(s: str) -> List[str]:
ret = []
for line in s.split("\n"):
if line.strip().startswith(":param"):
param_name = line.strip().split()[1].strip(':')
if param_name in PARAM_SKIP_LIST:
continue
ret.append(line.strip())
elif line.strip().startswith(":return"):
pass
elif line.strip() and ret:
ret[-1] = ret[-1] + " " + line.strip()

return ret


class OperationsUserDoc(Directive):
def run(self) -> List[Node]:

try:
from mantidimaging.core.operations.loader import load_filter_packages
except ImportError:
raise ValueError("operations_user_doc could not import load_filter_packages")

rst_lines = []

operations = load_filter_packages()
for op in operations:
# Title
rst_lines += make_heading(op.filter_name, "-")

# Description from class doc string
rst_lines += split_lines(inspect.cleandoc(op.__doc__))
rst_lines.append("")

# parameters from filter_func
if op.filter_func.__doc__ is not None:
rst_lines += get_params(op.filter_func.__doc__)
rst_lines.append("")

rst_lines.append(f":class:`{op.filter_name} API docs<{op.__module__}>`")
rst_lines.append("")

rst = ViewList()
for n, rst_line in enumerate(rst_lines):
rst.append(rst_line, "generated.rst", n)

node = nodes.section()
node.document = self.state.document

nested_parse_with_titles(self.state, rst, node)

return node.children


def setup(app):
app.add_directive("operations_user_doc", OperationsUserDoc)

return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}
1 change: 1 addition & 0 deletions docs/release_notes/next.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Fixes
- #1046 : Can't rotate NeXus images
- #1048 : NeXus Loader: ValueError: Illegal slicing argument for scalar dataspace
- #1055: Arithmetic operation breaks flat-fielding
- #898: Improve user documentation for operations

Developer Changes
-----------------
Expand Down
18 changes: 3 additions & 15 deletions docs/user_guide/operations/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,7 @@
Operations
==========

.. toctree::
:maxdepth: 1
:caption: Contents:
Operations List
---------------

../../api/mantidimaging.core.operations.circular_mask.circular_mask
../../api/mantidimaging.core.operations.clip_values.clip_values
../../api/mantidimaging.core.operations.crop_coords.crop_coords
../../api/mantidimaging.core.operations.flat_fielding.flat_fielding
../../api/mantidimaging.core.operations.gaussian.gaussian
../../api/mantidimaging.core.operations.median_filter.median_filter
../../api/mantidimaging.core.operations.outliers.outliers
../../api/mantidimaging.core.operations.rebin.rebin
../../api/mantidimaging.core.operations.ring_removal.ring_removal
../../api/mantidimaging.core.operations.roi_normalisation.roi_normalisation
../../api/mantidimaging.core.operations.rotate_stack.rotate_stack
../../api/mantidimaging.core.operations.remove_stripe.stripe_removal
.. operations_user_doc::
21 changes: 14 additions & 7 deletions mantidimaging/core/net/help_pages.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
# Copyright (C) 2021 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later

from typing import Optional

from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices

DOCS_BASE = "https://mantidproject.github.io/mantidimaging"
SECTION_API = f"{DOCS_BASE}/api/"
SECTION_USER_GUIDE = f"{DOCS_BASE}/user_guide/"


def open_api_webpage(page_url: str):
open_help_webpage(SECTION_API, page_url)
def open_user_operation_docs(operation_name: str):
page_url = "operations/index"
section = operation_name.lower().replace(" ", "-")
open_help_webpage(SECTION_USER_GUIDE, page_url, section)


def open_help_webpage(section_url: str, page_url: str, section: Optional[str] = None):
if section is not None:
url = f"{section_url}{page_url}.html#{section}"
else:
url = f"{section_url}{page_url}.html"

def open_help_webpage(section_url: str, page_url: str):
url = QUrl(f"{section_url}{page_url}.html")
if not QDesktopServices.openUrl(url):
raise RuntimeError(f"Url could not be opened: {url.toString()}")
if not QDesktopServices.openUrl(QUrl(url)):
raise RuntimeError(f"Url could not be opened: {url}")
2 changes: 2 additions & 0 deletions mantidimaging/core/net/test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (C) 2021 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
34 changes: 34 additions & 0 deletions mantidimaging/core/net/test/test_help_pages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright (C) 2021 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later

import unittest
from unittest import mock
from PyQt5.QtCore import QUrl

from mantidimaging.core.net.help_pages import open_user_operation_docs, open_help_webpage, SECTION_USER_GUIDE


class HelpPagesTest(unittest.TestCase):
@mock.patch("mantidimaging.core.net.help_pages.open_help_webpage")
def test_open_user_operation_docs(self, open_func: mock.Mock):
open_user_operation_docs("Crop Coordinates")
open_func.assert_called_with(SECTION_USER_GUIDE, "operations/index", "crop-coordinates")

@mock.patch("mantidimaging.core.net.help_pages.QDesktopServices.openUrl")
def test_open_help_webpage(self, open_url: mock.Mock):
open_help_webpage(SECTION_USER_GUIDE, "reconstructions/center_of_rotation")
expected = QUrl(
"https://mantidproject.github.io/mantidimaging/user_guide/reconstructions/center_of_rotation.html")
open_url.assert_called_with(expected)

@mock.patch("mantidimaging.core.net.help_pages.QDesktopServices.openUrl")
def test_open_help_webpage_with_section(self, open_url: mock.Mock):
open_help_webpage(SECTION_USER_GUIDE, "operations/index", "crop-coordinates")
expected = QUrl(
"https://mantidproject.github.io/mantidimaging/user_guide/operations/index.html#crop-coordinates")
open_url.assert_called_with(expected)

@mock.patch("mantidimaging.core.net.help_pages.QDesktopServices.openUrl")
def test_open_help_webpage_error(self, open_url: mock.Mock):
open_url.return_value = False
self.assertRaises(RuntimeError, open_help_webpage, SECTION_USER_GUIDE, "reconstructions/center_of_rotation")
2 changes: 1 addition & 1 deletion mantidimaging/core/operations/arithmetic/arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def _arithmetic_func(data: np.ndarray, div_val: float, mult_val: float, add_val:


class ArithmeticFilter(BaseFilter):
"""Add, subtract, multiply, or divide an image with given values.
"""Add, subtract, multiply, or divide all grey values of an image with the given values.
Intended to be used on: Any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class CircularMaskFilter(BaseFilter):
Intended to be used on: Reconstructed slices
When: To remove reconstruction artifacts near the outside of the image.
When: To remove reconstruction artifacts on the outer edge of the image.
Caution: Ensure that the radius does not mask data from the sample.
"""
Expand Down
6 changes: 3 additions & 3 deletions mantidimaging/core/operations/clip_values/clip_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@


class ClipValuesFilter(BaseFilter):
"""Clips pixel values of the image based on the parameters. Can be used as
a way to mask a.
"""Clips grey values of the image based on the parameters. Can be used to remove outliers
and noise (e.g. negative values) from reconstructed images.
Intended to be used on: Projections
Intended to be used on: Projections and reconstructed slices
When: To remove a range of pixel values from the data.
Expand Down
4 changes: 2 additions & 2 deletions mantidimaging/core/operations/crop_coords/crop_coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
class CropCoordinatesFilter(BaseFilter):
"""Crop a region of interest from the image.
Intended to be used on: Projections, or reconstructed slices
Intended to be used on: A stack of projections, or reconstructed slices
When: To remove part of the image that contains only noise, this reduces
When: To select part of the image that is to be processed further; this reduces
memory usage and can greatly improve the speed of reconstruction.
Caution: Make sure the region of cropping does not crop parts of the sample
Expand Down
11 changes: 6 additions & 5 deletions mantidimaging/core/operations/divide/divide.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,25 @@


class DivideFilter(BaseFilter):
"""Divides the images by a value. That value is usually the pixel value,
and can be specified in either microns or cms.
"""Divides a stack of images by a value. That value can be the pixel size,
and can be specified in either microns or cms, to obtain attenuation values.
Intended to be used on: Reconstructed slices
When: To calculate attenuation values by dividing by the pixel size in Microns
When: To calculate attenuation values by dividing by the pixel size in microns
Caution: Check preview values before applying divide
"""
filter_name = "Divide"
link_histograms = True

@staticmethod
def filter_func(images: Images, value: Union[int, float] = 0e7, unit="micron", progress=None) -> Images:
def filter_func(images: Images, value: Union[int, float] = 0, unit="micron", progress=None) -> Images:
if unit == "micron":
value *= 1e-4

h.check_data_stack(images)
if value != 0e7 or value != -0e7:
if value != 0:
images.data /= value
return images

Expand Down
22 changes: 13 additions & 9 deletions mantidimaging/core/operations/flat_fielding/flat_fielding.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,20 @@ def enable_correct_fields_only(selected_flat_fielding_widget, flat_before_widget


class FlatFieldFilter(BaseFilter):
"""Uses the flat (open beam) and dark images to reduce the noise in the
projection images.
"""Uses the flat (open beam) and dark images to normalise a stack of images (radiograms, projections),
and to correct for a beam profile, scintillator imperfections and/or detector inhomogeneities. This
operation produces images of transmission values.
In practice, several open beam and dark images are averaged in the flat-fielding process.
Intended to be used on: Projections
When: As one of the first pre-processing steps to greatly reduce noise in the data
When: As one of the first pre-processing steps
Caution: Make sure the correct stacks are selected for flat and dark.
Caution: Check that the flat and dark images don't have any very bright pixels,
or this will introduce additional noise in the sample.
or this will introduce additional noise in the sample. Remove outliers before flat-fielding.
"""
filter_name = 'Flat-fielding'

Expand All @@ -73,13 +76,14 @@ def filter_func(images: Images,
progress=None) -> Images:
"""Do background correction with flat and dark images.
:param data: Sample data which is to be processed. Expected in radiograms
:param flat_before: Flat (open beam) image to use in normalization, for before the sample is imaged
:param flat_after: Flat (open beam) image to use in normalization, for after the sample is imaged
:param dark_before: Dark image to use in normalization, for before the sample is imaged
:param dark_after: Dark image to use in normalization, for before the sample is imaged
:param images: Sample data which is to be processed. Expected in radiograms
:param flat_before: Flat (open beam) image to use in normalization, collected before the sample was imaged
:param flat_after: Flat (open beam) image to use in normalization, collected after the sample was imaged
:param dark_before: Dark image to use in normalization, collected before the sample was imaged
:param dark_after: Dark image to use in normalization, collected before the sample was imaged
:param selected_flat_fielding: Select which of the flat fielding methods to use, just Before stacks, just After
stacks or combined.
:param use_dark: Whether to use dark frame subtraction
:param cores: The number of cores that will be used to process the data.
:param chunksize: The number of chunks that each worker will receive.
:return: Filtered data (stack of images)
Expand Down
4 changes: 2 additions & 2 deletions mantidimaging/core/operations/gaussian/gaussian.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
class GaussianFilter(BaseFilter):
"""Applies Gaussian filter to the data.
Intended to be used on: Projections
Intended to be used on: Projections or reconstructed slices
When: As a pre-processing step to reduce noise.
When: As a pre-processing or post-reconstruction step to reduce noise.
"""
filter_name = "Gaussian"
link_histograms = True
Expand Down
4 changes: 2 additions & 2 deletions mantidimaging/core/operations/median_filter/median_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ def validate(self, input: str, pos: int) -> Tuple[QValidator.State, str, int]:
class MedianFilter(BaseFilter):
"""Applies Median filter to the data.
Intended to be used on: Projections
Intended to be used on: Projections or reconstructed slices
When: As a pre-processing step to reduce noise.
When: As a pre-processing or post-reconstruction step to reduce noise.
"""
filter_name = "Median"
link_histograms = True
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ def _divide_by_counts(data=None, counts=None):


class MonitorNormalisation(BaseFilter):
"""Normalises the values of the data by the monitor counts read from the Sample log file.
"""Normalises the image data using the average count of a beam monitor from the
experiment log file. This scaling operation is an alternative to ROI normalisation
and allows to account for beam fluctuations and different exposure times of projections.
Intended to be used on: Projections
When: As a pre-processing step to normalise the value ranges of the data.
When: As a pre-processing step to normalise the grey value ranges of the data.
"""
filter_name = "Monitor Normalisation"
link_histograms = True
Expand Down
6 changes: 3 additions & 3 deletions mantidimaging/core/operations/outliers/outliers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@


class OutliersFilter(BaseFilter):
"""Removes pixel values that are found to be outliers by the parameters.
"""Removes pixel values that are found to be outliers as defined by the given parameters.
Intended to be used on: Projections
When: As a pre-processing step to reduce very bright or dead pixels in the data.
Caution: This should usually be the first step applied to the data, flat and dark
images, to remove pixels with very large values that will cause issues in the flat-fielding.
Caution: This should usually be one of the first steps applied to the data, flat and dark
images, to remove pixels with very large values that will cause issues for flat-fielding.
"""
filter_name = "Remove Outliers"
link_histograms = True
Expand Down
Loading

0 comments on commit 308202f

Please sign in to comment.