Python: 去除图像中黑色像素的快速方法

Python: Fast way for removing black pixel in image

我有一张包含黑色像素的图像。它可以是垂直线,也可以是简单的点。 我想用相邻像素(左右)的平​​均值替换这些像素。

黑色像素的左右相邻像素值都与黑色不同。

现在我有这个:

import numpy as np
from matplotlib import pyplot as plt
import time



#Creating test img
test_img = np.full((2048, 2048, 3), dtype = np.uint8, fill_value = (255,127,127))

#Draw vertical black line
test_img[500:1000,1500::12] = (0,0,0)
test_img[1000:1500,1000::24] = (0,0,0)
#Draw black point
test_img[250,250] = (0,0,0)
test_img[300,300] = (0,0,0)

#Fill hole functions
def fill_hole(img):

    #Find coords of black pixek
    imggray = img[:,:,0]

    
    coords = np.column_stack(np.where(imggray < 1))
    print(len(coords))

    #Return if no black pixel
    if len(coords) == 0:
        return img

    percent_black = len(coords)/(img.shape[0]*img.shape[1]) * 100
    print(percent_black)
    
    #Make a copy of input image
    out = np.copy(img)

    #Iterate on all black pixels
    for p in coords:

            #Edge management
            if p[0] < 1 or p[0] > img.shape[0] - 1 or p[1] < 1 or p[1] > img.shape[1] - 1:
                continue

            #Get left and right of each pixel
            left = img[p[0], p[1] - 1]
            right = img[p[0], p[1] + 1]

            #Get new pixel value
            r = int((int(left[0])+int(right[0])))/2
            g = int((int(left[1])+int(right[1])))/2
            b = int((int(left[2])+int(right[2])))/2

            out[p[0],p[1]] = [r,g,b] 
    return out

#Function call
start = time.time()
img = fill_hole(test_img)
end = time.time()
print(end - start)

此代码在我的示例中运行良好,但黑色像素列表的循环需要时间,具体取决于其大小。

有没有办法优化这个?

您总是对黑色像素应用相同的操作。所以它是高度可并行化的。将您的图像分成较小的矩形,并放置线程 and/or 进程来作用于每个小矩形。您可以尝试调整矩形大小以获得最佳性能。

另外,由于你的黑色像素结构非常特殊,你可以实施一些策略来避免检查一些你确定没有黑色像素的图像区域。一个想法是为每个图像列检查列的开头、中间和结尾的一些随机像素。然后,如果您在支票中发现没有黑色像素,请忽略该列。

一般来说,numpy 数组上的 for 循环通常会导致速度变慢,并且在大多数情况下可以通过 numpy built-in 函数来避免。在您的情况下,请考虑在图像上使用卷积,请参阅:

请注意,我在答案末尾使用 numba 添加了明显更快的实现。

我想确保使用 比你的普通桃色背景更棘手的 图像能正常工作,所以我污损了一个 2048x2048 可怜的老帕丁顿,就像你的涂鸦一样:

请注意,这只是一个令人讨厌的、不准确的 JPEG 表示,因为原始图像对于 imgur 来说太大了。

然后我运行这个代码:

#!/usr/bin/env python3

import cv2
import numpy as np

# Load image 2048x2048 RGB
im = cv2.imread('paddington.png')

# Make mask of black pixels, True where black
blackMask = np.all(im==0, axis=-1)
cv2.imwrite('DEBUG-blackMask.png', (blackMask*255).astype(np.uint8))

# Convolve with [0.5, 0, 0.5] to set each pixel to average of its left and right neighbours
kernel = np.array([0.5, 0, 0.5], dtype=float).reshape(1,-1)
print(kernel.shape)
convolved = cv2.filter2D(im, ddepth=-1, kernel=kernel, borderType=cv2.BORDER_REPLICATE)
cv2.imwrite('DEBUG-convolved.png', convolved)

# Choose either convolved or original image at each pixel
res = np.where(blackMask[...,None], convolved, im)
cv2.imwrite('result.png', res)

结果是(又一个令人讨厌的、调整大小的 JPEG):

计时在这里,并且可能会进一步改进 - 不确定您的代码达到什么计时或您需要什么:

In [55]: %timeit blackMask = np.all(im==0, axis=-1)
22.3 ms ± 29.1 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [56]: %timeit convolved = cv2.filter2D(im, ddepth=-1, kernel=kernel, borderType=cv2.BORDER_REPLICATE
    ...: )
2.66 ms ± 3.07 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [57]: %timeit res = np.where(blackMask[...,None], convolved, im)
22.7 ms ± 76.2 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

所以大约 46 毫秒 in-toto。请注意,您可以注释掉所有创建名为 DEBUG-xxx.png 的输出图像的行,因为它们仅用于调试并这样命名,因此我可以在测试后轻松清理。

我认为这 运行 在 numba 下非常好,但目前 llvmlite 在我的 M1 Mac 上不受支持,所以我无法尝试。 numba 类似。


优化

我考虑过对上面的代码进行一些优化。似乎有两个方面比它们可能要慢 - 制作掩码并将卷积值插入数组。所以,首先看制作面具,我最初是这样做的:

blackMask = np.all(im==0, axis=-1)

这花了 22 毫秒。我像这样用 numexpr 试过:

import numexpr as ne
R=im[...,0]
G=im[...,1]
B=im[...,2]
blackMask = ne.evaluate('(R==0)&(G==0)&(B==0)')

这会得到相同的结果,但只需要 1.88 毫秒而不是 22 毫秒,因此节省了 20 毫秒。

关于第三部分,将卷积值插入输出数组,我发现我也可以使用 numexpr 更快地完成这项工作。

所以,而不是:

res = np.where(blackMask[...,None], convolved, im)

我用过:

blackMask3 = np.dstack((blackMask, blackMask, blackMask))
res = ne.evaluate("where(blackMask3, convolved, im)")

这将我机器上的时间从 22 毫秒减少到 6 毫秒。所以现在总时间从 46ms 减少到 10.5ms (1.88ms + 2.66ms + 6ms)。


我仍然相信,这可以用 Numba 更快地完成,因为它确实落在 Numba 的最佳位置,具有大图像和可并行化代码。我无法在我的 M1 Mac 上安装 Numba,所以我找到了一个非常低的英特尔赛扬,其中可以安装 Numba 和 运行 以下代码。

low-spec200 英镑的英特尔赛扬机器(4 核,8GB DDR4 内存,eMMC 磁盘)击败了 5000 英镑的 M1 Mac(12 核,32GB DDR5 内存,NVMe SSD) 3 倍,刚好超过 3 毫秒:

#!/usr/bin/env python3

import cv2
import numpy as np
import numba as nb

@nb.jit('void(uint8[:,:,::3])', parallel=True)
def removeLines(im):
    # Ensure image is 3-channel
    assert (im.ndim == 3) and (im.shape[2] == 3)

    h, w = im.shape[0], im.shape[1]
    for y in nb.prange(h):
        for x in range(1,w-1):
            # Check if black, ignore if not
            sum = im[y,x,0] + im[y,x,1] + im[y,x,2]
            if sum != 0: continue

            # Pixel is black.
            # Replace with mean of left and right neighbours in all channels
            im[y, x, 0] = im[y, x-1, 0] // 2 + im[y, x+1, 0] // 2
            im[y, x, 1] = im[y, x-1, 1] // 2 + im[y, x+1, 1] // 2
            im[y, x, 2] = im[y, x-1, 2] // 2 + im[y, x+1, 2] // 2
    return

# Load image
im = cv2.imread('paddington.png')
removeLines(im)
cv2.imwrite('result.png', im)