10

使用双线性插值将 2x2 矩阵放大到 5x5 的示例程序。对于这种简单情况,OpenCV 产生的结果在边界处有伪影。

gy, gx = np.mgrid[0:2, 0:2]
gx = np.float32(gx)
print(gx)
res = cv2.resize(gx,(5,5), fx=0, fy=0, interpolation=cv2.INTER_LINEAR)
print(res)

输出:

[[ 0.  1.]
 [ 0.  1.]]

[[ 0.          0.1         0.5         0.89999998  1.        ]
 [ 0.          0.1         0.5         0.89999998  1.        ]
 [ 0.          0.1         0.5         0.89999998  1.        ]
 [ 0.          0.1         0.5         0.89999998  1.        ]
 [ 0.          0.1         0.5         0.89999998  1.        ]]

预期输出:

  [[0 0.25 0.5 0.75 1
    0 0.25 0.5 0.75 1
    0 0.25 0.5 0.75 1
    0 0.25 0.5 0.75 1
    0 0.25 0.5 0.75 1]]

问题是什么?

4

2 回答 2

12

TL;博士

我使用其他图像处理库(scikit-image、Pillow 和 Matlab)进行了测试,但它们都没有返回预期的结果。

奇怪的是,这种行为是由于执行双线性插值以获得有效结果的方法或某种约定而不是我认为的错误。

我已经发布了一个示例代码,以使用双线性插值执行图像大小调整(当然,检查是否一切正常,我不确定如何正确处理图像索引......)输出预期结果。


问题的部分答案。

其他一些图像处理库的输出是什么?

scikit 图像

Python 模块scikit-image包含许多图像处理算法。这里skimage.transform.resize方法 ( skimage.__version__: 0.12.3) 的输出:

  • mode='constant'(默认)

代码:

import numpy as np
from skimage.transform import resize

image = np.array( [
                [0., 1.],
                [0., 1.]
    ] )
print 'image:\n', image

image_resized = resize(image, (5,5), order=1, mode='constant')
print 'image_resized:\n', image_resized

结果:

image:
[[ 0.  1.]
 [ 0.  1.]]
image_resized:
[[ 0.    0.07  0.35  0.63  0.49]
 [ 0.    0.1   0.5   0.9   0.7 ]
 [ 0.    0.1   0.5   0.9   0.7 ]
 [ 0.    0.1   0.5   0.9   0.7 ]
 [ 0.    0.07  0.35  0.63  0.49]]
  • mode='edge'

结果:

image:
[[ 0.  1.]
 [ 0.  1.]]
image_resized:
[[ 0.   0.1  0.5  0.9  1. ]
 [ 0.   0.1  0.5  0.9  1. ]
 [ 0.   0.1  0.5  0.9  1. ]
 [ 0.   0.1  0.5  0.9  1. ]
 [ 0.   0.1  0.5  0.9  1. ]]
  • mode='symmetric'

结果:

image:
[[ 0.  1.]
 [ 0.  1.]]
image_resized:
[[ 0.   0.1  0.5  0.9  1. ]
 [ 0.   0.1  0.5  0.9  1. ]
 [ 0.   0.1  0.5  0.9  1. ]
 [ 0.   0.1  0.5  0.9  1. ]
 [ 0.   0.1  0.5  0.9  1. ]]
  • mode='reflect'

结果:

image:
[[ 0.  1.]
 [ 0.  1.]]
image_resized:
[[ 0.3  0.1  0.5  0.9  0.7]
 [ 0.3  0.1  0.5  0.9  0.7]
 [ 0.3  0.1  0.5  0.9  0.7]
 [ 0.3  0.1  0.5  0.9  0.7]
 [ 0.3  0.1  0.5  0.9  0.7]]
  • mode='wrap'

结果:

image:
[[ 0.  1.]
 [ 0.  1.]]
image_resized:
[[ 0.3  0.1  0.5  0.9  0.7]
 [ 0.3  0.1  0.5  0.9  0.7]
 [ 0.3  0.1  0.5  0.9  0.7]
 [ 0.3  0.1  0.5  0.9  0.7]
 [ 0.3  0.1  0.5  0.9  0.7]]

如您所见,默认调整大小模式 ( constant) 产生不同的输出,但边缘模式返回与 OpenCV 相同的结果。调整大小模式都不会产生预期的结果。

有关插值的更多信息:边缘模式

这张图片总结了我们案例中的所有结果:

边缘模式

枕头

枕头

是 Alex Clark 和 Contributors 的友好 PIL 分叉。PIL 是 Fredrik Lundh 和贡献者的 Python 图像库。

PIL.Image.Image.resizePIL.__version__: 4.0.0)呢?

代码:

import numpy as np
from PIL import Image

image = np.array( [
                [0., 1.],
                [0., 1.]
    ] )
print 'image:\n', image

image_pil = Image.fromarray(image)
image_resized_pil = image_pil.resize((5,5), resample=Image.BILINEAR)
print 'image_resized_pil:\n', np.asarray(image_resized_pil, dtype=np.float)

结果:

image:
[[ 0.  1.]
 [ 0.  1.]]
image_resized_pil:
[[ 0.          0.1         0.5         0.89999998  1.        ]
 [ 0.          0.1         0.5         0.89999998  1.        ]
 [ 0.          0.1         0.5         0.89999998  1.        ]
 [ 0.          0.1         0.5         0.89999998  1.        ]
 [ 0.          0.1         0.5         0.89999998  1.        ]]

Pillow图像大小调整与 OpenCV 库的输出相匹配。

MATLAB

Matlab 提出了一个名为Image Processing Toolbox的工具箱。此工具箱中的功能imresize允许调整图像大小。

代码:

image = zeros(2,1,'double');
image(1,2) = 1;
image(2,2) = 1;
image
image_resize = imresize(image, [5 5], 'bilinear')

结果:

image =

     0     1
     0     1


image_resize =

         0    0.1000    0.5000    0.9000    1.0000
         0    0.1000    0.5000    0.9000    1.0000
         0    0.1000    0.5000    0.9000    1.0000
         0    0.1000    0.5000    0.9000    1.0000
         0    0.1000    0.5000    0.9000    1.0000

同样,这不是 Matlab 的预期输出,而是与前面两个示例相同的结果。

自定义双线性图像大小调整方法

基本原则

有关更完整的信息,请参阅此关于双线性插值的Wikipedia 文章。

该图应该基本上说明了从2x2图像放大到4x4图像时会发生什么:

升级

通过最近邻插值,目标像素(0,0)将获得源像素的值以及(0,0)和 的像素。(0,1)(1,0)(1,1)

使用双线性插值,目标像素(0,0)将获得一个值,该值是源图像中 4 个相邻像素的线性组合:

维基百科:BilinearInterpolation.svg.png

四个红点显示数据点,绿点是我们想要插值的点。

R1计算为:R1 = ((x2 – x)/(x2 – x1))*Q11 + ((x – x1)/(x2 – x1))*Q21

R2计算为:R2 = ((x2 – x)/(x2 – x1))*Q12 + ((x – x1)/(x2 – x1))*Q22

最后,P计算为 和 的加权R1平均值R2P = ((y2 – y)/(y2 – y1))*R1 + ((y – y1)/(y2 – y1))*R2

使用之间归一化的坐标[0, 1]可以简化公式

C++ 实现

这篇博文(使用双三次插值调整图像大小)包含使用双线性插值执行图像大小调整的 C++ 代码。

这是我自己对要使用的代码的改编(与原始代码相比,对索引进行了一些修改,不确定是否正确cv::Mat

#include <iostream>
#include <opencv2/core.hpp>

float lerp(const float A, const float B, const float t) {
  return A * (1.0f - t) + B * t;
}

template <typename Type>
Type resizeBilinear(const cv::Mat &src, const float u, const float v, const float xFrac, const float yFrac) {
  int u0 = (int) u;
  int v0 = (int) v;

  int u1 = (std::min)(src.cols-1, (int) u+1);
  int v1 = v0;

  int u2 = u0;
  int v2 = (std::min)(src.rows-1, (int) v+1);

  int u3 = (std::min)(src.cols-1, (int) u+1);
  int v3 = (std::min)(src.rows-1, (int) v+1);

  float col0 = lerp(src.at<Type>(v0, u0), src.at<Type>(v1, u1), xFrac);
  float col1 = lerp(src.at<Type>(v2, u2), src.at<Type>(v3, u3), xFrac);
  float value = lerp(col0, col1, yFrac);

  return cv::saturate_cast<Type>(value);
}

template <typename Type>
void resize(const cv::Mat &src, cv::Mat &dst) {
  float scaleY = (src.rows - 1) / (float) (dst.rows - 1);
  float scaleX = (src.cols - 1) / (float) (dst.cols - 1);

  for (int i = 0; i < dst.rows; i++) {
    float v = i * scaleY;
    float yFrac = v - (int) v;

    for (int j = 0; j < dst.cols; j++) {
      float u = j * scaleX;
      float xFrac = u - (int) u;

      dst.at<Type>(i, j) = resizeBilinear<Type>(src, u, v, xFrac, yFrac);
    }
  }
}

void resize(const cv::Mat &src, cv::Mat &dst, const int width, const int height) {
  if (width < 2 || height < 2 || src.cols < 2 || src.rows < 2) {
    std::cerr << "Too small!" << std::endl;
    return;
  }

  dst = cv::Mat::zeros(height, width, src.type());

  switch (src.type()) {
    case CV_8U:
      resize<uchar>(src, dst);
      break;

    case CV_64F:
      resize<double>(src, dst);
      break;

    default:
      std::cerr << "Src type is not supported!" << std::endl;
      break;
  }
}

int main() {
  cv::Mat img = (cv::Mat_<double>(2,2) << 0, 1, 0, 1);
  std::cout << "img:\n" << img << std::endl;
  cv::Mat img_resize;
  resize(img, img_resize, 5, 5);
  std::cout << "img_resize=\n" << img_resize << std::endl;

  return EXIT_SUCCESS;
}

它产生:

img:
[0, 1;
 0, 1]
img_resize=
[0, 0.25, 0.5, 0.75, 1;
 0, 0.25, 0.5, 0.75, 1;
 0, 0.25, 0.5, 0.75, 1;
 0, 0.25, 0.5, 0.75, 1;
 0, 0.25, 0.5, 0.75, 1]

结论

在我看来,OpenCVresize()函数不太可能是错误的,因为我可以测试的其他图像处理库都没有产生预期的输出,而且可以产生具有良好参数的相同 OpenCV 输出。

我针对两个 Python 模块(scikit-image 和 Pillow)进行了测试,因为它们易于使用且面向图像处理。我还能够使用 Matlab 及其图像处理工具箱进行测试。

用于调整图像大小的双线性插值的粗略自定义实现会产生预期的结果。对我来说,有两种可能性可以解释这种行为:

  • 差异是这些图像处理库使用的方法所固有的,而不是错误(也许他们使用一种方法来有效地调整图像大小,与严格的双线性实现相比有一些损失?)?
  • 以某种方式正确插入排除边界是一种约定吗?

这些库是开源的,人们可以探索它们的源代码以了解差异的来源。

链接的答案表明插值仅在两个原始蓝点之间起作用,但我无法解释为什么会出现这种行为。

为什么会有这个答案?

这个答案,即使它部分回答了 OP 问题,也是我总结我发现的关于这个主题的几件事的好方法。我相信它也可以以某种方式帮助其他可能发现这一点的人。

于 2017-04-30T00:23:02.117 回答
0

正如我将在下面解释的,输出:

[[ 0.          0.1         0.5         0.9         1.        ]
 [ 0.          0.1         0.5         0.9         1.        ]
 [ 0.          0.1         0.5         0.9         1.        ]
 [ 0.          0.1         0.5         0.9         1.        ]
 [ 0.          0.1         0.5         0.9         1.        ]]

将是正确的解决方案。因此,虽然opencv舍入误差很小,但大部分是正确的。

原因:您的输入图像不假定图像角落中的值为“0”和“1”的图像,而是像素中心的图像。

所以这是您的 2x2 图像的错误模型:

在此处输入图像描述

相反,您的图像看起来像这样,在红点中定义了“颜色”。左边两个像素中心左边的一切都是白色,右边两个像素中心右边的一切都是黑色,像素中心之间的值被插值:

在此处输入图像描述

将图像转换为 5x5 像素:

在此处输入图像描述

并查看像素的中心,您会看到如何得到“0.1”和“0.9”而不是“0.25”和“0.75”

于 2021-11-22T15:56:32.657 回答