Skip to content

Instantly share code, notes, and snippets.

@stengoes
Created February 7, 2020 14:11
Show Gist options
  • Save stengoes/8d714fee5ae11f52dded9464bf5fc87f to your computer and use it in GitHub Desktop.
Save stengoes/8d714fee5ae11f52dded9464bf5fc87f to your computer and use it in GitHub Desktop.
Bicubic resize function on float array
// Conversion functions
function convert1Dto2D(image)
{
const tmp = []
let offset = 0;
for(let y = 0; y < image.height; y++)
{
tmp.push([]);
for(let x = 0; x < image.width; x++)
{
tmp[y][x] = image.data.readFloatLE(offset);
offset += 4;
}
}
image.data = tmp;
return image;
}
function convert2Dto1D(image)
{
const tmp = Buffer.allocUnsafe(image.height*image.width*image.depth*4);
let offset = 0;
for(let y = 0; y < image.height; y++)
for(let x = 0; x < image.width; x++)
offset = tmp.writeFloatLE(image.data[y][x], offset);
image.data = tmp;
return image;
}
// Main function
function resizeBicubic(image, newHeight, newWidth)
{
/* Helper functions */
function extrapolateImageBoundaries(image)
{
const height = image.length;
const width = image[0].length;
// Linearly extrapolate top and bottom of the image
image[-2] = [];
image[-1] = [];
image[height] = [];
image[height+1] = [];
for(var x = 0; x < width; x++)
{
// Top
const topDelta = image[0][x] - image[1][x];
image[-2][x] = image[0][x] + 2*topDelta;
image[-1][x] = image[0][x] + topDelta;
// Bottom
const bottomDelta = image[height-1][x] - image[height-2][x];
image[height][x] = image[height-1][x] + bottomDelta;
image[height+1][x] = image[height-1][x] + 2*bottomDelta;
}
// Linearly extrapolate left and right of the image
for(var y = -2; y < height+2; y++)
{
// Left
const leftDelta = image[y][0] - image[y][1];
image[y][-2] = image[y][0] + 2*leftDelta;
image[y][-1] = image[y][0] + leftDelta;
// Right
const rightDelta = image[y][width-1] - image[y][width-2];
image[y][width] = image[y][width-1] + rightDelta;
image[y][width+1] = image[y][width-1] + 2*rightDelta;
}
return image;
}
function createBicubicInterpolator(values)
{
//Coefficients: see https://en.wikipedia.org/wiki/Bicubic_interpolation
const a00 = values[1][1],
a01 = (-1/2)*values[1][0] + (1/2)*values[1][2],
a02 = values[1][0] + (-5/2)*values[1][1] + 2*values[1][2] + (-1/2)*values[1][3],
a03 = (-1/2)*values[1][0] + (3/2)*values[1][1] + (-3/2)*values[1][2] + (1/2)*values[1][3],
a10 = (-1/2)*values[0][1] + (1/2)*values[2][1],
a11 = (1/4)*values[0][0] + (-1/4)*values[0][2] + (-1/4)*values[2][0] + (1/4)*values[2][2],
a12 = (-1/2)*values[0][0] + (5/4)*values[0][1] + (-1)*values[0][2] + (1/4)*values[0][3] + (1/2)*values[2][0] + (-5/4)*values[2][1] + values[2][2] + (-1/4)*values[2][3],
a13 = (1/4)*values[0][0] + (-3/4)*values[0][1] + (3/4)*values[0][2] + (-1/4)*values[0][3] + (-1/4)*values[2][0] + (3/4)*values[2][1] + (-3/4)*values[2][2] + (1/4)*values[2][3],
a20 = values[0][1] + (-5/2)*values[1][1] + 2*values[2][1] + (-1/2)*values[3][1],
a21 = (-1/2)*values[0][0] + (1/2)*values[0][2] + (5/4)*values[1][0] + (-5/4)*values[1][2] + (-1)*values[2][0] + values[2][2] + (1/4)*values[3][0] + (-1/4)*values[3][2],
a22 = values[0][0] + (-5/2)*values[0][1] + 2*values[0][2] + (-1/2)*values[0][3] + (-5/2)*values[1][0] + (25/4)*values[1][1] + (-5)*values[1][2] + (5/4)*values[1][3] + 2*values[2][0] + (-5)*values[2][1] + 4*values[2][2] + (-1)*values[2][3] + (-1/2)*values[3][0] + (5/4)*values[3][1] + (-1)*values[3][2] + (1/4)*values[3][3],
a23 = (-1/2)*values[0][0] + (3/2)*values[0][1] + (-3/2)*values[0][2] + (1/2)*values[0][3] + (5/4)*values[1][0] + (-15/4)*values[1][1] + (15/4)*values[1][2] + (-5/4)*values[1][3] + (-1)*values[2][0] + 3*values[2][1] + (-3)*values[2][2] + values[2][3] + (1/4)*values[3][0] + (-3/4)*values[3][1] + (3/4)*values[3][2] + (-1/4)*values[3][3],
a30 = (-1/2)*values[0][1] + (3/2)*values[1][1] + (-3/2)*values[2][1] + (1/2)*values[3][1],
a31 = (1/4)*values[0][0] + (-1/4)*values[0][2] + (-3/4)*values[1][0] + (3/4)*values[1][2] + (3/4)*values[2][0] + (-3/4)*values[2][2] + (-1/4)*values[3][0] + (1/4)*values[3][2],
a32 = (-1/2)*values[0][0] + (5/4)*values[0][1] + (-1)*values[0][2] + (1/4)*values[0][3] + (3/2)*values[1][0] + (-15/4)*values[1][1] + 3*values[1][2] + (-3/4)*values[1][3] + (-3/2)*values[2][0] + (15/4)*values[2][1] + (-3)*values[2][2] + (3/4)*values[2][3] + (1/2)*values[3][0] + (-5/4)*values[3][1] + values[3][2] + (-1/4)*values[3][3],
a33 = (1/4)*values[0][0] + (-3/4)*values[0][1] + (3/4)*values[0][2] + (-1/4)*values[0][3] + (-3/4)*values[1][0] + (9/4)*values[1][1] + (-9/4)*values[1][2] + (3/4)*values[1][3] + (3/4)*values[2][0] + (-9/4)*values[2][1] + (9/4)*values[2][2] + (-3/4)*values[2][3] + (-1/4)*values[3][0] + (3/4)*values[3][1] + (-3/4)*values[3][2] + (1/4)*values[3][3];
return (y, x) => {
// Check for valid range
if(y < 0 || y > 1 || x < 0 || x > 1)
throw "Error: cannot interpolate outside the unit square from (0, 0) to (1, 1). Got y ="+y+", x="+x+".";
const y2 = y*y,
y3 = y*y2,
x2 = x*x,
x3 = x*x2;
return (a00 + a01*x + a02*x2 + a03*x3) +
(a10 + a11*x + a12*x2 + a13*x3) * y +
(a20 + a21*x + a22*x2 + a23*x3) * y2 +
(a30 + a31*x + a32*x2 + a33*x3) * y3;
}
}
function createGridBicubicInterpolator(image, scaleY, scaleX, translateY, translateX)
{
// Get image dimensions
const height = image.length;
const width = image[0].length;
// Linearly extrapolate image boundaries
image = extrapolateImageBoundaries(image);
// Create a grid of interpolators
const grid = [];
for(var y = -1; y < height; y++)
grid[y] = [];
return (y, x) => {
// Scale and translate y,x coordinates
y = (y * scaleY) + translateY;
x = (x * scaleX) + translateX;
// Check for valid range
if(y < -1 || y > height || x < -1 || x > width)
throw "Error: cannot interpolate outside the rectangle from (-1, -1) to (" + height + ", "+ width + "). Got y ="+y+", x="+x+".";
// Find the right grid cell
var y0 = Math.floor(y);
if(y === height) y0--;
var x0 = Math.floor(x);
if(x === width) x0--;
// Create only new interpolater when not already in grid (CACHE)!
if(!grid[y0][x0])
{
grid[y0][x0] = createBicubicInterpolator(
[
[image[y0-1][x0-1], image[y0-1][x0], image[y0-1][x0+1], image[y0-1][x0+2]],
[image[y0+0][x0-1], image[y0+0][x0], image[y0+0][x0+1], image[y0+0][x0+2]],
[image[y0+1][x0-1], image[y0+1][x0], image[y0+1][x0+1], image[y0+1][x0+2]],
[image[y0+2][x0-1], image[y0+2][x0], image[y0+2][x0+1], image[y0+2][x0+2]]
],
);
}
// Select the interpolator corresponding to that grid cell
const interpolator = grid[y0][x0];
// Translate coordinates back to unit square (0, 0) - (1, 1) before returning.
return interpolator(y-y0, x-x0);
}
}
// Validate input arguments
if(image.depth != 1)
throw "Error: resize() does currently only support images with depth = 1. Got depth = "+image.depth+".";
// Convert input image
image = convert1Dto2D(image);
// Initialize output
let newImage = {
"height": newHeight,
"width": newWidth,
"depth": image.depth,
"data": []
};
// Compute scale and translation needed for interpolation
const scaleY = (image.height / newHeight);
const scaleX = (image.width / newWidth);
const translateY = -0.5+scaleY/2;
const translateX = -0.5+scaleX/2;
// Create interpolator
const interpolate = createGridBicubicInterpolator(image.data, scaleY, scaleX, translateY, translateX);
// Apply interpolator
for(let y = 0; y < newHeight; y++)
{
newImage.data.push([]);
for(let x = 0; x < newWidth; x++)
newImage.data[y][x] = interpolate(y, x);
}
// Convert input image
newImage = convert2Dto1D(newImage);
// Return resized image
return newImage;
}
/* Example */
// Prepare human readable input
let image = {
"height": 4,
"width": 4,
"depth": 1,
"data": [
[0, 1, 2, 3],
[1, 2, 3, 4],
[2, 3, 4, 5],
[3, 4, 5, 6],
]
}
// Convert data to non-human readable input
// (the data format that is actually used for images the gui)
image = convert2Dto1D(image);
// Resize the image
let resizedImage = resizeBicubic(image, 20, 20);
// Convert output again to human readable data and show it
resizedImage = convert1Dto2D(resizedImage);
console.log(resizedImage);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment