ImIn (Image interpolation)
Barycentric and bilinear image interpolation and transformation in pure Python
Overview
ImIn (Image Interpolation) module contains Python code for bilinear image interpolation and barycentric image interpolation, working in any color modes from 8 bit L to 16 bit RGBA (presumably it works in other color modes as well but I can't read and write CMYK files to perform testing). That is, it contains functions for reading single pixel at float coordinates, interpolated from real pixels (having integer coordinates), as well as functions for transforming whole image (scaling, rotating, deforming etc.) using interpolation methods above.
It is well known that image interpolation authors tend to increase image analysis area size. With bilinear interpolation, pixel is considered as square, and interpolation takes place between four corners. With bicubic interpolation, more neighbour pixels are added to analysis. This is good for smooth gradients but what about some sharp edges? Why not to try using just half a pixel? For example, split a pixel square ■ diagonally into two triangles like ◤◢ or ◣◥, and use analysis within one half of a pixel using three corners?
Since I appeared to be unable to find such a triangular interpolation in any big programs like Photoshop or GIMP, I decided to write corresponding algorithm myself and see how it works.
It appears that the best coordinate system for triangles is barycentric coordinate system, which is widely used in science, in 3D graphics, sometimes used in 2D vector gradients, but, for unclear reasons, not used in regular 2D bitmap image processing.
So, I wrote my own barycentric interpolation code, and, after a long debugging of simple school math, it appeared to give rather interesting results when used, for example, for image rescaling, so I even tend to prefer using it for my images in some cases, regardless of the fact that Python code is always slower than optimized machine code in Photoshop.
At the same time I got bored by copying my own code for bilinear image interpolation from one program to another (yes, there is an interpolation in, say, POV-Ray 6³ Mosaic or img2mesh).
So, I decided to join my code for bilinear and barycentric image interpolation in some sort of a single module, which can be easily plugged into different image processing Python programs. So, here it goes.
Ladies and gentlemen! Mesdames et messieurs! Товарищи и товарищи! Let me introduce to you the one and only Image Interpolation (imin) module, combining simple yet carefully designed and even somewhat optimized bilinear and barycentric image interpolation in pure Python!
Source |
Bilinear |
Barycentric |
|---|---|---|
3x3 px (black pixel over white background) |
x20 times |
x20 times |
15x15 px (2x2 px chessboard pattern) |
x3 times |
x3 times |
22x16 px |
x5 times |
x5 times |
Let's perform a visual comparison of bilinear and barycentric image interpolation used for upscaling images of different nature.
Table 1 in this section contains various test cases. Obviously, bigger images like photos would be more interesting, but, unfortunately, to clearly see interpolation artifacts I should use at least x3 upscaling, so, if I use a photo that barely fits a screen as a source, result will not fit this web page.
In the first row, you may see a result of upscaling single black pixel over white background. Resulting images clearly show the difference between bilinear and barycentric interpolation.
In the second row, you see a problem case: a chessboard pattern. It's a problem for any interpolation: it's unclear what to find in there. Noise over flat color? A pattern?
As you can see, while bilinear interpolation simply blurs everything, barycentric interpolation tries to find diagonals. Most likely, it's not what you want, but, on the other hand, a chessboard really is a set of diagonals; any bishop, whether he's white or black, will confirm this.
(Needless to say, if you want a pixel-sized black and white chessboard to be treated as black and white, you better resort to ScaleNx image rescaling.)
In the third row, you see a small fragment of a photo. It seems like barycentric interpolation gives clearer appearance of overall eye outline shape; rhomb-shaped pupil, however, looks a bit weird. But well, it's a single pixel in original, so you the same rhomb as in the first row.
Module and example programs
Currently default branch is "Functional", bearing this name because it is supposed to be used as functions, not a class. Corresponding class equivalent is under development, and supposed to be published later.
Module content is briefly summarized below.
- __init__.py: This is not just an init file. Actually, it contains all code required to read image pixel at float coordinates, interpolated from surrounding pixels using either barycentric or bilinear method. If reading image pixels is all you need, you may copy __init__.py file alone and use it for your applications. Remember that I don't give a care to legal stuff, so you can use my code for free, completely or partially, and modify at will.
- displace.py: General purpose image displacement using either barycentric or bilinear interpolation. Exact type of displacement is controlled by fx(x, y) and fy(x, y) functions, given to displace as arguments.
- rescale.py: Image rescaling. Obviously, image rescaling is a specific case of displacement, and can be done with displacer, but specific case of rescaling gives a chance to add some specific speed optimization; therefore a separate code was created.
Detailed functions description and parameters for developers are given in a rather prolific docstrings.
Surely, during development module needs testing. Therefore some GUI shell programs were created for testing and illustration purposes:
- distorter.py: the main part of demo. Distorter provides examples of general purpose image displacer (displace.py above) usage. Currently demo includes only two functions: linear skewing and wave-like deformation with sine function;
- mdbiggener.py: image rescaler; provides a demo for rescale.py;
- revolver.py: image rotation program. Based entirely on displace.py, and separated as specific program just because rotation GUI should take only one argument (i.e. angle), while displacement currently takes two (one for x and other for y). To avoid making a general program with only half of GUI being functional, this particular example program was created.
All the final public functions, included into imin module and intended for actual use, have a rather generalized access scheme. For example, a single interpolated pixel with float x and y coordinates may be acquired like this:
pixel_value = pixel(source_image, x, y, edge, method)
where:
- pixel_value: pixel as list of channel values, e.g. [R, G, B];
- source_image: source image as nested 3D list (e.g., obtained by reading PPM file using PyPNM);
- x, y: pixel coordinates (not surprisingly);
- edge: edge extrapolation mode, either repeat edge (like Photoshop in most cases), or filling with zeroes (like Photoshop in least cases), or wrap around processing (which is good for seamless textures and which Photoshop, for unknown reason, practically do not support);
- method: interpolation method, either bilinear or barycentric.
Such a generalized syntax simplifies using imin module for writing image editing programs.
Note, however, that, while "Wrap around" mode is present in sample programs GUI, it does't always work as one may expect.
Imagine simple horizontal skewing, when top and bottom edges move against each other, and right and left ones become sort of diagonals. Assume that the source image is seamless pattern. With "Wrap" mode enabled, distorter.py keeps the horizontal part seamless, that is, the triangular part of image that goes away at the right side will appear at the left. What as to vertical part, since top and bottom edges move against each other, their match gets broken and seam formed.
However, if you switch from skewing to sine wave distortion, result of deforming a seamless pattern will be seamless. That's because I use exactly one wave cycle for a displacing function, making ends meet (literally). Well, actually, even with non-seamless sources, in case of sufficient deformations this function deforms edges so much that they become unrecognizable, and image turns into seamless-like (is this a real word?).
Now it's time to either step forward to Image Interpolation repository at GitHub for downloads...
...or Move back to Dnyarri`s Python freeware main page.