Skip to content

REGION_PROPERTIES

A stand-alone visualizer for analyzing an input array of image data. There are multiple input 'DataContainer' types for which this function is applicable: 'Image', 'Grayscale', or 'Matrix'. Often in image analysis, it is necessary to determine subvolumes / subregions inside a given image, whether for object count (e.g. the counting of cells on a glass plate), or object dimensional analysis (determining coutours of a region, centroid of a region relative to the pixel coordinate origin of the image, determining the semi-major or -minor axes of a region, etc.). This functionality is entirely provided by this node in a two-step process: - First, the regions of the INTEGER image are identified and labelled. - Second, the regions are analyzed. The first step is provided by the morphology library of scikit-image's label function, while the second is provided by scikit-image's regionprops function. After processing, the results of this node are visualized in the main UI, where the user can see: - The input array / image. - The semi-major and semi-minor axes of the contour drawn relative to the contour centroid. - The contour centroid. - The countour bounding-box. - A mouse-hover utility that displays the contour information to the user. Params: default : Image | Grayscale The input node to this function. If nothing is supplied, a demo test case is returned to illustrate the functionality of this node. Returns: fig : Plotly A Plotly figure containing the illustrated features as determined by this node.
Python Code
from flojoy import flojoy, Plotly, Image, Grayscale, Matrix
import plotly.express as px
import plotly.graph_objects as go
from skimage.draw import ellipse
from skimage.measure import label, regionprops, find_contours
from skimage.transform import rotate
import re
import numpy as np
from typing import Optional
import math
from blocks.DATA.VISUALIZATION.template import plot_layout
from PIL import Image as PILImage


@flojoy(deps={"scikit-image": "0.21.0"}, node_type="VISUALIZERS")
def REGION_PROPERTIES(default: Optional[Image | Grayscale | Matrix] = None) -> Plotly:
    """A stand-alone visualizer for analyzing an input array of image data.

    There are multiple input 'DataContainer' types for which
    this function is applicable: 'Image', 'Grayscale', or 'Matrix'.

    Often in image analysis, it is necessary to determine subvolumes / subregions
    inside a given image, whether for object count (e.g. the counting of
    cells on a glass plate), or object dimensional analysis (determining coutours of
    a region, centroid of a region relative to the pixel coordinate origin of the image,
    determining the semi-major or -minor axes of a region, etc.). This functionality
    is entirely provided by this node in a two-step process:

    - First, the regions of the INTEGER image are identified and labelled.
    - Second, the regions are analyzed.

    The first step is provided by the morphology library of scikit-image's label function,
    while the second is provided by scikit-image's regionprops function.

    After processing, the results of this node are visualized in the main UI,
    where the user can see:
    - The input array / image.
    - The semi-major and semi-minor axes of the contour drawn relative to the contour centroid.
    - The contour centroid.
    - The countour bounding-box.
    - A mouse-hover utility that displays the contour information to the user.

    Parameters
    ----------
    default : Image | Grayscale
        The input node to this function.
        If nothing is supplied, a demo test case is returned to illustrate the functionality of this node.

    Returns
    -------
    fig: Plotly
        A Plotly figure containing the illustrated features as determined by this node.
    """

    if default:
        if isinstance(default, Image):
            r = default.r
            g = default.g
            b = default.b
            a = default.a

            if a is None:
                image = np.stack((r, g, b), axis=2)
            else:
                image = np.stack((r, g, b, a), axis=2)
            image = PILImage.fromarray(image)
            image = np.array(
                image.convert("L"), dtype=np.uint8
            )  # a greyscale image that can be processed
        elif isinstance(default, Grayscale) or isinstance(default, Matrix):
            image = np.array(default.m)  # explicit typing just to be extra safe

    else:
        image = np.zeros((600, 600), dtype=np.uint8)
        rr, cc = ellipse(300, 350, 100, 220)
        image[rr, cc] = 1
        image = rotate(image, angle=15, order=0)
        rr, cc = ellipse(100, 100, 60, 50)
        image[rr, cc] = 1

    # Slight problem. If we're generating a dummy dataset, or if the input is of
    # type `Image`, then there is no problem for the below, since all values will
    # be in range 0-255 (uint8). This is good because the array `rgb_image` below
    # is guaranteed to be able to work and visualize the input array, and the `label`
    # are `regionprops` routines, which only accept integer array inputs, will be fine
    # as well. PROBLEM: if the input type is greyscale or matrix, meaning only a 2D
    # array, we have at no point enforced that the values of these arrays be uint8.
    # Indeed, they may not and in most cases will not be, and can have values within
    # an extreme dynamic range. To fix this, we need a case to explicitly handle greyscale
    # and matrix input data types, both for visualization and for region property analysis.
    original_dtype = str(np.min_scalar_type(image))
    if "int" in original_dtype:  # we are good, and all are integers
        pass
    elif "f" in original_dtype:  # matches 'float' and 'f8' etc
        nbits = int(re.search(r"\d+", str(original_dtype)).group())
        image = image.astype(getattr(np, f"int{nbits}"))
    else:
        raise TypeError(
            "Input array of insufficient data type to pass to the region analysis routines."
        )
    labels = label(image)
    rprops = regionprops(label_image=labels, intensity_image=image)

    rgb_image = np.zeros(
        (*image.shape, 3), dtype=np.uint8
    )  # only generated for plotting
    rgb_image[..., 0] = image * 255  # Red channel
    rgb_image[..., 1] = image * 255  # Green channel
    rgb_image[..., 2] = image * 255  # Blue channel
    layout = plot_layout(title=f"IMAGE with {labels.max()} objects")
    fig = px.imshow(img=rgb_image)
    fig.layout = layout

    properties = [
        "area",
        "eccentricity",
        "perimeter",
        "centroid",
        "orientation",
        "axis_major_length",
        "axis_minor_length",
    ]

    for props in rprops:
        y0, x0 = props.centroid
        orientation = props.orientation
        x1 = x0 + math.cos(orientation) * 0.5 * props.axis_minor_length
        y1 = y0 - math.sin(orientation) * 0.5 * props.axis_minor_length
        x2 = x0 - math.sin(orientation) * 0.5 * props.axis_major_length
        y2 = y0 - math.cos(orientation) * 0.5 * props.axis_major_length

        line_trace1 = go.Scatter(
            x=[x0, x1],
            y=[y0, y1],
            mode="lines",
            line=dict(color="red", width=2.5),
            showlegend=False,
        )
        line_trace2 = go.Scatter(
            x=[x0, x2],
            y=[y0, y2],
            mode="lines",
            line=dict(color="red", width=2.5),
            showlegend=False,
        )
        marker_trace = go.Scatter(
            x=[x0],
            y=[y0],
            mode="markers",
            marker=dict(color="green", size=15),
            showlegend=False,
        )

        fig.add_trace(line_trace1)
        fig.add_trace(line_trace2)
        fig.add_trace(marker_trace)

        minr, minc, maxr, maxc = props.bbox
        bx = [minc, maxc, maxc, minc, minc]
        by = [minr, minr, maxr, maxr, minr]

        bounding_box_trace = go.Scatter(
            x=bx, y=by, mode="lines", line=dict(color="blue", width=2), showlegend=False
        )
        fig.add_trace(bounding_box_trace)

    for index in range(labels.max()):
        label_i = rprops[index].label
        contour = find_contours(labels == label_i, 0.5)[0]
        y, x = contour.T
        hoverinfo = ""
        for prop_name in properties:
            val = getattr(rprops[index], prop_name)
            if type(val) == tuple:
                line = [
                    f" <b>{prop_name}_{idv}: {v:.2f}</b>" for idv, v in enumerate(val)
                ]
                hoverinfo += ",".join(line) + "<br>"
            else:
                hoverinfo += f"<b>{prop_name}: {val:.2f}</b><br>"
        fig.add_trace(
            go.Scatter(
                x=x,
                y=y,
                name=label_i,
                mode="lines",
                fill="toself",
                showlegend=False,
                hovertemplate=hoverinfo,
                hoveron="points+fills",
            )
        )

    fig.update_xaxes(range=[0, image.shape[0]])
    fig.update_yaxes(range=[0, image.shape[1]])
    return Plotly(fig=fig)

Find this Flojoy Block on GitHub

Example

Having problems with this example app? Join our Discord community and we will help you out!
React Flow mini map