#!/usr/bin/env python
from collections import namedtuple
import matplotlib.pyplot as plt
import numpy as np
from numpy.lib.arraysetops import isin
import scipy as sp
from scipy import interpolate
import cv2 as cv
from pathlib import Path
import os.path
from spatialmath.base import argcheck, getvector, e2h, h2e, transl2
from machinevisiontoolbox.base import iread, iwrite, colorname, \
int_image, float_image, idisp, sphere_rotate, name2color
[docs]class ImageProcessingMixin:
# ======================= image processing ============================= #
[docs] def LUT(self, lut, colororder=None):
"""
Apply lookup table
:param lut: lookup table
:type lut: array_like(256), ndarray(256,N)
:param colororder: colororder for output image, optional
:type colororder: str or dict
:return: transformed image
:rtype: :class:`Image`
For a greyscale image the LUT can be:
- (256,)
- (256,N) in which case the resulting image has ``N`` planes created
my applying the I'th column of the LUT to the input image
For a color image the LUT can be:
- (256,) and applied to every plane, or
- (256,N) where the LUT columns are applied to the ``N`` planes of
the input image.
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> import numpy as np
>>> img = Image([[100, 150], [200, 250]])
>>> img.LUT(np.arange(255, -1, -1, dtype='uint8')).A
.. note:: Works only for ``uint8`` and ``int8`` image and LUT.
:references:
- Robotics, Vision & Control for Python, Section 11.3, P. Corke, Springer 2023.
:seealso: `cv2.LUT <https://docs.opencv.org/master/d2/de8/group__core__array.html#gab55b8d062b7f5587720ede032d34156f>`_
"""
image = self.to_int()
lut = np.array(lut).astype(np.uint8)
if lut.ndim == 2:
lut = lut[np.newaxis, ...]
if self.nplanes == 1:
image = np.dstack((image,) * lut.shape[2])
out = cv.LUT(image, lut)
if colororder is None:
colororder = self.colororder
return self.__class__(self.like(out), colororder=colororder)
[docs] def apply(self, func, vectorize=False):
"""
Apply a function to an image
:param func: function to apply to image or pixel
:type func: callable
:return: transformed image
:rtype: :class:`Image`
If ``vectorize`` is False the function is called with a single argument
which is the underlying NumPy array, and it must return a NumPy array.
The return array can have different dimensions to its argument.
If ``vectorize`` is True the function is called for every pixel with a
single argument which is a scalar or a 1d-array of length equal to the
number of color planes. The return array will have the same dimensions
to its argument.
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> import numpy as np
>>> import math
>>> img = Image([[1, 2], [3, 4]])
>>> img.apply(np.sqrt).image
>>> img.apply(lambda x: math.sqrt(x), vectorize=True).image
.. note:: Slow when ``vectorize=True`` which involves a large number
of calls to ``func``.
:references:
- Robotics, Vision & Control for Python, Section 11.3, P. Corke, Springer 2023.
:seealso: :meth:`apply2`
"""
if vectorize:
func = np.vectorize(func)
return self.__class__(func(self.A), colororder=self.colororder)
[docs] def apply2(self, other, func, vectorize=False):
"""
Apply a function to two images
:param func: function to apply to image or pixel
:type func: callable
:raises ValueError: images must have same size
:return: transformed image
:rtype: :class:`Image`
If ``vectorize`` is False the function is called with two arguments
which are the underlying NumPy arrays, and it must return a NumPy array.
The return array can have different dimensions to its arguments.
If ``vectorize`` is True the function is called for every pixel in both
images with two arguments which are the corresponding pixel values as a
scalar or 1d-array of length equal to the number of color planes. The
function returns a scalar or a 1d-array. The return array will have the
same dimensions to its argument.
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> import numpy as np
>>> import math
>>> img1 = Image([[1, 2], [3, 4]])
>>> img2 = Image([[5, 6], [7, 8]])
>>> img1.apply2(img2, np.hypot).image
>>> img1.apply2(img2, lambda x, y: math.hypot(x,y), vectorize=True).image
.. note:: Slow when ``vectorize=True`` which involves a large number
of calls to ``func``.
:references:
- Robotics, Vision & Control for Python, Section 11.4, P. Corke, Springer 2023.
:seealso: :meth:`apply`
"""
if self.size != other.size:
raise ValueError('two images must have same size')
if vectorize:
func = np.vectorize(func)
return self.__class__(func(self.A, other.A), colororder=self.colororder)
[docs] def clip(self, min, max):
"""
Clip pixel values
:param min: minimum value
:type min: int or float
:param max: maximum value
:type max: int or float
:return: transformed image
:rtype: :class:`Image`
Pixels in the returned image will have values in the range [``min``, ``max``]
inclusive.
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img = Image([[1, 2], [3, 4]])
>>> img.clip(2, 3).A
:seealso: :func:`numpy.clip`
"""
return self.__class__(np.clip(self.A, min, max), colororder=self.colororder)
[docs] def roll(self, ru=0, rv=0):
"""
Roll image by row or column
:param ru: roll in the column direction, defaults to 0
:type ru: int, optional
:param rv: roll in the row direction, defaults to 0
:type rv: int, optional
:return: rolled image
:rtype: :class:`Image` instance
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img = Image([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
>>> img.roll(ru=1).A
>>> img.roll(ru=-1).A
>>> img.roll(rv=1).A
>>> img.roll(ru=1, rv=-1).A
:seealso: :func:`numpy.roll`
"""
return self.__class__(np.roll(self.image, (ru, rv), (1, 0)))
[docs] def normhist(self):
"""
Histogram normalisaton
:return: normalised image
:rtype: :class:`Image` instance
Return a histogram normalized version of the image which highlights
image detail in low-contrast areas of an image.
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img = Image([[10, 20, 30], [40, 41, 42], [70, 80, 90]])
>>> img.normhist().A
.. note::
- The histogram of the normalized image is approximately uniform,
that is, all grey levels ae equally likely to occur.
- Color images automatically converted to grayscale
:references:
- Robotics, Vision & Control for Python, Section 11.3, P. Corke, Springer 2023.
:seealso: `cv2.equalizeHist <https://docs.opencv.org/master/d6/dc7/group__imgproc__hist.html#ga7e54091f0c937d49bf84152a16f76d6e>`_
"""
out = cv.equalizeHist(self.to_int())
return self.__class__(self.like(out))
[docs] def stretch(self, max=1, range=None, clip=True):
"""
Image normalisation
:param max: maximum value of output pixels, defaults to 1
:type max: int, float, optional
:param range: range[0] is mapped to 0, range[1] is mapped to max
:type range: array_like(2), optional
:param clip: clip pixel values to interval [0, max], defaults to True
:type clip: bool, optional
:return: Image with pixel values stretched to M across r
:rtype: :class:`Image`
Returns a normalised image in which all pixel values are linearly mapped
to the interval of 0.0 to ``max``. That is, the minimum pixel value is
mapped to 0 and the maximum pixel value is mapped to ``max``.
If ``range`` is specified then ``range[0]`` is mapped to 0.0 and
``range[1]`` is mapped to ``max``. If ``clip`` is False then pixels
less than ``range[0]`` will be mapped to a negative value and pixels
greater than ``range[1]`` will be mapped to a value greater than
``max``.
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img = Image([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
>>> img.stretch().A
:references:
- Robotics, Vision & Control, Section 12.1, P. Corke,
Springer 2011.
"""
# TODO make all infinity values = None?
im = self.A
if range is None:
mn = np.min(im)
mx = np.max(im)
else:
range = argcheck.getvector(range)
mn = range[0]
mx = range[1]
zs = (im - mn) / (mx - mn) * max
if range is not None and clip:
zs = np.maximum(0, np.minimum(max, zs))
return self.__class__(zs)
[docs] def thresh(self, t=None, opt='binary'):
r"""
Image threshold
:param t: threshold value
:type t: scalar, str
:param option: threshold option, defaults to 'binary'
:type option: str, optional
:return: thresholded image
:rtype: :class:`Image`
Apply a threshold ``t`` to the image. Various thresholding options are
supported:
================ =====================================================================================================================
Option Function
================ =====================================================================================================================
``'binary'`` :math:`Y_{u,v} = \left\{ \begin{array}{l} m \mbox{, if } X_{u,v} > t \\ 0 \mbox{, otherwise} \end{array} \right.`
``'binary_inv'`` :math:`Y_{u,v} = \left\{ \begin{array}{l} 0 \mbox{, if } X_{u,v} > t \\ m \mbox{, otherwise} \end{array} \right.`
``'truncate'`` :math:`Y_{u,v} = \left\{ \begin{array}{l} t \mbox{, if } X_{u,v} > t \\ X_{u,v} \mbox{, otherwise} \end{array} \right.`
``'tozero'`` :math:`Y_{u,v} = \left\{ \begin{array}{l} X_{u,v} \mbox{, if } X_{u,v} > t \\ 0 \mbox{, otherwise} \end{array} \right.`
``'tozero_inv'`` :math:`Y_{u,v} = \left\{ \begin{array}{l} 0 \mbox{, if } X_{u,v} > t \\ X_{u,v} \mbox{, otherwise} \end{array} \right.`
================ =====================================================================================================================
where :math:`m` is the maximum value of the image datatype.
If threshold ``t`` is a string then the threshold is determined
automatically:
+---------------+-----------------------------------------------------+
|threshold | algorithm |
+===============+=====================================================+
|``'otsu'`` | Otsu's method finds the threshold that minimizes |
| | the within-class variance. This technique is |
| | effective for a bimodal greyscale histogram. |
+---------------+-----------------------------------------------------|
|``'triangle'`` | The triangle method constructs a line between the |
| | histogram peak and the farthest end of the |
| | histogram. The threshold is the point of maximum |
| | distance between the line and the histogram. This |
| | technique is effective when the object pixels |
| | produce a weak peak in the histogram. |
+---------------+-----------------------------------------------------+
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img = Image([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
>>> img.thresh(5).image
.. note::
- The threshold is applied to all color planes
- If threshold is 'otsu' or 'triangle' the image must be greyscale,
and the computed threshold is also returned.
:references:
- A Threshold Selection Method from Gray-Level Histograms, N. Otsu.
IEEE Trans. Systems, Man and Cybernetics Vol SMC-9(1), Jan 1979,
pp 62-66.
- Automatic measurement of sister chromatid exchange frequency"
Zack (Zack GW, Rogers WE, Latt SA (1977),
J. Histochem. Cytochem. 25 (7): 741–53.
- Robotics, Vision & Control for Python, Section 12.1.1, P. Corke, Springer 2023.
:seealso: :meth:`ithresh` :meth:`adaptive_threshold` :meth:`otsu` `opencv.threshold <https://docs.opencv.org/3.4/d7/d1b/group__imgproc__misc.html#gae8a4a146d1ca78c626a53577199e9c57>`_
"""
# dictionary of threshold options from OpenCV
options_dict = {
'binary': cv.THRESH_BINARY,
'binary_inv': cv.THRESH_BINARY_INV,
'truncate': cv.THRESH_TRUNC,
'tozero': cv.THRESH_TOZERO,
'tozero_inv': cv.THRESH_TOZERO_INV,
}
threshold_dict = {
'otsu': cv.THRESH_OTSU,
'triangle': cv.THRESH_TRIANGLE
}
flag = options_dict[opt]
if isinstance(t, str):
# auto threshold requested
flag |= threshold_dict[t]
threshvalue, imt = cv.threshold(
src=self.to_int(),
thresh=0.0,
maxval=self.maxval,
type=flag)
return self.__class__(self.like(imt)), self.like(int(threshvalue), maxint=255)
elif argcheck.isscalar(t):
# threshold is given
_, imt = cv.threshold(
src=self.image,
thresh=t,
maxval=self.maxval,
type=flag)
return self.__class__(imt)
else:
raise ValueError(t, 't must be a string or scalar')
[docs] def ithresh(self):
r"""
Interactive thresholding
:return: selected threshold value
:rtype: scalar
The image is displayed with a binary threshold displayed in a simple
Matplotlib GUI along with the histogram and a slider for threshold
value. Adjusting the slider changes the thresholded image view.
The displayed image is
.. math:: Y_{u,v} = \left\{ \begin{array}{l} m \mbox{, if } X_{u,v} > t \\ 0 \mbox{, otherwise} \end{array} \right.
:references:
- Robotics, Vision & Control for Python, Section 12.1.1.1, P. Corke, Springer 2023.
:seealso: :meth:`thresh` :meth:`adaptive_threshold` :meth:`otsu` `opencv.threshold <https://docs.opencv.org/3.4/d7/d1b/group__imgproc__misc.html#gae8a4a146d1ca78c626a53577199e9c57>`_
"""
# ACKNOWLEDGEMENT: https://matplotlib.org/devdocs/gallery/widgets/range_slider.html
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
from matplotlib import colors
#N = 128
Ncolors = 256
img = self.image
t = int((img.max() + img.min()) / 2)
x = np.linspace(self.min(), self.max(), Ncolors)
fig, axs = plt.subplots(1, 2, figsize=(10, 5))
plt.subplots_adjust(bottom=0.25)
def colormap(t):
X = np.tile(x > t, (3, 1)).T # N x 3 colormap
X = np.hstack([X, np.ones((Ncolors, 1))]) # N x 4
return colors.LinearSegmentedColormap.from_list('threshold_colormap', X)
im = axs[0].imshow(img, cmap="gray")
im.set_cmap(colormap(t))
axs[1].hist(img.flatten(), bins='auto')
axs[1].set_title('Histogram of pixel intensities')
# Create the Slider
slider_ax = plt.axes([0.20, 0.1, 0.60, 0.03])
slider = Slider(slider_ax, "Threshold", img.min(), img.max(), t)
# Create the Vertical lines on the histogram
lower_limit_line = axs[1].axvline(slider.val, color='k')
thresh = t
def update(val):
# The val passed to a callback by the Slider
# Update the image's colormap
# im.norm.vmin = val
# im.norm.vmax = val
nonlocal thresh
im.set_cmap(colormap(val))
# Update the position of the vertical line
lower_limit_line.set_xdata([val, val])
# Redraw the figure to ensure it updates
fig.canvas.draw_idle()
thresh = val
slider.on_changed(update)
plt.show(block=True)
return thresh
# def ithresh2(self):
# # ACKNOWLEDGEMENT: https://matplotlib.org/devdocs/gallery/widgets/range_slider.html
# import numpy as np
# import matplotlib.pyplot as plt
# from matplotlib.widgets import RangeSlider
# #N = 128
# img = self.image
# fig, axs = plt.subplots(1, 2, figsize=(10, 5))
# plt.subplots_adjust(bottom=0.25)
# im = axs[0].imshow(img)
# axs[1].hist(img.flatten(), bins='auto')
# axs[1].set_title('Histogram of pixel intensities')
# # Create the RangeSlider
# slider_ax = plt.axes([0.20, 0.1, 0.60, 0.03])
# slider = RangeSlider(slider_ax, "Threshold", img.min(), img.max())
# # Create the Vertical lines on the histogram
# lower_limit_line = axs[1].axvline(slider.val[0], color='k')
# upper_limit_line = axs[1].axvline(slider.val[1], color='k')
# def update(val):
# # The val passed to a callback by the RangeSlider will
# # be a tuple of (min, max)
# # Update the image's colormap
# im.norm.vmin = val[0]
# im.norm.vmax = val[1]
# # Update the position of the vertical lines
# lower_limit_line.set_xdata([val[0], val[0]])
# upper_limit_line.set_xdata([val[1], val[1]])
# # Redraw the figure to ensure it updates
# fig.canvas.draw_idle()
# slider.on_changed(update)
# plt.show(block=True)
def adaptive_threshold(self, C=0, width=3):
#TODO options
# looks like Niblack
im = self.to_int()
out = cv.adaptiveThreshold(
src=im,
maxValue=255,
adaptiveMethod=cv.ADAPTIVE_THRESH_MEAN_C,
thresholdType=cv.THRESH_BINARY,
blockSize=width*2+1,
C=C
)
return self.__class__(self.like(out))
[docs] def otsu(self):
"""
Otsu threshold selection
:return: Otsu's threshold
:rtype: scalar
Compute the optimal threshold for binarizing an image with a
bimodal intensity histogram. ``t`` is a scalar threshold that
maximizes the variance between the classes of pixels below and above
the thresold ``t``.
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img = Image.Read('street.png')
>>> img.otsu()
.. note::
- Converts a color image to greyscale.
- OpenCV implementation gives slightly different result to
MATLAB Machine Vision Toolbox.
:references:
- A Threshold Selection Method from Gray-Level Histograms, N. Otsu.
IEEE Trans. Systems, Man and Cybernetics Vol SMC-9(1), Jan 1979,
pp 62-66.
- An improved method for image thresholding on the valley-emphasis
method. H-F Ng, D. Jargalsaikhan etal. Signal and Info Proc.
Assocn. Annual Summit and Conf (APSIPA). 2013. pp1-4
- Robotics, Vision & Control for Python, Section 12.1.1, P. Corke, Springer 2023.
:seealso: :meth:`thresh` :meth:`ithresh` :meth:`adaptive_threshold` `opencv.threshold <https://docs.opencv.org/3.4/d7/d1b/group__imgproc__misc.html#gae8a4a146d1ca78c626a53577199e9c57>`_
"""
_, t = self.thresh(t='otsu')
return t
[docs] def blend(self, image2, alpha, beta=None, gamma=0):
r"""
Image blending
:param image2: second image
:type image2: :class:`Image`
:param alpha: fraction of image
:type alpha: float
:param beta: fraction of ``image2``, defaults to 1-``alpha``
:type beta: float, optional
:param gamma: gamma nonlinearity, defaults to 0
:type gamma: int, optional
:raises ValueError: images are not same size
:raises ValueError: images are of different type
:return: blended image
:rtype: :class:`Image`
The resulting image is
.. math::
\mathbf{Y} = \alpha \mathbf{X}_1 + \beta \mathbf{X}_2 + \gamma
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img1 = Image.Constant(3, value=4)
>>> img2 = Image([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
>>> img1.blend(img2, 0.5, 2).A
.. note::
- For integer images the result is saturated.
- For a multiplane image each plane is processed independently.
:seealso: :meth:`choose` `cv2.addWeighted <https://docs.opencv.org/master/d2/de8/group__core__array.html#gafafb2513349db3bcff51f54ee5592a19>`_
"""
if self.shape != image2.shape:
raise ValueError('images are not the same size')
if self.isint != image2.isint:
raise ValueError('images must be both int or both floating type')
if beta is None:
beta = 1 - alpha
out = cv.addWeighted(self.A, alpha, image2.A, beta, gamma)
return self.__class__(out, colororder=self.colororder)
[docs] def choose(self, image2, mask):
r"""
Pixel-wise image merge
:param image2: second image
:type image2: :class:`Image`, array_like(3), str
:param mask: image mask
:type mask: ndarray(H,W)
:raises ValueError: image and mask must be same size
:raises ValueError: image and image2 must be same size
:return: merged images
:rtype: :class:`Image`
Return an image where each pixel is selected from the corresponding
pixel in self or ``image2`` according to the corresponding pixel values
in ``mask``. If the element of ``mask`` is zero/false the pixel value
from self is selected, otherwise the pixel value from ``image2`` is selected:
.. math::
\mathbf{Y}_{u,v} = \left\{ \begin{array}{ll}
\mathbf{X}_{1:u,v} & \mbox{if } \mathbf{M}_{u,v} = 0 \\
\mathbf{X}_{2:u,v} & \mbox{if } \mathbf{M}_{u,v} > 0
\end{array} \right.
If ``image2`` is a scalar or 1D array it is taken as the pixel value,
and must have the same number of elements as the channel depth of self.
If ``image2`` is a string it is taken as a colorname which is looked up
using ``name2color``.
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img1 = Image.Constant(3, value=10)
>>> img2 = Image.Constant(3, value=80)
>>> img = Image([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
>>> img1.choose(img2, img >=5).image
>>> img1 = Image.Constant(3, value=[0,0,0])
>>> img1.choose('red', img>=5).red().image
.. note::
- If image and ``image2`` are both greyscale then the result is
greyscale.
- If either of image and ``image2`` are color then the result is color.
- If one image is double and the other is integer, then the integer
image is first converted to a double image.
- ``image2`` can contain a color descriptor which is one of: a scalar
value corresponding to a greyscale, a 3-vector corresponding to a
color value, or a string containing the name of a color which is
found using :meth:`name2color`.
:seealso: :func:`~machinevisiontoolbox.base.color.name2color` `opencv.bitwise_and <https://docs.opencv.org/master/d2/de8/group__core__array.html#ga60b4d04b251ba5eb1392c34425497e14>`_
"""
im1 = self.image
if isinstance(mask, self.__class__):
mask = mask.A > 0
elif not isinstance(mask, np.ndarray):
raise ValueError('bad type for mask')
mask = mask.astype(np.uint8)
if im1.shape[:2] != mask.shape:
raise ValueError('image and mask must be same size')
if isinstance(image2, self.__class__):
# second image is Image type
im2 = image2.image
if im1.shape != im2.shape:
raise ValueError('image and image2 must be same size')
else:
# second image is scalar, 3-vector or str
dt = self.dtype
shape = self.shape[:2]
if isinstance(image2, (int, float)):
# scalar
im2 = np.full(shape, image2, dtype=dt)
else:
# possible color value
if isinstance(image2, str):
# it's a colorname, look it up
color = self.like(name2color(image2))
else:
try:
color = argcheck.getvector(image2, 3)
except:
raise ValueError('expecting a scalar, string or 3-vector')
if self.isbgr:
color = color[::-1]
im2 = np.dstack((
np.full(shape, color[0], dtype=dt),
np.full(shape, color[1], dtype=dt),
np.full(shape, color[2], dtype=dt)))
if im1.ndim == 2 and im2.ndim > 2:
im1 = np.repeat(np.atleast_3d(im1), im2.shape[2], axis=2)
m = cv.bitwise_and(mask, np.uint8([1]))
m_not = cv.bitwise_xor(mask, np.uint8([1]))
out = cv.bitwise_and(im1, im1, mask=m_not) \
+ cv.bitwise_and(im2, im2, mask=mask)
return self.__class__(out, colororder=self.colororder)
[docs] def paste(self,
pattern,
pt,
method='set',
position='topleft',
copy=False,
zero=True):
"""
Paste an image into an image
:param pattern: image to be pasted
:type pattern: :class:`Image`, ndarray(H,W)
:param pt: coordinates (u,v) where pattern is pasted
:type pt: array_like(2)
:param method: options for image merging, one of: ``'set'`` [default],
``'mean'``, ``'add'``
:type method: str
:param position: ``pt`` is one of: ``'topleft'`` [default] or ``'centre'``
:type position: str, optional
:param copy: copy image before pasting, defaults to False
:type copy: bool, optional
:param zero: zero-based coordinates (True, default) or 1-based coordinates (False)
:type zero: bool, optional
:raises ValueError: pattern is positioned outside the bounds of the image
:return: original image with pasted pattern
:rtype: :class:`Image`
Pastes the ``pattern`` into the image which is modified inplace. The
pattern can be incorporated into the specified image by:
========== ================================================================
method description
========== ================================================================
``'set'`` overwrites the pixels in image
``'add'`` adds to the pixels in image
``'mean'`` sets pixels to the mean of the pixel values in pattern and image
========== ================================================================
The ``position`` of the pasted ``pattern`` in the image can be specified
by its top left corner (umin, vmin) or its centre in the image.
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img1 = Image.Constant(5, value=10)
>>> pattern = Image([[11, 12], [13, 14]])
>>> img1.copy().paste(pattern, (1,2)).image
>>> img1.copy().paste(pattern, (1,2), method='add').image
>>> img1.copy().paste(pattern, (1,2), method='mean').image
.. note::
- Pixels outside the pasted region are unaffected.
- If ``copy`` is False the image is modified in place
- For ``position='centre'`` an odd sized pattern is assumed. For
an even dimension the centre pixel is the one at dimension / 2.
- Multi-plane images are supported.
- If the pattern is multiplane and the image is singleplane, the image planes
are replicated and colororder is taken from the pattern.
- If the image is multiplane and the pattern is singleplane, the pattern planes
are replicated.
"""
# TODO can likely replace a lot of this with np.where?
# check inputs
pt = argcheck.getvector(pt)
if isinstance(pattern, np.ndarray):
pattern = self.__class__(pattern)
# TODO check optional inputs valid
# TODO need to check that centre+point+pattern combinations are valid
# for given canvas size
cw = self.width
ch = self.height
pw = pattern.width
ph = pattern.height
colororder = self.colororder
if position in ('centre', 'center'):
left = pt[0] - pw // 2
top = pt[1] - ph // 2
elif position == 'topleft':
left = pt[0] # x
top = pt[1] # y
else:
raise ValueError('bad position specified')
if not zero:
left += 1
top += 1
# indices must be integers
left = int(left)
top = int(top)
if (top+ph) > ch:
raise ValueError(ph, 'pattern falls off bottom edge')
if (left+pw) > cw:
raise ValueError(pw, 'pattern falls off right edge')
npc = pattern.nplanes
nc = self.nplanes
if npc > nc:
# pattern has multiple planes, replicate the canvas
# sadly, this doesn't work because repmat doesn't work on 3D
# arrays
# o = np.matlib.repmat(canvas.image, [1, 1, npc])
o = np.dstack([self.A for i in range(npc)])
colororder = pattern.colororder
else:
if copy:
o = self.image.copy()
else:
o = self.image
if npc < nc:
pim = np.dstack([pattern.A for i in range(nc)])
# pattern.image = np.matlib.repmat(pattern.image, [1, 1, nc])
else:
pim = pattern.image
if method == 'set':
if pattern.iscolor:
o[top:top+ph, left:left+pw, :] = pim
else:
o[top:top+ph, left:left+pw] = pim
elif method == 'add':
if pattern.iscolor:
o[top:top+ph, left:left+pw, :] = o[top:top+ph,
left:left+pw, :] + pim
else:
o[top:top+ph, left:left+pw] = o[top:top+ph,
left:left+pw] + pim
elif method == 'mean':
if pattern.iscolor:
old = o[top:top+ph, left:left+pw, :]
k = ~np.isnan(pim)
old[k] = 0.5 * (old[k] + pim[k])
o[top:top+ph, left:left+pw, :] = old
else:
old = o[top:top+ph, left:left+pw]
k = ~np.isnan(pim)
old[k] = 0.5 * (old[k] + pim[k])
o[top:top+ph, left:left+pw] = old
elif method == 'blend':
# compute the mean using float32 to avoid overflow issues
bg = o[top:top+ph, left:left+pw].astype(np.float32)
fg = pim.astype(np.float32)
blend = 0.5 * (bg + fg)
blend = blend.astype(self.dtype)
# make masks for foreground and background
fg_set = (fg > 0).astype(np.uint8)
bg_set = (bg > 0).astype(np.uint8)
# blend is valid
blend_mask = cv.bitwise_and(fg_set, bg_set)
# only fg is valid
fg_mask = cv.bitwise_and(fg_set, cv.bitwise_xor(bg_set, 1))
# only bg is valid
bg_mask = cv.bitwise_and(cv.bitwise_xor(fg_set, 1), bg_set)
# merge them
out = cv.bitwise_and(blend, blend, mask=blend_mask) \
+ cv.bitwise_and(bg, bg, mask=bg_mask) \
+ cv.bitwise_and(fg, fg, mask=fg_mask)
o[top:top+ph, left:left+pw] = out
else:
raise ValueError('method is not valid')
if copy:
return self.__class__(o, copy=copy, colororder=colororder)
else:
self.A = o
return self
[docs] def invert(self):
r"""
Invert image
:return: _description_
:rtype: _type_
For an integer image
.. math:: Y_{u,v} = \left\{ \begin{array}{l} p_{\mbox{max}} \mbox{, if } X_{u,v} = 0 \\ p_{\mbox{min}} \mbox{, otherwise} \end{array}\right.
where :math:`p_{\mbox{min}}` and :math:`p_{\mbox{max}}` are respectively
the minimum and maximum value of the datatype.
For a float image
.. math:: Y_{u,v} = \left\{ \begin{array}{l} 1.0 \mbox{, if } X_{u,v} = 0 \\ 0.0 \mbox{, otherwise} \end{array}\right.
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img = Image([[0, 1], [2, 3]])
>>> img.invert().image
"""
if self.isint:
out = np.where(self.image == 0, self.like(self.maxval), self.like(self.minval))
elif self.isfloat:
out = np.where(self.image == 0, 1.0, 0.0)
return self.__class__(out)
# def scalespace(self, n, sigma=1):
# im = self.copy()
# g = []
# scale = 0.5
# scales = []
# lap = []
# L = Kernel.Laplace()
# scale = sigma
# for i in range(n):
# im = im.smooth(sigma)
# g.append(im)
# lap.append(im.convolve(L))
# scales.append(scale)
# scale = np.sqrt(scale ** 2 + sigma ** 2)
# scales.append(scale)
# g.append(im)
# x = (g[-1] - g[-2]) * scale ** 2
# lap.append(x)
# return g, lap, scales
# --------------------------------------------------------------------------- #
if __name__ == "__main__":
import pathlib
import os.path
from machinevisiontoolbox import Image
# a = Image.Read('street.png')
# a.ithresh()
a = Image.Read('castle2.png')
b = a.labels_MSER()
#exec(open(pathlib.Path(__file__).parent.parent.absolute() / "tests" / "test_processing.py").read()) # pylint: disable=exec-used