使用 OpenCV 和 Python 进行手指检测和跟踪
TL;DR。代码在这里。
手指检测是许多计算机视觉应用的重要功能。在本应用中,我们采用基于直方图的方法将手从背景帧中分离出来。为了获得最佳效果,我们使用阈值和滤波技术进行背景消除。
我在检测手指时面临的挑战之一是区分手部与背景,并识别指尖。我将向你展示我在这个项目中使用的手指追踪技术。想了解手指检测和追踪的具体操作,请观看此视频。
在需要追踪用户手部运动的应用中,肤色直方图非常有用。该直方图可用于从图像中减去背景,仅保留包含肤色的部分。
检测皮肤的一个更简单的方法是找到特定 RGB 或 HSV 范围内的像素。如果您想了解更多关于此方法的信息,请关注此处。
上述方法的问题在于,光照条件和肤色的变化会严重影响皮肤检测。而直方图则更准确,并且能够将当前的光照条件考虑在内。
框架上绘制了绿色矩形,用户将手放在这些矩形内。应用程序从用户手上采集肤色样本,然后创建直方图。
矩形使用以下函数绘制:
def draw_rect(frame): | |
rows, cols, _ = frame.shape | |
global total_rectangle, hand_rect_one_x, hand_rect_one_y, hand_rect_two_x, hand_rect_two_y | |
hand_rect_one_x = np.array( | |
[6 * rows / 20, 6 * rows / 20, 6 * rows / 20, 9 * rows / 20, 9 * rows / 20, 9 * rows / 20, 12 * rows / 20, | |
12 * rows / 20, 12 * rows / 20], dtype=np.uint32) | |
hand_rect_one_y = np.array( | |
[9 * cols / 20, 10 * cols / 20, 11 * cols / 20, 9 * cols / 20, 10 * cols / 20, 11 * cols / 20, 9 * cols / 20, | |
10 * cols / 20, 11 * cols / 20], dtype=np.uint32) | |
hand_rect_two_x = hand_rect_one_x + 10 | |
hand_rect_two_y = hand_rect_one_y + 10 | |
for i in range(total_rectangle): | |
cv2.rectangle(frame, (hand_rect_one_y[i], hand_rect_one_x[i]), | |
(hand_rect_two_y[i], hand_rect_two_x[i]), | |
(0, 255, 0), 1) | |
return frame |
def draw_rect(frame): | |
rows, cols, _ = frame.shape | |
global total_rectangle, hand_rect_one_x, hand_rect_one_y, hand_rect_two_x, hand_rect_two_y | |
hand_rect_one_x = np.array( | |
[6 * rows / 20, 6 * rows / 20, 6 * rows / 20, 9 * rows / 20, 9 * rows / 20, 9 * rows / 20, 12 * rows / 20, | |
12 * rows / 20, 12 * rows / 20], dtype=np.uint32) | |
hand_rect_one_y = np.array( | |
[9 * cols / 20, 10 * cols / 20, 11 * cols / 20, 9 * cols / 20, 10 * cols / 20, 11 * cols / 20, 9 * cols / 20, | |
10 * cols / 20, 11 * cols / 20], dtype=np.uint32) | |
hand_rect_two_x = hand_rect_one_x + 10 | |
hand_rect_two_y = hand_rect_one_y + 10 | |
for i in range(total_rectangle): | |
cv2.rectangle(frame, (hand_rect_one_y[i], hand_rect_one_x[i]), | |
(hand_rect_two_y[i], hand_rect_two_x[i]), | |
(0, 255, 0), 1) | |
return frame |
这里没什么复杂的。我创建了四个数组hand_rect_one_x
、hand_rect_one_y
、来保存每个矩形的坐标。然后代码会遍历这些数组,并使用 将它们绘制到框架上。这里只是数组的长度,hand_rect_two_x
即。hand_rect_two_y
cv2.rectangle
total_rectangle
9
现在用户知道将手掌放在哪里了,下一步是从这些矩形中提取像素并使用它们生成 HSV 直方图。
def hand_histogram(frame): | |
global hand_rect_one_x, hand_rect_one_y | |
hsv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) | |
roi = np.zeros([90, 10, 3], dtype=hsv_frame.dtype) | |
for i in range(total_rectangle): | |
roi[i * 10: i * 10 + 10, 0: 10] = hsv_frame[hand_rect_one_x[i]:hand_rect_one_x[i] + 10, | |
hand_rect_one_y[i]:hand_rect_one_y[i] + 10] | |
hand_hist = cv2.calcHist([roi], [0, 1], None, [180, 256], [0, 180, 0, 256]) | |
return cv2.normalize(hand_hist, hand_hist, 0, 255, cv2.NORM_MINMAX) |
def hand_histogram(frame): | |
global hand_rect_one_x, hand_rect_one_y | |
hsv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) | |
roi = np.zeros([90, 10, 3], dtype=hsv_frame.dtype) | |
for i in range(total_rectangle): | |
roi[i * 10: i * 10 + 10, 0: 10] = hsv_frame[hand_rect_one_x[i]:hand_rect_one_x[i] + 10, | |
hand_rect_one_y[i]:hand_rect_one_y[i] + 10] | |
hand_hist = cv2.calcHist([roi], [0, 1], None, [180, 256], [0, 180, 0, 256]) | |
return cv2.normalize(hand_hist, hand_hist, 0, 255, cv2.NORM_MINMAX) |
这里函数将输入帧转换为 HSV。我们使用 Numpy 创建一张大小为 1000 像素、[90 * 10]
包含3
颜色通道的图像,并将其命名为ROI (感兴趣区域)。然后,它从绿色矩形中取出 900 像素的值,并将它们放入 ROI 矩阵中。
使用 ROI 矩阵创建cv2.calcHist
肤色直方图,并cv2.normalize
使用范数 Type 对该矩阵进行归一化cv2.NORM_MINMAX
。现在,我们有一个直方图来检测帧中的皮肤区域。
现在用户知道将手掌放在哪里了,下一步是从这些矩形中提取像素并使用它们生成 HSV 直方图。
现在我们得到了肤色直方图,可以用它来查找帧中包含肤色的成分。OpenCV 为我们提供了一种便捷的方法,cv2.calcBackProject
它使用直方图来分离图像中的特征。我使用这个函数将肤色直方图应用到帧中。如果您想了解更多关于反投影的知识,可以阅读这里和这里。
def hist_masking(frame, hist): | |
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) | |
dst = cv2.calcBackProject([hsv], [0, 1], hist, [0, 180, 0, 256], 1) | |
disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (31, 31)) | |
cv2.filter2D(dst, -1, disc, dst) | |
ret, thresh = cv2.threshold(dst, 150, 255, cv2.THRESH_BINARY) | |
thresh = cv2.merge((thresh, thresh, thresh)) | |
return cv2.bitwise_and(frame, thresh) |
def hist_masking(frame, hist): | |
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) | |
dst = cv2.calcBackProject([hsv], [0, 1], hist, [0, 180, 0, 256], 1) | |
disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (31, 31)) | |
cv2.filter2D(dst, -1, disc, dst) | |
ret, thresh = cv2.threshold(dst, 150, 255, cv2.THRESH_BINARY) | |
thresh = cv2.merge((thresh, thresh, thresh)) | |
return cv2.bitwise_and(frame, thresh) |
在前两行中,我将输入帧转换为 HSV 格式,然后应用了cv2.calcBackProject
肤色直方图hist
。接下来,我使用了滤波和阈值函数来平滑图像。最后,我使用该cv2.bitwise_and
函数对输入帧进行了蒙版处理。最终的帧应该只包含帧中的肤色区域。
现在我们有一个仅包含肤色区域的框架,但我们真正想要的是找到指尖的位置。使用 OpenCV,你可以在框架中查找轮廓。如果你不知道什么是轮廓,可以阅读这里。利用轮廓,你可以找到凸起缺陷,这可能是指尖的位置。
在我的应用中,我需要找到用户瞄准的指尖。为此,我确定了凸度缺陷,即距离轮廓质心最远的位置。具体代码如下:
def manage_image_opr(frame, hand_hist): | |
hist_mask_image = hist_masking(frame, hand_hist) | |
contour_list = contours(hist_mask_image) | |
max_cont = max_contour(contour_list) | |
cnt_centroid = centroid(max_cont) | |
cv2.circle(frame, cnt_centroid, 5, [255, 0, 255], -1) | |
if max_cont is not None: | |
hull = cv2.convexHull(max_cont, returnPoints=False) | |
defects = cv2.convexityDefects(max_cont, hull) | |
far_point = farthest_point(defects, max_cont, cnt_centroid) | |
print("Centroid : " + str(cnt_centroid) + ", farthest Point : " + str(far_point)) | |
cv2.circle(frame, far_point, 5, [0, 0, 255], -1) | |
if len(traverse_point) < 20: | |
traverse_point.append(far_point) | |
else: | |
traverse_point.pop(0) | |
traverse_point.append(far_point) | |
draw_circles(frame, traverse_point) |
def manage_image_opr(frame, hand_hist): | |
hist_mask_image = hist_masking(frame, hand_hist) | |
contour_list = contours(hist_mask_image) | |
max_cont = max_contour(contour_list) | |
cnt_centroid = centroid(max_cont) | |
cv2.circle(frame, cnt_centroid, 5, [255, 0, 255], -1) | |
if max_cont is not None: | |
hull = cv2.convexHull(max_cont, returnPoints=False) | |
defects = cv2.convexityDefects(max_cont, hull) | |
far_point = farthest_point(defects, max_cont, cnt_centroid) | |
print("Centroid : " + str(cnt_centroid) + ", farthest Point : " + str(far_point)) | |
cv2.circle(frame, far_point, 5, [0, 0, 255], -1) | |
if len(traverse_point) < 20: | |
traverse_point.append(far_point) | |
else: | |
traverse_point.pop(0) | |
traverse_point.append(far_point) | |
draw_circles(frame, traverse_point) |
然后确定最大的轮廓。对于最大的轮廓,它会找到外壳、质心和缺陷。
现在你已经找到了所有这些缺陷,你只需要找到距离轮廓中心最远的那个点。这个点被假设为指向手指的点。中心点是紫色,最远的点是红色。就这样,你找到了指尖。
到目前为止,所有困难的部分都已完成,现在我们要做的就是创建一个list
来存储 帧中变化的位置farthest_point
。您可以自行决定要存储多少个变化点。我这里只存储20
点。
最后,感谢您阅读这篇文章。想了解更多精彩文章,您也可以在 Twitter 上关注我 iamarpandey,或在 Github 上 关注我amarlearning。
祝你编程愉快!🤓
鏂囩珷鏉ユ簮锛�https://dev.to/amarlearning/finger-detection-and-tracking-using-opencv-and-python-586m