-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfinger detection using ConvexHull algorithm
218 lines (161 loc) · 7.44 KB
/
finger detection using ConvexHull algorithm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
import cv2
import numpy as np
# Used for eucledian distance calculation
from sklearn.metrics import pairwise
## GLOBAL VARIABLES
# initial background is set to None
background = None
# Start with a halfway point between 0 and 1 of accumulated weight
accumulated_weight = 0.5
# ROI for hand detection.
# top right corner for filming. Can resize based on your camera resolution
roi_top = 20
roi_bottom = 300
roi_right = 250
roi_left = 600
##
def calc_accum_avg(frame, accumulated_weight):
'''
Update background and weights
Given a frame and a previous accumulated weight, computed the weighted average of the image passed in.
'''
# Grab the background
global background
# For first time, create the background from a copy of the frame.
if background is None:
background = frame.copy().astype("float")
return None
# compute weighted average, accumulate it and update the background
cv2.accumulateWeighted(frame, background, accumulated_weight)
def segment(frame, threshold=50):
'''
Segment the hand from background
:param frame:
:param threshold:
:return:
'''
global background
# Calculates the Absolute Differentce between the backgroud and the passed in frame
diff = cv2.absdiff(background.astype("uint8"), frame)
# Apply a threshold to the image so we can grab the foreground
# We only need the threshold, so we will throw away the first item in the tuple with an underscore _
_, thresholded = cv2.threshold(diff, threshold, 255, cv2.THRESH_BINARY)
# Grab the external contours form the image
# Again, only grabbing what we need here and throwing away the rest
contours, hierarchy = cv2.findContours(thresholded.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# If length of contours list is 0, then we didn't grab any contours!
if len(contours) == 0:
return None
else:
# Given the way we are using the program, the largest external contour should be the hand (largest by area)
# This will be our segment
hand_segment = max(contours, key=cv2.contourArea)
# Return both the hand segment and the thresholded hand image
return (thresholded, hand_segment)
def count_fingers(thresholded, hand_segment):
'''
Count Fingers using Convex Hull approach
:param thresholded:
:param hand_segment:
:return:
'''
# Calculated the convex hull of the hand segment
conv_hull = cv2.convexHull(hand_segment)
# Now the convex hull will have at least 4 most outward points, on the top, bottom, left, and right.
# Let's grab those points by using argmin and argmax. Keep in mind, this would require reading the documentation
# And understanding the general array shape returned by the conv hull.
# Find the top, bottom, left , and right.
# Then make sure they are in tuple format
top = tuple(conv_hull[conv_hull[:, :, 1].argmin()][0])
bottom = tuple(conv_hull[conv_hull[:, :, 1].argmax()][0])
left = tuple(conv_hull[conv_hull[:, :, 0].argmin()][0])
right = tuple(conv_hull[conv_hull[:, :, 0].argmax()][0])
# In theory, the center of the hand is half way between the top and bottom and halfway between left and right
cX = (left[0] + right[0]) // 2
cY = (top[1] + bottom[1]) // 2
# find the maximum euclidean distance between the center of the palm
# and the most extreme points of the convex hull
# Calculate the Euclidean Distance between the center of the hand and the left, right, top, and bottom.
distance = pairwise.euclidean_distances([(cX, cY)], Y=[left, right, top, bottom])[0]
# Grab the largest distance
max_distance = distance.max()
# Create a circle with 90% radius of the max euclidean distance
radius = int(0.9 * max_distance)
circumference = (2 * np.pi * radius)
# Not grab an ROI of only that circle
circular_roi = np.zeros(thresholded.shape[:2], dtype="uint8")
# draw the circular ROI
cv2.circle(circular_roi, (cX, cY), radius, 255, 10)
# Using bit-wise AND with the cirle ROI as a mask.
# This then returns the cut out obtained using the mask on the thresholded hand image.
circular_roi = cv2.bitwise_and(thresholded, thresholded, mask=circular_roi)
# Grab contours in circle ROI
contours, hierarchy = cv2.findContours(circular_roi.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# Finger count starts at 0
count = 0
# loop through the contours to see if we count any more fingers.
for cnt in contours:
# Bounding box of countour
(x, y, w, h) = cv2.boundingRect(cnt)
# Increment count of fingers based on two conditions:
# 1. Contour region is not the very bottom of hand area (the wrist)
out_of_wrist = ((cY + (cY * 0.20)) > (y + h))
# 2. Number of points along the contour does not exceed 25% of the circumference of the circular ROI (otherwise we're counting points off the hand)
limit_points = ((circumference * 0.20) > cnt.shape[0])
if out_of_wrist and limit_points:
count += 1
return count
if __name__ =="__main__":
cam = cv2.VideoCapture(0)
# Intialize a frame count
num_frames = 0
# keep looping, until interrupted
while True:
# get the current frame
ret, frame = cam.read()
# flip the frame so that it is not the mirror view
frame = cv2.flip(frame, 1)
# clone the frame
frame_copy = frame.copy()
# Grab the ROI from the frame
roi = frame[roi_top:roi_bottom, roi_right:roi_left]
# Apply grayscale and blur to ROI
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (7, 7), 0)
# For the first 30 frames we will calculate the average of the background.
# We will tell the user while this is happening
if num_frames < 60:
calc_accum_avg(gray, accumulated_weight)
if num_frames <= 59:
cv2.putText(frame_copy, "WAIT! GETTING BACKGROUND AVG.", (200, 400), cv2.FONT_HERSHEY_SIMPLEX, 1,
(0, 0, 255), 2)
cv2.imshow("Finger Count", frame_copy)
else:
# now that we have the background, we can segment the hand.
# segment the hand region
hand = segment(gray)
# First check if we were able to actually detect a hand
if hand is not None:
# unpack
thresholded, hand_segment = hand
# Draw contours around hand segment
cv2.drawContours(frame_copy, [hand_segment + (roi_right, roi_top)], -1, (255, 0, 0), 1)
# Count the fingers
fingers = count_fingers(thresholded, hand_segment)
# Display count
cv2.putText(frame_copy, str(fingers), (70, 45), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
# Also display the thresholded image
cv2.imshow("Thesholded", thresholded)
# Draw ROI Rectangle on frame copy
cv2.rectangle(frame_copy, (roi_left, roi_top), (roi_right, roi_bottom), (0, 0, 255), 5)
# increment the number of frames for tracking
num_frames += 1
# Display the frame with segmented hand
cv2.imshow("Finger Count", frame_copy)
# Close windows with Esc
k = cv2.waitKey(1) & 0xFF
if k == 27:
break
# Release the camera and destroy all the windows
cam.release()
cv2.destroyAllWindows()