【Python图像处理】RGB颜色转HSV颜色的快速实现
传送门
思路
使用NumPy。NumPy对数组和矩阵的运算有大幅度的提速。因此,使用NumPy设计算法时,应该充分利用这一特性,尽可能用NumPy中的矩阵运算来代替遍历等耗时的操作。
RGB转HSV
非矩阵的方法
根据RGB和HSV的转换公式可以构建出以下数值计算的代码,使用控制语句实现分段函数,使用python内置函数实现数学运算。 然而,以下代码只对一个像素点进行转换,对于一张1000*1000的图片,需要循环调用100万次。显然,这是一种容易理解的算法,但性能并不好。
def rgb2hsv(r, g, b):
r, g, b = r / 255.0, g / 255.0, b / 255.0
mx = max(r, g, b)
mn = min(r, g, b)
df = mx - mn
if mx == mn:
h = 0
elif mx == r:
h = (60 * ((g - b) / df) + 360) % 360
elif mx == g:
h = (60 * ((b - r) / df) + 120) % 360
elif mx == b:
h = (60 * ((r - g) / df) + 240) % 360
if mx == 0:
s = 0
else:
s = df / mx
v = mx
return h, s, v
NumPy的方法
算术运算部分比较简单,可以直接转换为NumPy中的操作。以下代码对应上述代码的第2-5行。其中np.max和np.min的axis=-1
表示在数组的最后一个维度中求最值,也就是在每个像素点的[r, g, b]中求最值,keepdims=True
表示求出最值后保持原来的维度。代码中的注释表示数组的形状。
def rgb2hsv_mat(img):
rgbImg = np.array(img, dtype=np.float) / 255.0 # (height, width, 3)
maxVal = np.max(rgbImg, axis=-1, keepdims=True) # (height, width, 1), 若keepdims=False则为(height, width)
minVal = np.min(rgbImg, axis=-1, keepdims=True) # (height, width, 1)
difVal = maxVal - minVal # (height, width, 1)
实现比较复杂的是其中的分段函数,如何对数组中的每个像素点同时进行判断呢?这里需要用到掩码(mask)的概念。NumPy支持逻辑运算和布尔运算,如rgbImg == 255
将输出大小与rgbImg相同的矩阵,这个结果就是掩码数组,其中每个元素的值是rgbImg中对应位置的元素与255作==
运算的结果。利用掩码数组作为因子可以在操作时“过滤”掉不满足条件的元素。
该算法根据每个点的最大值的不同采用不同的H值计算方法,因此需要对maxVal
数组作4个布尔运算,得到4个掩码数组,代码如下。mask0比较好理解。在mask1中,rgbImg[:, :, :1]
表示对数组中每个点只取r的值,且保留原数组维度,因此布尔表达式maxVal == rgbImg[:, :, :1]
就是判断数组中每个点的最大值是否等于r所得到的掩码数组。对于g, b的判断同理。
根据每个点最大值的不同,计算各点H的值的公式也有所不同。使用NumPy时,将各公式的计算结果乘上对应的掩码数组,不满足要求的位置都被置为0,只有满足要求的位置有计算结果,然后加到结果数组中。需要特别注意的是,mx == mn
、mx == r
、mx == g
和mx == b
同时成立时只能取mx == r
时的结果,也就是对于同一个位置如果mask0
、mask1
、mask2
和mask3
中的取值均为1,需要将mask1
、mask2
和mask3
中该位置上的1置为0,只考虑最先出现1的掩码,这与if xxx else if xxx
的逻辑是相对应的。因此对于上面求出的每个mask,都需要将其和其前面的每一个mask的非~
作与&
操作,如mask1 &= ~mask0
.
最后,difVal作为计算公式的除数需要考虑除0的问题。由于difVal
中为0的位置在后续掩码中一定为0,因此可将difVal中这些位置的上的数置为任意非0的数,避免出现除0异常。后续S和V的求法比较简单,请直接见最终代码。
最终代码
def rgb2hsv_mat(img):
rgbImg = np.array(img, dtype=np.float) / 255.0
maxVal = np.max(rgbImg, axis=-1, keepdims=True)
minVal = np.min(rgbImg, axis=-1, keepdims=True)
difVal = maxVal - minVal
h, w, _ = rgbImg.shape
HSV = np.zeros([h, w, 3]) # 初始化HSV图像的数组用于存储结果
mask0 = np.array(maxVal == minVal, dtype=np.int) # 判断mx == mn
mask1 = np.array(maxVal == rgbImg[:, :, :1], dtype=np.int) # 判断mx == r
mask2 = np.array(maxVal == rgbImg[:, :, 1:2], dtype=np.int) # 判断mx == g
mask3 = np.array(maxVal == rgbImg[:, :, 2:], dtype=np.int) # 判断mx == b
for i in range(4):
for j in range(i + 1, 4):
masks[i] &= ~masks[j]
difValNonZero = difVal - (difVal == 0)
H = HSV[:, :, :1]
H += mask1 * ((60 * ((rgbImg[:, :, 1:2] - rgbImg[:, :, 2:3]) / difValNonZero) + 360) % 360)
H += mask2 * ((60 * ((rgbImg[:, :, 2:3] - rgbImg[:, :, 0:1]) / difValNonZero) + 120) % 360)
H += mask3 * ((60 * ((rgbImg[:, :, 0:1] - rgbImg[:, :, 1:2]) / difValNonZero) + 240) % 360)
mask4 = np.array(maxVal != 0, dtype=np.int)
S = HSV[:, :, 1:2]
maxValNonZero = maxVal - (maxVal == 0)
S += mask4 * (difVal / maxValNonZero)
S *= np.array(S >= 0, dtype=np.int)
V = HSV[:, :, 2:]
V += maxVal
return HSV
对比实验
下图是两种方法的计算时间随图片变长变化的曲线,蓝色是非NumPy的方法的时间曲线,黄色是NumPy的方法的时间曲线。
下图是NumPy方法单独的时间曲线,根据曲线可以推测其时间复杂度仍然为,因此NumPy的方法并没有降低算法的时间复杂度,只是从编译层面对Python中的数学运算进行了优化。
image.png