一、概况

在一个 "大数据智能太阳能光伏" 项目中,有这么一个需求:
通过无人机(大疆)对整片光伏区域进行航拍(包括红外图像与可见光图像),再航拍的图片通过4G/5G实时传输到应用程序,应用程序基于计算机视觉技术,自动识别出图片中光伏组件可能存在的故障,并结合其他实时监控与分析数据,从而判断光伏组件是否存在故障并给出报警,下图是该需要的整体业务路程。

这是一个人工智能在计算机视觉领域的典型应用,要实现这些功能涉及到技术非常多如:无人机图像传输,图像识别,光伏组件实时数据监测与分析等,本文从“怎样分离出原始图片中的特定区域(光伏组件串)“这个很小的片段入手, 来阐述计算机视觉分析究竟个什么样子。文中不涉及到任何算法的原理与细节,我们先梳理实现的整体步骤,并先让代码能跑起来可以看到实际的运行结果。
二、整体思路
通过上面的描述,我们的目标非常清晰,就是要从一张带光伏组件的照片中,分离出来一块块的光伏组件串,下图为原始图片与结果输出后的图片。


那应该如何实现这个功能呢?
整体实现流程大概可分为如6个步骤
1 读取原始图片-->2 转化图片的色彩空间-->3 获取二值化图片 -->4 对图片进行形态学处理-> 5 获取图片轮廓- > 6 切割并保存区域图片
下面对每一个步骤进行说明并提供相应的代码
代码实现使用OpenCV,OpenCV是一个开源的计算机视觉库,它轻量,功能强大,实现了图像处理和计算机视觉方面的诸多算法,可以运行在Linux、Windows、Android和Mac OS操作系统上。同时提供了Python、Ruby、MATLAB等语言的接口。
1、读取原始图片
整个功能实现的过程,就是对图片不断变化的过程。所有,首先需要将磁盘上的图片文件读取到内存并转化为opencv能处理的数据,该数据是一个三维数组(300,400,3),每个维度分别表示高、宽、通道数,数组中的值表示每个像素点不同通道的取值0-255之间,同时由于原始文件的尺寸比较大(3000*4000)为了方便查看,将原始文件等比例缩小。下面是函数实现,传入图片路径与图片缩小比例(默认为1不缩小)返回缩小后的原始图片数组。
def read_resize_image(path, size=1.0):
"""
读取原始图片文件并从新设置大小
1) 使用imread读取文件
2)使用resize从新设置文件大小
3) 函数返回的是图片的原始数据,可理解为一个三维数组,每个维度分别表示长、宽、通道数,数组中的值表示图片每个像素点不同通道的取值
"""
original_image = cv2.imread(path)
if size != 1.0:
height, width = original_image.shape[:2]
size = (int(width * size), int(height * size))
original_image = cv2.resize(original_image, size, interpolation=cv2.INTER_AREA)
print(original_image)
print(original_image.shape)
return original_image
2、转化图片的色彩空间
彩色的图片默认的是RGB色彩空间,RGB分别代表三个基色(R-红色、G-绿色、B-蓝色),具体的色彩值由三个基色叠加而成,一张彩色图片就是由三个基色取不同的值叠加而成。

那为什么还要转化到HSV色彩空间呢?因为RGB通道并不能很好地反映出物体具体的颜色信息,而相比RGB,HSV通过,色调、饱和度、能够非常直观的表达色彩的色调,明亮、与鲜艳程度,方便进行颜色之间的对比。 比如红色在HSV空间中H取值的范围为010和160180,不同的色彩可以通过不同的H、S、V的不同取值范围进行区分,在下面转化二值化图片处理中就会使用HSV的取值范围。
下面是获取HSV图片的函数,传入原始图片数据,返回HSV图片数据。
def get_hsv_image(original_image):
"""
获取hsv色彩空间图片
1) 先将BGR色彩转化为HSV色彩
"""
hsv_image = cv2.cvtColor(original_image, cv2.COLOR_BGR2HSV) # 将BGR色彩转化为HSV色彩
cv2.imshow("HSV Image", hsv_image)
print(hsv_image.shape)
return hsv_image
下图为HSV图像

3、获取二值化图片
对于RGB或者HSV图片,每个像素点的值由0-255的数值组成,而在选取某个区域时,需要标识出感兴趣的区域,与不感兴趣的区域,这是一个典型的非1即0问题。而二值化图像就具有该特征,即每个像素点的值只能是0或者255,对应的颜色就只有黑色与白色,如下图,光伏组件是白色区域,其他的背景是黑色区域。
具体实现:先设定一个HSV的阀值,二值化的核心原理就是利用阀值作为分隔条件,如大于阀值为0(黑色)小于阀值为255(白色)。可以使用窗口动态调节阀值,通过实时观测二值化分割效果来确定阀值(通过窗口动态获取阀值的方法可以参考《滑动块动态获取二值化阀值》),这里我们初步选取的阀值如下:
lower = np.array([44, 57, 130])
upper = np.array([120, 191, 249])
mask_image = get_mask_image(hsv_image, lower, upper) # 获取二值化图像 通过HSV的高低阈值,提取图像部分区域
下面函数用来对HSV图片进行二值化,函数传入HSV图片与最低阀值和最高阀值,返回的结果为二值化图片,一个数值由0或255构成二维数组(见打印结果)
def get_mask_image(hsv_image, lower, upper):
"""
获取二值化图片
1)通过阀值获取二值化图像
"""
mask_image = cv2.inRange(hsv_image, lower, upper) # 获取二值化图片
cv2.imshow("Mask Image", mask_image)
print(mask_image)
print(mask_image.shape)
return mask_image

4、对图片进行形态学处理
通过二值化处理之后的图片,可以明显的区分光伏组件串,但是存在一个问题:即光伏组件串直接的缝隙被分开了,我们希望能去掉缝隙将组件串作为一个整体识别出来,而这就需要用到图像形态学处理。
形态学处理一般包括两个操作"腐蚀”与"膨胀",其目的是为了改变物体的形状,通过字面意思也很好理解,腐蚀就是”变瘦”,膨胀就是”变胖”。
下面的函数进行形态学处理,函数传入二值化图片,返回处理过后的图片。
def get_morphology_image(mask_image):
"""
对二值化图像进行形态学处理-消除噪音影响
1) 先进行膨胀处理
膨胀就是使用算法,来将图像的边缘扩大些。作用就是将目标的边缘或者是内部的坑填掉,去掉较小的孔洞
2) 再进行腐蚀处理
腐蚀:腐蚀会把物体的边界腐蚀掉,主要应用在去除白噪声,也可以断开连在一起的物体
"""
kernel = np.ones((5, 5), np.uint8)
dialationIamge = cv2.dilate(mask_image, kernel, iterations=1) # 膨胀处理
eroded_iamge = cv2.erode(dialationIamge, kernel, iterations=1) # 腐蚀处理
cv2.imshow("Eroded Iamge ", eroded_iamge) # 显示形态学处理过后的图片
return eroded_iamge
进过形态学处理过后的效果,相比原来二值化图片,处理后的光伏组件串之间的间隙消除了。

5、获取组件串轮廓区域
通过上面5个步骤的处理,终于得到了一张背景为黑色,目标物体为白色的图片,接下来就需要将图片中白色区域轮廓识别出来,代码如下函数,传入一个形态学处理过后的二值化图片,返回所有的组件轮廓, 代码含义可见注释。
def get_contours(eroded_iamge):
"""
获取光伏组件串图片的轮廓并在原始图像上绘制轮廓
输入为形态学处理后的二值图像,黑色为背景,白色为目标
cv2.RETR_EXTERNAL 只检测外轮廓
cv2.CHAIN_APPROX_SIMPLE
"""
# contours是轮廓本身,hierarchy 每条轮廓对应的属性(后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号)
contours, hierarchy = cv2.findContours(eroded_iamge, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 获取组件区域轮廓
return contours
6、切割并保存区域图片
最后一步,根据轮廓数据在原始图片上进行区域切割并将切割后图片保存至磁盘,实现函数如下,传入轮廓区域与原始图片,返回轮廓处理的数量。
def get_and_save_areas(contours, original_image):
"""
在原始图片中切割并保存轮廓区域
"""
min_area = 300 # 过滤那些小面积的噪音区域
index = 0 # 用来标识组件区域的数量
for numcontours, contour in enumerate(contours):
iamge_area = cv2.contourArea(contour)
if iamge_area > min_area:
x, y, w, h = cv2.boundingRect(contour)
module_image = original_image[y:y + h, x: x + w]
index = index + 1
cv2.imwrite(
'Output/' + str(x) + '_' + str(y) + '_' + str(w) + '_' + str(h) + '_' + str(iamge_area) + '_' + str(
index) + '.jpg', module_image) # 文件名以坐标(X、Y),图片的高度与宽度,图片面积,文件序号
return index
全部完整代码如下:
为了方便阅读与整理思路,将每个处理步骤封装为一个函数,并使用一个总函数来组合每个处理步骤。
import cv2
import numpy as np
def read_resize_image(path, size=1.0):
"""
读取原始图片文件并从新设置大小
1) 使用imread读取文件
2)使用resize从新设置文件大小
3) 函数返回的是图片的原始数据,可理解为一个三维数组,每个维度分别表示长、宽、通道数,数组中的值表示图片每个像素点不同通道的取值
"""
original_image = cv2.imread(path)
if size != 1.0:
height, width = original_image.shape[:2]
size = (int(width * size), int(height * size))
original_image = cv2.resize(original_image, size, interpolation=cv2.INTER_AREA)
# print(original_image)
# print(original_image.shape)
return original_image
def get_hsv_image(original_image):
"""
获取hsv色彩空间图片
1) 先将BGR色彩转化为HSV色彩
"""
hsv_image = cv2.cvtColor(original_image, cv2.COLOR_BGR2HSV) # 将BGR色彩转化为HSV色彩
cv2.imshow("HSV Image", hsv_image)
print(hsv_image.shape)
return hsv_image
def get_mask_image(hsv_image, lower, upper):
"""
获取二值化图片
1)通过阀值获取二值化图像
"""
mask_image = cv2.inRange(hsv_image, lower, upper) # 获取二值化图片
cv2.imshow("Mask Image", mask_image)
print(mask_image)
print(mask_image.shape)
return mask_image
def get_morphology_image(mask_image):
"""
对二值化图像进行形态学处理-消除噪音影响
1) 先进行膨胀处理
膨胀就是使用算法,来将图像的边缘扩大些。作用就是将目标的边缘或者是内部的坑填掉,去掉较小的孔洞
2) 再进行腐蚀处理
腐蚀:腐蚀会把物体的边界腐蚀掉,主要应用在去除白噪声,也可以断开连在一起的物体
"""
kernel = np.ones((5, 5), np.uint8)
dialationIamge = cv2.dilate(mask_image, kernel, iterations=1) # 膨胀处理
eroded_iamge = cv2.erode(dialationIamge, kernel, iterations=1) # 腐蚀处理
cv2.imshow("Eroded Iamge ", eroded_iamge) # 显示形态学处理过后的图片
return eroded_iamge
def get_contours(eroded_iamge):
"""
获取光伏组件串图片的轮廓并在原始图像上绘制轮廓
输入为形态学处理后的二值图像,黑色为背景,白色为目标
cv2.RETR_EXTERNAL 只检测外轮廓
cv2.CHAIN_APPROX_SIMPLE
"""
# contours是轮廓本身,hierarchy 每条轮廓对应的属性(后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号)
contours, hierarchy = cv2.findContours(eroded_iamge, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 获取组件区域轮廓
return contours
def get_and_save_areas(contours, original_image):
"""
在原始图片中切割并保存轮廓区域
"""
min_area = 300 # 过滤那些小面积的噪音区域
index = 0 # 用来标识组件区域的数量
for numcontours, contour in enumerate(contours):
iamge_area = cv2.contourArea(contour)
if iamge_area > min_area:
x, y, w, h = cv2.boundingRect(contour)
module_image = original_image[y:y + h, x: x + w]
index = index + 1
cv2.imwrite(
'Output/' + str(x) + '_' + str(y) + '_' + str(w) + '_' + str(h) + '_' + str(iamge_area) + '_' + str(
index) + '.jpg', module_image) # 文件名以坐标(X、Y),图片的高度与宽度,图片面积,文件序号
return index
def execute():
"""
组合执行每个一个处理步骤
"""
path = 'Images/DJI_0537.jpg'
original_image = read_resize_image(path, 0.3) # 原始图像为3000X40000为方便观察,先将图片缩小5倍 既0.2
hsv_image = get_hsv_image(original_image) # 将rgb原始图像转化为hsv图像
lower = np.array([44, 57, 130])
upper = np.array([120, 191, 249])
mask_image = get_mask_image(hsv_image, lower, upper) # 获取二值化图像 通过HSV的高低阈值,提取图像部分区域
eroded_iamge = get_morphology_image(mask_image) # 对二值化的图像进行形态学处理
contours = get_contours(eroded_iamge) # 获取光伏组件轮廓区域
get_and_save_areas(contours, original_image) # 将获取的轮廓区域截取图片并保存到文件
cv2.drawContours(original_image, contours, -1, (0, 0, 255), 1) # 在原图上绘制轮廓
cv2.imshow("Original Image", original_image) # 显示原始带轮廓图像
execute() # 执行入口函数
while True:
cv2.waitKey(1)
网友评论