Created
February 7, 2020 14:11
-
-
Save stengoes/8d714fee5ae11f52dded9464bf5fc87f to your computer and use it in GitHub Desktop.
Bicubic resize function on float array
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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