7.7. Compositing images

The previous page’s drawing primitives paint geometric marks onto an image – a line, a rectangle, a piece of text. That covers most annotations an algorithm needs to make visible, but not all of them. Sometimes the annotation is itself an image: a captured reference frame to display side by side with the current one, a thumbnail of a previous capture shown in a corner of the preview, a previously stored template visualised on top of a live frame for calibration. The mechanism for drawing one image onto another is a single method – draw_image() – with enough parameters to handle the position, the scaling, the colour palette, and the transparency that real composition needs.

7.7.1. The basic call

In its simplest form, draw_image takes another Image and a position to draw it at:

reference = image.Image("/sdcard/reference.bmp")
img.draw_image(reference, x=10, y=10)

The destination is img; the source is reference; the source’s top-left pixel lands at (10, 10) of img, and the rest of the source’s pixels follow to the right and downward from there. Pixels of the destination that the source covers are overwritten with the source’s corresponding pixels; pixels outside the source’s footprint are left alone.

If the source extends past the edge of the destination, the parts that fall off are silently clipped – the same forgiving behaviour set_pixel shows for out-of-range positions. Application code does not have to clamp the position to the image dimensions in advance; it can pass the position it wants and let the method handle the clipping.

7.7.2. Loading a file inline

draw_image accepts a file path in place of the Image argument and loads the file before composing it:

img.draw_image("/sdcard/reference.bmp", x=10, y=10)

That looks like a convenience – one line instead of two – and it is, but the difference is more than syntax. Constructing an Image from a file allocates a buffer to hold the decoded pixels, and that buffer survives until garbage collection releases it. Passing the path directly to draw_image lets the module decode the file into a scratch buffer, composite from it, and release the buffer when the call returns, without the application code having to hold a reference to a separate Image between frames.

7.7.3. Scaling

When the source and the destination are different sizes – a low-resolution capture being composed onto a higher-resolution canvas, or a thumbnail that needs to be sized to a particular fraction of the frame – two scale parameters take care of resizing the source as it is drawn:

img.draw_image(reference, x=10, y=10, x_scale=2.0, y_scale=2.0)

x_scale and y_scale are independent floats; passing both at the same value scales uniformly, and passing different values stretches or shrinks the source along one axis. The scaling happens at draw time; the source reference is not modified.

A bitmask of hint flags decides how the scaling actually interpolates between pixels. image.BILINEAR produces smoother results at the cost of more computation; image.BICUBIC produces still smoother results and costs more again; the default uses nearest-neighbour, which is the cheapest and the right choice when the source is already at the destination’s pixel resolution. Aspect-handling flags – SCALE_ASPECT_KEEP, SCALE_ASPECT_EXPAND, SCALE_ASPECT_IGNORE – decide what to do when the source’s aspect ratio does not match the rectangle it is being drawn into.

7.7.4. Alpha blending

By default, draw_image replaces destination pixels with source pixels. When the goal is a translucent overlay – so the destination shows through the source – the alpha parameter controls how the two are mixed. alpha=0 shows only the destination (no source); alpha=255 is the default and shows only the source (full replacement); intermediate values mix the two proportionally:

img.draw_image(overlay, x=0, y=0, alpha=128)

A separate alpha_palette argument is the module’s only per-pixel alpha mechanism. It takes a GRAYSCALE image whose values are used as alpha at the matching position in the source – a heatmap whose alpha varies with its intensity, for example. The alpha has to be supplied as that separate grayscale argument; a source image that carries its own alpha channel (a PNG with transparency, say) does not bring it through automatically.

7.7.5. Source ROI and palette

Two further parameters round out the composition mechanism:

  • roi=(x, y, w, h) restricts the source to a sub-rectangle of itself, so only that rectangle gets composed onto the destination. Useful for cropping inside the same call, without preparing a cropped intermediate.

  • color_palette substitutes each source pixel’s value through a lookup table before drawing – the same mechanism to_rainbow() and to_ironbow() use, exposed here so an overlay can be palettised on its way onto the destination without a separate conversion pass.

Both compose with everything else on the call: the scaling, the alpha, the destination-side mask argument, and the destination-side roi parameter that scopes the write to a rectangle of the destination.