Measure width of finger in an image with OpenCV
一只修长的手与一枚安静的硬币同框,已知硬币的直径,用opencv来测量五只手指的宽度和长度,如下动图所示。本文介绍此测量方式的实现方法。
本文示例的完整代码已上传至Github:measure-width-of-finger.
参考资料
measuring-size-of-objects-in-an-image-with-opencv
感谢原作者的精彩教程!
The “pixels per metric” ratio
如measuring-size-of-objects-in-an-image-with-opencv中所述,要测量图中手指的宽度,我们需要一个参考对象来对尺寸进行“校准”,我们的参考对象要有两个重要属性:
1. 我们应该知道该参考对象的可测量的真实尺寸,例如宽度,高度或者直径,单位为毫米或厘米。
2. 我们应该能够根据对象的位置(例如始终将参考对象放置在图像的左上角)或通过外观(例如独特的颜色)轻松地在图像中找到该参考对象。无论哪种情况,我们的参考对象都应该以某种方式唯一地标识。
在本文所讲述的范例中,我用参考资料中的硬币和从网络上找到的一张手掌的图片拼接为示例图片,硬币位于图的左上角。简便起见,将硬币的直径定义为单位1,手指的宽度和长度均以硬币的直径为参考对象,并使用参考对象来定义我们的pixel_per_metric,利用这个参数即可计算出与硬币同框的手指的宽度和长度。
pixels_per_metric = object_width / know_width
Measure width of finger in an image with OpenCV
下面我们来逐步讲解实现测量手指宽度与长度的代码。
新建一个文件,命名为measureSize.py,插入以下代码:
1 2 3 4 5 6 7 8 9 10 |
# import the necessary packages from scipy.spatial import distance as dist from imutils import perspective from imutils import contours import numpy as np import argparse import imutils import math import cv2 |
以上代码用于导入本示例中需要用到的Python库,大家在参考本文代码时,请确认上述库已安装。
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 |
def midpoint(ptA, ptB): return ((ptA[0] + ptB[0]) * 0.5, (ptA[1] + ptB[1]) * 0.5) def calSize(cnt): # compute the rotated bounding box of the contour box = cv2.minAreaRect(cnt) box = cv2.cv.BoxPoints(box) if imutils.is_cv2() else cv2.boxPoints(box) box = np.array(box, dtype="int") # order the points in the contour such that they appear # in top-left, top-right, bottom-right, and bottom-left # order, then draw the outline of the rotated bounding # box box = perspective.order_points(box) cv2.drawContours(orig, [box.astype("int")], -1, (0, 255, 0), 2) # unpack the ordered bounding box, then compute the midpoint # between the top-left and top-right coordinates, followed by # the midpoint between bottom-left and bottom-right coordinates (tl, tr, br, bl) = box (tltrX, tltrY) = midpoint(tl, tr) (blbrX, blbrY) = midpoint(bl, br) # compute the midpoint between the top-left and top-right points, # followed by the midpoint between the top-righ and bottom-right (tlblX, tlblY) = midpoint(tl, bl) (trbrX, trbrY) = midpoint(tr, br) # draw the midpoints on the image cv2.circle(orig, (int(tltrX), int(tltrY)), 5, (255, 0, 0), -1) cv2.circle(orig, (int(blbrX), int(blbrY)), 5, (255, 0, 0), -1) cv2.circle(orig, (int(tlblX), int(tlblY)), 5, (255, 0, 0), -1) cv2.circle(orig, (int(trbrX), int(trbrY)), 5, (255, 0, 0), -1) # draw lines between the midpoints cv2.line(orig, (int(tltrX), int(tltrY)), (int(blbrX), int(blbrY)), (255, 0, 255), 2) cv2.line(orig, (int(tlblX), int(tlblY)), (int(trbrX), int(trbrY)), (255, 0, 255), 2) # compute the Euclidean distance between the midpoints dA = dist.euclidean((tltrX, tltrY), (blbrX, blbrY)) dB = dist.euclidean((tlblX, tlblY), (trbrX, trbrY)) # if the pixels per metric has not been initialized, then # compute it as the ratio of pixels to supplied metric # (in this case, inches) global pixelsPerMetric if pixelsPerMetric is None: pixelsPerMetric = dB / args["width"] # compute the size of the object dimA = dA / pixelsPerMetric dimB = dB / pixelsPerMetric # draw the object sizes on the image cv2.putText(orig, "{:.1f}".format(dimA), (int(tltrX - 15), int(tltrY - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (255, 255, 255), 2) cv2.putText(orig, "{:.1f}".format(dimB), (int(trbrX + 10), int(trbrY)), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (255, 255, 255), 2) |
以上代码时本示例中用到的两个子函数midpoint和calSize。
midpoint函数用于计算两个坐标点之间的中点。
calSize函数用于计算传入轮廓的最小外接矩形,计算外接矩形中每一条边的中点,以及长边和宽边中点之间的距离,并将计算值显示在图像中。
1 2 3 4 5 6 7 |
# construct the argument parse and parse the arguments ap = argparse.ArgumentParser() ap.add_argument("-i", "--image", required=True, help="path to the input image") ap.add_argument("-w", "--width", type=float, required=True, help="width of the left-most object in the image ") args = vars(ap.parse_args()) |
以上代码解析命令行输入参数。我们需要两个参数:
–image,是输入图像的路径,其中包含我们要测量的对象;
–width,是参考对象的宽度(以毫米为单位),假定是–image中左上角的对象
运行本文中完整代码的方式为:打开命令行,切换至measureSize_V1.0.py所在路径,输入以下代码运行。
1 |
python measureSize_V1.0.py -i testPalm.png -w 1 |
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 |
pixelsPerMetric = None # load the image, convert it to grayscale, and blur it slightly image = cv2.imread(args["image"]) orig = image.copy() gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (7, 7), 0) # threshold the image, then perform a series of erosions + # dilations to remove any small regions of noise thresh = cv2.threshold(gray, 45, 255, cv2.THRESH_BINARY)[1] thresh = cv2.erode(thresh, None, iterations=2) thresh = cv2.dilate(thresh, None, iterations=2) # find contours in thresholded image, then grab the largest one cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) #grab_contours:return the actual contours array cnts = imutils.grab_contours(cnts) # sort the contours from left-to-right (cnts, _) = contours.sort_contours(cnts) #Calculate the pixelsPerMetric according the reference calSize(cnts[0]) #get the contour with the max area c = max(cnts, key=cv2.contourArea) |
以上代码中,先定义全局变量pixelsPerMetric,用于辅助计算手指的宽度。
接下来加载图像,将其转换为灰度,然后使用高斯滤镜对其进行平滑处理。 然后,我们执行边缘检测以及扩张和腐蚀,以消除边缘图中边缘之间的任何间隙 。在我们的边缘图中找到与参考对象相对应的轮廓,并且从左到右对这些轮廓进行排序。
在本例中,我们默认图中仅包含参考对象和手掌。因此,最左侧的轮廓是我们的参考对象,轮廓面积最大的区域为手掌,找到图中面积最大的轮廓,并进行下一步处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Get the palm defects hull=cv2.convexHull(c,returnPoints=False) #defects is a Three-dimensional array: N * 1 * 4 #defects[0]=N,defects[n][0]: start point index,end point index, far Point index, far distance #start/end/far point index in contour defects=cv2.convexityDefects(c,hull) # convert the N*1*4 array to N*4 defectsSort = np.reshape(defects,(defects.shape[0],defects.shape[2])) #sort the new N*4 by the distance from small to large defectsSort = defectsSort[np.argsort(defectsSort[:,3]), :] #get 6 largest distance elements in defects. Take them as the effect segment point of finger defectsSort = defectsSort[(defects.shape[0] - 6):] |
解释上述代码之前,我们先来说明一下函数convexHull和convexityDefects。函数convexHull可以得到手掌轮廓的凸缺陷集合,convexityDefects可以得到各凸缺陷的特征点。
在上述代码中,convexityDefects返回值defects,是一个N*1*4的数组,其中N表示凸缺陷的数量。
defects[N][0][0]:第N个凸缺陷的起始点(startPoint);
defects[N][0][1]: 第N个凸缺陷的结束点(endPoint);
defects[N][0][2]:第N个凸缺陷的最远点(farPoint);
defects[N][0][3]:第N个凸缺陷的深度(depth);
这里我们认为convexityDefects得到的结果中,深度值较大的前6位是手指根部。为了方便根据深度值进行排序,将N*1*4转换为N*4的数组,并根据深度值从小到大进行排序,排序之后,取出深度值较大的6个点。
如下图所示,图中标注的红绿蓝的圆点深度值较大的前6位。蓝色圆点表示convexityDefects找到的最远点(farPoint),绿色为起始点(startPoint),红色为结束点(endPoint)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#get the finger roughly area sPts=[] ePts=[] fPts=[] for i in range(defectsSort.shape[0]): #start Point, endPoint, far Point, the depth of far point to convexity s,e,f,d=defectsSort[i] sPts.append(tuple(c[s][0])) ePts.append(tuple(c[e][0])) fPts.append(tuple(c[f][0])) sPts = np.array(sPts) ePts = np.array(ePts) fPts = np.array(fPts) # sort the sPts/ePts/fPts from left to right based on fPts x-coordinates sPtsSort = sPts[np.argsort(fPts[:, 0]), :] ePtsSort = ePts[np.argsort(fPts[:, 0]), :] fPtsSort = fPts[np.argsort(fPts[:, 0]), :] mPtsSort = np.floor(np.add(sPtsSort,ePtsSort)/2) |
分别从取出代表指根部分凸缺陷的起点,终点,最远点分别存放在相应的数组中。以最远点X轴坐标从左至右进行排序,起点,终点,最远点均按此顺序进行排序。
排序完成后,取起点与终点的中间坐标值,存放在数组mPtsSort 中,这里我们称其为凸缺陷的中点。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#get exact finger area proimage = thresh.copy() ROI = np.ones(thresh.shape, np.uint8) #imgroi = np.ones(thresh.shape, np.uint8) for index in range(len(fPtsSort) - 1): nIndex = index + 1 finger = [fPtsSort[index],mPtsSort[index],sPtsSort[index],ePtsSort[nIndex],mPtsSort[nIndex],fPtsSort[nIndex]] finger = np.array(finger,np.int32) cv2.drawContours(ROI, [finger],-1,(255,255,255),-1) imgroi= cv2.bitwise_and(ROI,proimage) imgroi = cv2.threshold(imgroi, 45, 255, cv2.THRESH_BINARY)[1] imgroi = cv2.erode(imgroi, None, iterations=2) |
在上述循环中,用当前凸缺陷的起点,最远点,中点,下一个凸缺陷的终点,中点,最远点组成一个轮廓,此轮廓包围的区域用白色绘制在黑色背景的ROI中。
接下来,ROI与二值化图像proimage进行与操作,即可将当前凸缺陷包含的手指的图像单独取出。
取出的包含手指部分的图像可能会有少许粘连,因此对图像进行腐蚀操作,让粘连部分分离,分离之后的imgroi如下图所示。
1 2 3 4 5 6 7 8 9 |
roiCnts,hierarchy = cv2.findContours(imgroi, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in roiCnts: calSize(cnt) cv2.imshow('measure size',orig) cv2.waitKey(0) cv2.destroyAllWindows() |
从imgroi中找到五根手指图像的轮廓,再调用calSize函数分别计算手指宽度并绘制在显示图像中。
本文到此结束,感谢阅读,欢迎关注。