# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
Arithmetic computation module
-----------------------------
This module provides arithmetic operations for images, such as pixel-wise addition,
subtraction, multiplication, division, as well as operations with constants
and combined arithmetic formulas.
Main features include:
- Pixel-wise addition, subtraction, multiplication, and division between images
- Application of arithmetic operations with constants to images
- Support for quadratic difference and general arithmetic expressions
These functions are typically used for basic algebraic processing and normalization
of image data.
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
# Note:
# ----
# - All `guidata.dataset.DataSet` parameter classes must also be imported
# in the `sigima.params` module.
# - All functions decorated by `computation_function` must be imported in the upper
# level `sigima.proc.image` module.
from __future__ import annotations
import warnings
import numpy as np
from sigima.enums import MathOperator
from sigima.objects.image import ImageObj
from sigima.proc.base import (
ArithmeticParam,
ConstantParam,
)
from sigima.proc.decorator import computation_function
from sigima.proc.image.base import (
dst_1_to_1,
dst_2_to_1,
dst_n_to_1,
restore_data_outside_roi,
)
from sigima.tools.datatypes import clip_astype
# NOTE: Only parameter classes DEFINED in this module should be included in __all__.
# Parameter classes imported from other modules (like sigima.proc.base) should NOT
# be re-exported to avoid Sphinx cross-reference conflicts. The sigima.params module
# serves as the central API point that imports and re-exports all parameter classes.
__all__ = [
"addition",
"addition_constant",
"arithmetic",
"average",
"difference",
"difference_constant",
"division",
"division",
"division_constant",
"product",
"product_constant",
"quadratic_difference",
"standard_deviation",
]
# MARK: compute_n_to_1 functions -------------------------------------------------------
# Functions with N input images and 1 output image
# --------------------------------------------------------------------------------------
# Those functions are perfoming a computation on N input images and return a single
# output image. If we were only executing these functions locally, we would not need
# to define them here, but since we are using the multiprocessing module, we need to
# define them here so that they can be pickled and sent to the worker processes.
# Also, we need to systematically return the output image object, even if it is already
# modified in place, because the multiprocessing module will not be able to retrieve
# the modified object from the worker processes.
[docs]
@computation_function()
def addition(src_list: list[ImageObj]) -> ImageObj:
"""Add images in the list and return the result image object
Args:
src_list: list of input image objects
Returns:
Output image object (modified in place)
"""
dst = dst_n_to_1(src_list, "Σ") # `dst` data is initialized to `src_list[0]` data
for src in src_list[1:]:
dst.data = np.add(dst.data, src.data, dtype=float)
restore_data_outside_roi(dst, src_list[0])
return dst
[docs]
@computation_function()
def average(src_list: list[ImageObj]) -> ImageObj:
"""Compute the average of images in the list and return the result image object
Args:
src_list: list of input image objects
Returns:
Output image object (modified in place)
"""
dst = dst_n_to_1(src_list, "µ") # `dst` data is initialized to `src_list[0]` data
for src in src_list[1:]:
dst.data = np.add(dst.data, src.data, dtype=float)
dst.data /= len(src_list)
restore_data_outside_roi(dst, src_list[0])
return dst
[docs]
@computation_function()
def standard_deviation(src_list: list[ImageObj]) -> ImageObj:
"""Compute the element-wise standard deviation of multiple images.
The first image in the list defines the "base" image. All other images are
used to compute the element-wise standard deviation with the base image.
.. note::
If all images share the same region of interest (ROI), the standard deviation
is computed only within the ROI.
.. warning::
It is assumed that all images have the same size and x-coordinates.
Args:
src_list: List of source images.
Returns:
Image object representing the standard deviation of the source images.
"""
dst = dst_n_to_1(src_list, "𝜎") # `dst` data is initialized to `src_list[0]` data
assert dst.data is not None
y_array = np.array([src.data for src in src_list], dtype=dst.data.dtype)
dst.data = np.std(y_array, axis=0, ddof=0)
restore_data_outside_roi(dst, src_list[0])
return dst
[docs]
@computation_function()
def product(src_list: list[ImageObj]) -> ImageObj:
"""Multiply images in the list and return the result image object
Args:
src_list: list of input image objects
Returns:
Output image object (modified in place)
"""
dst = dst_n_to_1(src_list, "Π") # `dst` data is initialized to `src_list[0]` data
for src in src_list[1:]:
dst.data = np.multiply(dst.data, src.data, dtype=float)
restore_data_outside_roi(dst, src_list[0])
return dst
[docs]
@computation_function()
def addition_constant(src: ImageObj, p: ConstantParam) -> ImageObj:
"""Add **dst** and a constant value and return the new result image object
Args:
src: input image object
p: constant value
Returns:
Result image object **src** + **p.value** (new object)
"""
# For the addition of a constant value, we convert the constant value to the same
# data type as the input image, for consistency.
value = np.array(p.value).astype(dtype=src.data.dtype)
dst = dst_1_to_1(src, "+", str(value))
dst.data = np.add(src.data, value, dtype=float)
restore_data_outside_roi(dst, src)
return dst
[docs]
@computation_function()
def difference_constant(src: ImageObj, p: ConstantParam) -> ImageObj:
"""Subtract a constant value from an image and return the new result image object
Args:
src: input image object
p: constant value
Returns:
Result image object **src** - **p.value** (new object)
"""
# For the subtraction of a constant value, we convert the constant value to the same
# data type as the input image, for consistency.
value = np.array(p.value).astype(dtype=src.data.dtype)
dst = dst_1_to_1(src, "-", str(value))
dst.data = np.subtract(src.data, value, dtype=float)
restore_data_outside_roi(dst, src)
return dst
[docs]
@computation_function()
def product_constant(src: ImageObj, p: ConstantParam) -> ImageObj:
"""Multiply **dst** by a constant value and return the new result image object
Args:
src: input image object
p: constant value
Returns:
Result image object **src** * **p.value** (new object)
"""
# For the multiplication by a constant value, we do not convert the constant value
# to the same data type as the input image, because we want to allow the user to
# multiply an image by a constant value of a different data type. The final data
# type conversion ensures that the output image has the same data type as the input
# image.
dst = dst_1_to_1(src, "×", str(p.value))
dst.data = np.multiply(src.data, p.value, dtype=float)
restore_data_outside_roi(dst, src)
return dst
[docs]
@computation_function()
def division_constant(src: ImageObj, p: ConstantParam) -> ImageObj:
"""Divide an image by a constant value and return the new result image object
Args:
src: input image object
p: constant value
Returns:
Result image object **src** / **p.value** (new object)
"""
# For the division by a constant value, we do not convert the constant value to the
# same data type as the input image, because we want to allow the user to divide an
# image by a constant value of a different data type. The final data type conversion
# ensures that the output image has the same data type as the input image.
dst = dst_1_to_1(src, "/", str(p.value))
dst.data = np.divide(src.data, p.value, dtype=float)
restore_data_outside_roi(dst, src)
return dst
# MARK: compute_2_to_1 functions -------------------------------------------------------
# Functions with N input images + 1 input image and N output images
# --------------------------------------------------------------------------------------
[docs]
@computation_function()
def arithmetic(src1: ImageObj, src2: ImageObj, p: ArithmeticParam) -> ImageObj:
"""Compute arithmetic operation on two images
Args:
src1: input image object
src2: input image object
p: arithmetic parameters
Returns:
Result image object
"""
initial_dtype = src1.data.dtype
title = p.operation.replace("obj1", "{0}").replace("obj2", "{1}")
dst = src1.copy(title=title)
o, a, b = p.operator, p.factor, p.constant
# Apply operator
if o in (MathOperator.MULTIPLY, MathOperator.DIVIDE) and a == 0.0:
dst.data = np.ones_like(src1.data) * b
elif o == MathOperator.ADD:
dst.data = np.add(src1.data, src2.data, dtype=float) * a + b
elif o == MathOperator.SUBTRACT:
dst.data = np.subtract(src1.data, src2.data, dtype=float) * a + b
elif o == MathOperator.MULTIPLY:
dst.data = np.multiply(src1.data, src2.data, dtype=float) * a + b
elif o == MathOperator.DIVIDE:
dst.data = np.divide(src1.data, src2.data, dtype=float) * a + b
# Eventually convert to initial data type
if p.restore_dtype:
dst.data = clip_astype(dst.data, initial_dtype)
restore_data_outside_roi(dst, src1)
return dst
[docs]
@computation_function()
def difference(src1: ImageObj, src2: ImageObj) -> ImageObj:
"""Compute difference between two images
Args:
src1: input image object
src2: input image object
Returns:
Result image object **src1** - **src2** (new object)
"""
dst = dst_2_to_1(src1, src2, "-")
dst.data = np.subtract(src1.data, src2.data, dtype=float)
restore_data_outside_roi(dst, src1)
return dst
[docs]
@computation_function()
def quadratic_difference(src1: ImageObj, src2: ImageObj) -> ImageObj:
"""Compute quadratic difference between two images
Args:
src1: input image object
src2: input image object
Returns:
Result image object (**src1** - **src2**) / sqrt(2.0) (new object)
"""
dst = dst_2_to_1(src1, src2, "quadratic_difference")
dst.data = np.subtract(src1.data, src2.data, dtype=float) / np.sqrt(2.0)
restore_data_outside_roi(dst, src1)
return dst
[docs]
@computation_function()
def division(src1: ImageObj, src2: ImageObj) -> ImageObj:
"""Compute division between two images
Args:
src1: input image object
src2: input image object
Returns:
Result image object **src1** / **src2** (new object)
"""
dst = dst_2_to_1(src1, src2, "/")
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=RuntimeWarning)
dst.data = np.divide(src1.data, src2.data, dtype=float)
restore_data_outside_roi(dst, src1)
return dst