Skip to content

Instantly share code, notes, and snippets.

@kiyoshi-tokunaga
Created February 21, 2015 18:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kiyoshi-tokunaga/1fe290bf3b63c2533d9e to your computer and use it in GitHub Desktop.
Save kiyoshi-tokunaga/1fe290bf3b63c2533d9e to your computer and use it in GitHub Desktop.
点と面の肌色領域推定結果を用いたグラフカットによる肌色検出
# https://gist.github.com/parosky/7890436
# https://github.com/nagadomi/lbpcascade_animeface

import cv2
import numpy as np

GC_UNKNOWN = 5

def unique(a):
    """ remove duplicate columns and rows
        from http://stackoverflow.com/questions/8560440 """
    order = np.lexsort(a.T)
    a = a[order]
    diff = np.diff(a, axis=0)
    ui = np.ones(len(a), 'bool')
    ui[1:] = (diff != 0).any(axis=1)
    return a[ui]


def detect_faces(im):
    hc = cv2.CascadeClassifier('lbpcascade_animeface.xml')
    faces = hc.detectMultiScale(im, minSize=(30, 30))
    if len(faces) == 0:
        raise Exception('no faces')
    return faces


def canny_edges(im):
    edge_im = cv2.Canny(im, 100, 200)
    kernel = np.ones((3, 3), np.uint8)
    opening_im = cv2.morphologyEx(edge_im, cv2.MORPH_CLOSE, kernel)
    return opening_im


def detect_contours(im):
    opening_im = canny_edges(im)
    ret, thresh = cv2.threshold(opening_im, 127, 255, 0)
    height, width = thresh.shape[:2]
    thresh[0:3, 0:width-1] = 255
    thresh[height-3:height-1, 0:width-1] = 255
    thresh[0:height-1, 0:3] = 255
    thresh[0:height-1, width-3:width-1] = 255
    _, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

    im_size = width * height
    ret = []
    for contour in contours:
        area =  cv2.contourArea(contour)
        if area < im_size/4:
            ret.append(contour)

    return ret


def mask_skin_detection(image, flood_diff=3, min_face_size=(30,30),
                   num_iter=3, verbose=False, step=1):
    faces = detect_faces(image)
    if len(faces) == 0:
        raise Exception('no faces')

    image_original = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)

    image2 = np.copy(image_original)
    image2[image2==[0,0,0]] = 1

    skin_color = np.zeros((len(faces), 3))
    for i, face in enumerate(faces):
        image_face = image2[face[1]:face[1]+face[3],face[0]:face[0]+face[2]]
        skin_color[i] = np.array([image_face[image_face.shape[0]/2, image_face.shape[1]/2]])

    mask = np.zeros(image_original.shape)
    for i in range(num_iter):
        # for each pixel, call floodFill(image2) if it is close to skin_color
        for y in range(0, image2.shape[0], step):
            if verbose:
                print('iter: %d, y:%d' % (i, y))
            for x in range(0, image2.shape[1], step):
                color = image2[y,x]
                if (color!=(0,0,0)).any():
                    if any((np.abs(skin_color-color)<=(flood_diff,)*3).all(1)):
                        cv2.floodFill(image2, None, (x,y), (0, 0, 0),
                                      loDiff=(flood_diff,)*3, upDiff=(flood_diff,)*3)

        # update mask image and skin_color
        mask[image2==(0,0,0)] = 255
        skin_color = image_original[mask.nonzero()]
        skin_color.shape = (skin_color.shape[0]/3, 3)
        skin_color = unique(skin_color)

    mask = np.bool_(mask)
    return mask


def calc_face_histgram_normalized(im, faces, padding=0):
    im_hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)

    hsv_histgrams = []
    for i, face in enumerate(faces):
        origin_x, origin_y, width, height = face
        area = (height - padding*2)*(width - padding*2)
        roi_image = im_hsv[(origin_y + padding):(origin_y + height - padding), (origin_x + padding):(origin_x + width - padding)]
        cv2.imshow('win', roi_image)
        cv2.waitKey(1000)

        h_h = cv2.calcHist([roi_image], [0], None, [180], [0, 180], None, 0)
        h_s = cv2.calcHist([roi_image], [1], None, [256], [0, 256], None, 0)
        h_v = cv2.calcHist([roi_image], [2], None, [256], [0, 256], None, 0)

        # normalization and append
        if area == 0:
            area = 1
        hsv_histgrams.append([h_h/area, h_s/area, h_v/area])

    return hsv_histgrams


def mask_from_contour(im_shape, contour):
    mask = np.zeros(im_shape, np.uint8)
    cv2.drawContours(mask, [contour], 0, 255, -1)
    return mask

def mask_from_contours(im_shape, fg_contours, bg_contours):
    mask = np.zeros(im_shape, np.uint8)
    mask.fill(cv2.GC_PR_BGD)
    for contour in fg_contours:
        cv2.drawContours(mask, [contour], 0, cv2.GC_FGD, -1)
    for contour in bg_contours:
        cv2.drawContours(mask, [contour], 0, cv2.GC_BGD, -1)
    return mask


def mask_from_point_and_contour(im_shape, mask_skin_point, fg_contours, bg_contours):
    mask = np.zeros(im_shape, np.uint8)
    mask.fill(GC_UNKNOWN)

    for contour in fg_contours:
        cv2.drawContours(mask, [contour], 0, cv2.GC_PR_FGD, -1)
    for contour in bg_contours:
        cv2.drawContours(mask, [contour], 0, cv2.GC_PR_BGD, -1)

    height, width = im_shape
    for y in range(height):
        for x in range(width):
            gc_label = mask[y][x]
            if mask_skin_point[y, x, 0]:
                if gc_label == cv2.GC_PR_FGD:
                    mask[y][x] = cv2.GC_FGD
                else:
                    mask[y][x] = cv2.GC_FGD
            else:
                if gc_label == cv2.GC_PR_BGD:
                    mask[y][x] = cv2.GC_BGD
                else:
                    mask[y][x] = cv2.GC_PR_BGD

    return mask


def calc_contour_histgram_normalized(im, contour):
    mask = mask_from_contour(im.shape, contour)
    area = cv2.contourArea(contour)
    im_hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)
    im_mask = cv2.inRange(mask, 10, 255)

    h_h = cv2.calcHist([im_hsv], [0], im_mask, [180], [0, 180], None, 0)
    h_s = cv2.calcHist([im_hsv], [1], im_mask, [256], [0, 256], None, 0)
    h_v = cv2.calcHist([im_hsv], [2], im_mask, [256], [0, 256], None, 0)

    # normalization and return
    if area == 0:
        area = 1
    return [h_h/area, h_s/area, h_v/area]


def blown_skin_mask(im, mask, diff_color):
    height, width = im.shape[:2]
    hsv_im = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)
    for y in range(height):
        for x in range(width):
            if (mask[y][x]):
                h = hsv_im[y, x, 0] + diff_color[0]
                s = hsv_im[y, x, 1] + diff_color[1]
                v = hsv_im[y, x, 2] + diff_color[2]
                if h < 0:
                    hsv_im[y, x, 0] = h + 180
                elif h > 180:
                    hsv_im[y, x, 0] = h - 180
                else:
                    hsv_im[y, x, 0] = h
                if s < 0:
                    hsv_im[y, x, 1] = 0
                elif s > 255:
                    hsv_im[y, x, 1] = 255
                else:
                    hsv_im[y, x, 1] = s
                if v < 0:
                    hsv_im[y, x, 2] = 0
                elif v > 255:
                    hsv_im[y, x, 2] = 255
                else:
                    hsv_im[y, x, 2] = v
    return cv2.cvtColor(hsv_im, cv2.COLOR_HSV2BGR)


def calc_hsv_diff(skin_hist):
    blown_color = np.array([13, 110, 220])
    return blown_color - skin_color


def max_bin_histgram(hist):
    idx, _ = np.unravel_index(hist.argmax(), hist.shape)
    return idx


if __name__ == '__main__':
    thresh = 2.1
    thresh_ng = 0.6
    filename = 'test.jpg'
    filename_out = (''.join(filename.split('.')[:-1])
                    + 'gc_out%d.' + filename.split('.')[-1])
    image = cv2.imread(filename)
    im_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)

    # detect face
    faces = detect_faces(image)
    face_hists = calc_face_histgram_normalized(image, faces, padding=40)
    contours = detect_contours(image)
    skin_contours = []
    skin_hists = []
    bg_contours = []

    # detect contour
    for iter in range(1):
        face_hists.extend(skin_hists)
        skin_hists = []
        for index, contour in enumerate(contours):
            hists = calc_contour_histgram_normalized(image, contour)
            for face_hist in face_hists:
                h_dist = cv2.compareHist(face_hist[0], hists[0], 0)
                s_dist = cv2.compareHist(face_hist[1], hists[1], 0)
                v_dist = cv2.compareHist(face_hist[2], hists[2], 0)
                if (h_dist + s_dist + v_dist) > thresh:
                    skin_contours.append(contour)
                    skin_hists.append(hists)
                elif (h_dist + s_dist + v_dist) < thresh_ng:
                    bg_contours.append(contour)

    skin_color = np.array([max_bin_histgram(face_hists[0][0]), max_bin_histgram(face_hists[0][1]), max_bin_histgram(face_hists[0][2])])
    diff_color = calc_hsv_diff(skin_color)

    # graph cut
    mask_skin = mask_skin_detection(image, num_iter=2, verbose=True)
    mask = mask_from_point_and_contour(image.shape[:2], mask_skin, skin_contours, bg_contours)

    bgd_model = np.zeros((1, 65), np.float64)
    fgd_model = np.zeros((1, 65), np.float64)

    mask, bgd_model, fgd_model = cv2.grabCut(image, mask, None, bgd_model, fgd_model, 5, cv2.GC_INIT_WITH_MASK)
    gc_mask = np.where((mask==2)|(mask==0), 0, 1).astype('uint8')
    im_masked = image*gc_mask[:, :, np.newaxis]
    im_blown = blown_skin_mask(image, gc_mask, diff_color)

    # output image
    cv2.imwrite(filename_out % 1, im_blown)
    cv2.imwrite(filename_out % 2, im_masked)
    im_concat = np.concatenate((image, im_blown), axis=1)
    cv2.imwrite(filename_out % 3, im_concat)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment