OpenCV,几何问题

形状的表示方式

矩形

  • 矩形可以用多边形/四个顶角(contour)或中心+长宽+旋转角度(box2d)两种形式进行表示

多边形形式, 及数据排序

  • 获取某个位置的顶角坐标(如最高点坐标),也可以用不带旋转的选定框间接获取。注意输入的数据poly其类型应为二维nparray
    sort_y = poly[poly[:,1].argsort()]
    sort_x = poly[poly[:,0].argsort()]
    y1 = sort_y[0,1]
    y2 = sort_y[-1,1]
    x1 = sort_x[0,0]
    x2 = sort_x[-1,0]

中心点与长宽、旋转的形式

  • 包含了矩形的中心、长宽、相对于水平轴的角度((x,y),(w,h),angle)
  • box2d数据类型转换为用四点坐标表示矩形的contour类型:先用cv2.boxPoints()转换为浮点型的坐标,再强制类型转换为int
contour = np.int0(cv2.boxPoints(rect))
cv2.drawContours(img, [contour], 0, (0, 0, 255), 4)  # green

绘制图形

用polylines绘制多边形(如四边形)

cv2.polylines(img, [np.array([[0,0],[3,0],[3,2],[0,2]])], isClosed=True, color=(0, 0, 255),thickness=2)

轮廓Contour

轮廓的数据类型

  • OpenCV给出的contour的形状为(n,1,2),取出其中一个点的(x,y)值使用索引contour[i][0]
  • 代码示例,-1为绘制所有contour
a = np.array([[100,100],[100,200],[200,200],[200,100]], dtype=np.int32)
b = np.array([[[300,300]],[[300,400]],[[400,400]],[[400,300]]], dtype=np.int32)
cv2.drawContours(img, [a], -1, (0, 255, 0), 2)
cv2.drawContours(img, [b], -1, (0, 0, 255), 2)
  • 注意参数类型为列表,其中每一项为np.array

从图像中提取轮廓 ^extract-contour

  • 第二个参数可以为cv2.RETR_EXTERNAL(无内部轮廓)、cv2.RETR_LISTcv2.RETR_TREE
  • 第三个参数可以为cv2.CHAIN_APPROX_NONEcv2.CHAIN_APPROX_SIMPLE
  • 代码示例
def find_all_contours(mask, size_min=50):
    all_contours, hierarchy = cv2.findContours(mask,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
    cont_no = [] ## serial number of contours
    for i in range(len(all_contours)):
        area = cv2.contourArea(all_contours[i])
        if area > size_min:
            cont_no.append(i)
 
    cont = [all_contours[i] for i in cont_no]
    return cont

轮廓的重心和面积

  • 面积有两种计算方式,cv2.contourArea()M['m00']
m = cv2.moments(contour)
cx = int(m["m10"] / m["m00"])
cy = int(m["m01"] / m["m00"])
area = cv2.contourArea(contour)

绘制轮廓

  • 注意和polylines()绘制多边形不一样!但也可以用于绘制多边形
  • 使用cv2.drawContours()
    • 传入的第二个参量contours是一个list,包含若干个外形。每一个外形都是一个维度为(n,1,2)的矩阵(numpy.array)
    • 第二个参量为绘制第一个外形,-1为全部,0为第一个
cv2.drawContours(img, contours, -1, (0, 0, 255),2)

使用drawContours()绘制多边形时,多边形的顶点传入时用方括号包裹,例

cv2.drawContours(img, [np.array([[0,0],[3,0],[3,2],[0,2]])], 0, (0, 0, 255),2)

填充轮廓

  • contours为上面从图像中提取轮廓find_all_contours的返回值,即一个列表,列表中的每个元素为一个轮廓
cv2.fillPoly(img, pts=contours, color=255)

合并多个轮廓

获取轮廓的边界点(一个列表)

  • 通过np.squezze()实现
contours, hierarchy = cv2.findContours(bw_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
all_contours = np.vstack(contours).squeeze()
 
for idx, contour in enumerate(contours):
    boundary = contour.squeeze()

Mask的最大外接多边形与最大内接四边形

包裹给定目标的最小外接长方形(带旋转角度)minAreaRect()

  • cv2.minAreaRect() 一般需要先生成一个轮廓列表,然后逐一检查轮廓,为其附以相应的矩形 输入为一个单一轮廓(一维列表,不能是轮廓列表/二维列表) 输出为矩形的中心(x,y),大小(w,h),旋转角度,注意不同OpenCV版本中的旋转方式可能有所不同! angle: The rotation angle in a clockwise direction. When the angle is 0, 90, 180, 270 etc., the rectangle becomes an up-right rectangle. OpenCV官方文档中描述矩形使用的是width and height

    width and height of the rectangle

for cont in contours:
    min_rect = cv2.minAreaRect(cont)  # min_area_rectangle
    min_rect = np.int64(cv2.boxPoints(min_rect)) # turn the ((x,y),(w,h),angle) to the position of four corners
    cv2.drawContours(img, [min_rect], 0, (0, 0, 255), 4)

填充给定目标的最大内接长方形(带旋转角度)

  • 效率比较低,慎用
import numpy as np
import cv2
import copy
 
## https://stackoverflow.com/a/30418912/5008845
## Find largest rectangle containing only zeros in an N×N binary matrix
def find_min_rect(src, threshold=100):
    # return: top left corner and bottom right corner
    # [x0, y0, x1, y1]
 
    rows = src.shape[0]
    cols = src.shape[1]
    W = np.zeros([rows, cols])
    H = np.zeros([rows, cols])
 
    maxRect = [0,0,0,0]
    maxArea = .0
 
    for r in range(rows):
        for c in range(cols):
            if src[r, c] > threshold:
                i = 0
                if r > 0:
                    i = H[r-1, c]
 
                H[r, c] = 1.0 + i
 
                j = 0
                if c > 0:
                    j = W[r, c-1]
 
                W[r, c] = 1.0 +j
 
            minw = W[r,c];
            for i in range(int(H[r,c])):
                minw = min(minw, W[r-i, c])
                area = (i+1) * minw;
                if area > maxArea:
                    maxArea = area;
                    maxRect = [c - minw + 1, r - i, c+1, r+1]
 
    return [int(i) for i in maxRect]
 
def area(box):
    return (box[2]-box[0])*(box[3]-box[1])
 
def find_rect_w_rotation(src, angle):
    maxdim = src.shape[0]//2
    ## Rotate the image
    m = cv2.getRotationMatrix2D((maxdim,maxdim), angle, 1)
    # Mat1b rotated
    rotated = cv2.warpAffine(src, m, src.shape)
 
    ## Keep the crop with the polygon
    pts = cv2.findNonZero(rotated)
    box = cv2.boundingRect(pts)
    x = box[0]
    y = box[1]
    w = box[2]
    h = box[3]
    crop = rotated[y:y+h,x:x+w]
 
    ## Solve the problem: "Find largest rectangle containing only zeros in an binary matrix"
    ## https://stackoverflow.com/questions/2478447/find-largest-rectangle-containing-only-zeros-in-an-n%C3%97n-binary-matrix
    p = find_min_rect(crop);
 
    return [p[0]+x, p[1]+y, p[2]+x, p[3]+y]
 
 
def find_best_angle(src, size=-1):
    # The input should be a square image
    if size == -1:
        work = src
        maxdim = work.shape[0]
        ...
    else:
        maxdim = size*2
        # resize image
        work =  cv2.resize(src, (maxdim, maxdim), interpolation = cv2.INTER_AREA)
 
    # ## Store best data
    bestRect = [0,0,0,0]
    bestAngle = 0 # int
 
    ## For each angle
    # for angle in range(90):
    for angle in range(-30, 30):
        p = find_rect_w_rotation(work, angle)
        ## If best, save result
        if (area(p) > area(bestRect)):
            # Correct the crop displacement
            # bestRect = r + box.tl();
            bestRect = [p[0], p[1], p[2], p[3]]
            bestAngle = angle
 
    return bestAngle
 
## https://stackoverflow.com/questions/32674256
## How to adapt or resize a rectangle inside an object without including (or with a few numbers) of background pixels?
def largest_rect_in_non_convex_poly(src, thumbnail_size = -1):
    ## Create a matrix big enough to not lose points during rotation
    ptz = cv2.findNonZero(src)
    bbox = cv2.boundingRect(ptz) #bbox is [x, y, w, h]
    x = bbox[0]
    y = bbox[1] 
    width = bbox[2]
    height = bbox[3]
 
    if width%2==1:
        width -= 1
    ## height += 1 may leads to a y+height > 720 case 
    if height%2==1:
        height -= 1
 
    maxdim = max(width, height)
    work = np.zeros([2*maxdim, 2*maxdim])
    work[maxdim-height//2:maxdim+height//2,maxdim-width//2:maxdim+width//2] \
        = copy.deepcopy(src[y:y+height,x:x+width])
 
    # use a thumbnail to accelerate finding the best angle first
    # or set the size = -1 to use the original image to find the best angle
    bestAngle = find_best_angle(work, thumbnail_size)
    # use the original image and the found best angle to find the largest rectangle
    bestRect = find_rect_w_rotation(work, bestAngle)
 
    ## Apply the inverse rotation
    m_inv = cv2.getRotationMatrix2D((maxdim, maxdim), -bestAngle, 1)
    x0 = bestRect[0]
    y0 = bestRect[1]
    x1 = bestRect[2]
    y1 = bestRect[3]
    ## OpenCV on Python often wants points in the form
    ## np.array([ [[x1, y1]], ..., [[xn, yn]] ])
    rectPoints = np.array([ [[x0, y0]], [[x1, y0]], [[x1, y1]], [[x0, y1]] ])
    rotatedRectPoints = cv2.transform(rectPoints, m_inv)
 
    ## Apply the reverse translations
    for i in range(len(rotatedRectPoints)):
        ## rotatedRectPoints += bbox.tl() - Point(maxdim - bbox.width / 2, maxdim - bbox.height / 2)
        rotatedRectPoints[i][0][0] += x - maxdim + width / 2
        rotatedRectPoints[i][0][1] += y - maxdim + height / 2
 
    ## Get the rotated rect
    ## (center(x, y), (width, height), angle of rotation) = cv2.minAreaRect(points)
    rrect = cv2.minAreaRect(rotatedRectPoints)
 
    return rrect
 
 
if __name__ == '__main__':
    img = cv2.imread("./mask.png", cv2.IMREAD_GRAYSCALE)
 
    # Compute largest rect inside polygon
    rect = largest_rect_in_non_convex_poly(img, thumbnail_size=100)
    print(rect)
    box = cv2.boxPoints(rect)
    box = np.int0(box)
    res = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
    cv2.drawContours(res,[box],0,(0,0,255),2)
    cv2.imshow("Result", res)
    cv2.waitKey()
 
    # p = find_min_rect(img)
    # print(p)
    # r = [[p[0],p[1]],[p[0],p[3]],[p[2],p[3]],[p[2],p[1]]]
    # res = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
    # for i in range(len(r)):
    #     cv2.line(res, r[i], r[(i+1)%4], (0, 0, 255), 2)
 
    # cv2.imshow("Result", res)
    # cv2.waitKey()

检测点是否在多边形内

  • 三个参数分别为多边形、待检测的点,及返回数据类型。如返回数据类型为False,则-1表示在多边形外,0表示在多边形上,1表示在多边形内。否则返回数据为点距离多边形的距离,负数表示在多边形外,正数表示在多边形内。
polygon = [[506, 195], [995, 179], [997, 248], [508, 265]]
pt = (100,40)
dist = cv2.pointPolygonTest(np.array(polygon), pt, False)

凸包ConvexHull

Hull的数据类型

  • Hull的数据类型是一个顶点列表,相当于一个多边形(polyon)

绘制ConvexHull的外形

  • 注意用[ ]将其转换为列表,否则画出来的只是几个角点 cv2.drawContours(contour2_img, [hull],-1,255,1)

Hull的填充

  • “cv2.fillConvexPoly(img, hull, 255) → None`

多个contour合并为一个hull ^merge-contour

def get_single_hull(contours):
    cont = np.vstack([i for i in contours])
    hull = cv2.convexHull(cont)
    return hull
  • 注意如果要进行绘制类的操作, 需要将其放入列表中,如:cv2.drawContours(hull_map, [single_hull], -1, 255, 1)
  • 新合成的单一大轮廓在部分位置并非直接应用小轮廓的外沿,图3为图1(多个contour)和图2(合并为一个hull)进行与运算的结果
  • 解决方案:对hull做dilation,然后再进行与运算,结果如图4

连通性

  • 连通性检查connectedComponents
def imshow_components(labels):
    # Map component labels to hue val
    label_hue = np.uint8(179*labels/np.max(labels))
    blank_ch = 255*np.ones_like(label_hue)
    labeled_img = cv2.merge([label_hue, blank_ch, blank_ch])
 
    # cvt to BGR for display
    labeled_img = cv2.cvtColor(labeled_img, cv2.COLOR_HSV2BGR)
 
    # set bg label to black
    labeled_img[label_hue==0] = 0
 
    cv2.imshow('labeled.png', labeled_img)
    cv2.waitKey()
 
num_labels, labels_im = cv2.connectedComponents(grey_img)
label_info = [{'no':i+1, 'area':0, 'roi':[[rope.shape[1],rope.shape[0]],[0,0]], 'center':[0,0]} for i in range(num_labels-1)]
 
## [[x1, y1], [x2, y2]] is the ROI to save some time
for iy in range(y1,y2):
    for ix in range(x1,x2):
        if labels_im[iy, ix] > 0:
            px = label_info[labels_im[iy, ix]-1]
            px['area'] += 1
            px['center'][0] += ix
            px['center'][1] += iy
            if ix < px['roi'][0][0]:
                px['roi'][0][0] = ix
            if iy < px['roi'][0][1]:
                px['roi'][0][1] = iy
            if ix > px['roi'][1][0]:
                px['roi'][1][0] = ix
            if iy > px['roi'][1][1]:
                px['roi'][1][1] = iy
 
for i in label_info:
    i['center'][0] //= i['area']
    i['center'][1] //= i['area']
 
print(label_info)
 
imshow_components(labels_im)

拓扑

提取骨架

  • 使用scikit-image的skeletonize?
    from skimage.morphology import skeletonize
     
    ## mask is a numpy array taht contains only 0s and 255s
    flesh = np.where(mask>100, 1, 0)
    skeleton = skeletonize(flesh)
    output = (np.where(skeleton==True, 255, 0)).astype(np.uint8)
    • 返回类型为二维strlen(skeleton)为行数(y方向),其中一行len(skeleton[0])为列数(x方向),列表中元素为True/False
  • 使用scikit-image的mdeial axis skeletonization?
    • from skimage.morphology import medial_axis