Source code for homcloud.interface

"""HomCloud python interface

This module provides the interface to HomCloud from python program.

The API document is written by sphinx with Napolen.
https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html
"""

from itertools import chain
import subprocess
from tempfile import TemporaryDirectory

from PIL import Image
import numpy as np

import homcloud.pict.utils as pict_utils
import homcloud.view_index_pict as view_index_pict
import homcloud.pict.slice3d as pict_slice3d

from homcloud.license import LICENSE_TERMS

__all__ = [
    "LICENSE_TERMS",  # .homcloud.licence
    "HomCloudError",
    "VolumeNotFound",  # .exceptions
    "PDList",
    "PD",
    "Pair",  # .pd
    "HistoSpec",
    "Mesh",
    "PIVectorizeSpec",
    "PIVectorizerMesh",
    "Histogram",
    "MaskHistogram",
    "SliceHistogram",  # .histogram
    "Volume",
    "OptimalVolume",
    "EigenVolume",
    "StableVolume",
    "StableSubvolume",  # .optimal_volume
    "PHTrees",  # .phtrees
    "BitmapPHTrees",  # .bitmap_phtrees
    "distance_transform",  # .distance_transform
    "BitmapOptimal1Cycle",  # .bitmap_optimal_1_cycle
    "GraphOptimal1Cycle",  # .graph_optimal_1_cycle
    "Optimal1Cycle",  # .optimal_1_cycle
    "example_data",
    "loadtxt_nd",
    "draw_volumes_on_2d_image",
    "draw_birthdeath_pixels_2d",
    "show_slice3d",
]
from .exceptions import *
from .pd import *
from .histogram import *
from .optimal_volume import *
from .phtrees import *
from .bitmap_phtrees import *
from .bitmap_optimal_1_cycle import *
from .distance_transform import *
from . import bitmap as bitmap
from . import distance


[docs] def example_data(name): """Returns example data. Returns the tetrahedron 3D pointcloud for name == "tetrahedron". Args: name: Name of the data Examples: >>> import homcloud.interface as hc >>> hc.example_data("tetrahedron") array([[ 0., 0., 0.], [ 8., 0., 0.], [ 5., 6., 0.], [ 4., 2., 6.]]) """ if name == "tetrahedron": return np.array( [ [0.0, 0.0, 0.0], [8.0, 0.0, 0.0], [5.0, 6.0, 0.0], [4.0, 2.0, 6.0], ] ) if name == "bitmap_01_5x5x5": return np.array( [ [[0, 0, 1, 1, 1], [0, 0, 0, 0, 1], [0, 0, 0, 0, 1], [0, 0, 0, 0, 1], [0, 0, 0, 1, 1]], [[0, 0, 1, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 1, 1, 0]], [[0, 0, 1, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 1, 0, 0]], [[0, 0, 1, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 1, 0, 0]], [[1, 1, 1, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 1, 1, 0, 0]], ], dtype=bool, ) if name == "bitmap_levels_5x5": return np.array( [ [0, 0, 1, 1, 1], [0, 4, 0, 5, 1], [0, 8, 0, 3, 1], [0, 0, 4, 7, 1], [0, 0, 2, 1, 1], ], dtype=int, ) raise ValueError("Unknown example name: %s" % name)
[docs] def loadtxt_nd(path): """ Load a n-dimensional data from text. The format of the text file is as follows. * First line represents the shape of data. For example, if the shape of your d ata is 200x230x250, first line should be `200 230 250`. * The following lines are floating point number values in x-fastest direction * A line starting with `#` is skipped as a comment * An empty line is also skipped. The following is an example:: # 4x3x2 3D voxel data 4 3 2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Args: path (string): The path of the text file. """ return pict_utils.load_nd_picture_from_text(path)
def draw_pixels_on_2d_image(image, pixels, color, alpha=1.0): for y, x in pixels: if x < 0 or y < 0 or x >= image.size[0] or y >= image.size[1]: continue blended_color = tuple((np.array(image.getpixel((x, y))) * (1 - alpha) + np.array(color) * alpha).astype(int)) image.putpixel((x, y), blended_color) return image
[docs] def draw_volumes_on_2d_image(nodes, image, color, alpha=1.0, birth_position=None, death_position=None, marker_size=1): """ Draws optimal volumes for bitmap filtration on an image. Args: nodes (list of :class:`BitmapPHTrees.Node`): The tree nodes to be drawn. image (string or numpy.ndarray or PIL.Image.Image): The base image data. * string: The image file whose name is `image` is used. * numpy.ndarray: 2D array is treated as grayscale image. * PIL.Image.Image: The image data If PIL.Image.Image object is given, the object is overwrited. color (tuple[int, int, int]): The color (RGB) of the volumes. alpha (float): The alpha value. The volume is drawn by using alpha blending. birth_position (tuple[int, int, int] or None): If not None, birth positions are drawn by the given color death_position (tuple[int, int, int] or None): If not None, death positions are drawn by the given color marker_size (int): The marker size of birth positions and death positions. 1, 3, 5, ... are available. Returns: PIL.Image.Image: The image data which optimal volumes are drawn. """ image = to_pil_image(image) def draw_marker(position, color): d = marker_size // 2 pixels = [(position[0] + dy, position[1] + dx) for dx in range(-d, d + 1) for dy in range(-d, d + 1)] draw_pixels_on_2d_image(image, pixels, color) pixels = set(chain.from_iterable([map(tuple, node.volume()) for node in nodes])) draw_pixels_on_2d_image(image, pixels, color, alpha) if birth_position: for node in nodes: draw_marker(node.birth_position(), birth_position) if death_position: for node in nodes: draw_marker(node.death_position(), death_position) return image
[docs] def draw_birthdeath_pixels_2d( pairs, image, draw_birth=False, draw_death=False, draw_line=False, scale=1, marker_type="filled-diamond", marker_size=1, with_label=False, birth_color=(255, 0, 0), death_color=(0, 0, 255), line_color=(0, 255, 0), ): """ Draw birth/death pixels on the given image. This function returns PIL.Image.Image object. Please see the `document of pillow <https://pillow.readthedocs.io/en/latest/reference/Image.html>`_ to know how to treat this object. Args: pairs (list of :class:`Pair`): The birth-death pairs. image (string or numpy.ndarray or PIL.Image.Image): The image data. * string: The image file whose name is `image` is used. * numpy.ndarray: 2D array is treated as grayscale image. * PIL.Image.Image: The image data draw_birth (bool): Birth pixels are drawn if True. draw_death (bool): Death pixels are drawn if True. draw_line (bool): The lines connecting each birth pixels and death pixels are drawn. scale (int): Image scaling factor. marker_type (string): The type of the markers. You can choose one of the followings: * "filled-diamond" * "point" * "square" * "filled-square" * "circle" * "filled-circle" marker_size (int): The size of the marker. with_label (bool): Show birth and death times beside each birth/death pixel marker. birth_color (tuple[int, int, int]): The color of birth pixel markers. death_color (tuple[int, int, int]): The color of death pixel markers. line_color (tuple[int, int, int]): The color of lines. Returns: PIL.Image.Image: The image data which birth/death pixels are drawn. """ image = to_pil_image(image) output_image, marker_drawer = view_index_pict.setup_images(image, scale, not with_label) marker_drawer.scale = scale marker_drawer.marker_size = marker_size marker_drawer.should_draw_birth_pixel = draw_birth marker_drawer.should_draw_death_pixel = draw_death marker_drawer.should_draw_line = draw_line marker_drawer.no_label = not with_label marker_drawer.birth_color = birth_color marker_drawer.death_color = death_color marker_drawer.line_color = line_color for pair in pairs: marker_drawer.draw_pair(pair) return output_image
def to_pil_image(image): if isinstance(image, str): return Image.open(image).convert("RGB") elif isinstance(image, np.ndarray): upper = np.max(image) lower = np.min(image) return Image.fromarray(((image - lower) / (upper - lower) * 250).astype(np.uint8)).convert("RGB") elif isinstance(image, Image.Image): return image else: raise RuntimeError("{} cannot be converted to PIL.Image".format(repr(image)))
[docs] def show_slice3d(volumes, slice=0, spacer=0, range=None, image_viewer="eog -n"): """ Display slices of 3D bitmap data. Args: volumes (list of numpy.ndarray): multiple 3D bitmap data. These data are aligned horizontally. slice (int): The direction of slicing: 0, 1, or 2 for z, y, x direction. spacer (int): The number of pixels between horizontally aligned slices. range (None or tuple[int, int]): The range of the slices. image_viewer (str): Command line of the image viewer to see slices. """ with TemporaryDirectory() as tmpdir: pict_slice3d.write_volume_slices(volumes, slice, spacer, range, tmpdir) subprocess.call("{} {}".format(image_viewer, tmpdir), shell=True)