注:本文使用的编程语言是python。
如果读者使用的是C++,有些代码可能需要自行变更!
前言
初学opencv的阶段,难免是从枯燥地啃文档和记函数开始。逐一而草率地“过”一遍函数用法,对于初学者而言,其实很难起到学习的进步。只有在具体的实例中,才能更好地理解函数用法和搭配 的 妙用。
笔者在视觉库cvzone和halcon的启发下,总结了些opencv实现的颜色和轮廓的提取&筛选方法,能够方便地应用在不同的项目之上。
如果读友是小白,在这里也推荐个B站上的油管搬运教程:opencv超实用实战项目,因为是手把手敲代码的,对初学者非常友好。还有,视频原作者是 巴基斯坦 的 CV工程师 Murtaza。这边附上他油管的主页 Murtaza’s Workshop,感兴趣的朋友可以看看。
言归正传,我们开始吧!
Part1. 颜色提取&筛选
颜色提取&筛选 是最直观的图像处理方式,简单粗暴但不失为有效。
其主要步骤如下:
- 1.将原图像由 RGB模型转为 HSV模型
(因为HSV模型有专门的 H色调 通道,更方便颜色的提取s) - 2.确定 目标提取颜色 HSV范围 (可以不止一个)
- 3.使用inRange()函数,获取图像掩膜
- 4.使用图像位操作,将掩膜进行合并
- 5.用掩膜覆盖原图像,使其仅保留预期的部分
针对步骤2,这里给出常用的HSV范围:
颜色 | HSV 范围 | |
---|---|---|
起始范围 | 结束范围 | |
全部 | (0, 0, 0) | (180, 255, 255) |
黑色 | (0, 0, 0) | (180, 255, 46) |
灰色 | (0, 0, 46) | (180, 43, 220) |
白色 | (0, 0, 211) | (180, 30, 255) |
红色 | (0, 43, 46) | (10, 255, 255) |
(156, 43, 46) | (180, 255, 255) | |
橙色 | (11, 43, 46) | (25, 255, 255) |
黄色 | (26, 43, 46) | (34, 255, 255) |
绿色 | (35, 43, 46) | (77, 255, 255) |
青色 | (78, 43, 46) | (99, 255, 255) |
蓝色 | (100, 43, 46) | (124, 255, 255) |
紫色 | (125, 43, 46) | (155, 255, 255) |
接下来,就是代码部分。为了使模块更通用,笔者是把功能写成python类进行封装。调用类实例化的对象,以实现颜色提取的功能。主要是在魔术方法__call__()内实现。
import cv2
import numpy as np
# 基本的导入模块
# 设置掩膜函数
def setMask(src: np.ndarray, mask: np.ndarray) -> np.ndarray:
channels = cv2.split(src)
# 通道分离
result = []
for i in range(len(channels)):
result.append(cv2.bitwise_and(channels[i], mask))
# 各通道于掩膜进行图像和操作
dest = cv2.merge(result)
# 通道合并
return dest
class ColorFilter(object):
def __init__(self):
self.colorRange = []
# 这里是颜色筛选的范围
# 存储格式为 [ ((H起始,S起始,V起始),(H结束,S结束,V结束)), ... ]
def __call__(self, src: np.ndarray) -> np.ndarray:
# 必要的函数注释
finalMask = np.zeros_like(src)[:, :, 0]
# finalMask指的是 最终合成的掩膜
hsv = cv2.cvtColor(src, cv2.COLOR_BGR2HSV)
# 将图像转为HSV通道
for each in self.colorRange:
# 逐一获取HSV范围
lower, upper = each
# HSV范围解构为 起始 和 结束
mask = cv2.inRange(hsv, lower, upper)
# 制作该HSV范围的掩膜
finalMask = cv2.bitwise_or(finalMask, mask)
# 掩膜合并 目标颜色 = 颜色1 + 颜色2 + ... + 颜色n
# 注:inRange()不在HSV范围内的部分 数值为 0
dest = setMask(src, finalMask)
# 设置掩膜
return dest
# 调用测试
if __name__ == '__main__':
src = cv2.imread(r'xxx.png') # 图片路径
colorFilter = ColorFilter() # 初始化ColorFilter()对象
colorFilter.colorRange = [
((xxx,xxx,xxx),(xxx,xxx,xxx)), # Color A
((xxx,xxx,xxx),(xxx,xxx,xxx)) # Color B
]
dest = colorFilter(src)
cv2.imshow('dest', dest)
cv2.waitKey(0)
笔者在这里以opencv的logo为例,对红、蓝色的区域进行提取。如下是效果图:
对于其他复杂的情形,可以修改上述代码中,对掩膜的图像位操作,以实现复杂的区域的合并、剪切、取反等效果。
Part2. 轮廓提取&筛选
相比较颜色提取,轮廓提取对于实际应用的普适性更好,也相应得更复杂。
其主要步骤如下:
- 1.彩色图转灰度图,二值化处理。
(如果图像噪声多,可适当使用滤波器) - 2.使用findContours()函数,提取轮廓
- 3.使用arcLength(),contourArea()函数获取 轮廓周长和面积
(也可以引入其他特征量) - 4.筛选轮廓,过滤掉无效轮廓
- 5.进行 绘制轮廓/制作掩膜 等操作
针对步骤3,这里给出halcon里常用的区域特征量:
特征名称 | 解释 | 取值范围 | 计算公式 |
面积(Area) | 轮廓的面积 | (0,+∞) | A |
周长(contlength) | 轮廓线总长 | (0,+∞) | P |
紧密度(Compactness) | 相同周长的圆和当前轮廓的面积比 | [1,+∞) | 4*Pi*A/P^2 |
圆度(Circularity) | 当前轮廓和最小外接圆的面积比 | (0,1] | A/(Pi*R外接^2) |
凸度(Convexity) | 当前轮廓和凸包的面积比 | (0,1] | A/A凸 |
矩形度(Rectangularity) | 当前轮廓和最小外接矩形的比值 | (0,1] | A/A矩 |
- 轮廓周长:arcLength()
- 轮廓面积:contourArea()
- 凸包检测:convexHull()
- 最小外接圆:minEnclosingCircle()
- 最小外接矩形:minAreaRect()
接下来,就是代码部分。同上,是封装在python类中实现。因为轮廓的筛选方式很多,这里实现的是简单的依据 轮廓面积和周长范围进行筛选。
import cv2
import numpy as np
# 导入相应的模块
class ContourFilter(object):
def __init__(self):
super(ContourFilter, self).__init__()
# 输入参数区
self.threshold = lambda image: cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 15, 21)
# 二值化算法,这里使用的是 自适应二值化,也可以使用Canny边缘检测算法 等。
self.areaRanges = []
# 轮廓面积范围
# 存储格式为 [(minArea1,maxArea1),(minArea2,maxArea2),...]
self.perimeterRanges = []
# 轮廓周长范围
# 存储格式同上
self.contourColor = (255, 127, 127)
# 轮廓的绘制颜色
self.contourThickness = 3
# 轮廓的绘制粗细
self.inPlace = False
# 是否处理后显示在原图上
self.paint = True
# 是否进行绘制
def __call__(self, src: np.ndarray) -> np.ndarray:
# 必要的函数注释
if not self.areaRanges:
self.areaRanges = [(0, float('inf'))]
if not self.perimeterRanges:
self.perimeterRanges = [(0, float('inf'))]
# 如果周长和面积范围未赋值(None),那么默认为(0,+∞)
if self.inPlace:
dest = src.copy()
else:
dest = np.zeros_like(src)
# 处理显示在 原图拷贝上 或者 在空图像上
if len(src.shape) == 3 and src.shape[2] == 3:
gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
elif len(src.shape) == 2:
gray = src
# 转灰度图处理,如果本身就是单通道,那么不进行转换
binary = self.threshold(gray)
# 二值化处理
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 轮廓提取,这里提取的是外轮廓且忽略轮廓层次信息
resultContours = []
# 轮廓的筛选结果列表
for contour in contours:
# 对每一轮廓进行遍历
perimeter = cv2.arcLength(contour, True)
# 计算轮廓长度
area = cv2.contourArea(contour)
# 计算轮廓面积
for perimeterRange, areaRange in zip(self.perimeterRanges, self.areaRanges):
if perimeterRange[0] < perimeter <= perimeterRange[1] and areaRange[0] < area <= areaRange[1]:
resultContours.append(contour)
# 记录符合筛选条件的轮廓
if self.paint:
cv2.drawContours(dest, resultContours, -1, self.contourColor, self.contourThickness)
# 绘制轮廓
return dest
# 调用测试
if __name__ == '__main__':
src = cv2.imread(r'xxx.png') # 图片路径
contourFilter = ContourFilter() # 初始化ContourFilter对象
contourFilter.areaRanges.append((minArea, maxArea))
contourFilter.perimeterRanges.append((minPerimeter, maxPerimeter))
# 对轮廓的面积和周长进行条件限制
dest = colorFilter(src)
cv2.imshow('dest', dest)
cv2.waitKey(0)
笔者在这里同样以opencv的logo为例,给出运行的效果图:
上述代码用例实现了轮廓的绘制,读者也可以自行更改代码,使其变体为生成掩膜、区域填充等功能。笔者在此就不加赘述。
结束语
笔者作为初涉计算机视觉领域的在校学生,技术水平有限。本文中倘若出现错误或者值得补充的地方,希望各位读者在评论中指出。衷心感谢每位看到本文的读者!
文章出处登录后可见!