Scipy旋转和缩放图像而不改变其尺寸

17

对于我的神经网络,我想通过为图像添加小的随机旋转和缩放来增强训练数据。但是问题在于当scipy应用旋转和缩放时,它会改变图像的大小。我需要仅在图像超出边界时剪切边缘部分,以确保所有图像都具有相同的大小。

def loadImageData(img, distort = False):
    c, fn = img
    img = scipy.ndimage.imread(fn, True)

    if distort:
        img = scipy.ndimage.zoom(img, 1 + 0.05 * rnd(), mode = 'constant')
        img = scipy.ndimage.rotate(img, 10 * rnd(), mode = 'constant')
        print(img.shape)

    img = img - np.min(img)
    img = img / np.max(img)
    img = np.reshape(img, (1, *img.shape))

    y = np.zeros(ncats)
    y[c] = 1
    return (img, y)
2个回答

45
scipy.ndimage.rotate接受一个reshape=参数:

reshape : bool, 可选参数

如果reshape为true,则输出形状将适应,以便输入数组完全包含在输出中。默认值为True。

因此,要“裁剪”边缘,您只需调用scipy.ndimage.rotate(img, ..., reshape=False)
from scipy.ndimage import rotate
from scipy.misc import face
from matplotlib import pyplot as plt

img = face()
rot = rotate(img, 30, reshape=False)

fig, ax = plt.subplots(1, 2)
ax[0].imshow(img)
ax[1].imshow(rot)

在此输入图片描述

对于scipy.ndimage.zoom而言,情况更加复杂。

一种朴素的方法是缩放整个输入数组,然后使用切片索引和/或零填充来使输出与输入大小相同。但是,在你增加图像尺寸的情况下,浪费时间去插值那些最终会被裁剪掉的边缘像素。

相反,您可以在应用zoom之前只对将落在输出数组范围内的输入部分进行索引:

import numpy as np
from scipy.ndimage import zoom


def clipped_zoom(img, zoom_factor, **kwargs):

    h, w = img.shape[:2]

    # For multichannel images we don't want to apply the zoom factor to the RGB
    # dimension, so instead we create a tuple of zoom factors, one per array
    # dimension, with 1's for any trailing dimensions after the width and height.
    zoom_tuple = (zoom_factor,) * 2 + (1,) * (img.ndim - 2)

    # Zooming out
    if zoom_factor < 1:

        # Bounding box of the zoomed-out image within the output array
        zh = int(np.round(h * zoom_factor))
        zw = int(np.round(w * zoom_factor))
        top = (h - zh) // 2
        left = (w - zw) // 2

        # Zero-padding
        out = np.zeros_like(img)
        out[top:top+zh, left:left+zw] = zoom(img, zoom_tuple, **kwargs)

    # Zooming in
    elif zoom_factor > 1:

        # Bounding box of the zoomed-in region within the input array
        zh = int(np.round(h / zoom_factor))
        zw = int(np.round(w / zoom_factor))
        top = (h - zh) // 2
        left = (w - zw) // 2

        out = zoom(img[top:top+zh, left:left+zw], zoom_tuple, **kwargs)

        # `out` might still be slightly larger than `img` due to rounding, so
        # trim off any extra pixels at the edges
        trim_top = ((out.shape[0] - h) // 2)
        trim_left = ((out.shape[1] - w) // 2)
        out = out[trim_top:trim_top+h, trim_left:trim_left+w]

    # If zoom_factor == 1, just return the input array
    else:
        out = img
    return out

例如:

zm1 = clipped_zoom(img, 0.5)
zm2 = clipped_zoom(img, 1.5)

fig, ax = plt.subplots(1, 3)
ax[0].imshow(img)
ax[1].imshow(zm1)
ax[2].imshow(zm2)

enter image description here


scipy.ndimage.zoom 太慢了(不知道为什么),所以你的函数对于一个(480,640)的图像需要大约500毫秒。 - MohamedEzz
可能是样条插值导致了减速。此外,缩放中心对于放大是右下角,对于缩小是中心,这很令人困惑。我会使用cv2.resize代替。 - MohamedEzz
1
@MohamedEzz “同时,缩放中心是缩小时的中心和放大时的右下角,这很令人困惑。” - 缩放应该围绕图像中心进行,但是当 zoom_factor > 1 时,我计算缩放区域的边界框时存在错误,现在已经修复。clipped_zoom 将关键字参数传递给 scipy.ndimage.zoom,因此如果三次样条插值太慢,可以传递 order=0。我不怀疑 cv2.resize 更快,但 OpenCV 是一个庞大的依赖项,而 OP 正在寻求基于 scipy 的解决方案。 - ali_m
感谢澄清。关于运行时间,在我的回答中,我快速测试了order=3和order=0,但仍然要慢得多。同意问题主要是关于Scipy,但我认为没有限制不使用更好的cv2。 - MohamedEzz
int(np.round(h * zoom_factor)) 中的四舍五入似乎有时会导致生成的图像比目标小1个像素。然后计算得到-1作为差异,输出的图像像素大小为1。改用 np.ceil() 而不是 np.round() 似乎可以解决这个问题。 - kg_sYy
感谢您发布整个函数,非常有帮助! - Laurin Herbsthofer

14

我建议使用cv2.resize,因为它比scipy.ndimage.zoom快得多,可能是因为支持更简单的插值方法。

对于一张480x640的图像:

  • cv2.resize花费约2毫秒
  • scipy.ndimage.zoom花费约500毫秒
  • scipy.ndimage.zoom(...,order=0)花费约175毫秒

如果你正在实时进行数据增强,这种速度提升是非常宝贵的,因为它意味着在更短的时间内进行更多的实验。

这里是一个使用cv2.resizeclipped_zoom版本。

def cv2_clipped_zoom(img, zoom_factor=0):

    """
    Center zoom in/out of the given image and returning an enlarged/shrinked view of 
    the image without changing dimensions
    ------
    Args:
        img : ndarray
            Image array
        zoom_factor : float
            amount of zoom as a ratio [0 to Inf). Default 0.
    ------
    Returns:
        result: ndarray
           numpy ndarray of the same shape of the input img zoomed by the specified factor.          
    """
    if zoom_factor == 0:
        return img


    height, width = img.shape[:2] # It's also the final desired shape
    new_height, new_width = int(height * zoom_factor), int(width * zoom_factor)
    
    ### Crop only the part that will remain in the result (more efficient)
    # Centered bbox of the final desired size in resized (larger/smaller) image coordinates
    y1, x1 = max(0, new_height - height) // 2, max(0, new_width - width) // 2
    y2, x2 = y1 + height, x1 + width
    bbox = np.array([y1,x1,y2,x2])
    # Map back to original image coordinates
    bbox = (bbox / zoom_factor).astype(np.int)
    y1, x1, y2, x2 = bbox
    cropped_img = img[y1:y2, x1:x2]
    
    # Handle padding when downscaling
    resize_height, resize_width = min(new_height, height), min(new_width, width)
    pad_height1, pad_width1 = (height - resize_height) // 2, (width - resize_width) //2
    pad_height2, pad_width2 = (height - resize_height) - pad_height1, (width - resize_width) - pad_width1
    pad_spec = [(pad_height1, pad_height2), (pad_width1, pad_width2)] + [(0,0)] * (img.ndim - 2)
    
    result = cv2.resize(cropped_img, (resize_width, resize_height))
    result = np.pad(result, pad_spec, mode='constant')
    assert result.shape[0] == height and result.shape[1] == width
    return result

这是一个不错的函数。我认为如果它还能处理0缩放因子并返回原始图像,那就更好了。这可以通过在缩放参数开头添加控制语句来轻松实现。如果您同意,我建议进行编辑。 - Aelius

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