1. NodeBox 1
    1. Homepage
    2. NodeBox 3Node-based app for generative design and data visualization
    3. NodeBox OpenGLHardware-accelerated cross-platform graphics library
    4. NodeBox 1Generate 2D visuals using Python code (Mac OS X only)
  2. Gallery
  3. Documentation
  4. Forum
  5. Blog

Core Image

Description

The Core Image library for NodeBox adds image manipulation to NodeBox. It's like having control over Photoshop through simple Python programming commands. Core Image is a Mac OS X specific framework available from Mac OS X 1.4 (Tiger) and up (Leopard, ...). Core Image uses hardware acceleration whenever possible. You can apply filters and transformations to images with a real-time and interactive response. And, since all of the image manipulations are stored in a script, the process is non-destructive. The source images remain unchanged. The script is a recipe you can reuse on different source images.

Download

downloadcoreimage.zip (1.4MB)
Last updated for NodeBox 1.9.5
Licensed under GPL
Author: Tom De Smedt

Documentation

 


How to get the library up and running

Put the coreimage library folder in the same folder as your script so NodeBox can find the library. It takes some time to load the library the first time.

coreimage = ximport("coreimage")

 


Creating a new canvas

The canvas() command returns a new, empty canvas. It takes two parameters: the width and height of the canvas. You can set an optional quality parameter to "high" to use 128-bit pixel depth but this is unnecessary in most cases.

canvas = coreimage.canvas(150, 150)

 


Adding new layers to the canvas

A canvas is a container of many layers which you can position, scale , rotate, blend, filter, etc. You can create layers from image files, fill colors, linear or radial gradients, paths (ovals, text, ...), lists of pixels colors, another layer or canvas (i.e. a group of layers), or Quicktime movie frames.

 

canvas.append()

The canvas.append() method adds a new layer to the top of the canvas. It is placed at the center of the canvas by default.

canvas.append(filename)
canvas.append(clr)
canvas.append(clr1, clr2, type="linear", spread=0.0)
canvas.append(path, background=None, fill=None, stroke=None, strokewidth=None)
canvas.append([clr1, clr2, clr3, ...], w, h)
canvas.append(layer)
canvas.append(canvas)
canvas.append(layer.render())
canvas.append(canvas.flatten())
canvas.append(open(filename).read())
canvas.append(movieframe)

 

Examples

Consider we have the following images, leaf.jpg and lily.tif.


leaf.jpg

lily.tif

Core Image can use JPEG, GIF, PNG and TIFF images. If you are using images with an alpha channel the best way to import them is as transparent TIFF and not as a PNG (which can have jagged edges in Core Image).

 

A canvas with two layers from image files is easy enough:
l = canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
canvas.draw()
The canvas.append() method has four ptional parameters: the x and y position in pixels and w and h dimensions in pixels. The command returns the layer object.
Opaque fill layers are created from a standard NodeBox color(). The layer will fill the entire canvas by default.
l = canvas.append(color(1.0, 0.0, 0.5))
Paths from NodeBox can be transformed to a pixel layers as well. The canvas.append() method has optional path parameters: fill, background, stroke (black, transparent and transparent colors by default) and strokewidth.
l = canvas.append(color(1.0, 0.0, 0.5))
font("Georgia-Italic", 40)
p = textpath("text", 0, 0)
l = canvas.append(p, fill=color(1))
Linear gradients can be defined as two colors:
l = canvas.append(color(1), color(0))
Both linear and radial gradients are defined by two colors. If you also define the optional type parameter as "radial" you get a radial gradient. You control the range of the gradient center with the optional spread parameter (between 0.0 and 1.0).
l = canvas.append(color(1), color(0), type="radial")
You can also pass another canvas as a new layer. This way you have a subgroup of layers that stick together.
l = canvas.append(color(0))
group = coreimage.canvas(100, 100)
group.append("leaf.jpg")
group.append("lily.tif")
l = canvas.appebd(group)
l.rotate(45)
coreimage-layers7b
If you have some byte data, for example image data downloaded from a site with the Web library:
web = ximport("web")
url = "http://nodebox.net/code/g/header.jpg"
image_data = web.download(url)
canvas = coreimage.canvas(150, 150)
canvas.append(image_data, x=-190)
coreimage-layers7cA layer from individual pixels can be created by supplying a list of colors and a width and a height (thanks to Duane Bailey):
def fractal(x, y, depth=64):
    z = complex(x, y)
    o = complex(0, 0)
    for i in range(depth):
        if abs(o) <= 2: o = o*o + z
        else: 
            return i
    return 0 #default, black
 
pixels = []
w = h = 150
for i in range(w):
    for j in range(h):
        v = fractal(float(i)/w, float(j)/h)
        pixels.append(color(v/10.0, v/20.0, v/10.0))
 
l = canvas.append(pixels, w, h)

 

If you think canvas.append() is too generic, there are various aliases that make it more understandable what kind of layer you're creating (all return a new layer): canvas.fill(), canvas.path(), canvas.gradient(), canvas.group(), canvas.bytes(), canvas.pixels().

 

Layer copies

Existing layers in a canvas can be duplicated. The copy appears right above the duplicated layer:

l = canvas.append("leaf.jpg")
dupl = l.duplicate()

You can copy layers from one canvas and append them to another canvas:

offscreen = coreimage.canvas(150,150)
l = offscreen.append("leaf.jpg").copy()
canvas.append(l)

You can pre-render layers with layer.render() (or a canvas with canvas.flatten()) once and then use them as new layers or in another canvas. The advantage is that all the transformations and filters are calculated only once instead of individually for each copy. You can think of this as "flattening" a layer. See also the chapter on dynamics and caching.

 

Layer type

You can check what type of layer you are working with using the following properties:

  • layer.is_file: a layer created from an image file.
  • layer.is_fill: an opaque fill color.
  • layer.is_gradient: a linear or radial gradient.
  • layer.is_linear_gradient: a linear gradient.
  • layer.is_radial_gradient: a radial gradient.
  • layer.is_path: a layer created from a NodeBox path.
  • layer.is_pixels: a layer created from a list of colors.
  • layer.has_layers: a layer created from a canvas object.
  • layer.has_shadow: a dropshadow will be rendered for this layer.
  • layer.is_mask: a layer that is part of the alpha mask of another layer.
l = canvas.append("leaf.jpg")
print l.is_file
>>> True

 


Retrieving and arranging layers

A canvas object acts as a list of layers. With a for-loop you can address each layer it contains:

for i in range(20):
    canvas.append("lily.tif")
 
for layer in canvas:
    layer.x = random(canvas.w)
    layer.y = random(canvas.h)

Layers can be named and easily retrieved later on. The canvas.append() method has an optional name parameter. You can then search the canvas for layer names with the recursive canvas.find() command or address layers as canvas properties:

canvas.append("leaf.jpg", name="bg")
l = canvas.find("bg")
print l.name, l.x, l.y
>>> bg, 75, 75
canvas.append("leaf.jpg", name="bg")
l = canvas.bg

             

 

Layer order

Each new layer is added to the top of the canvas. You can change the ordering of a layer with the following commands:

  • layer.up(): moves the layer up one position in the layer stack
  • layer.down(): moves the layer down one position in the layer stack
  • layer.to_front(): moves the layer to the foreground of the canvas
  • layer.to_back(): moves the layer to the background of the canvas

 

back = canvas.append("leaf.jpg")
front = canvas.append("lily.tif")
back.to_front()

The layer.index property contains the layer's position in the canvas. Like any list in NodeBox the indices start from zero:

canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
print l.index
>>> 1

The layer.hide() method excludes the layer from the output:

canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
l.hide()

 


Moving and transforming layers

Individual layers in the canvas can be moved, scaled, rotated, flipped, distorted and cropped.

You can use a layer's x and y properties to move it around. These properties store the layer's position measured in pixels from the top-left of the canvas.
canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
l.x = 0
l.y = 0
The same can be achieved using the layer.translate() method. This command accepts two parameters, either absolute pixel values or relative positions between 0.0 and 1.0.
canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
l.translate(1.0, 1.0)
You can scale images using pixel size or relative size. By supplying two parameters you can scale width and height individually.
canvas.append("leaf.jpg")
for i in range(10):
    l = canvas.append("lily.tif")
    l.x = i*20
    l.scale(1.0 - i*0.1)
Rotating layers is just as easy:
canvas.append("leaf.jpg")
for i in range(10):
    l = canvas.append("lily.tif")
    l.rotate(i*10)
There's also the layer.flip_vertical() and layer.flip_horizontal() methods of course.
canvas.append("leaf.jpg")
canvas.append("lily.tif").flip_vertical()


The layer.distort() method has optional dx0, dy0, dx1, dy1, dx2, dy2, dx3, dy3 parameters setting the displacement of the left top, right top, right bottom and left bottom corners of the layer (in pixels or between 0.0 and 1.0).
canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
l.distort(dy0=-500, dx2=-100)

coreimage-layers65
The layer.crop() cuts a box from the image, starting from the top left position, with the given width and height. Images are always cropped before they are rotated, scaled and distorted.
canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
l.crop(0, 0, 80, 80)

          

 

Origin point

By default, all layer transformations originate from the layer's center. However, you can change the layer's origin point, have the layer rotate from the bottom right corner for example. Let's turn on some of the visual aids in the Core Image library for a minute:

canvas.draw(helper=True)

 

We can clearly see the layer's bounding box and how it is positioned around the central origin point. The layer rotates from the center. When we call layer.origin_bottom_left() to change the layer's origin point to the bottom left, rotations will originate from over there. Also, now the layer's bottom left is at the center of the canvas instead of the layer's center. Think of the origin point as the layer's anchor/pivot/orbit.

The following commands set the origin point:

  • layer.origin_top_left()
  • layer.origin_top_right()
  • layer.origin_top_center()
  • layer.origin_bottom_left()
  • layer.origin_bottom_right()
  • layer.origin_bottom_center()
  • layer.origin_left_center()
  • layer.origin_right_center()
  • layer.origin_center()

Or you can just use layer.origin() to set it yourself:

print l.origin(0.23, 1.07)
>>> (0.23, 1.07)

 

Layer bounds

To get the bounding box coordinates of layer, use layer.bounds():

print l.bounds()
>>> (200.0, 78.647456721610681, 409.52033810627165, 288.167787843871)

To get the width and height of a transformed layer, use layer.size() or the width and height properties. Note that when you manipulate the layer with filters the actual rendered pixels may fall beyond the given bounds.

w, h = l.size()
print w, h
>>> (209.52033810627165, 209.5203311222603)
print l.width
print l.height

You can easily position a layer at the center of the canvas with the layer.center() method:

l.center()

           

 


Adjusting layer brightness, contrast and saturation

Aside from placing and transforming multiple layers in a canvas, we can also modify the color information in each layer.

 

The layer's brightness property ranges between -1.0 and 1.0:
canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
l.brightness = 0.25
The layer's contrast property ranges between 0.25 and 4.0:
canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
l.contrast = 1.5
The layer's saturation property ranges between 0.0 and 2.0:
canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
l.saturation = 2.0
The layer.desaturate() method removes all color information:
canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
l.desaturate()
print l.saturation
>>> 0.0
The layer.invert() method inverts all colors in the layer:
l = canvas.append("leaf.jpg")
l.invert()
l = canvas.append("lily.tif")
l.invert()

 

You can set a combination of brightness, contrast and saturation with the layer.adjust() method, which has three optional parameters brightness, contrast and saturation.

 


Layer opacity and blending modes

Layers can be composited by adjusting their individual transparency or by applying blend modes to create all sorts of effects.

 

The layer.blend() command takes one parameter ranging between 0 and 100 or between 0.0 and 1.0:
canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
l.blend(25)
print l.opacity
>>> 0.25
canvas.append("leaf.jpg")
for i in range(10):
    l = canvas.append("lily.tif")
    l.scale(0.5)
    l.x = random(canvas.w)
    l.y = random(canvas.h)
    l.blend(random())
Screen blend displays pixels that are lighter than those underneath.
canvas.append("leaf.jpg")
canvas.append("lily.tif", x=50)
l = canvas.append("lily.tif", x=100)
l.blend_screen()

A similar effect is layer.blend_lighten().

Multiply blend displays pixels that are darker than those underneath.
canvas.append("leaf.jpg")
canvas.append("lily.tif", x=50)
l = canvas.append("lily.tif", x=100)
l.blend_multiply()

A similar effect is layer.blend_darken().

Overlay uses a combination of screen and multiply.
canvas.append("leaf.jpg")
canvas.append("lily.tif", x=50)
l = canvas.append("lily.tif", x=100)
l.blend_overlay()
Soft light uses a combination of darken and lighten.
canvas.append("leaf.jpg")
canvas.append("lily.tif", x=50)
l = canvas.append("lily.tif", x=100)
l.blend_softlight()
Hard light uses a combination of screen and multiply.
canvas.append("leaf.jpg")
canvas.append("lily.tif", x=50)
l = canvas.append("lily.tif", x=100)
l.blend_hardlight()
Applying the hue of the blend layer to layers underneath:
canvas.append("leaf.jpg")
canvas.append("lily.tif")
l = canvas.append(color(1.0,0.0,0.5))
l.blend_hue(50)
Applying the hue and saturation of the blend layer to layers underneath:
canvas.append("leaf.jpg")
canvas.append("lily.tif")
l = canvas.append(color(1.0,0.0,0.5))
l.blend_color(50)

 

Use the layer.blendmode property to check what blending mode the layer is using.

print l.blendmode 
>>> multiply

You can set the opacity and blend mode together with the layer.blend() command:

l.blend(25, mode="multiply")
print l.opacity, l.blendmode
>>> 0.25, color
l.blend("multiply")

The layer.blend_normal() command removes any blend mode from the layer.

 


Applying alpha masks

The layer's alpha mask stores information where the layer shines through and where it is opaque. Since the layer.mask property is a canvas object, it's just as easy to work with alpha layers than it is with normal layers. You can position, transform, adjust, blend and filter them just as any other layer.

 

Applying a linear gradient alpha mask to the lily:
canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
m = l.mask.gradient()
m.rotate(45)
Using a NodeBox path as a mask:
canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
font("Georgia-Italic", 50)
p = textpath("text", 0, 0)
m = l.mask.append(p)
m.y += 25
Using the lily as a mask for a gradient layer:
canvas.append(color(0.4,0.0,0.2))
pink = color(1.0,0.0,0.5)
white = color(1.0,1.0,1.0)
l = canvas.gradient(pink, white)
l.mask.append("lily.tif")
Complex mask made up of multiple layers:
l = c.gradient(pink, white)
for i in range(3):
    m = l.mask.append("lily.tif")
    m.scale(1-i*0.4)
    m.rotate(20*i)
    m.blend("softlight")
    m.filter("zoomblur", amount=i*25)

 


Layer filters

The Core Image library supports a number of filters that manipulate the pixels in a layer. Blur and sharpen are standard so you can set these as properties:

 

The layer.blur takes one parameter ranging between 0.0 and 100.0:
canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
l.blur = 2
Sharpening the layer:
canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
l.sharpen = 10
Other filters use the layer.filter() method:
canvas.append("leaf.jpg")
l = canvas.append("lily.tif")
l.filter("zoomblur", dx=25)

 


Filter reference

There are many other filters like the zoom blur above. Each layer.filter() command takes its own optional parameters to tweak. Scroll down for visual examples.

Zoom blur
Simulates the effect of zooming the camera while capturing the layer. Parameters are dx, dy and amount ranging between 0.0 and 200.0.

layer.filter("zoomblur", amount=20, dx=0, dy=0)

Motion blur
Simulates a directional blur. Parameters are radius between 0.0 and 100.0 and angle.

layer.filter("motionblur", radius=20, angle=0)

Noise reduction
Edges in the layer below a threshold are blurred, edges above are sharpened. Parameters are noise between 0.0 and0.1 and sharpness between 0.0 and 2.0.

layer.filter("noisereduction", noise=0.02, sharpness=0.4

Bump distortion
Creates a bump or pinch from a point in the layer. Parameters are dx, dy, radius between 0.0 and 600.0 and scale between -1.0 and 1.0.

layer.filter("bumpdistortion", dx=0, dy=0, radius=300, scale=0.5)

Bump distortion linear | stretch (Mac OS X 10.5 and up)
Creates a bump or pinch from a line in the layer. Parameters are dx, dy, radius between 0.0 and 600.0,  scale between -1.0 and 1.0 and angle.

layer.filter("stretch", dx=0, dy=0, radius=300, scale=0.5, angle=