Matlab的imresize函数中用于插值的算法是什么?

3

我正在使用Matlab/Octave中的imresize()函数对给定的2D数组进行重新采样。我想了解在imresize中使用的特定插值算法是如何工作的。

(我在Windows上使用的是Octave)

例如:

A =  1 2 
     3 4

是一个二维数组。然后我使用命令

b=imresize(a,2,'linear'); 

基本上是将行和列的采样率增加2倍。
输出为:
1.0000   1.3333   1.6667   2.0000
1.6667   2.0000   2.3333   2.6667
2.3333   2.6667   3.0000   3.3333
3.0000   3.3333   3.6667   4.0000

我不明白这个线性插值是如何工作的。它被说成使用双线性插值,但是它是如何在边界处填充数据以及如何得到输出的呢?
第二个例子:
A = 

1   2   3   4
5   6   7   8
0   1   2   3
1   2   3   4
imresize(a,1.5,'linear') 是如何产生以下输出的?
(说明:该代码是图像处理中的一种方法,用于调整图像大小)
1.00000   1.60000   2.20000   2.80000   3.40000   4.00000
3.40000   4.00000   4.60000   5.20000   5.80000   6.40000
4.00000   4.60000   5.20000   5.80000   6.40000   7.00000
1.00000   1.60000   2.20000   2.80000   3.40000   4.00000
0.40000   1.00000   1.60000   2.20000   2.80000   3.40000
1.00000   1.60000   2.20000   2.80000   3.40000   4.00000
4个回答

2
以下代码展示了如何使用INTERP2执行双线性插值

A = [1 2; 3 4];
SCALE = 2;

xi = linspace(1,size(A,2),SCALE*size(A,2));  %# interpolated horizontal positions
yi = linspace(1,size(A,1),SCALE*size(A,1));  %# interpolated vertical positions
[X Y] = meshgrid(1:size(A,2),1:size(A,1));   %# pixels X-/Y-coords
[XI YI] = meshgrid(xi,yi);                   %# interpolated pixels X-/Y-coords
B = interp2(X,Y,A, XI,YI, '*linear');        %# interp values at these positions

结果与您的Octave代码输出一致:
B =
            1       1.3333       1.6667            2
       1.6667            2       2.3333       2.6667
       2.3333       2.6667            3       3.3333
            3       3.3333       3.6667            4

我应该提到,在MATLABOctave的IMRESIZE输出之间我得到了不同的结果。例如,当我在MATLAB上执行以下命令时,针对矩阵A=[1 2; 3 4],我得到了如下结果:
>> B = imresize([1 2; 3 4], 2, 'bilinear')
B =
            1         1.25         1.75            2
          1.5         1.75         2.25          2.5
          2.5         2.75         3.25          3.5
            3         3.25         3.75            4

这段话表明MATLAB的实现可能做了一些额外的事情...不幸的是,很难阅读IMRESIZE源代码,尤其是因为在某些时候它调用了一个MEX编译的函数(没有源代码形式可用)。
另外,似乎还有一个旧版本的函数:IMRESIZE_OLD(完全由m-code实现)。从我所理解的来看,它对图像执行某种仿射变换。也许更熟悉这种技术的人能够阐明这个问题...

编辑(2021)

请参考约翰的答案,以获得更详细的解释。


我的猜测是Matlab在上采样之前对输入数据运行高斯滤波器。Octave不会这样做(虽然Octave在帮助imresize中提到了这一点,但我检查时并没有这样做)。 - goldenmean
@ goldenmean:这可能可以解释这种差异。无论如何,为了回答您最初的问题,我链接的维基百科文章很好地解释了双线性插值过程,并提供了一个示例。 - Amro
1
我重新审视了IMRESIZE函数,请阅读我对另一个问题的回答:http://stackoverflow.com/questions/7758078/resizing-in-matlab-w-different-filters/7759981#7759981 - Amro
1
Octave中的imresize版本存在问题,而MATLAB中的imresize版本是正确的。关键在于2x2图像的左上角位于(0.5, 0.5),右下角位于(2.5, 2.5)。该区域分为2x2个像素。然后您希望将其扩展到4x4像素,这意味着新像素中心在旧坐标中为0.75、1.25、1.75和2.25。这就是您需要在xiyi中的内容。请参见此错误报告:https://savannah.gnu.org/bugs/?51769 - John
1
@Amro 是的,完全正确。我稍微简化了您的脚本,并添加了填充和imresize进行比较,请参见此处片段。请注意,在Octave online中的imresize目前是错误的,修复它的补丁昨天已经提交 - John
显示剩余3条评论

2

简而言之

结果是错误的,因为GNU Octave的imresize存在一个bug。最近已经修复了双线性插值(错误报告提交代码)。向下滚动到“像素插值”中获取正确的图像插值方法。

样本插值

让我们从样本的线性插值开始:

  0   1/3  2/3   1
  |    |    |    |
a=1----+----+----2=b

您可以使用以下公式将a到b的混合:
f(x) = (1 - x) * a + x * b。
一些示例:
- f(0) = (1 - 0) * a + 0 * b = a = 1 - f(1/3) = (1 - 1/3) * a + 1/3 * b = 2/3 + 2/3 = 4/3 = 1.3333 - f(1/2) = (1 - 1/2) * a + 1/2 * b = (a + b) / 2 = 1.5 - f(2/3) = (1 - 2/3) * a + 2/3 * b = 1/3 + 4/3 = 5/3 = 1.6667 - f(1) = (1 - 1) * a + 1 * b = b = 2
这对应于第一个示例的第一行。在双线性插值中,简单的线性插值用于x或y方向。通常不会在对角线或任何其他方向上使用简单的线性插值(您的第一个示例是退化情况)。
       0   1/3        1
       |    |    |    |
 0   a=1---f(x)--+----2=b
       |    |    |    |
      -+----+----+----+-
       |    |    |    |
2/3   -+---???---+----+-
       |    |    |    |
 1   c=3---g(x)--+----4=d
       |    |    |    |

其他点是如何计算的?我们在x方向的顶部和底部行使用简单的线性插值,然后在y方向上插值结果:
  • 在x方向上,顶部行:f(x) = (1 - x) * a + x * b,例如:f(1/3) = 4/3 = 1.3333
  • 在x方向上,底部行:g(x) = (1 - x) * c + x * d,例如:g(1/3) = 10/3 = 3.3333
  • 在y方向上,插值列:h(y) = (1 - y) * f(x) + y * g(x),例如:h(2/3) = 8/3 = 2.6667
看最后一个方程,我们也可以将f(x)和g(x)代入其中,得到: 这就是你得到的结果。
在第二个例子中,点略有不同,因为你从每个方向的4个点转换为6个点。
old: 0         1         2         3 (sample grid)
     |         |         |         |
     +-----+---+-+-----+-+---+-----+
     |     |     |     |     |     |
new: 0    3/5   6/5   9/5  12/5    3 (interpolation grid)

这是针对你第二个例子中x和y方向有效的。要使用上述公式,您必须将每个正方形映射到[0,1] x [0,1]。这是理论。Octave在内部使用interp2进行双线性插值。要使用interp2,您需要指定包含样本的矩阵和定义插值点的网格:
A = [1, 2;
     3, 4];
xi = linspace(1, size(A, 2), 4);
yi = linspace(1, size(A, 1), 4)';
B = interp2(A, xi, yi)

这给出了你得到的结果,但是它们是错误的

像素插值

如上所述,双线性插值的基础仍然有效,但插值网格是错误的。这是因为图像不是由样本点组成的,而是由像素组成的。像素是由其平均值表示的区域。因此,实际上图像的像素看起来像这样:

    0.5        1        1.5        2        2.5
0.5  +-------------------+-------------------+
     |                   |                   |
     |                   |                   |
     |                   |                   |
 1   |         o         |         o         |
     |                   |                   |
     |                   |                   |
     |                   |                   |
1.5  +-------------------+-------------------+
     |                   |                   |
     |                   |                   |
     |                   |                   |
 2   |         o         |         o         |
     |                   |                   |
     |                   |                   |
     |                   |                   |
2.5  +-------------------+-------------------+

因此,左上角像素的区域为[0.5, 1.5] x [0.5, 1.5],其中心位于(1, 1)。通过放大2倍,您想要的是以下新像素(在旧网格的坐标空间中,因为图像仍覆盖相同的区域):
    0.5  0.75  1   1.25 1.5  1.75  2   2.25 2.5
0.5  +---------+---------+---------+---------+
     |         |         |         |         |
0.75 |    x    |    x    |    x    |    x    |
     |         |         |         |         |
 1   +---------o---------+---------o---------+
     |         |         |         |         |
1.25 |    x    |    x    |    x    |    x    |
     |         |         |         |         |
1.5  +---------+---------+---------+---------+
     |         |         |         |         |
1.75 |    x    |    x    |    x    |    x    |
     |         |         |         |         |
 2   +---------o---------+---------o---------+
     |         |         |         |         |
2.25 |    x    |    x    |    x    |    x    |
     |         |         |         |         |
2.5  +---------+---------+---------+---------+

现在您将新中心x作为插值网格,旧中心o作为样本网格。您会发现,新的边界像素实际上需要外推。我们假设它外推常量,因此我们可以填充数组以再次进行插值,或限制插值网格。使用interp2的代码如下:
A = [1, 2;
     3, 4];
xi = linspace(0.75, 2.25, 4);
yi = linspace(0.75, 2.25, 4)';
xi(xi < 1) = 1; xi(xi > 2) = 2;
yi(yi < 1) = 1; yi(yi > 2) = 2;
B = interp2(A, xi, yi)

这里是一个更一般的解决方案(仅适用于整数输出大小),受到下面Amro评论他的答案的启发。如果允许缩放因子导致浮点输出大小。
在非整数输出大小上,新像素的数量将使最后一个像素重叠。例如,使用5/4 = 1.25的缩放因子,像素大小将为1 /(5/4)= 4/5 = 0.8。因此,将1.25倍缩放2x2图像会产生3x3图像。旧像素中心(采样网格)位于1和2处,而新像素中心(插值网格)位于0.9、1.7和2.5处。
    0.5                 1.5                 2.5
     |         1         |         2         |
old: +---------o---------+---------o---------+
new: +-------x-------+-------x-------+-------x-------+
     |      0.9      |      1.7      |      2.5      |
    0.5             1.3             2.1             2.9

这里有一些代码来展示这个问题:
img = [1, 2;
       3, 4];

% make interpolation grid
scale = 1.25
pixel_size = 1 / scale
out_size = ceil(size(img) / pixel_size)
xi = 0.5 + pixel_size / 2 + (0:out_size(1)-1) / scale
yi = 0.5 + pixel_size / 2 + (0:out_size(2)-1) / scale

% limit interpolation grid to sample grid bounds
xi(xi < 1) = 1;   xi(xi > size(img, 2)) = size(img, 2)
yi(yi < 1) = 1;   yi(yi > size(img, 1)) = size(img, 1)

% interpolate
scaled_interp = interp2(img, xi, yi', 'linear')

% Octave's imresize does not support anti-aliasing yet
scaled_resize_octave = imresize(img, scale, 'bilinear')
% Matlab's imresize uses anti-aliasing for downscaling, switch off to keep is simple
scaled_resize_matlab = imresize(img, scale, 'bilinear', 'Antialiasing', false)

% yields:
%    1.0000    1.7000    2.0000
%    2.4000    3.1000    3.4000
%    3.0000    3.7000    4.0000

这就是使用双线性插值调整大小的全部内容。对于双三次插值,Matlab使用对称填充和一种卷积算法,该算法加权4x4邻域。Octave的行为不同(即将到来的补丁)。

谢谢您抽出时间分享这个非常有用的内容(每当我忘记所有这些细节时,我都会参考它)!阅读“样本插值”部分和最后一部分让我想起了一个之前的问题,在那里我们看了双三次插值以及MATLAB和OpenCV之间的实现差异(在那种情况下,它是常数a=-0.5a=-0.75之间的差异,并且看到上面的错误报告似乎Octave也选择了不同的常数值)。 - Amro
@Amro 我看到了这个问题,甚至有类似的问题,人们认为Matlab使用a=-1,但是a=-0.5才是正确的。Octave根本不使用卷积双三次插值算法。因此,没有a可以确定Octave方法。该补丁目前仅适用于imresizeimrotate。也许可以将更改转移到interp2,但我想还没有错误报告。 - John

1

正如您所看到的,在您的示例中,每个角点都是您原始输入值之一。

中间值通过每个方向上的线性插值派生。例如,要计算b(3,2)

  • b(1,2) 是在 b(1,1)b(1,4) 之间的三分之一处。因此:

    b(1,2) = (1/3)*b(1,4) + (2/3)*b(1,1)
    
  • b(4,2) 是在 b(4,1)b(4,4) 之间的三分之一处。因此:

    b(4,2) = (1/3)*b(4,4) + (2/3)*b(4,1)
    
  • b(3,2) 是在 b(1,2)b(4,2) 之间的三分之二处。因此:

    b(3,2) = (2/3)*b(4,2) + (1/3)*b(1,2)
    

@OliCharlesworth - 编辑了原始帖子,添加了第二个示例,该示例通过分数缩放因子重新采样。在这种情况下,不清楚它如何计算其输出。请您检查上述内容。 - goldenmean
@Oli - 你对我在原帖中添加的第二个例子有什么意见吗? - goldenmean
@Oli - 在你修改的答案中,我觉得b(4,2)和b(3,2)的计算有误(权重颠倒了)。不是这样吗? - goldenmean
@ goldenmean:是的,很好的发现,我把最后一个计算搞反了! - Oliver Charlesworth
@Oli - 你对我在帖子中添加的第二个示例有什么意见吗? - goldenmean
显示剩余2条评论

1

我为Java适配了MATLAB的imresize函数:

import java.util.ArrayList;
import java.util.List;

public class MatlabResize {
    private static final double TRIANGLE_KERNEL_WIDTH = 2;

    public static double[][] resizeMatlab(double[][] data, int out_y, int out_x) {
        double scale_x = ((double)out_x)/data[0].length;
        double scale_y = ((double)out_y)/data.length;

        double[][][] weights_indizes = contribution(data.length, out_y, scale_y, TRIANGLE_KERNEL_WIDTH);
        double[][] weights = weights_indizes[0];
        double[][] indices = weights_indizes[1];

        final double[][] result = new double[out_y][data[0].length];
        double value = 0;

        for (int p=0; p<result[0].length; p++) {
            for (int i=0; i<weights.length; i++) {
                value = 0;

                for (int j=0; j<indices[0].length; j++) {
                    value += weights[i][j] * data[(int)indices[i][j]][p];
                }

                result[i][p] = value;
            }
        }

        weights_indizes = contribution(data[0].length, out_x, scale_x, TRIANGLE_KERNEL_WIDTH);
        weights = weights_indizes[0];
        indices = weights_indizes[1];

        final double[][] result2 = new double[result.length][out_x];
        for (int p=0; p<result.length; p++) {
            for (int i=0; i<weights.length; i++) {
                value = 0;

                for (int j=0; j<indices[0].length; j++) {
                    value += weights[i][j] * result[p][(int)indices[i][j]];
                }

                result2[p][i] = value;
            }
        }

        return result2;
    }

    public static double[][] resizeMatlab(double[][] data, double scale) {
        int out_x = (int)Math.ceil(data[0].length * scale);
        int out_y = (int)Math.ceil(data.length * scale);

        double[][][] weights_indizes = contribution(data.length, out_y, scale, TRIANGLE_KERNEL_WIDTH);
        double[][] weights = weights_indizes[0];
        double[][] indices = weights_indizes[1];

        final double[][] result = new double[out_y][data[0].length];
        double value = 0;

        for (int p=0; p<result[0].length; p++) {
            for (int i=0; i<weights.length; i++) {
                value = 0;

                for (int j=0; j<indices[0].length; j++) {
                    value += weights[i][j] * data[(int)indices[i][j]][p];
                }

                result[i][p] = value;
            }
        }

        weights_indizes = contribution(data[0].length, out_x, scale, TRIANGLE_KERNEL_WIDTH);
        weights = weights_indizes[0];
        indices = weights_indizes[1];

        final double[][] result2 = new double[result.length][out_x];
        for (int p=0; p<result.length; p++) {
            for (int i=0; i<weights.length; i++) {
                value = 0;

                for (int j=0; j<indices[0].length; j++) {
                    value += weights[i][j] * result[p][(int)indices[i][j]];
                }

                result2[p][i] = value;
            }
        }

        return result2;
    }


    private static double[][][] contribution(int length, int output_size, double scale, double kernel_width) {
        if (scale < 1.0) {
            kernel_width = kernel_width/scale;
        }

        final double[] x = new double[output_size];
        for (int i=0; i<x.length; i++) {
            x[i] = i+1;
        }

        final double[] u = new double[output_size];
        for (int i=0; i<u.length; i++) {
            u[i] = x[i]/scale + 0.5*(1 - 1/scale);
        }

        final double[] left = new double[output_size];
        for (int i=0; i<left.length; i++) {
            left[i] = Math.floor(u[i] - kernel_width/2);
        }

        int P = (int)Math.ceil(kernel_width) + 2;

        final double[][] indices = new double[left.length][P];
        for (int i=0; i<left.length; i++) {
            for (int j=0; j<=P-1; j++) {
                indices[i][j] = left[i] + j;
            }
        }

        double[][] weights = new double[u.length][indices[0].length];
        for (int i=0; i<u.length; i++) {
            for (int j=0; j<indices[i].length; j++) {
                weights[i][j] = u[i] - indices[i][j];
            }
        }

        if (scale < 1.0) {
            weights = triangleAntiAliasing(weights, scale);
        } else {
            weights = triangle(weights);
        }

        double[] sum = Matlab.sum(weights, 2);
        for (int i=0; i<weights.length; i++) {
            for (int j=0; j<weights[i].length; j++) {
                weights[i][j] = weights[i][j] / sum[i];
            }
        }

        for (int i=0; i<indices.length; i++) {
            for (int j=0; j<indices[i].length; j++) {
                indices[i][j] = Math.min(Math.max(indices[i][j], 1.0), length);
            }
        }

        sum = Matlab.sum(weights, 1);
        int a = 0;

        final List<Integer> list = new ArrayList<Integer>();
        for (int i=0; i<sum.length; i++) {
            if (sum[i] != 0.0) {
                a++;
                list.add(i);
            }
        }

        final double[][][] result = new double[2][weights.length][a];
        for (int i=0; i<weights.length; i++) {
            for (int j=0; j<list.size(); j++) {
                result[0][i][j] = weights[i][list.get(j)];
            }
        }
        for (int i=0; i<indices.length; i++) {
            for (int j=0; j<list.size(); j++) {
                result[1][i][j] = indices[i][list.get(j)]-1; //java indices start by 0 and not by 1
            }
        }

        return result;
    }

    private static double[][] triangle(final double[][] x) {
        for (int i=0; i<x.length; i++) {
            for (int j=0; j<x[i].length; j++) {
                if (-1.0 <= x[i][j] && x[i][j] < 0.0) {
                    x[i][j] = x[i][j] + 1;
                } else if (0.0 <= x[i][j] &&  x[i][j] < 1.0) {
                    x[i][j] = 1 - x[i][j];
                } else {
                    x[i][j] = 0;
                }
            }
        }

        return x;
    }

    private static double[][] triangleAntiAliasing(final double[][] x, final double scale) {
        for (int i=0; i<x.length; i++) {
            for (int j=0; j<x[i].length; j++) {
                x[i][j] = x[i][j] * scale;
            }
        }

        for (int i=0; i<x.length; i++) {
            for (int j=0; j<x[i].length; j++) {
                if (-1.0 <= x[i][j] && x[i][j] < 0.0) {
                    x[i][j] = x[i][j] + 1;
                } else if (0.0 <= x[i][j] &&  x[i][j] < 1.0) {
                    x[i][j] = 1 - x[i][j];
                } else {
                    x[i][j] = 0;
                }
            }
        }

        for (int i=0; i<x.length; i++) {
            for (int j=0; j<x[i].length; j++) {
                x[i][j] = x[i][j] * scale;
            }
        }

        return x;
    }
}

所以你是在说你的代码等同于MATLAB的imresize代码吗?你是否以某种方式反向工程了Amro提到的MEX编译函数?还是你仅仅实现了类似的功能,而没有验证它在不同的边角情况下产生相同的结果? - Jonas Heidelberg
2
嗯...等价的.. :) 我尝试理解MATLAB中的“imresize”函数并将其移植到JAVA。(此函数的源代码是可读的)使用该函数,我获得了(将600x600矩阵调整大小为192x192时)与MATLAB中相同的结果。但我还没有用单元测试来测试它。 - lhlmgr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接