之前的雷达云图实际不存在边缘探测的问题,这也是我后来意识到的问题。雷达云图之前的探测就是转为灰度图,然后二值化,形态学操作,再找边缘就可以了。但是我在做这个的时候发现两者之间区别还是很大的,我要是想把目标图片也二值化的结果很糟糕,我就在网上找了类似的。发现他没有二值化,直接高斯滤波之后就findcounter了。结果竟然挺棒的。很简单,就是直接用canny边缘算子滤波之后得到结果再寻找边缘。
这里是参考的代码
然后我就尝试了一下之后发现确实不用很复杂:
def detect_cnts(img):
# 读取输入图片
image = img
# 输入图片灰度化
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 对灰度图片执行高斯滤波
gray = cv2.GaussianBlur(gray, (7, 7), 0)
# 二值化
# 对滤波结果做边缘检测获取目标
edged = cv2.Canny(gray, 50, 100)
# 使用膨胀和腐蚀操作进行闭合对象边缘之间的间隙
edged = cv2.dilate(edged, None, iterations=1)
edged = cv2.erode(edged, None, iterations=1)
# 在边缘图像中寻找物体轮廓(即物体)
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts) # 提取轮廓
# 对轮廓按照从左到右进行排序处理
(cnts, _) = contours.sort_contours(cnts)
return cnts
该函数返回的就是探测到的边界
探测到边界之后理论上直接对外界矩形进行测量宽度就可以了,但是我的目标不是很规则,他是有点奇形怪状的,外接矩形的宽度和他实际的直径投影差的太多了,就得想办法。我一开始想根据外界矩形,把内部切割成几个部分重新检测得到结果。但是我发现最难的是再opencv里图像旋转的函数我不知道是怎么运作的,我自己写的点旋转函数和图像旋转并不重合,很重要的原因在于如果想要旋转后的图像中心不变,是需要对M矩阵进行平移变化的,我不懂。所以我没法知道矩形旋转后的坐标,就没法切割。于是想法变成了先把原图像按照外界矩形框旋转了再说。这里仅涉及到了图片旋转之后中心变了的问题的问题。这个方法也是我在网上找的。
def rotate_bound(image, angle):
# grab the dimensions of the image and then determine the
# center
(h, w) = image.shape[:2]
(cX, cY) = (w // 2, h // 2)
# grab the rotation matrix (applying the negative of the
# angle to rotate clockwise), then grab the sine and cosine
# (i.e., the rotation components of the matrix)
M = cv2.getRotationMatrix2D((cX, cY), angle, 1.0)
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
# compute the new bounding dimensions of the image
nW = int((h * sin) + (w * cos))
nH = int((h * cos) + (w * sin))
# adjust the rotation matrix to take into account translation
M[0, 2] += (nW / 2) - cX
M[1, 2] += (nH / 2) - cY
# perform the actual rotation and return the image
return cv2.warpAffine(image, M, (nW, nH))
用上面的方法旋转的话会出现黑边。我就想怎么去除,想了很久没想到。但是我找到了一个函数cv2.fillPoly,它可以通过mask使轮廓外的内容被覆盖成黑色。这样的话,旋转之后的图像就可以处理为只有我想要内容的图像,背景是黑色的,这样的话旋转出黑边也没有关系了。我在这里真的是很意外找到的,超级感谢。
def remove_backroudn(c, image):
"""
# 根据轮廓去除图像背景
:param c: 轮廓
:param image:
:return: 去除边界意外内容的图像
"""
# 全黑
mask = np.zeros(image.shape).astype(image.dtype)
# 将contours里填充白色
color = [255, 255, 255]
cv2.fillPoly(mask, [c], color)
# mask与real相与
result = cv2.bitwise_and(image, mask)
return result
为了避免直接以外界矩形的宽度为结果,我想了好久。我的想法是之前的到了背景为黑色的旋转图像。把目标旋转成垂直的,那么只要求出有颜色的点的横向最大距离,不就相当于最大的y半径了吗。所以此处求出所有非零点的索引,然后求同一水平线中,最左边的索引值(x最小)和最右边的索引值(x最大)的差值作为宽度。
注意到我其实一开始是想旋转成水平的,但是np.nonzero返回的是相同y在一起的,所以就变成竖直的,在相同的y下测量边界在x的最大变化值作为半径。
def cal_height(rotate_img):
"""
计算不为0的位置,然后横坐标相减,得出宽度
:param rotate_img_list:
:return:
"""
height_list = []
img_gray = cv2.cvtColor(rotate_img, cv2.COLOR_BGR2GRAY)
nonzero_index = np.nonzero(img_gray)
nonzero_index = np.array(nonzero_index)
for i in range(nonzero_index.shape[1]):
if i == 0:
height_max = nonzero_index[1][0]
height_min = nonzero_index[1][0]
continue
if nonzero_index[0][i] == nonzero_index[0][i-1]:
if height_max < nonzero_index[1][i]:
height_max = nonzero_index[1][i]
if height_min > nonzero_index[1][i]:
height_min = nonzero_index[1][i]
else:
height_list.append(height_max - height_min)
# 这里y变换的情况下,x的最大最小要重新赋值
height_max = nonzero_index[1][i]
height_min = nonzero_index[1][i]
height_result = max(height_list)
return height_result
上面说到想把图像旋转到目标为垂直状态,想法就是求最长边旋转到数值的角度就可以了。box是外切矩形的四个顶点坐标。
def cal_angle(box):
"""
根据轮廓计算外接矩形
:param c:
:return:
"""
# 计算旋转角度
(tl, tr, br, bl) = box
dA = cal_distance(tl, tr)
dB = cal_distance(tl, bl)
# 算出旋转角度beta
if dA > dB:
pot_1 = tr
pot_2 = br
else:
pot_1 = tr
pot_2 = tl
x_distance = pot_1[0] - pot_2[0]
y_distance = pot_1[1] - pot_2[1]
beta = math.atan(y_distance / x_distance) # 弧度制
beta = beta / math.pi * 180.0
return beta
遍历所有检测到的轮廓,轮廓的面积太小的话忽略。
def draw_outline(image, cnts):
# 循环遍历每一个轮廓
orig = image.copy()
for c in cnts:
# 如果当前轮廓的面积太少,认为可能是噪声,直接忽略掉
if cv2.contourArea(c) < 200:
continue
# 根据轮廓去除图像背景
result = remove_backroudn(c, image)
# 根据物体轮廓计算出外切矩形框
box = cv2.minAreaRect(c)
box = cv2.cv.BoxPoints(box) if imutils.is_cv2() else cv2.boxPoints(box)
box = np.array(box, dtype="int")
# 按照top-left, top-right, bottom-right, bottom-left的顺序对轮廓点进行排序,并绘制外切的BB,用绿色的线来表示
box = perspective.order_points(box)
cv2.drawContours(orig, [box.astype("int")], -1, (0, 255, 0), 2)
# 绘制BB的4个顶点,用红色的小圆圈来表示
for (x, y) in box:
print(x, y)
cv2.circle(orig, (int(x), int(y)), 5, (0, 0, 255), -1)
beta = cal_angle(box)
# 旋转
rotate_img = rotate_bound(result, beta)
# 计算高度
height_result = cal_height(rotate_img)
# 比例尺换算
# 计算高度书写位置
# 分别计算top-left 和top-right的中心点和bottom-left 和bottom-right的中心点坐标
(tl, tr, br, bl) = box
(tltrX, tltrY) = midpoint(tl, tr)
# 把高度写上
cv2.putText(orig, "{:.1f}in".format(height_result),
(int(tltrX - 15), int(tltrY - 10)), cv2.FONT_HERSHEY_SIMPLEX,
0.65, (255, 255, 255), 2)
return orig
使用过程很简单,先检测出所有轮廓,然后根据轮廓进行处理就可以了
def work(image):
cnts = detect_cnts(image)
result = draw_outline(image, cnts)
# result = np.concatenate((image,result), axis=1) # 把结果和输入一起显示用的
return result
最后在主函数调用work就可以了。结果如下,图里标注的单位是骗人的,实际就是像素,哈哈哈哈哈。
以上并没有和比例尺联系起来,测出的长度就是像素长度。可以把比例尺作为参数传入进行修改,这个就看大家想怎么做就怎么做了。
1.重构代码时候发现自己写的真的和一坨屎一样
2.其实最难的轮廓检测我写的是最简单的,所有的精力都放在代码的其他错误了
3.改代码并不适用于复杂环境,结果很难受。但是单纯背景下可以同时测量多个物体的尺寸大小(只要canny算子分的开,笑)
4.有一些参考我没写出来是当时连接都丢了,我懒得再找一边,但是直接百度肯定能找到的
好累啊,感觉根本就毕不了业,学的和屎一样。所以我要去召唤师峡谷了,寻找仅存的安慰。希望这些东西能帮助到大家,如果不能就在此祝大家生活愉快,笑。