【Python图像处理】RGB颜色转HSV颜色的快速实现

传送门

  • rgb转hsv的简单实现
  • 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 == mnmx == rmx == gmx == b同时成立时只能取mx == r时的结果,也就是对于同一个位置如果mask0mask1mask2mask3中的取值均为1,需要将mask1mask2mask3中该位置上的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

你可能感兴趣的:(【Python图像处理】RGB颜色转HSV颜色的快速实现)