OpenCV,几何问题
形状的表示方式
矩形
- 矩形可以用多边形/四个顶角(contour)或中心+长宽+旋转角度(box2d)两种形式进行表示
多边形形式, 及数据排序
- 获取某个位置的顶角坐标(如最高点坐标),也可以用不带旋转的选定框间接获取。注意输入的数据poly其类型应为二维
nparraysort_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_LIST或cv2.RETR_TREE - 第三个参数可以为
cv2.CHAIN_APPROX_NONE或cv2.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 heightwidth 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
- 输入为一个包含所有contours的列表(参考从图像中提取轮廓)
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?
- https://scikit-image.org/docs/stable/auto_examples/edges/plot_skeleton.html
from skimage.morphology import skeletonize - 代码示例(转换时注意数据类型的变换,需要把OpenCV Image类型转换为skimage类型,参考):
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)- 返回类型为二维
str,len(skeleton)为行数(y方向),其中一行len(skeleton[0])为列数(x方向),列表中元素为True/False
- https://scikit-image.org/docs/stable/auto_examples/edges/plot_skeleton.html
- 使用scikit-image的mdeial axis skeletonization?
from skimage.morphology import medial_axis