v0.0.9 ~ f0a51f7
Snowy
User's Guide | API Reference
add_border |
Extend the size of an image by adding borders. |
blur |
Resample an image and produce a new image with the same size. |
compose |
Compose a source image with alpha onto a destination image. |
compose_premultiplied |
Draw an image with premultiplied alpha over the destination. |
compute_normals |
Generate a 3-channel normal map from a height map. |
compute_skylight |
Compute ambient occlusion from a height map. |
compute_sobel |
Apply Sobel operator for edge detection. |
delinearize |
Transform colors from physically linear to perceptually linear. |
dereference_coords |
For each 2D value in the coord field, make a lookup in the source. |
dereference_cpcf |
For each 2D value in the coord field, make a lookup in the source. |
draw_polygon |
Draw a textured convex polygon into the target image. |
draw_triangle |
Draw a textured triangle into the target image. |
ensure_alpha |
If the incoming image is 3-channel, adds a 4th channel. |
export |
Export a numpy array to a PNG, JPEG, or EXR image file. |
extract_alpha |
Extract the alpha plane from an RGBA image. |
extract_rgb |
Extract the RGB planes from an RGBA image. |
from_planar |
Create a channel-major image into row-major image. |
generate_cpcf |
Create closest point coordinate field from a boolean field. |
generate_fBm |
Generate 2D fractional brownian motion by adding layers of noise. |
generate_gdf |
Create an generalized squared distance field from a scalar field. |
generate_noise |
Generate a single-channel gradient noise image. |
generate_sdf |
Create a signed distance field from a boolean field. |
generate_udf |
Create an unsigned distance field from a boolean field. |
gradient |
Compute X derivatives and Y derivatives. |
hflip |
Horizontally mirror the given image. |
hstack |
Horizontally concatenate a list of images with a border. |
linearize |
Transform colors from perceptually linear to physically linear. |
load |
Create a numpy array from the given PNG, JPEG, or EXR image file. |
reshape |
Add a trailing dimension to single-channel 2D images. |
resize |
Create a new numpy image with the desired size. |
rgb_to_luminance |
Read the first three color planes and return a grayscale image. |
rotate |
Rotate image counter-clockwise by a multiple of 90 degrees. |
show |
Display an image in a platform-specific way. |
to_planar |
Convert a row-major image into a channel-major image. |
unitize |
Remap the values so that they span the range from 0 to +1. |
unshape |
Remove the trailing dimension from single-channel 3D images. |
vflip |
Vertically mirror the given image. |
vstack |
Vertically concatenate a list of images with a border. |
Extend the size of an image by adding borders.
The sides
argument defaults to "LTRB"
, which enables borders for all four sides: Left, Top, Right, and Bottom. This can be used to select which borders you wish to add.
def add_border(image: np.ndarray, width=2, value=0, sides='ltrb'):
result = image
sides = sides.upper()
if 'L' in sides: result = add_left(result, width, value)
if 'T' in sides: result = add_top(result, width, value)
if 'R' in sides: result = add_right(result, width, value)
if 'B' in sides: result = add_bottom(result, width, value)
return result
Resample an image and produce a new image with the same size. For a list of available filters, see resize.
def blur(image, filter=GAUSSIAN, radius=4, wrapx=False, wrapy=False):
width, height = image.shape[1], image.shape[0]
return resize(image, width, height, filter, radius, wrapx, wrapy)
Compose a source image with alpha onto a destination image.
def compose(dst: np.ndarray, src: np.ndarray) -> np.ndarray:
a, b = ensure_alpha(src), ensure_alpha(dst)
alpha = extract_alpha(a)
result = b * (1.0 - alpha) + a * alpha
if dst.shape[2] == 3:
return extract_rgb(result)
return result
Draw an image with premultiplied alpha over the destination.
def compose_premultiplied(dst: np.ndarray, src: np.ndarray):
a, b = ensure_alpha(src), ensure_alpha(dst)
alpha = extract_alpha(a)
result = b * (1.0 - alpha) + a
if dst.shape[2] == 3:
return extract_rgb(result)
return result
Generate a 3-channel normal map from a height map. The normal components are in the range [-1,+1] and the size of the normal map is (width-1, height-1) due to forward differencing.
def compute_normals(elevation):
height, width, nchan = elevation.shape
assert nchan == 1
normals = np.empty([height - 1, width - 1, 3])
_compute_normals(elevation[:,:,0], normals)
return normals
Compute ambient occlusion from a height map.
def compute_skylight(elevation, verbose=False):
height, width, nchan = elevation.shape
assert nchan == 1
result = np.zeros([height, width])
_compute_skylight(result, elevation[:,:,0], verbose)
return io.reshape(np.clip(1.0 - result, 0, 1))
Apply Sobel operator for edge detection.
def compute_sobel(image: np.ndarray):
"Apply Sobel operator for edge detection."
assert len(image.shape) == 3, 'Shape is not rows x cols x channels'
assert image.shape[2] == 1, 'Image must be grayscale'
result = np.empty(image.shape)
_compute_sobel(result, image)
compute_sobel(image: np.ndarray):
"Apply Sobel operator for edge detection."
assert len(image.shape) == 3, 'Shape is not rows x cols x channels'
assert image.shape[2] == 1, 'Image must be grayscale'
result = np.empty(image.shape)
_compute_sobel(result, image)
return result
Transform colors from physically linear to perceptually linear. This is automatically performed when using export to a PNG or JPEG. See also linearize.
def delinearize(image, source_space=SRGB):
if source_space == SRGB:
return linear_to_sRGB(image)
return linear_to_gamma(image)
For each 2D value in the coord field, make a lookup in the source. This is useful for creating generalized voronoi diagrams.
def dereference_coords(source: np.ndarray, coords: np.ndarray):
assert len(coords.shape) == 3, 'Shape is not rows x cols x channels'
assert len(source.shape) == 3, 'Shape is not rows x cols x channels'
assert coords.shape[2] == 2, 'Coordinate must be 2-tuples'
voronoi = source.copy()
height, width = source.shape[:2]
coords = coords.copy()
coords[:,:,0] = np.clip(coords[:,:,0], 0, width - 1)
coords[:,:,1] = np.clip(coords[:,:,1], 0, height - 1)
_deref_coords(voronoi, source, coords)
return voronoi
For each 2D value in the coord field, make a lookup in the source. This is useful for creating generalized voronoi diagrams.
def dereference_coords(source: np.ndarray, coords: np.ndarray):
assert len(coords.shape) == 3, 'Shape is not rows x cols x channels'
assert len(source.shape) == 3, 'Shape is not rows x cols x channels'
assert coords.shape[2] == 2, 'Coordinate must be 2-tuples'
voronoi = source.copy()
height, width = source.shape[:2]
coords = coords.copy()
coords[:,:,0] = np.clip(coords[:,:,0], 0, width - 1)
coords[:,:,1] = np.clip(coords[:,:,1], 0, height - 1)
_deref_coords(voronoi, source, coords)
return voronoi
Draw a textured convex polygon into the target image. The vertices are specified with a nx5 array where each row is XYWUV. The UV coordinates address the source image in [0,+1] with +V going downward. The XY coordinates are in the range [-1,+1] and their domain is the entire target image with +Y going upward. The W coordinate is to allow for perspective-correct interpolation. If you don't know what that means, then set W to 1.
def draw_polygon(target: np.ndarray, source: np.ndarray,
vertices: np.ndarray):
assert len(target.shape) == 3, 'Target shape must be 3D.'
assert target.shape[2] == 4, 'Target must be RGBA.'
assert len(source.shape) == 3, 'Source shape must be 3D.'
assert source.shape[2] == 4, 'Source must be RGBA.'
assert vertices.shape[1] == 5, 'Vertices must be nx5.'
n = vertices.shape[0]
for tri in range(2, n):
indices = np.array([0, tri - 1, tri])
triangle = vertices[indices]
draw_triangle(target, source, triangle)
Draw a textured triangle into the target image. The vertices are specified with a 3x5 array where each row is XYWUV. The UV coordinates address the source image in [0,+1] with +V going downward. The XY coordinates are in the range [-1,+1] and their domain is the entire target image with +Y going upward. The W coordinate is to allow for perspective-correct interpolation. If you don't know what that means, then set W to 1.
def draw_triangle(target: np.ndarray, source: np.ndarray,
vertices: np.ndarray):
assert len(target.shape) == 3, 'Target shape must be 3D.'
assert target.shape[2] == 4, 'Target must be RGBA.'
assert len(source.shape) == 3, 'Source shape must be 3D.'
assert source.shape[2] == 4, 'Source must be RGBA.'
assert vertices.shape == (3, 5), 'Vertices must be 3x5.'
vertices = np.copy(vertices)
xy = vertices[:, :2]
w = vertices[:, 2:3]
uv = vertices[:, 3:]
w = 1.0 / w
xy *= w
uv *= w
w = w[:, 0]
height, width, _ = target.shape
xy[:, 0] = (xy[:, 0] + 1.0) * 0.5 * width
xy[:, 1] = height - 1 -(xy[:, 1] + 1.0) * 0.5 * height
v0, v1, v2 = xy
area = 1 / edge(v0, v1, v2)
source = source.astype(target.dtype, copy=False)
v0 = v0.astype(np.float32, copy=False)
v1 = v1.astype(np.float32, copy=False)
v2 = v2.astype(np.float32, copy=False)
uv = uv.astype(np.float32, copy=False)
w = w.astype(np.float32, copy=False)
_rasterize(target, source, area, v0, v1, v2, uv, w)
If the incoming image is 3-channel, adds a 4th channel.
def ensure_alpha(src: np.ndarray) -> np.ndarray:
assert len(src.shape) == 3
if src.shape[2] != 3:
return src
alpha = np.ones(src.shape[:2])
r, g, b = to_planar(src)
return from_planar(np.array([r, g, b, alpha]))
Export a numpy array to a PNG, JPEG, or EXR image file. This function automatically multiplies PNG / JPEG images by 255. See also unshape and delinearize (which this calls).
def export(image: np.ndarray, filename: str, delinearize=True):
assert filename.endswith('.png') or filename.endswith('.jpeg') or \
filename.endswith('.jpg') or filename.endswith('.exr')
_export(image, filename, delinearize)
Extract the alpha plane from an RGBA image. Note that this returns a copy, not a view. To manipulate the pixels in a view of the alpha plane, simply make a numpy slice, as in: alpha_view = myimage[:,:,3]
.
def extract_alpha(image: np.ndarray) -> np.ndarray:
assert len(image.shape) == 3 and image.shape[2] == 4
return np.dsplit(image, 4)[3].copy()
Extract the RGB planes from an RGBA image. Note that this returns a copy. If you wish to obtain a view that allows mutating pixels, simply use slicing instead. For example, to invert the colors of an image while leaving alpha intact, you can do: myimage[:,:,:3] = 1.0 - myimage[:,:,:3]
.
def extract_rgb(image: np.ndarray) -> np.ndarray:
assert len(image.shape) == 3 and image.shape[2] >= 3
planes = np.dsplit(image, image.shape[2])
return np.dstack(planes[:3])
Create a channel-major image into row-major image. This creates a copy, not a view.
def from_planar(image: np.ndarray) -> np.ndarray:
assert len(image.shape) == 3
return np.dstack(image)
Create closest point coordinate field from a boolean field.
def generate_cpcf(image: np.ndarray):
assert image.dtype == 'bool', 'Pixel values must be boolean'
assert len(image.shape) == 3, 'Shape is not rows x cols x channels'
assert image.shape[2] == 1, 'Image must be grayscale'
return _generate_cpcf(image)
Generate 2D fractional brownian motion by adding layers of noise. See also generate_noise.
def generate_fBm(width, height, freq, layers, seed, lacunarity=2,
persistence=2, wrapx=False, wrapy=False):
noise = generate_noise
n = np.zeros([height, width, 1])
amplitude = 1
for f in range(layers):
lseed = seed + int(f)
n += amplitude * noise(width, height, freq, lseed, wrapx, wrapy)
freq *= lacunarity
amplitude /= persistence
return n
Create an generalized squared distance field from a scalar field.
def generate_gdf(image: np.ndarray, wrapx=False, wrapy=False):
"Create an generalized squared distance field from a scalar field."
assert image.dtype == 'float64', 'Pixel values must be real'
assert len(image.shape) == 3, 'Shape is not rows x cols x channels'
assert image.shape[2] == 1, 'Image must be grayscale'
generate_gdf(image: np.ndarray, wrapx=False, wrapy=False):
"Create an generalized squared distance field from a scalar field."
assert image.dtype == 'float64', 'Pixel values must be real'
assert len(image.shape) == 3, 'Shape is not rows x cols x channels'
assert image.shape[2] == 1, 'Image must be grayscale'
return _generate_gdt(image, wrapx, wrapy)
Generate a single-channel gradient noise image. A frequency of 1.0 creates a single surflet across the width of the image, while a frequency of 4.0 creates a 4x4 grid such that the (2,2) surflet is centered. Noise values live within the [-1,+1] range.
def generate_noise(width, height, frequency, seed=1, wrapx=False,
wrapy=False, offset=[0,0]):
return _noise(width, height, frequency, seed, wrapx, wrapy, offset)
Create a signed distance field from a boolean field.
def generate_sdf(image: np.ndarray, wrapx=False, wrapy=False):
a = generate_udf(image, wrapx, wrapy)
b = generate_udf(image == 0.0, wrapx, wrapy)
return a - b
Create an unsigned distance field from a boolean field.
def generate_udf(image: np.ndarray, wrapx=False, wrapy=False):
assert image.dtype == 'bool', 'Pixel values must be boolean'
assert len(image.shape) == 3, 'Shape is not rows x cols x channels'
assert image.shape[2] == 1, 'Image must be grayscale'
return _generate_edt(image, wrapx, wrapy)
Compute X derivatives and Y derivatives.
def gradient(img):
nx, ny = np.gradient(unshape(img))
return reshape(nx), reshape(ny)
Horizontally mirror the given image.
def hflip(source: np.ndarray) -> np.ndarray:
assert len(source.shape) == 3, 'Shape is not rows x cols x channels'
assert source.dtype == np.float, 'Images must be doubles.'
h, w, c = source.shape
result = np.empty([h, w, c])
jit_hflip(result, source)
return result
Horizontally concatenate a list of images with a border. This is similar to numpy's hstack
except that it adds a border around each image. The borders can be controlled with the optional border_width
and border_value
arguments. See also vstack.
def hstack(images, border_width=2, border_value=0):
if border_width == 0: return np.hstack(images)
T, V = border_width, border_value
result = []
for image in images[:-1]:
result.append(add_border(image, T, V, 'LTB'))
result.append(add_border(images[-1], T, V))
return np.hstack(result)
Transform colors from perceptually linear to physically linear. This is automatically performed when using load on a PNG or JPEG. See also delinearize.
def linearize(image, target_space=SRGB):
if target_space == SRGB:
return sRGB_to_linear(image)
return gamma_to_linear(image)
Create a numpy array from the given PNG, JPEG, or EXR image file. Regardless of the pixel format on disk, PNG / JPEG images are always divided by 255, and PNG images are extended to 4 color channels. See also reshape and linearize (which this calls).
def load(filename: str, linearize=True) -> np.ndarray:
ext = filename[filename.rfind('.'):]
assert ext == '.png' or ext == '.jpeg' or ext == '.jpg' or ext == '.exr'
return reshape(np.float64(_load(filename, ext, not linearize)))
Add a trailing dimension to single-channel 2D images. See also unshape.
def reshape(image):
if len(image.shape) == 2:
image = np.reshape(image, image.shape + (1,))
return image
Create a new numpy image with the desired size. Either width or height can be null, in which case its value is inferred from the aspect ratio of the source image. Filter can be HERMITE, TRIANGLE, GAUSSIAN, NEAREST, LANCZOS, or MITCHELL.
def resize(source, width=None, height=None, filter=None, radius=1,
wrapx=False, wrapy=False):
assert len(source.shape) == 3, 'Shape is not rows x cols x channels'
assert width != None or height != None, 'Missing target size'
aspect = source.shape[1] / source.shape[0]
if width == None: width = height * aspect
if height == None: height = width / aspect
magnifying = width > source.shape[1]
if filter == None: filter = MITCHELL if magnifying else LANCZOS
return resample(source, width, height, filter, radius, wrapx, wrapy)
Read the first three color planes and return a grayscale image.
def rgb_to_luminance(image: np.ndarray):
"Read the first three color planes and return a grayscale image."
assert image.shape[2] >= 3
r, g, b = np.dsplit(image[:,:,:3], image.shape[2])
rgb_to_luminance(image: np.ndarray):
"Read the first three color planes and return a grayscale image."
assert image.shape[2] >= 3
r, g, b = np.dsplit(image[:,:,:3], image.shape[2])
return io.reshape(0.2125 * r + 0.7154 * g + 0.0721 * b)
Rotate image counter-clockwise by a multiple of 90 degrees.
def rotate(source: np.ndarray, degrees) -> np.ndarray:
assert len(source.shape) == 3, 'Shape is not rows x cols x channels'
assert source.dtype == np.float, 'Images must be doubles.'
h, w, c = source.shape
degrees %= 360
if degrees == 90:
result = np.empty([w, h, c])
rotate90(result, source)
elif degrees == 180:
result = np.empty([h, w, c])
rotate180(result, source)
elif degrees == 270:
result = np.empty([w, h, c])
rotate270(result, source)
else:
assert False, 'Angle must be a multiple of 90.'
return result
Display an image in a platform-specific way.
def show(image, delinearize=True):
if isinstance(image, np.ndarray):
show_array(image, delinearize)
elif isinstance(image, str):
show_filename(image)
else:
raise ValueError('Unsupported type')
Convert a row-major image into a channel-major image. This creates a copy, not a view.
def to_planar(image: np.ndarray) -> np.ndarray:
assert len(image.shape) == 3
result = np.array(np.dsplit(image, image.shape[2]))
return np.reshape(result, result.shape[:-1])
Remap the values so that they span the range from 0 to +1.
def unitize(img):
return (img - np.amin(img)) / (np.amax(img) - np.amin(img))
Remove the trailing dimension from single-channel 3D images. See also reshape.
def unshape(image):
if len(image.shape) == 3 and image.shape[2] == 1:
return np.reshape(image, image.shape[:2])
return image
Vertically mirror the given image.
def vflip(source: np.ndarray) -> np.ndarray:
assert len(source.shape) == 3, 'Shape is not rows x cols x channels'
assert source.dtype == np.float, 'Images must be doubles.'
h, w, c = source.shape
result = np.empty([h, w, c])
jit_vflip(result, source)
return result
Vertically concatenate a list of images with a border. This is similar to numpy's vstack
except that it adds a border around each image. The borders can be controlled with the optional border_width
and border_value
arguments. See also hstack.
def vstack(images, border_width=2, border_value=0):
if border_width == 0: return np.vstack(images)
T, V = border_width, border_value
result = []
for image in images[:-1]:
result.append(add_border(image, T, V, 'LTR'))
result.append(add_border(images[-1], T, V))
return np.vstack(result)