OpenCV的resize()函数结果错误?

12

使用双线性插值将2x2矩阵升级到5x5的示例程序。 OpenCV生成的结果在这种简单情况下存在边界伪影。

样例程序将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]]

问题是什么?


1
我认为它类似于链接 - olegkhr
这看起来很有趣...找到解决方案了吗? - Jeru Luke
1
我已经修改了OpenCV以获得我想要的结果,避免了0.5像素偏移。 - olegkhr
这个回答解决了你的问题吗?使用OpenCV插值1维数组 - Cris Luengo
3个回答

14

简而言之

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

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

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


部分回答问题。

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

scikit-image

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]]
  • 模式='边缘'

结果:

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. ]]
  • 模式='反射'

结果:

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相同的结果。然而,没有一个调整大小模式产生预期的结果。
有关插值:边缘模式的更多信息。
这张图片总结了我们这种情况下的所有结果:

edge modes

枕头

枕头

是由Alex Clark和贡献者推出的友好的PIL分支。 PIL是Fredrik Lundh和贡献者开发的Python图像处理库。

PIL.Image.Image.resize怎么样(PIL.__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得到的期望输出,但与前两个示例结果相同。

自定义双线性图像缩放方法

基本原理

请参阅维基百科上关于双线性插值的更完整信息。

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

upscaling

使用最近邻插值,目标像素在(0,0)处将获得源像素在(0,0)处以及(0,1)、(1,0)和(1,1)处的像素值。
使用双线性插值,目标像素在(0,0)处将获得源图像中4个相邻像素的线性组合。

Wikipedia: 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

最后,PR1R2 的加权平均值:P = ((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]

结论

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

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

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

  • 这些图像处理库使用的方法固有差异而不是bug(也许它们使用一种方法以某种损失高效地调整图像大小,与严格的双线性实现相比?)
  • 这是一种合适地插值但排除边框的约定吗?

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

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

为什么选择这个答案?

这个回答虽然只部分回答了问题,但是对我总结这个话题所找到的少量资料非常有帮助。我相信它也可以帮助其他可能会遇到同样问题的人。

1
"Pillow 图像调整大小与 OpenCV 库的输出匹配。现在,Pillow 实现已经修复并且正确无误。" - homm

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.        ]
 [ 0.          0.1         0.5         0.9         1.        ]]

正确的解决方案是使用opencv。虽然opencv存在小的舍入误差,但大多数情况下都是正确的。

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

因此,这是您的2x2图像外观的错误模型:

enter image description here

相反,您的图像看起来像这样,并且“颜色”在红点中定义。左两个像素中心左侧的所有内容都是白色,右两个像素中心右侧的所有内容都是黑色,并且像素中心之间的值被插值:

enter image description here

将图像转换为5x5像素:

enter image description here

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


这很蠢,但这确实是OpenCV模型,正如文档中所解释的那样。我说这很蠢,因为如果左上角像素是(0,0),那么插值图像中的左上角像素具有一些不同的坐标。在我看来,至少在每个合理的模型中,像素坐标是像素重心的坐标,也就是你放置红点的位置。左上角像素的中心在OpenCV模型中发生了偏移。这就是为令许多人感到不直观的原因。 - Cris Luengo
我不同意,我认为这并不愚蠢,而是明智的做法(所有像Photoshop等的图像编辑器都在这样做)。它允许对图像进行缩放而不会“裁剪”,保持完整内容不变(而不是单个像素)。虽然可能不直观,但我同意这一点。 - Jan Rüegg

0
Jan提供了一个很好的答案,解释了cv::resize行为背后的原因,但我想补充说,还有其他opencv函数可以用来实现OP期望的结果。其中一种选择是使用cv::remap
import cv2 as cv
import numpy as np

a = np.array(
    [
        [0, 1],
        [0, 1],
    ],
    dtype=np.float32,
)

orig_height, orig_width = a.shape
scaled_width = 5
scaled_height = 5

x = np.linspace(0, orig_width - 1, scaled_width, dtype=np.float32)
y = np.linspace(0, orig_height - 1, scaled_height, dtype=np.float32)
map_x, map_y = np.meshgrid(x, y)

scaled = cv.remap(a, map_x, map_y, interpolation=cv.INTER_LINEAR)

另一个选项是使用 cv::warpAffine

M = np.array(
    [
        [(scaled_width - 1) / (orig_width - 1), 0, 0],
        [0, (scaled_height - 1) / (orig_height - 1), 0],
    ],
    dtype=np.float32,
)

scaled = cv.warpAffine(a, M, dsize=(scaled_width, scaled_height), flags=cv.INTER_LINEAR)

这两个函数对于 OP 的示例输入都产生了预期的输出:

scaled:
[[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.]],

然而,interpolation=cv2.INTER_LINEAR并不总是产生预期的结果(例如,当将示例输入缩放到大小(4, 4)时)。也许通过实现对cv2.INTER_LINEAR_EXACT的支持可以解决这个问题。

如果使用scipy是一种替代方案,那么有更多的选择:

from scipy.ndimage import zoom
scaled = zoom(a, scaled_width / orig_width, order=1, prefilter=False)

from scipy.ndimage import map_coordinates
scaled = map_coordinates(a, np.meshgrid(y, x, indexing="ij"), order=1)

from scipy.interpolate import RegularGridInterpolator
interp = RegularGridInterpolator(
    (np.arange(orig_height), np.arange(orig_width)),
    a,
    method="linear",
)
scaled = interp(tuple(np.meshgrid(y, x, indexing="ij")))

from scipy.interpolate import RectBivariateSpline
interp = RectBivariateSpline(
    range(orig_height),
    range(orig_width),
    a,
    bbox=[0, orig_height - 1, 0, orig_width - 1],
    kx=1,
    ky=1,
    s=0,
)
scaled = interp(y, x)

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