Real-time Finger Detection

Source: Deep Learning on Medium


Go to the profile of Chin Huan Tan

Introduction

Finger detection is an interesting topic to explore in image processing, especially when it is applied in human-computer interaction. In this article, I’m going to explain the way to detect the number of fingers in the video captured by a laptop camera.

Overview

Basically, the first task is to detect hand in the video frame. This is the most challenging part. The proposed way is to use Background Subtraction and HSV Segmentation together to create a mask. After the hand is segmented, we will detect the number of fingers raised. There are 2 proposed methods. The first is to find the largest contour in the image which is assumed to be the hand. Then, we will find the convex hull and convexity defects which are most probably the space between fingers. This is a manual way of finding the number of fingers. The second way is to use a convolutional neural network with the mask as input to determine the number of fingers. Here is the link to the source code.

Hand detection

The most challenging part is to detect the hand in an image. There are many approaches published. For example, Background Subtraction by lzane, HSV Segmentation by Amar Prakash Pandey, detecting using Haar Cascade and neural network. However, we will only talk about background subtraction and HSV segmentation in this article.

Background subtraction

For the background subtraction to work, we need to have a background image (without the hand) first. To find the hand, we can subtract the image with hand from the background. By using OpenCV, this is quite easy to implement.

Note that the code is partially given here for explanation. For the fully functional program, go to the source code here.
First, we create a background subtractor when the background is clear (without hand).

bgSubtractor = cv2.createBackgroundSubtractorMOG2(history=10, varThreshold=30, detectShadows=False)

After the background subtractor is created, we can apply the background subtraction to every video frame to create a mask.

def bgSubMasking(self, frame):
"""Create a foreground (hand) mask
@param frame: The video frame
@return: A masked frame
"""
fgmask = bgSubtractor.apply(frame, learningRate=0)

kernel = np.ones((4, 4), np.uint8)

# The effect is to remove the noise in the background
fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_OPEN, kernel, iterations=2)
    # To close the holes in the objects
fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_CLOSE, kernel, iterations=2)

# Apply the mask on the frame and return
return cv2.bitwise_and(frame, frame, mask=fgmask)

Here is the result after the background subtraction is applied.

Note that the background is masked away.

However, there is a major problem here. The background subtraction alone will capture other moving objects in the video frames. Hence, we introduce another method.

HSV segmentation

In HSV (Hue, Saturation, Value) segmentation, the idea is to segment the hand based on the color. At first, we will sample the color of the hand. Then, we detect. Usually, a pixel in a frame or an image is represented as RGB (Red, Green, Blue). The reason we use HSV rather than RGB because RGB contains the information on the brightness of the color. Therefore, when we sample the color of the hand, we sample the brightness as well. This is an issue when we detect the hand because the hand has to be under the same brightness in order to be detected. The brightness of a color is encoded in the Value (V) in the HSV. Hence, when we sample the color of the hand, we sample only the Hue (H) and Saturation (S).

Based on the technique by Amar, we will place our hand at a location to take some samples of the hand color. By using the pixels, we form a histogram to represent the frequency of each color appears in the sample. This forms a probability distribution of the colors. By normalizing the histogram, we can find the probability of each color being a part of the hand.

handHist = cv2.calcHist([roi], [0, 1], None, [180, 256], [0, 180, 0, 256])
handHist = cv2.normalize(handHist, handHist, 0, 255, cv2.NORM_MINMAX)

The argument [0,1] means to take the channels, Hue and Saturation, ignoring the third channel, Value.
Argument [0, 180, 0, 256] specifies the range of values of Hue and Saturation. Hue ranges from 0 to 179 whereas the range of values of Saturation is from 0 to 255.

After we have created the normalized histogram of colors of the hand. We can now create the HSV mask. The mask is actually a map of probability. Each pixel contains the probability of that pixel being a part of the hand.

def histMasking(frame, handHist):
"""Create the HSV masking
@param frame: The video frame
@param handHist: The histogram generated
@return: A masked frame
"""
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
dst = cv2.calcBackProject([hsv], [0, 1], handHist, [0, 180, 0, 256], 1)
    disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (21, 21))
cv2.filter2D(dst, -1, disc, dst)
# dst is now a probability map
    # Use binary thresholding to create a map of 0s and 1s
# 1 means the pixel is part of the hand and 0 means not
ret, thresh = cv2.threshold(dst, 150, 255, cv2.THRESH_BINARY)
    kernel = np.ones((5, 5), np.uint8)
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=7)
    thresh = cv2.merge((thresh, thresh, thresh))
return cv2.bitwise_and(frame, thresh)

Below is the result after the HSV segmentation.

The downside of this segmentation is that the skin color will be detected but we want only the hand. Therefore, we will use “bitwise and” operation on the foreground mask and the HSV mask. The result will be our final mask.

histMask = histMasking(roi, handHist)
bgSubMask = bgSubMasking(roi)
mask = cv2.bitwise_and(histMask, bgSubMask)

Fingers counting

After we have gotten the mask, we can now count the number of fingers. We have 2 methods. One is to do it manually by finding convexity defects. Another one is using a convolutional neural network.

Manual method

Green: Contour
Red: Convex hull
Blue: Convexity defect

After the hand segmentation, the mask should contain only the hand. Therefore, in the manual method, we will start by finding the largest contour which is assumed to be the hand.

def threshold(mask):
"""Thresholding into a binary mask"""
grayMask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(grayMask, 0, 255, 0)
return thresh
def getMaxContours(contours):
"""Find the largest contour"""
maxIndex = 0
maxArea = 0
for i in range(len(contours)):
cnt = contours[i]
area = cv2.contourArea(cnt)
if area > maxArea:
maxArea = area
maxIndex = i
return contours[maxIndex]
thresh = threshold(mask)
_, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# There might be no contour when hand is not inside the frame
if len(contours) > 0:
maxContour = getMaxContours(contours)

After finding the largest contour, we will find its convex hull. The convex hull is simply a curve covering the contour. From the convex hull, we can find the convexity defects. Convexity defects are the place where the curve are bulged inside. These are assumed to be the spaces between the fingers. We will use this to determine the number of fingers.

def countFingers(contour):
hull = cv2.convexHull(contour, returnPoints=False)
if len(hull) > 3:
defects = cv2.convexityDefects(contour, hull)
cnt = 0
if type(defects) != type(None):
for i in range(defects.shape[0]):
s, e, f, d = defects[i, 0]
start = tuple(contour[s, 0])
end = tuple(contour[e, 0])
far = tuple(contour[f, 0])
angle = calculateAngle(far, start, end)

# Ignore the defects which are small and wide
# Probably not fingers
if d > 10000 and angle <= math.pi/2:
cnt += 1
return True, cnt
return False, 0

def calculateAngle(far, start, end):
"""Cosine rule"""
a = math.sqrt((end[0] - start[0])**2 + (end[1] - start[1])**2)
b = math.sqrt((far[0] - start[0])**2 + (far[1] - start[1])**2)
c = math.sqrt((end[0] - far[0])**2 + (end[1] - far[1])**2)
angle = math.acos((b**2 + c**2 - a**2) / (2*b*c))
return angle

When counting the convexity defects, we have to impose some limitations. We do not want to take all the convexity defects especially when there is a distortion on the contour. The limitations include the depth of the defects has to be larger than a certain value (10000 in the example above). We exclude small defects which are probably not the fingers. Besides, we exclude the defects which are wider than 90 degrees. We calculate the angle by using cosine rule.

The output will be the number of convexity defects. For example, if the number of convexity defects is two, then the number of fingers raised is three. However, using only these, we cannot differentiate between no finger raised and one finger raised. This can be solved by calculating the distance between the centroid of the contour and the highest point of the contour. If it is larger than a certain distance, the number of fingers raised is one, else no finger raised.

Convolutional neural network

Using a Convolutional Neural Network (CNN) actually simplifies a lot of work. Keras in python is a good option. It is relatively simple. Due to limited GPU memory, I have resized the video frame from 260*260 to 28*28. Feel free to try giving the original size as the input to the CNN. Below is how I construct my CNN model.

model = Sequential()
model.add(Conv2D(32, (3,3), activation=’relu’, input_shape=(28, 28, 1)))
model.add(Conv2D(64, (3,3), activation=’relu’))
model.add(MaxPooling2D((2,2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation=’relu’))
model.add(Dropout(0.5))
model.add(Dense(6, activation=’softmax’))

I have trained my model using approximately 1000 images per class and 200 images for testing. I have done some rotation, shifting and flipping on the training images. For more details, check out the source code.

I try to balance the number of training images to prevent a bias in the model.

Here are the samples of my training data.

The result looks pretty good. It achieves validation accurracy of 99% at the fifth epoch. However, the model is trained and tested on my own hand. It might not generalize well to other people’s hand. Therefore, I’m not posting my model. I have implemented a functionality to capture the images of your own hand. Preparing your own training images should be quite simple.

Finally, we can load the model and predict the result.

from keras.models import load_model
model = load_model("model_1.h5")
modelInput = cv2.resize(thresh, (28, 28))
modelInput = np.expand_dims(modelInput, axis=-1)
modelInput = np.expand_dims(modelInput, axis=0)
pred = self.model.predict(modelInput)
pred = np.argmax(pred[0])

Conclusion

Using the result from detection, we can use it as a command to interact with the computers (I have done key pressing in the source code). Of course, you can do more than that. However, there are still many improvements needed to make this application practical. Feel free to improve it.

References

  1. lzane/Fingers-Detection-using-OpenCV-and-Python. Retrieved from https://github.com/lzane/Fingers-Detection-using-OpenCV-and-Python
  2. amarlearning/Finger-Detection-and-Tracking. Retrieved from https://github.com/amarlearning/Finger-Detection-and-Tracking