像素的线性插值
过滤的方式并没有真正的对与错,因为结果是主观的,结果的质量取决于你,是否足够好,或者你觉得还有改进的余地。
还有各种各样的过滤方法,最近邻、线性、双线性、多项式、样条、Lanczos ......并且每种方法都可以有很多变化。还有一些因素,比如过滤输出格式是什么;屏幕,打印,视频。质量优先于速度还是内存效率。无论如何,当硬件为您实时完成时,为什么要升级。
看起来您具有正确的线性过滤基础知识
更新更正。线性和双线性是指同一类型的插值,双线性是2D,线性是1D
处理最后一个像素
在丢失像素的情况下,有几种选择,
- 假设颜色继续,所以只需复制最后一个像素。
- 假设下一个像素是背景、边框颜色或一些预定义的边缘颜色。
- 环绕到另一侧的像素(平铺地图的最佳选择)
- 如果您知道有背景图像,请使用其像素
- 只需删除最后一个像素(图像大小将小 1 个像素)
PS结果
对我来说,PhotoShop 结果看起来像是一种双线性过滤形式,尽管它应该保持原始像素颜色,所以使用了一些更复杂的东西。在不知道方法是什么的情况下,您将很难匹配它。
获得最佳结果的光谱
良好的过滤将找到特定点的频谱并根据该信息重建丢失的像素。
如果您认为一行像素不是值而是体积,那么一行像素会产生波形。任何复杂的波形都可以分解为一组更简单的基本纯音(频率)。然后,您可以通过在特定点添加所有频率来获得良好的近似值。
使用这种方法的滤波器通常用傅里叶或 FFT(快速傅里叶变换)表示,并且需要比标准线性插值进行大量处理。
RGB 值代表什么。
每个红色、绿色和蓝色通道代表该通道强度/亮度的平方根。(这是一个接近的通用近似值)因此,当您进行插值时,您需要转换为正确的值,然后进行插值,然后再转换回对数值。
正确插值
function interpolateLinear(pos,c1,c2){ // pos 0-1, c1,c2 are objects {r,g,b}
return {
r : Math.sqrt((c2.r * c2.r + c1.r * c1.r) * pos + c1.r * c1.r),
g : Math.sqrt((c2.g * c2.g + c1.g * c1.g) * pos + c1.g * c1.g),
b : Math.sqrt((c2.b * c2.b + c1.b * c1.b) * pos + c1.b * c1.b),
};
}
需要注意的是,绝大多数数字处理软件都不能正确插值。这部分是由于开发人员对输出格式的无知(为什么我会尽可能地谈论它),部分是由于遵守那些仅仅为了显示图像而苦苦挣扎的旧计算机,更不用说处理它了(尽管我不买那个借口)。
HTML5也不例外,几乎在所有插值中都错误地插值了像素值。这会产生暗带,其中存在强烈的色调对比和较暗的总亮度,用于放大和缩小的图像。一旦您注意到错误,它将永远让您烦恼,因为今天的硬件很容易胜任这项工作。
为了说明不正确的插值有多糟糕,下图显示了正确的(顶部)和使用 SVG 过滤器(底部)插值的 canvas 2D API。
二维线性插值(双线性)
通过依次执行每个轴来完成沿两个轴的插值。首先沿 x 轴插值,然后沿 y 轴插值。您可以将其作为 2 道工序或单道工序来执行。
以下函数将在任何子像素坐标处进行插值。此功能不是为速度而构建的,并且有很大的优化空间。
// Get pixel RGBA value using bilinear interpolation.
// imgDat is a imageData object,
// x,y are floats in the original coordinates
// Returns the pixel colour at that point as an array of RGBA
// Will copy last pixel's colour
function getPixelValue(imgDat, x,y, result = []){
var i;
// clamp and floor coordinate
const ix1 = (x < 0 ? 0 : x >= imgDat.width ? imgDat.width - 1 : x)| 0;
const iy1 = (y < 0 ? 0 : y >= imgDat.height ? imgDat.height - 1 : y | 0;
// get next pixel pos
const ix2 = ix1 === imgDat.width -1 ? ix1 : ix1 + 1;
const iy2 = iy1 === imgDat.height -1 ? iy1 : iy1 + 1;
// get interpolation position
const xpos = x % 1;
const ypos = y % 1;
// get pixel index
var i1 = (ix1 + iy1 * imgData.width) * 4;
var i2 = (ix2 + iy1 * imgData.width) * 4;
var i3 = (ix1 + iy2 * imgData.width) * 4;
var i4 = (ix2 + iy2 * imgData.width) * 4;
// to keep code short and readable get data alias
const d = imgDat.data;
for(i = 0; i < 3; i ++){
// interpolate x for top and bottom pixels
const c1 = (d[i2] * d[i2++] - d[i1] * d[i1]) * xpos + d[i1] * d[i1 ++];
const c2 = (d[i4] * d[i4++] - d[i3] * d[i3]) * xpos + d[i3] * d[i3 ++];
// now interpolate y
result[i] = Math.sqrt((c2 - c1) * ypos + c1);
}
// and alpha is not logarithmic
const c1 = (d[i2] - d[i1]) * xpos + d[i1];
const c2 = (d[i4] - d[i3]) * xpos + d[i3];
result[3] = (c2 - c1) * ypos + c1;
return result;
}
const upScale = 4;
// usage
const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const imgData2 = ctx.createImageData(ctx.canvas.width * upScale, ctx.canvas.height * upScale);
const res = new Uint8ClampedArray(4);
for(var y = 0; y < imgData2.height; y++){
for(var x = 0; x < imgData2.width; x++){
getPixelValue(imgData,x / upScale, y / upScale, res);
imgData2.data.set(res,(x + y * imgdata2.width) * 4);
}
}
示例高档画布 8 倍
该示例使用上述函数将测试图案放大 8 倍。显示三个图像。原来的 64 x 8 然后,使用对数双线性插值计算 upscale,然后使用 canvas 标准 API drawImage 来进行 upScale(使用默认插值,双线性)。
// helper functions create canvas and get context
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
// iterators
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); };
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
const upScale = 8;
var canvas1 = CImageCtx(64,8);
var canvas2 = CImageCtx(canvas1.width * upScale, canvas1.height * upScale);
var canvas3 = CImageCtx(canvas1.width * upScale, canvas1.height * upScale);
// imgDat is a imageData object,
// x,y are floats in the original coordinates
// Returns the pixel colour at that point as an array of RGBA
// Will copy last pixel's colour
function getPixelValue(imgDat, x,y, result = []){
var i;
// clamp and floor coordinate
const ix1 = (x < 0 ? 0 : x >= imgDat.width ? imgDat.width - 1 : x)| 0;
const iy1 = (y < 0 ? 0 : y >= imgDat.height ? imgDat.height - 1 : y) | 0;
// get next pixel pos
const ix2 = ix1 === imgDat.width -1 ? ix1 : ix1 + 1;
const iy2 = iy1 === imgDat.height -1 ? iy1 : iy1 + 1;
// get interpolation position
const xpos = x % 1;
const ypos = y % 1;
// get pixel index
var i1 = (ix1 + iy1 * imgData.width) * 4;
var i2 = (ix2 + iy1 * imgData.width) * 4;
var i3 = (ix1 + iy2 * imgData.width) * 4;
var i4 = (ix2 + iy2 * imgData.width) * 4;
// to keep code short and readable get data alias
const d = imgDat.data;
// interpolate x for top and bottom pixels
for(i = 0; i < 3; i ++){
const c1 = (d[i2] * d[i2++] - d[i1] * d[i1]) * xpos + d[i1] * d[i1 ++];
const c2 = (d[i4] * d[i4++] - d[i3] * d[i3]) * xpos + d[i3] * d[i3 ++];
// now interpolate y
result[i] = Math.sqrt((c2 - c1) * ypos + c1);
}
// and alpha is not logarithmic
const c1 = (d[i2] - d[i1]) * xpos + d[i1];
const c2 = (d[i4] - d[i3]) * xpos + d[i3];
result[3] = (c2 - c1) * ypos + c1;
return result;
}
const ctx = canvas1.ctx;
var cols = ["black","red","green","Blue","Yellow","Cyan","Magenta","White"];
doFor(8,j => eachOf(cols,(col,i) => {ctx.fillStyle = col; ctx.fillRect(j*8+i,0,1,8)}));
eachOf(cols,(col,i) => {ctx.fillStyle = col; ctx.fillRect(i * 8,4,8,4)});
const imgData = ctx.getImageData(0, 0, canvas1.width, canvas1.height);
const imgData2 = ctx.createImageData(canvas1.width * upScale, canvas1.height * upScale);
const res = new Uint8ClampedArray(4);
for(var y = 0; y < imgData2.height; y++){
for(var x = 0; x < imgData2.width; x++){
getPixelValue(imgData,x / upScale, y / upScale, res);
imgData2.data.set(res,(x + y * imgData2.width) * 4);
}
}
canvas2.ctx.putImageData(imgData2,0,0);
function $(el,text){const e = document.createElement(el); e.textContent = text; document.body.appendChild(e)};
document.body.appendChild(canvas1);
$("div","Next Logarithmic upscale using linear interpolation * 8");
document.body.appendChild(canvas2);
canvas3.ctx.drawImage(canvas1,0,0,canvas3.width,canvas3.height);
document.body.appendChild(canvas3);
$("div","Previous Canvas 2D API upscale via default linear interpolation * 8");
$("div","Note the overall darker result and dark lines at hue boundaries");
canvas { border : 2px solid black; }