Source code for creamas.domains.image.features

"""
.. py:module:: features
    :platform: Unix

Various feature implementations for images. See :class:`~creamas.rules.feature.Feature` for general usage of features.
"""

import numpy as np
import cv2

from creamas.rules.feature import Feature

__all__ = ['ImageComplexityFeature', 'ImageRednessFeature',
           'ImageGreennessFeature', 'ImageBluenessFeature',
           'ImageIntensityFeature', 'ImageBenfordsLawFeature',
           'ImageEntropyFeature', 'ImageSymmetryFeature']


def fractal_dimension(image):
    """Computes the fractal dimension of an image with box counting.
    Counts pixels with value 0 as empty and everything else as non-empty.
    Input image has to be grayscale.

    See, e.g `fractal dimension on Wikipedia <https://en.wikipedia.org/wiki/Fractal_dimension>`_.

    :param numpy.ndarray image: Grayscale image.
    :returns: The computed fractal dimension as *float*.
    """
    pixels = np.asarray(np.nonzero(image > 0)).transpose()

    lx = image.shape[1]
    ly = image.shape[0]
    if len(pixels) < 2:
        return 0
    scales = np.logspace(1, 4, num=20, endpoint=False, base=2)
    Ns = []
    for scale in scales:
        H, edges = np.histogramdd(pixels, bins=(np.arange(0, lx, scale), np.arange(0, ly, scale)))
        H_sum = np.sum(H > 0)
        if H_sum == 0:
            H_sum = 1
        Ns.append(H_sum)

    coeffs = np.polyfit(np.log(scales), np.log(Ns), 1)
    hausdorff_dim = -coeffs[0]

    return hausdorff_dim


def channel_portion(image, channel):
    """Computes the amount of color channel relative to other colors.

    :param numpy.ndarray image: RGB image.
    :param int channel: Channel which portion is to be computed
    :returns: Portion of the channel in the image as *float*.
    """
    # Separate color channels
    rgb = []
    for i in range(3):
        rgb.append(image[:, :, i].astype(int))
    ch = rgb.pop(channel)

    relative_values = ch - np.sum(rgb, axis=0) / 2
    relative_values = np.maximum(np.zeros(ch.shape), relative_values)

    return float(np.average(relative_values) / 255)


def intensity(image):
    """Compute the average intensity of the pixels in an image.
    Accepts both RGB and grayscale images.

    :param numpy.ndarray image: Image
    :returns: Average image intensity as *float*.
    """
    if len(image.shape) > 2:
        # Convert to grayscale
        image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) / 255
    elif issubclass(image.dtype.type, np.integer):
        image /= 255
    return float(np.sum(image) / np.prod(image.shape))


[docs]class ImageComplexityFeature(Feature): def __init__(self): """Feature that computes the fractal dimension of an image. The color values must be in range [0, 255] and type *int*. Returns a *float*. """ super().__init__('image_complexity', ['image'], float)
[docs] def extract(self, artifact, canny_threshold1=100, canny_threshold2=200): """Extract fractal dimension estimate from the given image artifact. The method first extracts edges using `Canny edge detection <https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_canny/py_canny.html>`_ and the resulting edge image is passed down to the fractal dimension estimator. """ img = artifact.obj if len(img.shape) > 2: img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) edges = cv2.Canny(img, canny_threshold1, canny_threshold2) return float(fractal_dimension(edges))
[docs]class ImageBenfordsLawFeature(Feature): def __init__(self, ): """Feature that computes the Benford's Law for the image. .. seealso:: `Benford's Law <https://en.wikipedia.org/wiki/Benford%27s_law>`_ """ super().__init__("image_Benfords_law", ['image'], float) # Histogram bin values for Benford self.b = [0.301, 0.176, 0.125, 0.097, 0.079, 0.067, 0.058, 0.051, 0.046] self.b_max = (1.0 - self.b[0]) + np.sum(self.b[1:])
[docs] def extract(self, artifact): """Extract Benford's law from the image. """ img = artifact.obj # Convert color image to black and white if len(img.shape) == 3: img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) hist = cv2.calcHist([img], [0], None, [9], [0, 256]) # Sort, reverse and rescale to give the histogram a sum of 1.0 h2 = np.sort(hist, 0)[::-1] * (1.0 / np.sum(hist)) # Compute Benford's Law feature total = np.sum([np.abs(h2[i] - self.b[i]) for i in range(len(h2))]) benford = float(1.0 - (total / self.b_max)) return 0.0 if benford < 0 else benford
[docs]class ImageEntropyFeature(Feature): MIN = 0.0 # Max entropy for 256 bins, i.e. the histogram has even distribution MAX = 5.5451774444795623 def __init__(self, normalize=False): """Compute entropy of an image. Entropy computation uses 256 bins and a grayscale image. :param bool normalize: Optional, normalize the returned entropy value. """ super().__init__('image_entropy', ['image'], float) self._normalize = normalize
[docs] def extract(self, artifact): """Extract entropy from the image. """ img = artifact.obj # Convert color image to black and white if len(img.shape) == 3: img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) img = img.astype(np.uint8) hg = cv2.calcHist([img], [0], None, [256], [0, 256]) # Compute probabilities for each bin in histogram hg = hg / (img.shape[0] * img.shape[1]) # Compute entropy based on bin probabilities e = -np.sum([hg[i] * np.log(hg[i]) for i in range(len(hg)) if hg[i] > 0.0]) e = float(e) if self._normalize: return e / ImageEntropyFeature.MAX else: return e
[docs]class ImageSymmetryFeature(Feature): HORIZONTAL = 1 VERTICAL = 2 DIAGONAL = 4 ALL_AXES = 7 def __init__(self, axis, use_entropy=True): """Compute symmetry of the image for given ax or combination of axis. Feature allows adding the computed symmetry with "liveliness" of the image using ``use_entropy=True``. If entropy is not used, simple images (e.g. single color images) will give high symmetry values. :param axis: :attr:`ImageSymmetryFeature.HORIZONTAL`, :attr:`ImageSymmetryFeature.VERTICAL`, :attr:`ImageSymmetryFeature.DIAGONAL`, :attr:`ImageSymmetryFeature.ALL_AXES` These can be combined, e.g. ``axis=ImageSymmetryFeature.HORIZONTAL + ImageSymmetryFeature.VERTICAL``. :param bool use_entropy: If **True** multiples the computed symmetry value with image's entropy ("liveliness"). """ super().__init__('image_symmetry', ['image'], float) self.axis = axis self.threshold = 13 b = "{:0>3b}".format(self.axis) self.horizontal = int(b[2]) self.vertical = int(b[1]) self.diagonal = int(b[0]) self.liveliness = use_entropy self.ief = ImageEntropyFeature(normalize=True) def _hsymm(self, left, right): fright = np.fliplr(right) delta = np.abs(left - fright) t = delta <= self.threshold sym = np.sum(t) / (left.shape[0] * left.shape[1]) return sym def _vsymm(self, up, down): fdown = np.flipud(down) delta = np.abs(up - fdown) t = delta <= self.threshold sym = np.sum(t) / (up.shape[0] * up.shape[1]) return sym def _dsymm(self, ul, ur, dl, dr): fdr = np.fliplr(np.flipud(dr)) fur = np.fliplr(np.flipud(ur)) d1 = np.abs(ul - fdr) d2 = np.abs(dl - fur) t1 = d1 <= self.threshold t2 = d2 <= self.threshold s1 = np.sum(t1) / (ul.shape[0] * ul.shape[1]) s2 = np.sum(t2) / (ul.shape[0] * ul.shape[1]) return (s1 + s2) / 2
[docs] def extract(self, artifact): """Extract symmetry of the image. """ img = artifact.obj if len(img.shape) == 3: img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) img = img * 1.0 cx = int(img.shape[0] / 2) cy = int(img.shape[1] / 2) n = 0 symms = 0.0 liv = 1.0 if self.horizontal: symms += self._hsymm(img[:, :cy], img[:, cx:]) n += 1 if self.vertical: symms += self._vsymm(img[:cx, :], img[cx:, :]) n += 1 if self.diagonal: symms += self._dsymm(img[:cx, :cy], img[:cx, cy:], img[cx:, :cy], img[cx:, cy:]) n += 1 if self.liveliness: liv = self.ief(artifact) return float(liv * (symms / n))
[docs]class ImageRednessFeature(Feature): def __init__(self): """Feature that measures the redness of an RGB image. Returns *float* in range [0, 1]. """ super().__init__('image_redness', ['image'], float)
[docs] def extract(self, artifact): """Extract redness of the image. """ return channel_portion(artifact.obj, 0)
[docs]class ImageGreennessFeature(Feature): def __init__(self): """Feature that measures the greenness of an RGB image. Returns *float* in range [0, 1]. """ super().__init__('image_greenness', ['image'], float)
[docs] def extract(self, artifact): """Extract greenness of the image. """ return channel_portion(artifact.obj, 1)
[docs]class ImageBluenessFeature(Feature): def __init__(self): """Feature that measures the blueness of an RGB image. Returns *float* in range [0, 1]. """ super().__init__('image_blueness', ['image'], float)
[docs] def extract(self, artifact): """Extract blueness of the image. """ return channel_portion(artifact.obj, 2)
[docs]class ImageIntensityFeature(Feature): def __init__(self): """Feature that measures the intensity of an image. Returns *float* in range [0, 1]. """ super().__init__('image_intensity', ['image'], float)
[docs] def extract(self, artifact): """Extract average intensity of the image. """ return intensity(artifact.obj)