Lanczos resampling of pixel maps in C
Lanczos resampling is an anti-aliasing technique originating from signal processing that is used in filtering and improving image quality. At a high-level, it filters out high-frequency artifacts for smooth interpolation. We'll look at how this works shortly.
We use PPM (Portable Pixel Map) image files because it's very basic and easier to read or write to than JPEG or many other major formats. Let's start with the reconstruction kernel which is defined in terms of
The kernel limits its output to a small neighborhood by clamping
static double sinc(double x) {
return x == 0.0 ? 1.0 : sin(M_PI * x) / (M_PI * x);
}
static double lanczos_kernel(double x, int a) {
return fabs(x) < a ? sinc(x) * sinc(x / a) : 0.0;
}
At a given channel
such that:
and
We start with
void lanczos_resample(const uint8_t* input, int iw, int ih, int ic,
uint8_t* output, int ow, int oh, int a) {
double x_ratio = (double)iw / ow;
double y_ratio = (double)ih / oh;
for (int oy = 0; oy < oh; oy++) {
for (int ox = 0; ox < ow; ox++) {
for (int c = 0; c < ic; c++) {
double acc = 0.0, w_s = 0.0;
// fractional source coords
double s_x = ox * x_ratio; // <--
double s_y = oy * y_ratio; // <--
}
}
}
}
*_ratio
is provided for scaling the input image itself which we pass around as a buffer. The looping is row-major since PPM files store the buffer sequentially.
We find the neighborhood by determining the range of pixels
int x_s = (int)floor(s_x) - a + 1;
int x_e = (int)floor(s_x) + a;
int y_s = (int)floor(s_y) - a + 1;
int y_e = (int)floor(s_y) + a;
The Lanczos kernel covers a symmetric range of input pixels around the central position which is why we consistently add
Why?
When computing the starting index
Consider
(nearest integer below ). starts from . ends at .
This specific range
-
For each pixel
in the neighborhood:- Compute its weight as the product of two Lanczos kernels:
- Accumulate the weighted contribution to the output pixel value:
- Accumulate the total weight:
- Compute its weight as the product of two Lanczos kernels:
for (int y = y_s; y <= y_e; y++) {
for (int x = x_s; x <= x_e; x++) {
if (x >= 0 && x < iw && y >= 0 && y < ih) {
double dx = s_x - x;
double dy = s_y - y;
double w = lanczos_kernel(dx, a) * lanczos_kernel(dy, a);
acc += input[(y * iw + x) * ic + c] * w;
w_s += w;
}
}
}
We essentially write to the output channel using the input buffer. It's extremely important to not select any pixels outside the range for visual clarity. The resulting image will appear with some level of blur and the pixelated parts get aliased.
output[(oy * ow + ox) * ic + c] = w_s > 0.0 ? (uint8_t)(acc / w_s) : 0;
I tried this with a sample image. The original image is at 42.9 kB, resampled output was at 171.7 kB (roughly 75% gain in size and this is important since PPM does not support compression). There's really not much else besides reading and writing to a PPM and selecting a scale factor (2 or 4 work well, find the final source below).
The PNG equivalents above do not carry all the details but still show a discrepancy in file size, resampled output image converted to PNG has a larger file size.
Acknowledgements #
Cornell CS 664 - Computer Vision Prof. Dan Huttenlocher, Fall 2003, sample house image.
Resources #
Cf. final source.
- Previous: Endianness in the JVM bytecode layout