gpt4 book ai didi

Python:如何使这个颜色阈值功能更有效

转载 作者:太空狗 更新时间:2023-10-30 00:53:59 26 4
gpt4 key购买 nike

我在 Python 中编写了一个自适应颜色阈值函数(因为 OpenCV 的 cv2.adaptiveThreshold 不符合我的需要)而且速度太慢了。我已经尽可能提高了它的效率,但是在 1280x720 的图像上它仍然需要将近 500 毫秒。

我将不胜感激任何可以使此功能更有效的建议!

该函数的作用如下:它使用一个像素厚度的十字形状作为结构元素。对于图像中的每个像素,它计算 ksize 的平均值四个方向的相邻像素独立 (即 ksize 个像素在左侧同一行、上方同一列、右侧同一行和下方同一列中的平均值)。我以四个平均值结束,每个方向一个。如果像素比左右平均值或顶部和底部平均值(加上一些常数 C)更亮,则该像素仅符合阈值标准。

我使用 numpy.roll() 同时为所有像素递增地计算这些平均值,但我仍然需要这样做 ksize次。 ksize通常为 20-50。

这是代码,相关部分实际上就是 for 循环内部发生的事情:

def bilateral_adaptive_threshold(img, ksize=20, C=0, mode='floor', true_value=255, false_value=0):

mask = np.full(img.shape, false_value, dtype=np.int16)

left_thresh = np.zeros_like(img, dtype=np.float32) #Store the right-side average of each pixel here
right_thresh = np.zeros_like(img, dtype=np.float32) #Store the left-side average of each pixel here
up_thresh = np.zeros_like(img, dtype=np.float32) #Store the top-side average of each pixel here
down_thresh = np.zeros_like(img, dtype=np.float32) #Store the bottom-side average of each pixel here

for i in range(1, ksize+1):
roll_left = np.roll(img, -i, axis=1)
roll_right = np.roll(img, i, axis=1)
roll_up = np.roll(img, -i, axis=0)
roll_down = np.roll(img, i, axis=0)

roll_left[:,-i:] = 0
roll_right[:,:i] = 0
roll_up[-i:,:] = 0
roll_down[:i,:] = 0

left_thresh += roll_right
right_thresh += roll_left
up_thresh += roll_down
down_thresh += roll_up

left_thresh /= ksize
right_thresh /= ksize
up_thresh /= ksize
down_thresh /= ksize

if mode == 'floor':
mask[((img > left_thresh+C) & (img > right_thresh+C)) | ((img > up_thresh+C) & (img > down_thresh+C))] = true_value
elif mode == 'ceil':
mask[((img < left_thresh-C) & (img < right_thresh-C)) | ((img < up_thresh-C) & (img < down_thresh-C))] = true_value
else: raise ValueError("Unexpected mode value. Expected value is 'floor' or 'ceil'.")

return mask

最佳答案

正如您在问题中所暗示的那样,该函数的主要部分是获取计算平均值所需的 4 个总和数组——这里,整个函数的平均 210 毫秒中的 190 毫秒。所以,让我们专注于此。

首先,必要的进口和便利的计时功能。

from timeit import default_timer as timer
import numpy as np
import cv2

## ===========================================================================

def time_fn(fn, img, ksize=20, iters=16):
start = timer()
for i in range(iters):
fn(img, ksize)
end = timer()
return ((end - start) / iters) * 1000

## ===========================================================================
# Our test image
img = np.uint8(np.random.random((720,1280)) * 256)

原始实现

我们可以通过以下方式减少您的函数,以便它只计算并返回 4 个求和数组。我们稍后可以使用它来检查优化版本是否返回相同的结果。
# Original code
def windowed_sum_v1(img, ksize=20):
left_thresh = np.zeros_like(img, dtype=np.float32)
right_thresh = np.zeros_like(img, dtype=np.float32)
up_thresh = np.zeros_like(img, dtype=np.float32)
down_thresh = np.zeros_like(img, dtype=np.float32)

for i in range(1, ksize+1):
roll_left = np.roll(img, -i, axis=1)
roll_right = np.roll(img, i, axis=1)
roll_up = np.roll(img, -i, axis=0)
roll_down = np.roll(img, i, axis=0)

roll_left[:,-i:] = 0
roll_right[:,:i] = 0
roll_up[-i:,:] = 0
roll_down[:i,:] = 0

left_thresh += roll_right
right_thresh += roll_left
up_thresh += roll_down
down_thresh += roll_up

return (left_thresh, right_thresh, up_thresh, down_thresh)

现在我们可以找到这个函数在本地机器上花费了多少时间:
>>> print "V1: %f ms" % time_fn(windowed_sum_v1, img, 20, 16)
V1: 188.572077 ms

改进 #1
numpy.roll肯定会涉及一些开销,但没有必要在这里深入研究。请注意,在滚动数组后,将溢出数组边缘的行或列归零。然后将其添加到累加器中。添加零不会改变结果,所以我们不妨避免这种情况。相反,我们可以添加整个数组的渐进式更小和适当偏移的切片,避免 roll和(在某种程度上)减少所需的添加总数。
# Summing up ROIs
def windowed_sum_v2(img, ksize=20):
h,w=(img.shape[0], img.shape[1])

left_thresh = np.zeros_like(img, dtype=np.float32)
right_thresh = np.zeros_like(img, dtype=np.float32)
up_thresh = np.zeros_like(img, dtype=np.float32)
down_thresh = np.zeros_like(img, dtype=np.float32)

for i in range(1, ksize+1):
left_thresh[:,i:] += img[:,:w-i]
right_thresh[:,:w-i] += img[:,i:]
up_thresh[i:,:] += img[:h-i,:]
down_thresh[:h-i,:] += img[i:,:]

return (left_thresh, right_thresh, up_thresh, down_thresh)

让我们测试一下并计时:
>>> print "Results equal (V1 vs V2): %s" % (np.array_equal(windowed_sum_v1(img), windowed_sum_v2(img)))
Results equal (V1 vs V2): True
>>> print "V2: %f ms" % time_fn(windowed_sum_v2, img, 20, 16)
V2: 110.861794 ms

这个实现只需要原来的 60% 的时间。我们能做得更好吗?

改进 #2

我们在那里仍然有一个循环。如果我们可以通过对某个优化函数的一次调用来替换重复添加,那就太好了。一个这样的函数是 cv2.filter2D ,计算如下:

filter2D

我们可以创建一个内核,这样我们要添加的点的权重为 1.0并且内核 anchor 定的点的权重为 0.0 .

例如,当 ksize=8 ,我们可以使用以下内核和 anchor 位置。

Kernels for ksize=8

该函数将如下所示:
# Using filter2d
def windowed_sum_v3(img, ksize=20):
kernel_l = np.array([[1.0] * (ksize) + [0.0]])
kernel_r = np.array([[0.0] + [1.0] * (ksize)])
kernel_u = np.array([[1.0]] * (ksize) + [[0.0]])
kernel_d = np.array([[0.0]] + [[1.0]] * (ksize))

left_thresh = cv2.filter2D(img, cv2.CV_32F, kernel_l, anchor=(ksize,0), borderType=cv2.BORDER_CONSTANT)
right_thresh = cv2.filter2D(img, cv2.CV_32F, kernel_r, anchor=(0,0), borderType=cv2.BORDER_CONSTANT)
up_thresh = cv2.filter2D(img, cv2.CV_32F, kernel_u, anchor=(0,ksize), borderType=cv2.BORDER_CONSTANT)
down_thresh = cv2.filter2D(img, cv2.CV_32F, kernel_d, anchor=(0,0), borderType=cv2.BORDER_CONSTANT)

return (left_thresh, right_thresh, up_thresh, down_thresh)

再次,让我们测试一下这个函数:
>>> print "Results equal (V1 vs V3): %s" % (np.array_equal(windowed_sum_v1(img), windowed_sum_v3(img)))
Results equal (V1 vs V3): True
>>> print "V2: %f ms" % time_fn(windowed_sum_v3, img, 20, 16)
V3: 46.652996 ms

我们减少到原来时间的 25%。

改进 #3

我们在浮点运算,但现在我们不做任何除法,内核只包含 1 和 0。这意味着我们可能会使用整数。您提到最大窗口大小为 50,这意味着我们使用 16 位有符号整数是安全的。整数数学往往更快,如果我们使用的代码正确矢量化,我们可能一次处理两次。让我们试一试,让我们也提供一个包装器,与以前的版本一样,以浮点格式返回结果。
# Integer only
def windowed_sum_v4(img, ksize=20):
kernel_l = np.array([[1] * (ksize) + [0]], dtype=np.int16)
kernel_r = np.array([[0] + [1] * (ksize)], dtype=np.int16)
kernel_u = np.array([[1]] * (ksize) + [[0]], dtype=np.int16)
kernel_d = np.array([[0]] + [[1]] * (ksize), dtype=np.int16)

left_thresh = cv2.filter2D(img, cv2.CV_16S, kernel_l, anchor=(ksize,0), borderType=cv2.BORDER_CONSTANT)
right_thresh = cv2.filter2D(img, cv2.CV_16S, kernel_r, anchor=(0,0), borderType=cv2.BORDER_CONSTANT)
up_thresh = cv2.filter2D(img, cv2.CV_16S, kernel_u, anchor=(0,ksize), borderType=cv2.BORDER_CONSTANT)
down_thresh = cv2.filter2D(img, cv2.CV_16S, kernel_d, anchor=(0,0), borderType=cv2.BORDER_CONSTANT)

return (left_thresh, right_thresh, up_thresh, down_thresh)

# Integer only, but returning floats
def windowed_sum_v5(img, ksize=20):
result = windowed_sum_v4(img, ksize)
return map(np.float32,result)

让我们来测试一下。
>>> print "Results equal (V1 vs V4): %s" % (np.array_equal(windowed_sum_v1(img), windowed_sum_v4(img)))
Results equal (V1 vs V4): True
>>> print "Results equal (V1 vs V5): %s" % (np.array_equal(windowed_sum_v1(img), windowed_sum_v5(img)))
Results equal (V1 vs V5): True
>>> print "V4: %f ms" % time_fn(windowed_sum_v4, img, 20, 16)
V4: 14.712223 ms
>>> print "V5: %f ms" % time_fn(windowed_sum_v5, img, 20, 16)
V5: 20.859744 ms

如果我们对 16 位整数没问题,我们会下降到 7%,如果我们想要浮点数,我们会下降到 10%。

进一步改进

让我们回到您编写的完整阈值函数。我们可以缩放内核,而不是将总和作为单独的步骤来获得平均值,这样 filter2D直接返回平均值。这只是一个很小的改进(~3%)。

同理,可以替换 C的加减法。 ,通过提供适当的 deltafilter2D称呼。这再次削减了几个百分点。

注意 :如果您实现上述两个更改,您可能会遇到由于浮点表示的限制而引起的一些差异。

另一种可能性是进行确定掩码所需的比较矩阵与标量的比较:
input < threshold
input - input < threshold - input
0 < threshold - input
0 < adjusted_threshold # determined using adjusted kernel

我们可以通过修改内核以减去按适当权重缩放的 anchor 像素值( ksize)来实现这一点。使用 numpy,这似乎只有很小的区别,尽管按照我的理解,我们可能会在算法的那部分节省一半的读取(而 filter2D 大概仍然读取并乘以相应的值,即使权重是0)。

阈值函数的最快实现

考虑到所有这些,我们可以像这样重写您的函数,并在大约 12.5% 的时间内获得与原始函数相同的结果:
def bilateral_adaptive_threshold5(img, ksize=20, C=0, mode='floor', true_value=255, false_value=0):
mask = np.full(img.shape, false_value, dtype=np.uint8)

kernel_l = np.array([[1] * (ksize) + [-ksize]], dtype=np.int16)
kernel_r = np.array([[-ksize] + [1] * (ksize)], dtype=np.int16)
kernel_u = np.array([[1]] * (ksize) + [[-ksize]], dtype=np.int16)
kernel_d = np.array([[-ksize]] + [[1]] * (ksize), dtype=np.int16)

if mode == 'floor':
delta = C * ksize
elif mode == 'ceil':
delta = -C * ksize
else: raise ValueError("Unexpected mode value. Expected value is 'floor' or 'ceil'.")

left_thresh = cv2.filter2D(img, cv2.CV_16S, kernel_l, anchor=(ksize,0), delta=delta, borderType=cv2.BORDER_CONSTANT)
right_thresh = cv2.filter2D(img, cv2.CV_16S, kernel_r, anchor=(0,0), delta=delta, borderType=cv2.BORDER_CONSTANT)
up_thresh = cv2.filter2D(img, cv2.CV_16S, kernel_u, anchor=(0,ksize), delta=delta, borderType=cv2.BORDER_CONSTANT)
down_thresh = cv2.filter2D(img, cv2.CV_16S, kernel_d, anchor=(0,0), delta=delta, borderType=cv2.BORDER_CONSTANT)

if mode == 'floor':
mask[((0 > left_thresh) & (0 > right_thresh)) | ((0 > up_thresh) & (0 > down_thresh))] = true_value
elif mode == 'ceil':
mask[((0 < left_thresh) & (0 < right_thresh)) | ((0 < up_thresh) & (0 < down_thresh))] = true_value

return mask

关于Python:如何使这个颜色阈值功能更有效,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42540173/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com