HSV空间替换纯色衣服颜色

本文将介绍如何在HSV空间替换衣服的颜色仅限纯色衣服

完整代码可见我的GithubGitHub - catchcodes/ChangeCloth: 在HSV空间替换衣服颜色

包括单纯opencv和用pyqt5实现图形界面的

HSV详解

HSV颜色空间介绍

HSV是一种将RGB色彩模型中的点在圆柱坐标系中的表示方法

  • 色相Hue是色彩的基本属性用角度度量取值范围为0°~360°在OpenCV中为0-180是由于8bit的最大值为255从红色开始按逆时针方向计算红色为0°绿色为120°,蓝色为240°


  • 饱和度Saturation表示颜色接近光谱色的程度一种颜色可以看成是某种光谱色与白色混合的结果其中光谱色所占的比例愈大颜色接近光谱色的程度就愈高颜色的饱和度也就愈高饱和度高颜色则深而艳光谱色的白光成分为0饱和度达到最高通常取值范围为0%~100%值越大颜色越饱和在OpenCV中为0-255


  • 明度Value明度表示颜色明亮的程度对于光源色明度值与发光体的光亮度有关对于物体色此值和物体的透射比或反射比有关光照对此值影响最大通常取值范围为0%到100%在OpenCV中为0-255

RGB转换为HSV

$ r = R/255.0, \ g = G/255.0 , \ b=B/255.0 $

$ max = \max(r, g, b), \ min = \min(r, g, b) $



HSV与HSL的对比

HSL也称HSIIIntensity或HLS

上一张经典的对比图图源Wikipedia

  • 在HSL中亮度L分量跨越从黑色经过选择的色相到白色的完整范围

  • 在HSV中V分量只走一半行程从黑色到选择的色相

下列左图为HSL右图为HSV

代码实现

第一步点击图片获取要替换的衣服BGR颜色也可直接根据图片H分量范围去选择

img = cv2.imread(r"图片路径")
rows, cols, _ = img_hsv.shape

cv2.namedWindow("ChangeColor")
cv2.resizeWindow("ChangeColor", rows, cols)
# 创建一个鼠标事件响应
cv2.setMouseCallback("ChangeColor", get_color_BGR)

# 这是setMouseCallback的声明原型<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>参数分别为窗口名<span class="bd-box"><h-char class="bd bd-beg"><h-inner>、</h-inner></h-char></span>鼠标响应函数<span class="bd-box"><h-char class="bd bd-beg"><h-inner>、</h-inner></h-char></span>传给鼠标响应函数参数
# def setMouseCallback(windowName: Any,
#                      onMouse: Any,
#                      param: Any = None) -> None

# 回调函数<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span> 即鼠标事件响应函数
def get_color_BGR(envent, x, y, flags, param):
    if envent == cv2.EVENT_LBUTTONDOWN:
        # 注意<span class="bd-box"><h-char class="bd bd-beg"><h-inner>:</h-inner></h-char></span>opencv的行列与坐标相反(row, col) -> (y, x)
        BGR = img[y, x].tolist()     

第二步将获取到的BGR值转为HSV

HSV = [0, 0, 256]  # 一个不存在的HSV值

def bgr2hsv(b, g, r):
    # 限定像素BGR值的范围
    assert (0 <= r < 256 and 0 <= g < 256 and 0 <= b < 256)

    r, g, b = r / 255.0, g / 255.0, b / 255.0
    Max = max(r, g, b)
    Min = min(r, g, b)
    D = Max - Min

    HSV[2] = int(Max * 255)

    HSV[1] = int(255 * (D / Max)) if Max != 0 else 0

    if D == 0:
        HSV[0] = 0
    elif Max == r:
        HSV[0] = (60 * ((g - b) / D)) % 360
    elif Max == g:
        HSV[0] = (120 + 60 * ((b - r) / D)) % 360
    elif Max == b:
        HSV[0] = (240 + 60 * ((r - g) / D)) % 360

    HSV[0] = int(HSV[0] / 2)
    return HSV

第三步限定H分量的范围找到衣服的区域

img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# HSV 的下界限
lower = np.array([HSV[0] - 7, 50, 50])
# HSV 的上界限
upper = np.array([HSV[0] + 7, 255, 255])
# 找到H分量在这个范围的区域
mask = cv2.inRange(img_hsv, lower, upper)

# 腐蚀和膨胀的核kernel<span class="bd-box"><h-char class="bd bd-end"><h-inner>(</h-inner></h-char></span>可根据图片自己设计<span class="bd-box"><h-char class="bd bd-beg"><h-inner>)</h-inner></h-char></span>
kernel = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8)
# 腐蚀图像<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>除去干扰点
eroded = cv2.erode(mask, kernel, iterations=1)
# 膨胀图像<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>恢复腐蚀前的大小
dilated = cv2.dilate(eroded, kernel, iterations=1)

第四步在窗口中控制H分量和显示图像

# 在‘ChangeColor’窗口中创建滑动控制条
# 参数介绍 createTrackbar(const String& trackbarname, const String& winname, int *value, int count, TrackbarCallback onChange = 0, void *userdata = 0)
# 分别为滑动条名称<span class="bd-box"><h-char class="bd bd-beg"><h-inner>、</h-inner></h-char></span>窗口名称<span class="bd-box"><h-char class="bd bd-beg"><h-inner>、</h-inner></h-char></span>初始值<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>范围<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>滑块更改时的回调函数<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>传给回调函数的参数<span class="bd-box"><h-char class="bd bd-end"><h-inner>(</h-inner></h-char></span>默认为0<span class="bd-box"><h-char class="bd bd-beg"><h-inner>)</h-inner></h-char></span>
cv2.createTrackbar('H', 'ChangeColor', 140, 180, lambda x: None)
# cv2.createTrackbar('S', 'ChangeColor', 100, 255, lambda x: None)
# cv2.createTrackbar('V', 'ChangeColor', 100, 255, lambda x: None)

# 获取滑块的值
h = cv2.getTrackbarPos('H', 'ChangeColor')
# s = cv2.getTrackbarPos('S', 'ChangeColor')
# v = cv2.getTrackbarPos('V', 'ChangeColor')

for row in range(rows):
    for col in range(cols):
        if dilated[row, col] == 255:
            img_hsv.itemset((row, col, 0), h)
            # img_hsv.itemset((row, col, 1), s)
            # img_hsv.itemset((row, col, 2), v)

img = cv2.cvtColor(img_hsv, cv2.COLOR_HSV2BGR)
cv2.imshow("ChangeColor", img)

完整代码

可以直接在我的Github上Clonehttps://github.com/catchcodes/ChangeCloth

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# @FileName  :main.py
# @Time      :2023/4/7 19:46
# @Author    :Ouyang Bin

import cv2
import numpy as np

# 不存在的HSV值
HSV = [0, 0, 256]
newHSV = [0, 0, 256]


def bgr2hsv(b, g, r):
    # 限定像素BGR值的范围
    assert (0 <= r < 256 and 0 <= g < 256 and 0 <= b < 256)

    r, g, b = r / 255.0, g / 255.0, b / 255.0
    Max = max(r, g, b)
    Min = min(r, g, b)
    D = Max - Min

    HSV[2] = int(Max * 255)

    HSV[1] = int(255 * (D / Max)) if Max != 0 else 0

    if D == 0:
        HSV[0] = 0
    elif Max == r:
        HSV[0] = (60 * ((g - b) / D)) % 360
    elif Max == g:
        HSV[0] = (120 + 60 * ((b - r) / D)) % 360
    elif Max == b:
        HSV[0] = (240 + 60 * ((r - g) / D)) % 360

    HSV[0] = int(HSV[0] / 2)
    return HSV


# 回调函数
def get_color_BGR(envent, x, y, flags, param):
    if envent == cv2.EVENT_LBUTTONDOWN:
        # 注意<span class="bd-box"><h-char class="bd bd-beg"><h-inner>:</h-inner></h-char></span>opencv的行列与坐标相反(row, col) -> (y, x)
        BGR = img[y, x].tolist()
        bgr2hsv(BGR[0], BGR[1], BGR[2])


img = cv2.imread(r"C:\Users\19941\Pictures\cloth.jpg")
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
rows, cols, _ = img_hsv.shape

cv2.namedWindow("ChangeColor")
cv2.resizeWindow("ChangeColor", rows, cols)
cv2.setMouseCallback("ChangeColor", get_color_BGR)

cv2.createTrackbar('H', 'ChangeColor', 140, 180, lambda x: None)
# cv2.createTrackbar('S', 'ChangeColor', 100, 255, lambda x: None)
# cv2.createTrackbar('V', 'ChangeColor', 100, 255, lambda x: None)

times = 0

while True:
    cv2.imshow('ChangeColor', img)

    if newHSV != HSV:
        if times == 0:
            # HSV 的下界限
            lower = np.array([HSV[0] - 7, 50, 50])
            # HSV 的上界限
            upper = np.array([HSV[0] + 7, 255, 255])
            mask = cv2.inRange(img_hsv, lower, upper)

            # 核
            kernel = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8)
            # 腐蚀图像<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>除去干扰点
            eroded = cv2.erode(mask, kernel, iterations=1)
            # 膨胀图像
            dilated = cv2.dilate(eroded, kernel, iterations=1)

            times += 1

        h = cv2.getTrackbarPos('H', 'ChangeColor')
        # s = cv2.getTrackbarPos('S', 'ChangeColor')
        # v = cv2.getTrackbarPos('V', 'ChangeColor')

        for row in range(rows):
            for col in range(cols):
                if dilated[row, col] == 255:
                    img_hsv.itemset((row, col, 0), h)
                    # img_hsv.itemset((row, col, 1), s)
                    # img_hsv.itemset((row, col, 2), v)

        img = cv2.cvtColor(img_hsv, cv2.COLOR_HSV2BGR)
        cv2.imshow("ChangeColor", img)
    else:
        img = cv2.cvtColor(img_hsv, cv2.COLOR_HSV2BGR)
        cv2.imshow("ChangeColor", img)

    if cv2.waitKey(1) & 0xff == 27:
        break

cv2.destroyAllWindows()