整体思路:

  1.原图灰度化  

  2.灰度图截取mask区域  

  3.mask区域二值化  

  4.二值化图像运算(开运算)  

  5.原灰图轮廓提取 

  6.不规则轮廓校准(外接矩形/内接矩形)

注:代码依次头尾连接哦!

0.第三方库导入

import cv2 as cv
import numpy as np
import imutils
import matplotlib.pyplot as plt
import MightexUSBcameraSDK as Cam
import math

1.原图灰度化

 img = cv.imread(r"D:\picture\p7.png", cv.COLOR_GRAY2BGR)
gray = cv.imread(r"D:\picture\p7.png", cv.COLOR_BGR2GRAY)

2.灰度图截取mask区域

代码中省去了图中右侧的两个像素值分布图。从原灰度图选取一个mask区间,区间是基于图左两张图的XY像素坐标,截取自己感兴趣的mask区域,省去毫不关心并可能影响图像处理的区域。


mask = np.zeros(gray.shape[:2], np.uint8)
mask[0:430, 240:530] = 255 # 裁剪出mask区域
mask_hist = cv.calcHist([gray], [0], mask, [256], [0, 256]) # 计算mask的直方图 灰度图通道=[0] # 8 通过位运算,计算有mask的灰度图片
mask_img = cv.bitwise_and(gray, gray, mask=mask)
# show_image(mask_img, "gray image with mask", 3, "X pixel pos", "Y pixel pos")
# show_histogram(mask_hist, "histogram with masked gray image", 4,
# "m", "Gray value (black~white:0~255)", "Number of pixels")
# plt.show() # 显示画布 img = mask_img # img 作为原灰图,mask_img 为mask区域的灰度图

3.mask区域二值化

图像二值化,可理解为将图形与背景,通过一个阈值X,低于阈值X的视为黑色0,高于X的视为灰色1。

        # (1).灰度化图像
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# (2).高斯滤波
blurred = cv.GaussianBlur(gray, (5, 5), 0)
# thresh = cv.threshold(blurred, 64, 80, cv.THRESH_BINARY)[1]
# (3).二值化图像(化为0,1)
thresh = cv.threshold(blurred, 61, 80, cv.THRESH_BINARY)[1] # 调整高/低阈值,以匹配最好的识别效果
cv.imshow("thresh", thresh) # thresh——二值化,具体将闭合轮廓整体填充为灰色,其余(背景)为黑色

4.二值化图像运算(开运算)

  本人所关心的轮廓是基于类似椭圆状的区域部分,所以应想办法将尖端 "腐蚀" 掉。

  先腐蚀后膨胀——即为开运算;相对的反过来就是——闭运算。这两类运算目的,是为了消除原图拍摄时产生的轮廓(内/外)毛刺、尖端、或者噪音。此处我用开运算主要是为了腐蚀掉我不关心的轮廓尖端。

  很明显通过腐蚀后再膨胀,轮廓变得更加圆滑,并消除了尖端。

        # (4).开运算[先膨胀-后腐蚀],尝试去除噪声(去除尖端)
img2 = thresh.copy()
k = np.ones((10, 10), np.uint8) # 卷积核 如(10, 10)= 10X10的矩阵(或称数组)
thresh_open = cv.morphologyEx(img2, cv.MORPH_OPEN, k) # 开运算[先膨胀-后腐蚀] cv.imshow("open operation", thresh_open) # 暂时屏蔽

5.原灰图轮廓提取

  cnts 返回所有轮廓集合,并计算轮廓质心(cX, cY),注意这里指质心,而不是规则图形的中心,质心会随着图中部分轮廓的凹陷而稍微偏移。c 很好理解,则是cnts遍历出来的每一个轮廓。将 c 取出是为了对每个轮廓进行内接矩形的捕捉等等,从代码中可以看出 c 被当成一个参数传入进行运算。

  可以print (M)进行打印查看,就能理解cX,cY是通过轮廓总面积进行计算求取。

  x_min, x_max, y_min, y_max,则是求取轮廓的四点极值,也就是后面计算内接矩的一个标准范围。

        # (5).下一步是使用轮廓检测​​找到这些白色区域的位置:返回轮廓个数
cnts = cv.findContours(thresh_open.copy(), cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) # RETR_EXTERNAL
cnts = imutils.grab_contours(cnts) # 返回轮廓 contours —— cnts
# (6).cnts 返回的是所有轮廓,所以需要for循环来遍历每一个轮廓
for i, c in enumerate(cnts):
# 计算轮廓区域的图像矩。 在计算机视觉和图像处理中,图像矩通常用于表征图像中对象的形状。
# 这些力矩捕获了形状的基本统计特性,包括对象的面积,质心(即,对象的中心(x,y)坐标),
# 方向以及其他所需的特性。
M = cv.moments(c)
# m00是图像面积(白色区域)的总和,或者说连通域的面积;而这时m10和m01是图像白色区域上x和y坐标值的累计
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
# 1. 绘制最大内接圆
r = drawInCircle(thresh_open, img, c, cX, cY) # 2. 计算最小外接正矩形的四个顶点,是否绘制外矩形框
x_min, x_max, y_min, y_max = drawOutRectgle(c, False) # 3. 最大内接矩形
x1, x2, y1, y2 = drawInRectgle(img, c, cX, cY, x_min, x_max, y_min, y_max)
cv.drawContours(img, [c], -1, (0, 255, 0), 1) # 最外层轮廓绘制
cv.circle(img, (cX, cY), 1, (255, 255, 255), -1) # 轮廓中心点
cv.putText(img, "center%d=%s avg=%d" % (i, bgr_val, gray_avg), (cX - 90, cY - 16),
cv.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)

6.不规则轮廓校准(外接矩形/内接矩形)

(1)最大内接圆

遍历每个轮廓的所有坐标,寻求基于中心坐标(轮廓质心)最大圆直径

  白字补充的分别是质心的灰度值,和白框矩形内所有亮度的平均灰度值,这里可以忽略不是重点。

        def drawInCircle(img_open, img, cont, cX, cY):
# 绘制最大内接圆 # 最大内接圆——检索轮廓的方式
c = cont # 单个轮廓
contours = cv.findContours(img_open.copy(), cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
src = img_open.copy()
raw_dist = np.empty(src.shape, dtype=np.float32)
for ii in range(src.shape[0]):
for jj in range(src.shape[1]):
raw_dist[ii, jj] = cv.pointPolygonTest(c, (jj, ii), True) # 检测点坐标,与c轮廓坐标的距离
minVal, maxVal, _, maxDistPt = cv.minMaxLoc(raw_dist) # minMaxLoc查找最小和最大元素值及其位置ma
maxVal = abs(maxVal)
cv.circle(img, (cX, cY), np.int(maxVal)-1, (255, 255, 255), 1, cv.LINE_8, 0) # 最大内接圆
height = np.int(maxVal)-1
return np.int(maxVal)-1

(2)最大内接正矩形(中心延展算法——个人理解)

  中心延展法,其实就是中心坐标从 (cX, cY) 开始向四周延展。理解很简单,即从单个像素(cX, cY) 分别从第二、三象限,到第一、四象限(数学上以中心坐标)进行+1延展,但是此处图像很明显X:Y的比例大致将近 5:1或者4:1,故而算法实现中不能Y延展1,再到X延展1来类推。通过比例进行Y延展1,再到X延展4,再判断是否超出轮廓,若超出轮廓X部分需要遍历返回步进1(即),直到到达轮廓。例如X向左延展需要同时判定,第二、三象限皆满足条件(不超轮廓),向右延展则满足一、四象限类推,Y也是如此。

  “# 取轴更长范围作for循环”,这前部分代码是自动计算了轮廓的X:Y的比例关系,radio 为长轴比短轴的比例,也直接在延展中将X、Y的步进值与radio联系上,比较“人性化”的计算,所以当遇到Y比X长的轮廓,这个算法也适用。这部分可以写死简化,看个人取舍。

  重点是理解核心算法,位于注释 "# 第二象限延展" 即为算法的开端,当完成第二象限的延展时,返回一个标志位提示完成;当四个象限延展判断完成时,则提前终止for循环。

  有人会好奇,为何我用的不是双for循环对X、Y的遍历,其实只要理解中心延展法,其实如何写就是自己的喜好;起初我也是用双for,但是发现没必要而且更复杂,就想到了比例延展的思路,没必要每次只延展步进1,这样效率也比较低;按比例延展,只要超出轮廓才进行遍历返回(每次返回1直到回到轮廓上)。

        def drawInRectgle(img, cont, cX, cY, x_min, x_max, y_min, y_max):
"""绘制不规则最大内接正矩形"""
# img 对应的是原图, 四个极值坐标对应的是最大外接矩形的四个顶点
c = cont # 单个轮廓
# print(c)
range_x, range_y = x_max - x_min, y_max - y_min # 轮廓的X,Y的范围
x1, x2, y1, y2 = cX, cX, cY, cY # 中心扩散矩形的四个顶点x,y
cnt_range, radio = 0, 0
shape_flag = 1 # 1:轮廓X轴方向比Y长;0:轮廓Y轴方向比X长
if range_x > range_y: # 判断轮廓 X方向更长
radio, shape_flag = int(range_x / range_y), 1
range_x_left = cX - x_min
range_x_right = x_max - cX
if range_x_left >= range_x_right: # 取轴更长范围作for循环
cnt_range = int(range_x_left)
if range_x_left < range_x_right:
cnt_range = int(range_x_right)
else: # 判断轮廓 Y方向更长
radio, shape_flag = int(range_y / range_x), 0
range_y_top = cY - y_min
range_y_bottom = y_max - cY
if range_y_top >= range_y_bottom: # 取轴更长范围作for循环
cnt_range = int(range_y_top)
if range_y_top < range_y_bottom:
cnt_range = int(range_y_bottom)
print("X radio Y: %d " % radio)
print("---------new drawing range: %d-------------------------------------" % cnt_range)
flag_x1, flag_x2, flag_y1, flag_y2 = False, False, False, False
radio = 5 # 暂时设5,统一比例X:Y=5:1 因为发现某些会出现X:Y=4:1, 某些会出现X:Y=5:1
if shape_flag == 1:
radio_x = radio - 1
radio_y = 1
else:
radio_x = 1
radio_y = radio - 1
for ix in range(1, cnt_range, 1): # X方向延展,假设X:Y=3:1,那延展步进值X:Y=3:1
# 第二象限延展
if flag_y1 == False:
y1 -= 1 * radio_y # 假设X:Y=1:1,轮廓XY方向长度接近,可理解为延展步进X:Y=1:1
p_x1y1 = cv.pointPolygonTest(c, (x1, y1), False)
p_x2y1 = cv.pointPolygonTest(c, (x2, y1), False)
if p_x1y1 <= 0 or y1 <= y_min or p_x2y1 <= 0: # 在轮廓外,只进行y运算,说明y超出范围
for count in range(0, radio_y - 1, 1): # 最长返回步进延展
y1 += 1 # y超出, 步进返回
p_x1y1 = cv.pointPolygonTest(c, (x1, y1), False)
if p_x1y1 <= 0 or y1 <= y_min or p_x2y1 <= 0:
pass
else:
break
# print("y1 = %d, P=%d" % (y1, p_x1y1))
flag_y1 = True if flag_x1 == False:
x1 -= 1 * radio_x
p_x1y1 = cv.pointPolygonTest(c, (x1, y1), False) # 满足第二象限的要求,像素都在轮廓内
p_x1y2 = cv.pointPolygonTest(c, (x1, y2), False) # 满足第三象限的要求,像素都在轮廓内
if p_x1y1 <= 0 or x1 <= x_min or p_x1y2 <= 0: # 若X超出轮廓范围
# x1 += 1 # x超出, 返回原点
for count in range(0, radio_x-1, 1): #
x1 += 1 # x超出, 步进返回
p_x1y1 = cv.pointPolygonTest(c, (x1, y1), False) # 满足第二象限的要求,像素都在轮廓内
p_x1y2 = cv.pointPolygonTest(c, (x1, y2), False) # 满足第三象限的要求,像素都在轮廓内
if p_x1y1 <= 0 or x1 <= x_min or p_x1y2 <= 0:
pass
else:
break
# print("x1 = %d, P=%d" % (x1, p_x1y1))
flag_x1 = True # X轴像左延展达到轮廓边界,标志=True
# 第三象限延展
if flag_y2 == False:
y2 += 1 * radio_y
p_x1y2 = cv.pointPolygonTest(c, (x1, y2), False)
p_x2y2 = cv.pointPolygonTest(c, (x2, y2), False)
if p_x1y2 <= 0 or y2 >= y_max or p_x2y2 <= 0: # 在轮廓外,只进行y运算,说明y超出范围
for count in range(0, radio_y - 1, 1): # 最长返回步进延展
y2 -= 1 # y超出, 返回原点
p_x1y2 = cv.pointPolygonTest(c, (x1, y2), False)
if p_x1y2 <= 0 or y2 >= y_max or p_x2y2 <= 0: # 在轮廓外,只进行y运算,说明y超出范围
pass
else:
break
# print("y2 = %d, P=%d" % (y2, p_x1y2))
flag_y2 = True # Y轴像左延展达到轮廓边界,标志=True
# 第一象限延展
if flag_x2 == False:
x2 += 1 * radio_x
p_x2y1 = cv.pointPolygonTest(c, (x2, y1), False) # 满足第一象限的要求,像素都在轮廓内
p_x2y2 = cv.pointPolygonTest(c, (x2, y2), False) # 满足第四象限的要求,像素都在轮廓内
if p_x2y1 <= 0 or x2 >= x_max or p_x2y2 <= 0:
for count in range(0, radio_x - 1, 1): # 最长返回步进延展
x2 -= 1 # x超出, 返回原点
p_x2y1 = cv.pointPolygonTest(c, (x2, y1), False) # 满足第一象限的要求,像素都在轮廓内
p_x2y2 = cv.pointPolygonTest(c, (x2, y2), False) # 满足第四象限的要求,像素都在轮廓内
if p_x2y1 <= 0 or x2 >= x_max or p_x2y2 <= 0:
pass
elif p_x2y2 > 0:
break
# print("x2 = %d, P=%d" % (x2, p_x2y1))
flag_x2 = True
if flag_y1 and flag_x1 and flag_y2 and flag_x2:
print("(x1,y1)=(%d,%d)" % (x1, y1))
print("(x2,y2)=(%d,%d)" % (x2, y2))
break
# cv.line(img, (x1,y1), (x2,y1), (255, 0, 0))
cv.rectangle(img, (x1, y1), (x2, y2), (255, 255, 255), 1, 8) return x1, x2, y1, y2

(3)最小外接正矩形

  最小外接正矩形,也就是常见的人脸识别的绿框框,是不是有点像帽子(滑稽)。这个opencv已经内置boundingRect()方法,可以直接引用。

        def drawOutRectgle(cont, isdrawing=False):
# 最小外接正矩形————用于计算轮廓内每个像素灰度值(去除 矩形-外轮廓)
c = cont
st_x, st_y, width, height = cv.boundingRect(c) # 获取外接正矩形的xy
# 对应的四个顶点(0,1,2,3) 0:左上,1:右上,2:右下,3:左下
bound_rect = np.array([[[st_x, st_y]], [[st_x + width, st_y]],
[[st_x + width, st_y + height]], [[st_x, st_y + height]]])
if isdrawing:
cv.drawContours(img, [bound_rect], -1, (0, 0, 255), 1) # 绘制最小外接正矩形
x_min, x_max, y_min, y_max = st_x, st_x + width, st_y, st_y + height # 矩形四顶点
# 通过每一个最小外接正矩形(四个顶点坐标),判断矩形内累加坐标像素的灰度值,除去小于阈值的像素(在轮廓外)
return x_min, x_max, y_min, y_max

python-opencv 图像捕捉多个不规则轮廓,与轮廓内接区域(圆/矩形)思路-持续更新编辑中(会附上详细的思路解释和图片)的更多相关文章

  1. Python OpenCV 图像相识度对比

    强大的openCV能做什么我就不啰嗦,你能想到的一切图像+视频处理. 这里,我们说说openCV的图像相似度对比, 嗯,说好听一点那叫图像识别,但严格讲, 图像识别是在一个图片中进行类聚处理,比如图片 ...

  2. 基于Python & Opencv 图像-视频-处理算法

    Alg1:图像数据格式之间相互转换.png to .jpg(其他的请举一反三) import cv2 import glob def png2jpg(): images = glob.glob('*. ...

  3. python+opencv 图像预处理

    一 python 生成随机字符串序列+ 写入到图片上 from PIL import Image,ImageDraw,ImageFont import numpy as np import rando ...

  4. python Opencv图像基础操作

    读取并显示图像 如果读取图像首先要导入OpenCV包,方法为: import cv2 读取并显示图像 img = cv2.imread("C:\test1.jpg") OpenCV ...

  5. OpenCV——图像的矩(计算矩、轮廓面积、轮廓或曲线长度)

    图像矩描述了图像的全局特征 一阶矩与形状有关 二阶距显示曲线围绕直线平均值的扩展程度 三阶矩是关于平均值的对称性测量 由二阶和三阶矩可以导出7个不变矩,不变矩是图像的统计特性,满足平移.伸缩.旋转的不 ...

  6. 利用Python覆盖图像的某一部分,即改变图形一块区域(Region)的RGBA值

    原图如下: 改变过后的图如下: 查阅API写法如下: from PIL import Image from PIL import ImageDraw pilim = Image.open('1.jpg ...

  7. LeetCode python实现题解(持续更新)

    目录 LeetCode Python实现算法简介 0001 两数之和 0002 两数相加 0003 无重复字符的最长子串 0004 寻找两个有序数组的中位数 0005 最长回文子串 0006 Z字型变 ...

  8. 【python+opencv】轮廓发现

    python+opencv---轮廓发现 轮廓发现---是基于图像边缘提取的基础寻找对象轮廓的方法, 所有边缘提取的阈值选定会影响最终轮廓发现的结果. 介绍两种API使用: -cv.findConto ...

  9. opencv 图像轮廓

    图片解析: 原图: code: #include <opencv\cv.h> #include <opencv\highgui.h> #include <opencv\c ...

随机推荐

  1. IDEA新增类的快捷键

    1.选中新建类所在包,鼠标右击.然后依次点击New——>Java Class就可以弹出输入类名的弹出框了.         2.选中新建类所在包,按下alt+insert然后选择弹出框中的Jav ...

  2. 安卓APP开发的初步了解

    今天成功安装了Android Studio 并且对APP的开发框架结构进行了初步了解 如上图:app基本结构情况 下面来仔细解释一下各个方面目录的作用 首先 manifests目录:包含Android ...

  3. JS 节点笔记

    h5新增自定义属性     为了保存并使用数据,有一些数据不必要保存到数据库中:     data开头作为自定义属性并赋值     兼容性获取element.getAttribute("da ...

  4. JS DOM笔记

    js的组成     ECMAScript:JS的语法     DOM:页面文档对象模型     BOM:浏览器对象模型     web APIs     是浏览器提供的一套操作浏览器功能和页面元素的A ...

  5. http接口封装mqtt协议

    前言 .Net Core 3.1 WebApi 列出了mqtt客户端的封装目的是为了了解运作机制 1.封装mqtt客户端 mqtt底层协议基于MQTTnet 版本2.8.5 github地址 实例化[ ...

  6. java多线程(三):多线程单例模式,双重检查,volatile关键字

    一.事先准备 首先准备一个运行用的代码: public class Singleton { public static void main(String[] args) { Thread[] thre ...

  7. 《Head First 设计模式》:命令模式

    正文 一.定义 命令模式将"请求"封装成对象(命令对象),以便使用不同的"请求"来参数化其他对象. 要点: 命令模式可将"动作的请求者"从& ...

  8. NeuralCoref: python的共指消解工具教程

    转载地址 https://blog.csdn.net/blmoistawinde/article/details/81782971 共指消解 首先简要地说说共指消解是什么,有什么用处.假设机器正在阅读 ...

  9. String、StringBuilder、StringBuffer三者的区别

    StringBuffer.StringBuilder和String都可以用来代表字符串.String类是不可变类,任何对String的改变都会引发新的String对象的生成:StringBuffer. ...

  10. 笔记:安装VM Tools、vim编辑器、压缩包、Linux用户管理

    一.VM Tools安装 1.作用:方便我们在虚拟机和宿主机之间复制数据或移动文件等. 2.安装步骤: step1:在菜单栏找到虚拟机---->找到安装vm tools ,点击: step2:进 ...