NumPy调整/缩放图像大小

194

我想在将图像转换为numpy数组的同时改变图像的比例。

例如,我有这个可乐瓶的图像:bottle-1

它被转换为一个形状为(528, 203, 3)的numpy数组,我想将它缩放到与第二个图像相同的尺寸: bottle-2

该图像的形状为(140, 54, 3)

如何在保持原始图像的同时更改图像的大小到特定形状?其它答案建议从中删除每隔一行或第三行,但我想做的基本上是缩小图像,就像通过图像编辑器一样,只是用Python代码实现。是否有任何库可以在numpy / SciPy中完成此操作?


你能展示一下你的NumPy数组的代码吗? - ShpielMeister
2
@sascha 根据您提供的页面,此方法已被弃用。 - Paul Panzer
1
@ShpielMeister 我无法让IntelliJ完整地打印出numpy数组,因为当输出很大时,它总是会加上省略号...,所以我只能在控制台中看到部分数组输出。 - Brian Hamill
10个回答

215

是的,你可以安装opencv(这是一个用于图像处理和计算机视觉的库),并使用cv2.resize函数。例如使用:

import cv2
import numpy as np

img = cv2.imread('your_image.jpg')
res = <b>cv2.resize(img, dsize=(54, 140), interpolation=cv2.INTER_CUBIC)</b>

在这里img是一个包含原始图像的numpy数组,而res是一个包含调整大小后图像的numpy数组。一个重要的方面是interpolation参数:有几种方法可以调整图像大小,特别是当你缩小图像并且原始图像的尺寸不是调整后图像尺寸的倍数时。可能的插值模式有:

  • INTER_NEAREST - 最近邻插值
  • INTER_LINEAR - 双线性插值(默认使用)
  • INTER_AREA - 使用像素区域关系重新采样。这可能是图像缩减的首选方法,因为它提供了无莫尔纹的结果。但是当图像被缩放时,它类似于INTER_NEAREST方法。
  • INTER_CUBIC - 在4x4像素邻域上进行双三次插值
  • INTER_LANCZOS4 - 在8x8像素邻域上进行Lanczos插值

与大多数选项一样,没有“最佳”选项,因为对于每个调整大小方案,都有一些情况下可以优先选择一种策略。


5
我刚试了这段代码,它可以工作!只需要更改一个地方,即dsize应该为dsize=(54, 140),因为它按照x和y的顺序进行调整大小,而numpy数组的形状是y然后x(y是行数,x是列数)。 - Brian Hamill
12
我尽量避免使用cv2,因为它会交换图像的尺寸并按BGR通道格式加载。我更喜欢使用skimage.io.imread('image.jpg')skimage.transform.resize(img)。https://scikit-image.org/docs/dev/install.html - Eduardo Pignatelli
2
@EduardoPignatelli 我避免使用skimage.transform.resize,因为你无法控制它使用的插值算法。但是,这可能并不重要,这取决于人们的用例。 - Decker
4
skimage.transform.resize 通过 'order' 参数提供了一些控制。其中 order=0 表示最近邻插值,1 表示双线性插值,2 表示双二次插值,3 表示双三次插值等等。然而,它不支持区域平均法和 lanczos 插值。 - Tapio
1
@TapioFriberg 啊,是的,我改正了。我看到skimage.transform.warp的“order”参数下定义的算法文档。在某些时候,更新文档以包括类型“Bi-quartic”等引用可能会有所帮助,例如,在文档中没有其他地方定义(截至2019年12月10日)-一行代码对未来用户可能是有益的。 - Decker
显示剩余2条评论

127

虽然可能可以仅使用numpy来完成此操作,但该操作不是内置的。话虽如此,您可以使用基于numpy构建的scikit-image来进行这种类型的图像处理。

Scikit-Image缩放文档在此处

例如,您可以对图像执行以下操作:

from skimage.transform import resize
bottle_resized = resize(bottle, (140, 54))

这将为您处理插值、抗锯齿等问题。


2
谢谢!这个答案也可以用!虽然我在使用 anti_aliasing 标志时遇到了一些问题,但看起来它已经从最新版本的 0.13.1 中被移除了。 - Brian Hamill
22
即使您的原始图像是uint8类型,此函数将返回浮点数形式的ndarray图像。 - sziraqui
5
这是一种很棒的技巧,因为它适用于任何数量的通道。我尝试将RGB数据与深度点云数据结合使用,结果保留了我想要的关系。 - Darth Egregious
@DarthEgregious,jakevdp -> 当我按照你所描述的方法将(137,236,3)数组调整为(64,64)大小时,我的随机噪声数据变成了单色。这是正常的吗?因为它看起来好像失去了所有信息。 - Deshwal
1
应该是(64,64,3),对吧? - Darth Egregious
1
@sziraqui preserve_range=True 可以保留范围 skimage.transform.resize(..., , preserve_range=True) - Safi

35

一行numpy代码实现下采样(降采样)(2倍):

smaller_img = bigger_img[::2, ::2]

并且进行上采样(增加采样率)(倍数为2):

bigger_img = smaller_img.repeat(2, axis=0).repeat(2, axis=1)

(假设图片大小为HxWxC。请注意,此方法仅允许整数倍缩放(例如2倍,但不支持1.5倍))


2
在skimage中,当你真的不需要/想要发生在幕后的转换为float64时,有很好的解决方案来操作分割标签掩模。 - Patrice Carbonneau
作为重复的替代方案,您也可以使用 np.kron(..., np.ones((2,2,1)))(对于 HxWxC 图像),但我不确定哪个更快。 - flawr
简单就是天才 - Brett Young
谢谢!如果你想避免使用另一个库,这是一个黄金解决方案! - phi

34

如果您从Google来到这里,寻找一种在numpy数组中快速降采样图像以用于机器学习应用的方法,这里有一个超级快的方法(改编自这里)。该方法仅在输入维度为输出维度的倍数时有效。

以下示例从128x128降采样到64x64(这可以轻松更改)。

通道最后排序

# large image is shape (128, 128, 3)
# small image is shape (64, 64, 3)
input_size = 128
output_size = 64
bin_size = input_size // output_size
small_image = large_image.reshape((output_size, bin_size, 
                                   output_size, bin_size, 3)).max(3).max(1)

通道优先排序

# large image is shape (3, 128, 128)
# small image is shape (3, 64, 64)
input_size = 128
output_size = 64
bin_size = input_size // output_size
small_image = large_image.reshape((3, output_size, bin_size, 
                                      output_size, bin_size)).max(4).max(2)

对于灰度图像,只需将3更改为1,如下所示:

通道优先排序

# large image is shape (1, 128, 128)
# small image is shape (1, 64, 64)
input_size = 128
output_size = 64
bin_size = input_size // output_size
small_image = large_image.reshape((1, output_size, bin_size,
                                      output_size, bin_size)).max(4).max(2)

这种方法使用了等效的最大池化技术,这是我发现的最快速的做法。


7
large_image[:, ::2, ::2] 返回分辨率减半的图像。 - Tronic
3
@LasseKärkkäinen 但它并没有下采样,它只是选择每个像素中的另一个。不同之处在于最终函数“max”可以更改为以稍微更好的方式选择或计算像素(例如使用“min”或“mean”)。如果这不重要,那么您的方法很有用(而且更快)。 - Waylon Flinn
@L.Kärkkäinen,这个的相反是什么,是将分辨率转换为双倍吗? - rayzinnz
3
@rayzinnz np.repeat(np.repeat(a, 2, axis=0), 2, axis=1) - Tronic
2
.max(4).max(2)替换为.mean(4).mean(2)是否可以作为一种快速的线性插值下采样方法? - HockeyStick
@HockeyStick 应该可以,但我记得在我的测试中它要慢得多。 - Waylon Flinn

21

如果有人在这里寻找一个简单的方法来缩放/调整Python中的图像大小,而不使用额外的库,那么这里有一个非常简单的图像大小调整函数:

#simple image scaling to (nR x nC) size
def scale(im, nR, nC):
  nR0 = len(im)     # source number of rows 
  nC0 = len(im[0])  # source number of columns 
  return [[ im[int(nR0 * r / nR)][int(nC0 * c / nC)]  
             for c in range(nC)] for r in range(nR)]

示例用法:将 (30 x 30) 的图像调整大小为 (100 x 200):

import matplotlib.pyplot as plt

def sqr(x):
  return x*x

def f(r, c, nR, nC):
  return 1.0 if sqr(c - nC/2) + sqr(r - nR/2) < sqr(nC/4) else 0.0

# a red circle on a canvas of size (nR x nC)
def circ(nR, nC):
  return [[ [f(r, c, nR, nC), 0, 0] 
             for c in range(nC)] for r in range(nR)]

plt.imshow(scale(circ(30, 30), 100, 200))

输出:scaled image

这可以用来缩小/缩放图像,并且在numpy数组中运行良好。


我认为嵌套的列表推导式会影响可读性。 - Porter Child

5

如果想要批量改变(numpy)数组的大小(插值),pytorch提供了一个更快的函数名为torch.nn.functional.interpolate,只需记得先使用np.transpose将通道从batchxWxHx3更改为batchx3xWxH。


4

几年后我又回到这里,发现目前的答案可以归为以下几类:

  1. 使用外部库(OpenCV、SciPy等)
  2. 使用二次幂缩放
  3. 使用最近邻插值

这些解决方案都是可行的,我提供以下内容只是为了完整性。它比以上三种方法有三个优点:(1) 可以接受任意分辨率,甚至非二次幂的缩放因子;(2) 它仅使用纯Python+Numpy,没有使用外部库;(3) 它对所有像素进行插值处理,获得更美观的结果。

它并没有很好地利用Numpy,因此速度不快,特别是对于大图像来说。如果你只需要对较小的图像进行重新缩放,则应该可以接受。我根据用户的自由选择提供Apache或MIT许可证。

import math
import numpy

def resize_linear(image_matrix, new_height:int, new_width:int):
    """Perform a pure-numpy linear-resampled resize of an image."""
    output_image = numpy.zeros((new_height, new_width), dtype=image_matrix.dtype)
    original_height, original_width = image_matrix.shape
    inv_scale_factor_y = original_height/new_height
    inv_scale_factor_x = original_width/new_width

    # This is an ugly serial operation.
    for new_y in range(new_height):
        for new_x in range(new_width):
            # If you had a color image, you could repeat this with all channels here.
            # Find sub-pixels data:
            old_x = new_x * inv_scale_factor_x
            old_y = new_y * inv_scale_factor_y
            x_fraction = old_x - math.floor(old_x)
            y_fraction = old_y - math.floor(old_y)

            # Sample four neighboring pixels:
            left_upper = image_matrix[math.floor(old_y), math.floor(old_x)]
            right_upper = image_matrix[math.floor(old_y), min(image_matrix.shape[1] - 1, math.ceil(old_x))]
            left_lower = image_matrix[min(image_matrix.shape[0] - 1, math.ceil(old_y)), math.floor(old_x)]
            right_lower = image_matrix[min(image_matrix.shape[0] - 1, math.ceil(old_y)), min(image_matrix.shape[1] - 1, math.ceil(old_x))]

            # Interpolate horizontally:
            blend_top = (right_upper * x_fraction) + (left_upper * (1.0 - x_fraction))
            blend_bottom = (right_lower * x_fraction) + (left_lower * (1.0 - x_fraction))
            # Interpolate vertically:
            final_blend = (blend_top * y_fraction) + (blend_bottom * (1.0 - y_fraction))
            output_image[new_y, new_x] = final_blend

    return output_image

示例重新缩放:

原始图像: 原始分辨率 1280x720

缩小一半: 半尺寸

放大1.25倍: 1.25倍尺寸


3

SciPy的imresize()方法是另一种调整大小的方法,但它将从SciPy v 1.3.0开始被移除。SciPy使用PIL图像调整大小方法:Image.resize(size, resample=0)

size - 请求的像素大小,作为2元组:(宽度,高度)。
resample - 可选的重采样滤波器。这可以是以下其中之一:PIL.Image.NEAREST(使用最近邻),PIL.Image.BILINEAR(线性插值),PIL.Image.BICUBIC(三次样条插值)或PIL.Image.LANCZOS(高质量降采样滤波器)。如果省略或图像具有模式“1”或“P”,则设置为PIL.Image.NEAREST。

链接在此处: https://pillow.readthedocs.io/en/3.1.x/reference/Image.html#PIL.Image.Image.resize


3
很不幸,imresize()已被弃用,在SciPy 1.3.0中将被移除。 - MiniQuark
1
这个问题明确说明了图像是一个numpy数组,你不能在其上使用Pillow。 - darda

1

有没有numpy / SciPy中的库可以做到这一点

当然可以。您可以不使用OpenCV,scikit-image或PIL来完成此操作。

图像调整基本上是将每个像素的坐标从原始图像映射到其调整后的位置。

由于图像的坐标必须是整数(将其视为矩阵),因此,如果映射的坐标具有小数值,则应对像素值进行插值以将其近似到整数位置(例如,获取最接近该位置的像素称为最近邻插值)。

您所需要的就是一个为您执行此插值的函数。SciPy有interpolate.interp2d

您可以使用它来调整numpy数组中的图像,例如arr

W, H = arr.shape[:2]
new_W, new_H = (600,300)
xrange = lambda x: np.linspace(0, 1, x)

f = interp2d(xrange(W), xrange(H), arr, kind="linear")
new_arr = f(xrange(new_W), xrange(new_H))

当然,如果您的图像是RGB格式,您需要为每个通道执行插值。
如果您想了解更多信息,我建议观看Resizing Images - Computerphile

可能会不起作用,基于这个答案:https://dev59.com/sVoU5IYBdhLWcg3wCz2X#37872172 - random_dsp_guy
个人而言,我总是不建议使用OpenCV... - Jiadong

-1
import cv2
import numpy as np

image_read = cv2.imread('filename.jpg',0) 
original_image = np.asarray(image_read)
width , height = 452,452
resize_image = np.zeros(shape=(width,height))

for W in range(width):
    for H in range(height):
        new_width = int( W * original_image.shape[0] / width )
        new_height = int( H * original_image.shape[1] / height )
        resize_image[W][H] = original_image[new_width][new_height]

print("Resized image size : " , resize_image.shape)

cv2.imshow(resize_image)
cv2.waitKey(0)

6
欢迎来到StackOverflow。很高兴你想通过回答别人的问题来帮助他们。然而,我看不出你的回答相比已经使用了cv2和适当的调整大小函数的现有答案有什么价值,而你重新实现了一个次优的调整大小函数,其表现不如最近邻插值。 - NOhs

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