Source code for ibb.widgets._image_canvas

import traitlets
import traittypes
import ipywidgets
import numpy as np
from .._frontend import module_name, module_version
from .._util import cast_alpha

__all__ = ['ImageCanvas']


def array_to_binary(ar, obj=None):
    if ar is not None:
        mv = memoryview(ar)
        return {'data': mv, 'shape': ar.shape[:-1]}
    else:
        return None


[docs]@ipywidgets.register class ImageCanvas(ipywidgets.DOMWidget): """ This widget is capable of displaying numpy array as images and draw polygons on them. It scales the images to fit in the view and ensures the polygons are scaled accordingly as well. It is also capable to provide hover/click statuses for the displayed polygons. Args: enable_rect (Boolean): Whether to enable the rectangle functionality; Default **True** auto_clear (Boolean): Whether to clear the polygons when drawing a new image; Default **True** enlarge (Boolean): Whether to enlarge an image to take up the most space in the canvas; Default **True** color (String): Default color to draw polygons; Default **#1F77B4** alpha (String): Default alpha fill value for the polygons; Default **00** size (Integer): Default border thickness for the polygons; Default **2** hover_style (Dict): Default hover style (can contain color,alpha and/or size properties); Default **None** click_style (Dict): Default click style (can contain color,alpha and/or size properties); Default **None** Attributes: image (numpy.ndarray): Image data in HWC order. See :meth:`ImageCanvas.validate_image` for more information polygons (dict): polygons to draw. See :meth:`ImageCanvas.validate_polygons` for more information clicked (Integer): Index of the clicked rectangle hovered (Integer): Index of the hovered rectangle save (Bool): Save image and polygons """ _model_module = traitlets.Unicode(module_name).tag(sync=True) _model_name = traitlets.Unicode('ImageCanvasModel').tag(sync=True) _model_module_version = traitlets.Unicode(module_version).tag(sync=True) _view_module = traitlets.Unicode(module_name).tag(sync=True) _view_name = traitlets.Unicode('ImageCanvasView').tag(sync=True) _view_module_version = traitlets.Unicode(module_version).tag(sync=True) # Settings enable_poly = traitlets.Bool(True).tag(sync=True) auto_clear = traitlets.Bool(True) enlarge = traitlets.Bool(True).tag(sync=True) color = traitlets.Unicode('#1F77B4').tag(sync=True) alpha = traitlets.Unicode('00').tag(sync=True) size = traitlets.Int(2).tag(sync=True) hover_style = traitlets.Dict(default_value=None, allow_none=True).tag(sync=True) click_style = traitlets.Dict(default_value=None, allow_none=True).tag(sync=True) # Attributes image = traittypes.Array(None, allow_none=True).tag(sync=True, to_json=array_to_binary) polygons = traitlets.List(None, allow_none=True).tag(sync=True) clicked = traitlets.Int(None, allow_none=True).tag(sync=True) hovered = traitlets.Int(None, allow_none=True).tag(sync=True) save = traitlets.Bool(False).tag(sync=True) def __init__(self, **kwargs): for attr in ('color', 'alpha', 'size'): if attr in kwargs: kwargs[attr] = getattr(self, f'_validate_{attr}')({'value': kwargs[attr]}) if 'hover_style' in kwargs and kwargs['hover_style'] is not None: try: kwargs['hover_style'] = self._validate_fx(kwargs['hover_style']) except Exception as err: raise ValueError(f'Wrong value in hover_style: {err}') from err if 'click_style' in kwargs and kwargs['click_style'] is not None: try: kwargs['click_style'] = self._validate_fx(kwargs['click_style']) except Exception as err: raise ValueError(f'Wrong value in click_style: {err}') from err super().__init__(**kwargs) @traitlets.validate('image') def validate_image(self, proposal): """ Validate correct image shape and dtype and cast to RGBA uint8 (0-255) Valid data types: - RGBA uint8 (0-255) - RGB uint8 (0-255) - Grayscale uint8 (0-255) - RGBA float (0-1) - RGB float (0-1) - Grayscale float (0-1) """ img = proposal['value'] if self.auto_clear: self.polygons = None self.hovered = None self.clicked = None if img is None: return img if not isinstance(img, np.ndarray): raise TypeError(f'image should by a numpy array or None [{type(img)}]') if img.dtype == np.uint8: if img.ndim == 2: img = np.dstack((img, img, img, 255 * np.ones(img.shape, dtype=np.uint8))) return img elif img.ndim == 3 and img.shape[2] in (3, 4): if img.shape[2] == 3: img = np.dstack((img, 255 * np.ones(img.shape[:-1], dtype=np.uint8))) return img else: raise ValueError(f'Image shape not supported [{img.shape}, {img.dtype}]') elif img.dtype in (np.float32, np.float64): if img.ndim == 2: img = np.dstack((img, img, img, np.ones(img.shape, dtype=img.dtype))) elif img.ndim == 3 and img.shape[2] == 3: img = np.dstack((img, np.ones(img.shape[:-1], dtype=img.dtype))) if img.ndim == 3 and img.shape[2] == 4: return (img * 255).astype(np.uint8) else: raise ValueError(f'Image shape not supported [{img.shape}, {img.dtype}]') else: raise TypeError(f'Image type not supported [{img.dtype}]') @traitlets.validate('polygons') def validate_polygons(self, proposal): """ Validate correct polygon data Valid data types: - list of dictionaries with keys: coords, color<optional>, alpha<optional>, size<optional> - None (clears) Warning: The individual data values are not validated, as that would slow down everything! It is up to the user to ensure that the values have the following types: - coords: 2D numpy array with X,Y coordinates - color: RGB string - alpha: 2 character HEX string (00-FF) - size: Integer """ poly = proposal['value'] self.hovered = None self.clicked = None if poly is None: return poly if isinstance(poly, list): for p in poly: if ('coords' not in p) or (not isinstance(p['coords'], list)): raise ValueError('polygon coords attribute not a np.ndarray or missing') return poly else: raise TypeError(f'Polygons should be a list<dict> [{type(poly)}]') @traitlets.validate('color') def _validate_color(self, proposal): """ Validate correct color type Valid data types: - RGB String: '#XXXXXX' or 'rgb(xx, xx, xx)' (any JS compatible RGB string) """ col = proposal['value'] if not isinstance(col, str): raise TypeError(f'Color should be an RGB string [{type(col)}]') return col @traitlets.validate('alpha') def _validate_alpha(self, proposal): """ Validate default fill alpha Valid types: - String: Hex alpha value (00 - ff) - Integer: integer alpha (0-255) - Float: percentage alpha (0-1) """ return cast_alpha(proposal['value']) @traitlets.validate('size') def _validate_size(self, proposal): """ Validate default border size. """ size = proposal['value'] if size < 0: raise ValueError('Border size should be bigger or equal than zero [{size}]') return size def _validate_fx(self, val): """ Validate hover/clicked attributes. These attributes should be dicts that can contain color, alpha and/or size values """ if 'color' in val: val['color'] = self._validate_color({'value': val['color']}) else: val['color'] = None if 'alpha' in val: val['alpha'] = self._validate_alpha({'value': val['alpha']}) else: val['alpha'] = None if 'size' in val: val['size'] = self._validate_size({'value': val['size']}) else: val['size'] = None return val