《OpenCV计算机视觉开发实践 基于Python朱文伟,李建英 著清华大学出版社 新华书店文轩正版 图书》【摘要 书评 试读】- 京东图书
在机器视觉中,有时需要对产品进行检测和计数。比如,网购的维生素片,有时候忘了有没有吃过,就想对瓶子里的药片计数。物体计数在日常生活和生产活动中应用非常广泛。其难点无非是对于产品的图像分割。本节将介绍一下机器视觉检测和计数的实现。
18.1 基 本 原 理
物体计数使用了基于形态学的基础上衍生出来的、基于距离变换的分水岭算法(Watershed Algorithm),其实现的效果更具普遍性。因此,我们这里所说的物体计数是基于形态学的物体检测和计数。至于什么是形态学,这里就不再赘述了,因为本章是应用章节,不会再讲很多理论。这里,我们以药片为计数对象,来讲一下整体思路:
(1)读取图片。
(2)形态学处理(在二值化前进行适度形态学处理,效果俱佳)。
(3)二值化。
(4)提取轮廓(进行药片分割)。
(5)获取轮廓索引,并筛选所需要的轮廓。
(6)画出轮廓,显示计数。
18.2 相 关 函 数
这里所说的相关函数,不是把稍后物品计数示例中的所有库函数都解释一遍,而是把在本书前面没有讲解过的函数,罗列出来解释一下。在前面章节已经讲解过的函数这里不再解释。
在图像上绘制文字
在OpenCV中,调用cv2.putText函数可添加文字到指定位置,为需要在图片中加入文字的场景提供了一种比较方便的直接方式。注意:OpenCV 不支持显示中文字符,使用 cv2.putText时添加的文本字符串不能包含中文字符(包括中文标点符号)。
该函数原型如下所示:
cv2.putText(img, text, org, fontFace, fontScale, color, thickness=None, lineType=None, bottomLeftOrigin=None)
参数img表示需要绘制文本的图像;text表示需要绘制的文本内容;org表示需要绘制的位置,图像中文本字符串的左下角,可以用一个元组来表示x、y坐标,例如(10, 100)表示x=10,y=100;fontFace表示字体类型,对应的字体类型如下:
- cv2.FONT_ITALIC:斜体字体。
- cv2.FONT_HERSHEY_PLAIN:小尺寸无衬线字体。
- cv2.FONT_HERSHEY_SIMPLEX:正常大小的无衬线字体。
- cv2.FONT_HERSHEY_DUPLEX:正常大小的无衬线字体,比FONT_HERSHEY_SIMPLEX更复杂。
- cv2.FONT_HERSHEY_COMPLEX:正常大小的衬线字体。
- cv2.FONT_HERSHEY_TRIPLEX:正常大小的衬线字体,比FONT_HERSHEY_COMPLEX更复杂。
- cv2.FONT_HERSHEY_SCRIPT_SIMPLEX:手写体字体。
- cv2.FONT_HERSHEY_SCRIPT_COMPLEX:手写体字体,比FONT_HERSHEY_SCRIPT _SIMPLEX更复杂。
参数fontScale表示字体的大小,字体比例因子乘以font-specific基本大小;color表示文本字体颜色,设置三通道的元组BGR,比如(255,0,0),常见颜色取值如下:红色(0, 0, 255)、绿色(0, 128, 0)、蓝色(255, 0, 0)、黄色(0, 255, 255)、紫色(128, 0, 128)、橙色(0, 165, 255)、白色(255, 255, 255)、黑色 (0, 0, 0)、灰色 (128, 128, 128)。
参数thickness表示字体粗细,默认为1;参数lineType表示线条类型,默认为cv2.LINE_AA;参数bottomLeftOrigin表示坐标原点,如果为真,则图像数据原点位于左下角,否则它在左上角。其中,org参数定义了文本起始位置,可以用一个元组来表示x、y坐标,例如(10, 100)表示x=10,y=100。
下面我们看几个小例子。
【例18.1】 在现成图片上绘制英文字符
(1)打开PyCharm,新建一个项目,项目名称是pythonProject。
(2)在main.py中输入代码如下:
import cv2
img = cv2.imread('test.png') # 读取彩色图像(BGR)
cv2.putText(img, 'starlight', (100, 40), cv2.FONT_HERSHEY_COMPLEX, 1, (0, 255, 0), 2, cv2.LINE_AA)
cv2.imshow('test', img) # 显示叠加图像
cv2.waitKey() # 等待按键命令
图18‑1
(3)运行程序,运行结果如图18-1所示。
我们可以看到,字符串“starligth”已经显示在图片上了。下面我们再看一个实例,在一个自己构造的蓝色背景区域上画文字。
【例18.2】 在定义画布上画文字
(1)打开PyCharm,新建一个项目,项目名称是pythonProject。
(2)在main.py中输入代码如下:
import cv2
import numpy as np
bkcolor = (0, 0, 0) #定义黑色
txtimage = np.zeros((100, 300, 3), dtype=np.uint8)
txtimage[:] = bkcolor #画布赋值黑色
cv2.putText(txtimage, "hello world", (20,30), cv2.FONT_HERSHEY_COMPLEX, 1, (255,255,255), 2); # 在画布上画字符串hello world
cv2.imshow('result', txtimage) # 显示图像
cv2.waitKey() # 等待按键命令
图18‑2
np.zeros()是NumPy库中一个基础且功能强大的函数,用于创建一个特定形状和类型的新数组,其中所有元素的初始值都为0。该函数在数据处理、科学计算和各种编程任务中都有广泛应用。代码中我们使用np.zeros((100, 300, 3), dtype=np.uint8)创建了高为100、宽为300、具有3个颜色空间(红绿蓝)的画布,并以unin8类型存储。
(3)运行程序,运行结果如图18-2所示。
在灰度图上使用OpenCV库的putText函数时,由于putText函数需要一个颜色参数,如果直接传递一个灰度图作为背景图像,文本可能不会显示任何颜色。这是因为OpenCV中的颜色通常以BGR格式表示,而不是常见的RGB格式。在灰度图中,每个像素只有一个灰度值,没有色彩信息。解决方法是在绘制文本之前,将灰度图转换为BGR图像。可以通过调用cv2.cvtColor函数,将灰度图转换为BGR图像,然后再使用putText函数。
【例18.3】 在灰度图上使用putText函数
(1)打开PyCharm,新建一个项目,项目名称是pythonProject。
(2)在main.py中输入代码如下:
import cv2
import numpy as np
# 创建一个灰度图
gray_image = np.zeros((100, 300), dtype=np.uint8)
# 将灰度图转换为BGR图像
bgr_image = cv2.cvtColor(gray_image, cv2.COLOR_GRAY2BGR)
# 设置文本参数
font = cv2.FONT_HERSHEY_SIMPLEX
text = "Hello, World!"
position = (50, 60) # 文本在图像中的位置
font_scale = 1
font_color = (255, 255, 255) # 文本颜色,这里是白色
line_type = 2
# 在BGR图像上绘制文本
cv2.putText(bgr_image, text, position, font, font_scale, font_color, line_type)
# 显示图像
cv2.imshow('Image with Text', bgr_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
图18‑3
在这个例子中,font_color被设置为(255, 255, 255),这是BGR格式表示的白色。cv2.putText函数将在转换后的BGR图像上绘制文本。注意,在展示图像之前,不需要再次将BGR图像转换为灰度图。
(3)运行程序,运行结果如图18-3所示。
18.3 代码实现药片计数
前面章节讲解了物品计数的基本原理,本节将通过代码来实现药片计数的应用。
【例18.4】 实现药片计数
(1)打开PyCharm,新建一个项目,项目名称是pythonProject。
(2)在main.py中输入代码如下:
import cv2
import numpy as np
# 返回一个随机定义的颜色
def random_color():
color_b = np.random.randint(100, 255)
color_g = np.random.randint(100, 255)
color_r = np.random.randint(100, 255)
return (color_b, color_g, color_r)
# 读取药片图像文件,第二个参数省略,则表示返回彩色图像
src = cv2.imread("med.png")
# 检查图像是否成功读取
if src is None:
print("Error: 图像未成功读取,请检查文件路径是否正确。")
exit()
cv2.imshow('srcImage', src); # 显示源图像
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (20,20), (-1, -1));
dst = cv2.morphologyEx(src, cv2.MORPH_OPEN, kernel);#执行形态学开运算操作
cv2.imshow('morphology result',dst);# 显示形态学开运算后的图像
dst = cv2.cvtColor(dst, cv2.COLOR_RGB2GRAY); #将RGB格式的图像转换为灰度图像
ret,src_binary=cv2.threshold(dst, 100, 255, cv2.THRESH_OTSU);#进行图像阈值分割
cv2.imshow( "Binarization", src_binary); #显示二值化后图像
pt = (0, 0)
cnts,hierarchy = cv2.findContours(src_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE,None,None,pt);
#print("contours: {}".format(cnts))# 打印出轮廓列表
cv2.setRNGSeed(12345); #为了后续使用随机函数,这里先设置随机数种子
a = len(cnts)
print(a) #打印药片个数
for i in range(0, a, 1):
area = cv2.contourArea(cnts[i]); #用于计算图像轮廓的面积,参数是图像的轮廓点
if area <500: #如果面积小于500
continue;
contour = cnts[i]
partial_contour = contour[:1]
for point in partial_contour:
x, y = point[0]
color = random_color(); #得到一个随机颜色值
cv2.drawContours(src, cnts, i, color, 2, 8);
cv2.putText(src, str(i), (x,y), cv2.FONT_HERSHEY_COMPLEX, 1,color, 2);
cv2.imshow("count result", src);
cv2.waitKey(0);
形态学操作在处理图像时特别有用,尤其是在去噪、边缘检测、填充孔洞等场景中。在上面代码中,首先,我们读取图像文件med.png并显示。然后,调用函数getStructuringElement用于生成一个结构元素,这个结构元素主要用于形态学操作,如膨胀、腐蚀、开运算和闭运算;我们传给它的第一个参数为cv2.MORPH_RECT,表示矩形结构元素,这是最常见的选择,所有像素的权重都相等。接着,调用函数morphologyEx来执行形态学操作,如腐蚀、膨胀、开运算、闭运算等,这里传给它的第二个参数是cv2.MORPH_OPEN,表示开运算。开运算操作完毕后显示图像。随后调用函数cvtColor,将RGB格式的图像转换为灰度图像。
接着,调用函数thresold进行图像阈值分割,即利用图像中像素的像素值大小的差别,选择一个适当的阈值,将图像分割为目标区域(target_area)与背景区域(background_area),生成一个我们需要的二值图像,主要特点是黑白分明。二值图将为我们裁剪目标区域,进行目标识别与分析,剔除不必要的背景区域,消除不必要区域对于图像处理的干扰。然后显示二值化后的图像。
再接下来,调用findContours函数查找图像轮廓。所谓图像轮廓,就是具有相同颜色或者强度的连续点组成的曲线。轮廓通常用来对图像进行分析,对物体进行识别与检测等。
为了保证图像检测的准确性,首先需要将图像进行二值化或者Canny(边缘检测)处理。图像轮廓是由一系列连续的像素点组成,这些像素点位于物体边界上。轮廓的特点是在物体和背景之间的边界位置,因此可以用来表示物体的形状和结构。轮廓可以是闭合的,也可以是开放的,具体取决于物体的形状。我们把找到的轮廓列表放在cnts中,调用函数len就可以知道有多少个轮廓,那么也就知道药片的数量了。最后我们设计一个for循环,在里面首先调用ContourArea计算整个或部分轮廓的面积,调用函数drawContours绘制图像轮廓,再调用函数putText在每个药片旁绘制一个数字,用来表示药片计数的序号。
(3)运行程序,运行结果如图18-4所示。
由结果图中可以看到,原图在经过形态学处理后,可以去除很多细节,简化后续的药片分割操作。但是,我们在计数结果图上发现,索引17号药片并没有完全分割,实际上修改形态学的结构元素尺寸(改为(22,22)),也可以完全分离这两个药片。也就是把函数getStructuringElement的调用改为:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (22,22), (-1, -1));
再运行程序,可以发现17号药片也切割成功了,如图18-5所示。