The Boombox Incident


In Seinfeld episode #163, “The Slicer”, George has just landed a cushy job at Kruger Industrial Smoothing, when he sees himself in the background of a family photo on his new boss’s desk.

kruger-family-untouched

George is instantly reminded of the “boombox incident”, in which he had, years earlier, embarrassed himself in front of the Kruger family.

Later, upon hearing about the photo, Kramer suggests that George sneak the photo off to have himself airbrushed out of the picture, so that Kruger doesn’t remember the incident and fire George.

George enacts the plan and things are going fine until he receives the touched-up photo: the clerk has removed Kruger from the family photo instead of George.

kruger_family_airbrushed

The clerk mistook Kruger in the photo for George, since in the picture George had hair but Kruger was bald. Removing the only bald person from the photo was a pretty reasonable thing for the photo store clerk to do. I figured this is something that photo editors have to do frequently, so I decided to automate it.

The process for removing bald people from photos is as follows:

  • detect faces in an image using off-the-shelf tools
  • for each face
    • roughly locate the forehead/hair region
    • get the dominant color of the face and of the top of the head
    • compare the two colors
    • consider a bald subject to be one where the two colors are very close, i.e. the top of the head is the same color as the face (skin tone)
    • attempt to inpaint the region containing the bald individual

Face detection

Face detection in OpenCV can be accomplished with a cascade classifier. A pre-trained model from the OpenCV data repository is made available. The underlying algorithm at play is the Viola-Jones object detection framework, which uses a coarse-to-fine cascade of Haar-feature matching to identify human faces.

haar_features

face_cascade = cv2.CascadeClassifier('./haarcascade_frontalface_default.xml') def detect_faces(image): h, w, _ = image.shape gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) min_size = int(w*0.12) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=1, minSize=(min_size, min_size), flags = cv2.cv.CV_HAAR_SCALE_IMAGE ) return faces g_and_k_img = cv2.imread('./input_images/george-and-kruger.jpg')
tmp = np.copy(g_and_k_img)
for (x, y, w, h) in detect_faces(g_and_k_img): cv2.rectangle(tmp, (x, y), (x+w, y+h), (0, 255, 0), 2)

george_and_kruger_detection

seinfeld_detection

Once the faces in a photo have been located, we need to examine just above each face region to determine whether the person is bald or has hair. This approach assumes, of course, that the faces are oriented vertically. A better method might be to locate the eyes and/or mouth using other cascade classifiers and then approximate the forehead position from there.

def forehead_region(img, face_loc): img_h, img_w = img.shape[:2] x, y, w, h = face_loc fore_w = int(w * 0.33) fore_h = int(h * 0.25) fore_x = min(x + ((w - fore_w) // 2), img_w) fore_y = max(y - fore_h, 0) return img[fore_y:fore_y + fore_h, fore_x:fore_x + fore_w, :]

elaine_forehead_detail

Dominant color

The algorithm presented here compares the color in the forehead/top-of-head region to that of the face region. To find the dominant color, we can use k-means clustering. This quantizes all pixel BGR values to be one of k colors, and we chose the most frequent. Simply averaging all colors in the image can also work well (and is faster) if the region is tightly bound.

def dominant_color(img): # via https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_ml/py_kmeans/py_kmeans_opencv/py_kmeans_opencv.html
 Z = img.reshape((-1,3)) Z = np.float32(Z) # define criteria, number of clusters(K) and apply kmeans()
 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 200, 1.0) K = 3 _, labels, palette = cv2.kmeans( Z, K, criteria, 10, cv2.KMEANS_RANDOM_CENTERS ) # via https://stackoverflow.com/a/43111221
 _, counts = np.unique(labels, return_counts=True) dominant = palette[np.argmax(counts)] return np.uint8(dominant)
elaine_face_color elaine_hair_color

Bald?

With the dominant color for each subject’s face and hair regions obtained, we can compare the values to determine baldness. Comparing colors programatically is not as simple as taking the Euclidean distance of the BGR color vectors, because the resultant differences don’t correspond well to human color difference perception. A better distance metric is ΔE* in the CIE Lab color space. I used the colormath package to perform those distance calculations.

We compare the color difference to a threshold, and this heuristic is the baldness detection. It probably gives false positives for subjects with hair color close to their skin tone.

# via http://hanzratech.in/2015/01/16/color-difference-between-2-colors-using-python.html
from colormath.color_objects import sRGBColor, LabColor
from colormath.color_conversions import convert_color
from colormath.color_diff import delta_e_cie2000 def is_bald(img, face, thresh=40): face_img = face_region(img, face) forehead_img = forehead_region(img, face) face_color = dominant_color(face_img) forehead_color = dominant_color(forehead_img) face_rgb = sRGBColor(face_color[2], face_color[1], face_color[0]) fore_rgb = sRGBColor(forehead_color[2], forehead_color[1], forehead_color[0]) delta = delta_e_cie2000( convert_color(face_rgb, LabColor), convert_color(fore_rgb, LabColor) ) return delta < thresh
low_delta high_delta

final_detection_0

final_detection_1

final_detection_2

Inpainting

Now that bald individuals have been identified, they can be removed from the image. OpenCV has an inpainting function, which is really only meant for removing small strokes from an image. The results of applying it here are…not ideal.

def rm_bald(img): tmp = np.copy(img) img_h, img_w = img.shape[:2] for b in bald_locs(img): x, y, h, w = b img_h, img_w = tmp.shape[:2] mask = np.zeros(img[:,:,0].shape, np.uint8) mask[max(0, y-50):min(img_h, y+h), max(0, x-10):min(img_w, x+w+10)] = 1 mask[max(0, y+h):min(img_h, y+int(2.5*h)), max(0, x-10-w//2):min(img_w, x + w + int(w/2) + 10) ] = 1 tmp = cv2.inpaint(tmp, mask, 10, cv2.INPAINT_TELEA) return tmp

inpainted_result

References: