OpenCV resize() 结果错误?

OpenCV resize() result is wrong?

使用双线性插值将 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]]

有什么问题?

TL;DR

我测试了其他图像处理库(scikit-image、Pillow 和 Matlab),其中 none return 预期结果。

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

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


问题的部分答案。

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

scikit-图像

Python模块scikit-image contains lot of image processing algorithms. Here the outputs of the 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) 产生不同的输出,但边缘模式 return 产生与 OpenCV 相同的结果。 None 调整大小模式产生预期的结果。

有关 Interpolation: Edge Modes 的更多信息。

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

抱枕

Pillow

is the friendly PIL fork by Alex Clark and Contributors. PIL is the Python Imaging Library by Fredrik Lundh and Contributors.

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. The function 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 的预期输出,但与前面两个示例的结果相同。

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

基本原理

请参阅 Bilinear interpolation 上的这篇维基百科文章以获取更完整的信息。

该图应该基本上说明了从 2x2 图像放大到 4x4 图像时发生的情况:

使用最近邻插值,(0,0) 处的目标像素将获得 (0,0) 处的源像素值以及 (0,1)、[=39= 处的像素值] 和 (1,1).

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

The four red dots show the data points and the green dot is the point at which we want to interpolate.

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 计算为 R1R2 的加权平均值:P = ((y2 – y)/(y2 – y1))*R1 + ((y – y1)/(y2 – y1))*R2.

使用 [0, 1] 之间归一化的坐标简化了 formula.

C++ 实现

此博客 post (Resizing Images With Bicubic Interpolation) 包含使用双线性插值执行图像大小调整的 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]

结论

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

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

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

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

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

linked answer 显示插值仅在两个原始蓝点之间起作用,但我无法解释为什么会出现这种情况。

为什么这个答案?

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

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

[[ 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”