目标:裁剪图像,以便仅数字保留在图像中

问题:性能下降

我有裁剪图像的代码。
图像像素为0或255。之间没有值。

背景为0(黑色),字母/数字介于0(不包括)和255(白色)之间)。

此代码用于裁剪Mnist数据集的Mnist数字。



图像源

代码可以完成此操作,但是它可以处理2个for s,并且需要很长时间!如何优化它?

def crop_image(data_to_crop):
    cropped_data = []

    for z in xrange(data_to_crop.shape[0]):

        img = data_to_crop[z]
        img = img.reshape(data_to_crop.shape[0], data_to_crop.shape[1])

        rx = -1
        upy = -1

        lx = -1
        by = -1

        for x in xrange(data_to_crop.shape[0]):
            for y in xrange(data_to_crop.shape[1]):

                px = img[x, y]

                if px > 0:

                    if rx == -1 or x > rx:
                        rx = x

                    if lx == -1 or x < lx:
                        lx = x

                    if upy == -1 or y > upy:
                        upy = y

                    if by == -1 or y < by:
                        by = y

        img = img[lx:rx, by:upy]

        cropped_data.append(img)

    return cropped_data


评论

您已将此问题标记为opencv,但尚不清楚代码中是否使用过OpenCV。你能澄清一下吗?

我通常使用cv2之后再调整大小。如果可以快速使用,我可以使用Cv2方法!也许我标记了错误的原因。但是,是的,我确实将opencv和cv2用于python。

通常,正如我在您的数据中看到的那样,要删除的内容远少于需要保留的内容。因此,我建议不要扫描整个图像以检测所有非黑色像素,而应逐行扫描所有四个边缘,以检测要去除的黑色条纹–并在非黑色像素出现后立即中断扫描找到了。请注意,这些条纹在角落重叠,您可能需要避免扫描这些重叠区域两次。但是,逐像素扫描仍然是非常无效的...

#1 楼

使用NumPy进行矢量化
当使用cv2.imreadskimage.io.imreadscipy.misc.imread进行读取时,您已经将图像数据作为NumPy数组了。现在,NumPy支持各种矢量化功能,我们可以使用它们大大加快速度。
I.裁剪以删除整个图像中的所有黑色行和列
要解决我们的问题,一种方法是查找行和列中至少有一个像素的行和列,该像素大于某个下限或阈值,像素值。因此,如果您确定黑色区域绝对为零,则可以将该阈值设置为0。因此,如果img代表图像数据,则您将具有两个布尔数组:(img>tol).any(1)(img>tol).any(0)
接下来,您可以使用这些布尔数组对图像数据进行索引,以使用广播索引来提取有效边界框。 np._ix-
np.ix_((img>tol).any(1),(img>tol).any(0))

最后,我们用它索引图像数据以获取最终提取的数据,这是必需的边界框数据。 />
def crop_image(img,tol=0):
    # img is 2D image data
    # tol  is tolerance
    mask = img>tol
    return img[np.ix_(mask.any(1),mask.any(0))]

样品结果
1]公差为0时:

2]公差为80(更紧)时:

II。在保留内部全部为黑色的行或列的同时进行裁剪
要在保留内部全部为黑色的行或列的同时进行裁剪的图像,该实现方式应与之前的方法很接近。这里的基本思想是获取决定边界框的行和列的起点,终点索引。我们将从与前一个相同的行和列开始使用相同的掩码ANY匹配。然后,argmax将有助于获取沿行和列的那些匹配的开始和停止索引。我们将使用这些索引对2D输入数组进行切片,这是所需的裁剪图像输出。实现看起来像这样-
def crop_image_only_outside(img,tol=0):
    # img is 2D image data
    # tol  is tolerance
    mask = img>tol
    m,n = img.shape
    mask0,mask1 = mask.any(0),mask.any(1)
    col_start,col_end = mask0.argmax(),n-mask0[::-1].argmax()
    row_start,row_end = mask1.argmax(),m-mask1[::-1].argmax()
    return img[row_start:row_end,col_start:col_end]

示例运行-
输入:

输出:


基准测试
由于我们正在讨论此Q&A的性能,因此让我们测试一下该方法与其他方法的比较。然后切片输入数组。查找所有索引可能会变得昂贵。我们将尝试计时,看看会对性能产生多大影响。我们将使用前面部分中使用的示例数据-
# @Gareth Rees's solution
def crop_with_argwhere(image):
    # Mask of non-black pixels (assuming image has a single channel).
    mask = image > 0
    
    # Coordinates of non-black pixels.
    coords = np.argwhere(mask)
    
    # Bounding box of non-black pixels.
    x0, y0 = coords.min(axis=0)
    x1, y1 = coords.max(axis=0) + 1   # slices are exclusive at the top
    
    # Get the contents of the bounding box.
    cropped = image[x0:x1, y0:y1]
    return cropped

时间-
# Import the "4" digit image from previous section
In [50]: from skimage import io
    ...: im = io.imread('https://i.stack.imgur.com/tmdiH.png')

# @Gareth Rees's solution
In [51]: %timeit crop_with_argwhere(im)
1000 loops, best of 3: 1.4 ms per loop

In [52]: %timeit crop_image_only_outside(im,tol=0)
10000 loops, best of 3: 81.8 µs per loop

@Gareth Rees's solution的内存效率在性能上很明显。

扩展到通用的2D或3D图像数据案例
假设我们要检查沿最后一个尺寸/轴的所有通道上的crop_image_only_outside匹配项,那么扩展将只是沿最后一个轴执行ALL缩小。因此,我们将拥有通用解决方案来处理numpy.all2D图像数据案例,例如-
def crop_image(img,tol=0):
    # img is 2D or 3D image data
    # tol  is tolerance
    mask = img>tol
    if img.ndim==3:
        mask = mask.all(2)
    mask0,mask1 = mask.any(0),mask.any(1)
    return img[np.ix_(mask0,mask1)]

def crop_image_only_outside(img,tol=0):
    # img is 2D or 3D image data
    # tol  is tolerance
    mask = img>tol
    if img.ndim==3:
        mask = mask.all(2)
    m,n = mask.shape
    mask0,mask1 = mask.any(0),mask.any(1)
    col_start,col_end = mask0.argmax(),n-mask0[::-1].argmax()
    row_start,row_end = mask1.argmax(),m-mask1[::-1].argmax()
    return img[row_start:row_end,col_start:col_end]


评论


\ $ \ begingroup \ $
如果图像的非黑色部分断开连接,该如何工作?例如,如果图像中间有一些全黑的行(或列)怎么办? OP希望将图像的这些部分包括在裁剪中,但是此答案中的代码将排除它们。
\ $ \ endgroup \ $
–加雷斯·里斯(Gareth Rees)
16 Jun 24'10:53



\ $ \ begingroup \ $
谢谢你,它非常方便!顺便说一句,我认为您不需要(或不想)从crop_image_only_outside()的row_ / col_ends中减去一个。
\ $ \ endgroup \ $
– A C
19年11月4日在7:28



\ $ \ begingroup \ $
@AC糟糕,确实是一个错误。感谢您指出这一点!固定。
\ $ \ endgroup \ $
– Divakar
19年11月4日在8:06

#2 楼

如您所知,在Python中循环遍历单个像素非常慢。您需要组织计算,以便在整个图像上使用一系列NumPy(或SciPy,Scikit-Image或OpenCV)操作。在这种情况下,您可以使用numpy.argwhere查找非黑色区域的边界框:

# Mask of non-black pixels (assuming image has a single channel).
mask = image > 0

# Coordinates of non-black pixels.
coords = np.argwhere(mask)

# Bounding box of non-black pixels.
x0, y0 = coords.min(axis=0)
x1, y1 = coords.max(axis=0) + 1   # slices are exclusive at the top

# Get the contents of the bounding box.
cropped = image[x0:x1, y0:y1]


(请注意,这依赖于一些非黑色像素;如果整个图像是黑色,则coords将是空的,在这种情况下,您将不得不寻找其他事情。)

评论


\ $ \ begingroup \ $
这是一种非常有效的方法。但是,因为我们应该参考图像坐标,所以我猜代码中的x0应该是y0,相同的约定适用于x1,y0和y1。
\ $ \ endgroup \ $
–库尔伯尔
18年5月7日在7:22



\ $ \ begingroup \ $
@Kulbear:是的:如果图像以行优先顺序存储(通常以NumPy的形式存储),则交换名称x和y是有意义的。
\ $ \ endgroup \ $
–加雷斯·里斯(Gareth Rees)
18年5月7日在9:05

\ $ \ begingroup \ $
如果可以为多个频道的图像提供一些代码,那就太好了:)
\ $ \ endgroup \ $
–疯子
18年7月31日在19:47

\ $ \ begingroup \ $
@maniac:这可能是Stack Overflow的一个好问题。
\ $ \ endgroup \ $
–加雷斯·里斯(Gareth Rees)
18年7月31日在20:43

\ $ \ begingroup \ $
与公认的答案相比,我认为这是一种更好的农作物实施方法。此版本仅裁剪图像的外部区域,并且不触摸外围像素边界内的“空白”区域。例如,上面的版本将以下图像裁剪为:((x))到此((x))中。
\ $ \ endgroup \ $
–krafter
18-09-7在9:06