"""Library to draw an antialiased line.
http://stackoverflow.com/questions/3122049/drawing-an-anti-aliased-line-with-thepython-imaging-library
https://en.wikipedia.org/wiki/Xiaolin_Wu%27s_line_algorithm
https://yellowsplash.wordpress.com/2009/10/23/fast-antialiased-circles-and-ellipses-from-xiaolin-wus-concepts/
https://stackoverflow.com/questions/37589165/drawing-an-antialiased-circle-as-described-by-xaolin-wu#37714284
"""
import math
import numpy as np
from matplotlib.path import Path
[docs]
def draw_polygon_mask(canvas, points, colour, threshold, fill=True):
"""Make a 2D mask [ny,nx] on canvas
Optionally, fill the polygon with the same colour.
Args:
canvas: numpy array
points: polygon points (list of tuples (x,y)) (float)
colour: colour to draw (mask index) (int)
threshold: only pixels with an alpha > threshold will be drawn (float, 0.0-1.0)
fill: whether to fill the circle (boolean)
"""
# Flag the voxels that will be in the mask
mask = np.zeros_like(canvas, dtype=bool)
# Colour the voxels on the polygon with the True colour
pn = points[len(points) - 1] # Close the polygon
for p in points:
x1, y1 = pn
x2, y2 = p
draw_line_mask(mask, x1, y1, x2, y2, True, threshold)
pn = p
# canvas.save("/tmp/polygon.png", "PNG")
if fill:
# Colour the voxels inside the polygon with the True colour
inside = point_in_polygon(mask, points)
mask = np.logical_or(mask, inside)
# Set voxels in the mask to the given colour
canvas[mask] = colour
[docs]
def plot(canvas, x, y, steep, colour, alpha, threshold):
"""Draws an anti-aliased pixel on a line."""
if steep:
x, y = y, x
# if x < canvas.shape[1] and y < canvas.shape[0] and x >= 0 and y >= 0:
if 0 <= x < canvas.shape[1] and 0 <= y < canvas.shape[0]:
x = int(x)
y = int(y)
if alpha >= threshold:
canvas[y, x] = colour
[docs]
def iround(x):
"""Rounds x to the nearest integer."""
return ipart(x + 0.5)
[docs]
def ipart(x):
"""Floors x."""
return math.floor(x)
[docs]
def fpart(x):
"""Returns the fractional part of x."""
return x - math.floor(x)
[docs]
def rfpart(x):
"""Returns the 1 minus the fractional part of x."""
return 1 - fpart(x)
[docs]
def draw_line_mask(canvas, x1, y1, x2, y2, colour, threshold):
"""Draw line mask on NumPy array.
Apply the Xialon Wu anti-aliasing algorithm for drawing line.
Draw points only when the alpha blending is above a set threshold.
Only given colour value is drawn. Intended usage is as a mask index.
Args:
canvas: 2D numpy array (np.ndarray)
x1 (float): line end points
y1 (float): line end points
x2 (float): line end points
y2 (float): line end points
colour (int): colour to draw 'mask index'
threshold (float): only pixels with an alpha > threshold will be drawn, 0.0-1.0
"""
dx = x2 - x1
# if not dx:
# # Vertical line
# draw_line((x1, y1, x2, y2), fill=col, width=1)
# return
dy = y2 - y1
steep = abs(dx) < abs(dy)
if steep:
x1, y1 = y1, x1
x2, y2 = y2, x2
dx, dy = dy, dx
if x2 < x1:
x1, x2 = x2, x1
y1, y2 = y2, y1
try:
gradient = float(dy) / float(dx)
except ZeroDivisionError:
gradient = 1.0
# Handle first endpoint
xend = round(x1)
yend = y1 + gradient * (xend - x1)
xgap = rfpart(x1 + 0.5)
xpxl1 = xend # this will be used in the main loop
ypxl1 = ipart(yend)
plot(canvas, xpxl1, ypxl1, steep, colour, rfpart(yend) * xgap, threshold)
plot(canvas, xpxl1, ypxl1 + 1, steep, colour, fpart(yend) * xgap, threshold)
intery = yend + gradient # first y-intersection for the main loop
# handle second endpoint
xend = round(x2)
yend = y2 + gradient * (xend - x2)
xgap = fpart(x2 + 0.5)
xpxl2 = xend # this will be used in the main loop
ypxl2 = ipart(yend)
plot(canvas, xpxl2, ypxl2, steep, colour, rfpart(yend) * xgap, threshold)
plot(canvas, xpxl2, ypxl2 + 1, steep, colour, fpart(yend) * xgap, threshold)
# main loop
for x in range(int(xpxl1 + 1), int(xpxl2)):
plot(canvas, x, ipart(intery), steep, colour, rfpart(intery), threshold)
plot(canvas, x, ipart(intery) + 1, steep, colour, fpart(intery), threshold)
intery = intery + gradient
[docs]
def point_in_polygon(canvas, polygon):
ny, nx = canvas.shape
# Create vertex coordinates for each grid cell...
# (<0,0> is at the top left of the grid in this system)
x, y = np.meshgrid(np.arange(nx), np.arange(ny))
x, y = x.flatten(), y.flatten()
points = np.vstack((x, y)).T
path = Path(polygon)
grid = path.contains_points(points)
grid = grid.reshape((ny, nx))
return grid
[docs]
def draw_circle_mask(canvas, center_x, center_y, outer_radius, colour, threshold, fill=True):
"""Draw circle mask on NumPy array.
Apply algorithm for drawing anti-aliased circle.
Draw points only when the alpha blending is above a set threshold.
Only given _colour value is drawn. Intended usage is as a mask index.
Optionally, fill the circle with the same _colour.
Reference:
https://stackoverflow.com/questions/37589165/drawing-an-antialiased-circle-as-described-by-xaolin-wu#37714284
Args:
canvas: numpy array
center_x, center_y: center of circle in array coordinates (int)
outer_radius: radius of circle in array dimension (float)
colour: _colour to draw (mask index) (int)
threshold: only pixels with an alpha > threshold will be drawn (float, 0.0-1.0)
fill: whether to fill the circle (boolean)
"""
def _draw_8point(_canvas, _cx, _cy, _i, _j, _colour):
"""Draws 8 points, one on each octant."""
# Square symmetry
local_coord = [(_i * (-1) ** (k % 2), _j * (-1) ** (k // 2)) for k in range(4)]
# Diagonal symmetry
local_coord += [(j_, i_) for i_, j_ in local_coord]
for i_, j_ in local_coord:
# print("_draw_8point", _cy + j_, _cx + i_)
_canvas[_cy + j_, _cx + i_] = _colour
i = 0
j = outer_radius
last_fade_amount = 0
# fade_amount = 0
max_opaque = 1.0
while i < j:
height = math.sqrt(max(outer_radius * outer_radius - i * i, 0))
fade_amount = max_opaque * (math.ceil(height) - height)
if fade_amount < last_fade_amount:
# Opaqueness reset so drop down a row.
j -= 1
last_fade_amount = fade_amount
# We're fading out the current _j row, and fading in the next one down.
if max_opaque - fade_amount > threshold:
_draw_8point(canvas, center_x, center_y, i, j, colour)
if fade_amount > threshold:
_draw_8point(canvas, center_x, center_y, i, j - 1, colour)
i += 1
if fill:
boundary_fill4(canvas, center_x, center_y, colour, colour)
[docs]
def draw_ellipse_mask(canvas, center_x, center_y, outer_radius, colour, threshold, fill=True):
"""Draw ellipse mask on NumPy array.
Apply algorithm for drawing anti-aliased ellipse.
Draw points only when the alpha blending is above a set threshold.
Only given _colour value is drawn. Intended usage is as a mask index.
Optionally, fill the ellipse with the same _colour.
Reference:
https://yellowsplash.wordpress.com/2009/10/23/fast-antialiased-circles-and-ellipses-from-xiaolin-wus-concepts/
https://stackoverflow.com/questions/37589165/drawing-an-antialiased-circle-as-described-by-xaolin-wu#37714284
Args:
canvas: numpy array
center_x, center_y: center of ellipse in array coordinates (int)
outer_radius: radius of circle in array dimension (float)
colour: colour to draw (mask index) (int)
threshold: only pixels with an alpha > threshold will be drawn (float, 0.0-1.0)
fill: whether to fill the circle (boolean)
"""
def _draw_4point(_canvas, _cx, _cy, x, y, _colour):
# Draw the 8 symmetries
print("_draw_8point", _cy, _cx, y, x)
print("_draw_8point", _cy + y, _cx - x)
print("_draw_8point", _cy + y, _cx + x)
print("_draw_8point", _cy - y, _cx - x)
print("_draw_8point", _cy - y, _cx + x)
_canvas[_cy + y, _cx - x] = _colour
_canvas[_cy + y, _cx + x] = _colour
_canvas[_cy - y, _cx - x] = _colour
_canvas[_cy - y, _cx + x] = _colour
i = 0
j = outer_radius
last_fade_amount = 0
# fade_amount = 0
max_opaque = 1.0
while i < j:
height = math.sqrt(max(outer_radius * outer_radius - i * i, 0))
fade_amount = max_opaque * (math.ceil(height) - height)
if fade_amount < last_fade_amount:
# Opaqueness reset so drop down a row.
j -= 1
last_fade_amount = fade_amount
# We're fading out the current j row, and fading in the next one down.
if max_opaque - fade_amount > threshold:
_draw_4point(canvas, center_x, center_y, i, j, colour)
if fade_amount > threshold:
_draw_4point(canvas, center_x, center_y, i, j - 1, colour)
i += 1
if fill:
boundary_fill4(canvas, center_x, center_y, colour, colour)
[docs]
def flood_fill4(canvas, start_x, start_y, old_value, fill_value):
x, y = start_x, start_y
if 0 <= x < canvas.shape[1] and 0 <= y < canvas.shape[0]:
if canvas[y, x] == old_value:
canvas[y, x] = fill_value
# Attempt to propagate in each of four directions
flood_fill4(canvas, x, y - 1, old_value, fill_value)
flood_fill4(canvas, x, y + 1, old_value, fill_value)
flood_fill4(canvas, x - 1, y, old_value, fill_value)
flood_fill4(canvas, x + 1, y, old_value, fill_value)
[docs]
def boundary_fill4(canvas, start_x, start_y, boundary_value, fill_value):
x, y = start_x, start_y
if 0 <= x < canvas.shape[1] and 0 <= y < canvas.shape[0]:
if canvas[y, x] != boundary_value and canvas[y, x] != fill_value:
canvas[y, x] = fill_value
boundary_fill4(canvas, x, y - 1, boundary_value, fill_value)
boundary_fill4(canvas, x, y + 1, boundary_value, fill_value)
boundary_fill4(canvas, x - 1, y, boundary_value, fill_value)
boundary_fill4(canvas, x + 1, y, boundary_value, fill_value)