"""Module for creating illustrations.
To create an annotated image we need an instance of the
:class:`jicbioimage.illustrate.AnnotatedImage` class.
>>> from jicbioimage.illustrate import AnnotatedImage
Suppose that we have an existing image.
>>> from jicbioimage.core.image import Image
>>> im = Image((50,50))
We can use this image to create an canvas instance populated with the data
as a RGB gray scale image.
>>> canvas = AnnotatedImage.from_grayscale(im)
The :class:`jicbioimage.illustrate.Canvas` instance has built in annotation
functionality.
One can use it to draw crosses.
>>> canvas.draw_cross(10, 20)
One can use it to mask out bitmaps (in the example below with the color cyan).
>>> bitmap = np.zeros((50, 50), dtype=bool)
>>> bitmap[30:40, 30:40] = True
>>> canvas.mask_region(bitmap, color=(0, 255, 255))
One can use it to add text at particular locations on the canvas.
>>> canvas.text_at("Hello", 30, 60)
"""
import os.path
import PIL.ImageFont
import numpy as np
import skimage.draw
import jicbioimage.core.image
__version__ = "0.6.1"
HERE = os.path.dirname(__file__)
DEFAULT_FONT_PATH = os.path.join(HERE, "fonts", "UbuntuMono-R.ttf")
[docs]class Canvas(jicbioimage.core.image._BaseImage):
"""Class for building up annotated images."""
@staticmethod
[docs] def blank_canvas(width, height):
"""Return a blank canvas to annotate.
:param width: xdim (int)
:param height: ydim (int)
:returns: :class:`jicbioimage.illustrate.Canvas`
"""
canvas = np.zeros((height, width, 3), dtype=np.uint8)
return canvas.view(Canvas)
[docs] def draw_cross(self, position, color=(255, 0, 0), radius=4):
"""Draw a cross on the canvas.
:param position: (row, col) tuple
:param color: RGB tuple
:param radius: radius of the cross (int)
"""
y, x = position
for xmod in np.arange(-radius, radius+1, 1):
xpos = x + xmod
if xpos < 0:
continue # Negative indices will draw on the opposite side.
if xpos >= self.shape[1]:
continue # Out of bounds.
self[int(y), int(xpos)] = color
for ymod in np.arange(-radius, radius+1, 1):
ypos = y + ymod
if ypos < 0:
continue # Negative indices will draw on the opposite side.
if ypos >= self.shape[0]:
continue # Out of bounds.
self[int(ypos), int(x)] = color
[docs] def draw_line(self, pos1, pos2, color=(255, 0, 0)):
"""Draw a line between pos1 and pos2 on the canvas.
:param pos1: position 1 (row, col) tuple
:param pos2: position 2 (row, col) tuple
:param color: RGB tuple
"""
r1, c1 = tuple([int(round(i, 0)) for i in pos1])
r2, c2 = tuple([int(round(i, 0)) for i in pos2])
rr, cc = skimage.draw.line(r1, c1, r2, c2)
self[rr, cc] = color
[docs] def mask_region(self, region, color=(0, 255, 0)):
"""Mask a region with a color.
:param region: :class:`jicbioimage.core.region.Region`
:param color: RGB tuple
"""
self[region] = color
[docs] def text_at(self, text, position, color=(255, 255, 255),
size=12, antialias=False, center=False):
"""Write text at x, y top left corner position.
By default the x and y coordinates represent the top left hand corner
of the text. The text can be centered vertically and horizontally by
using setting the ``center`` option to ``True``.
:param text: text to write
:param position: (row, col) tuple
:param color: RGB tuple
:param size: font size
:param antialias: whether or not the text should be antialiased
:param center: whether or not the text should be centered on the
input coordinate
"""
def antialias_value(value, normalisation):
return int(round(value * normalisation))
def antialias_rgb(color, normalisation):
return tuple([antialias_value(v, normalisation) for v in color])
def set_color(xpos, ypos, color):
try:
self[ypos, xpos] = color
except IndexError:
pass
y, x = position
font = PIL.ImageFont.truetype(DEFAULT_FONT_PATH, size=size)
mask = font.getmask(text)
width, height = mask.size
if center:
x = x - (width // 2)
y = y - (height // 2)
for ystep in range(height):
for xstep in range(width):
normalisation = mask[ystep * width + xstep] / 255.
if antialias:
if normalisation != 0:
rgb_color = antialias_rgb(color, normalisation)
set_color(x + xstep, y+ystep, rgb_color)
else:
if normalisation > .5:
set_color(x + xstep, y + ystep, color)
[docs]class AnnotatedImage(Canvas):
"""Class for building up annotated images."""
@staticmethod
[docs] def from_grayscale(im, channels_on=(True, True, True)):
"""Return a canvas from a grayscale image.
:param im: single channel image
:channels_on: channels to populate with input image
:returns: :class:`jicbioimage.illustrate.Canvas`
"""
xdim, ydim = im.shape
canvas = np.zeros((xdim, ydim, 3), dtype=np.uint8)
for i, include in enumerate(channels_on):
if include:
canvas[:, :, i] = im
return canvas.view(AnnotatedImage)