掘金 后端 ( ) • 2024-05-13 10:28

前言

在识别二维码之前,首先要划分出二维码的区域,在本篇文章中将从零开始实现二维码分割的功能,并详细介绍用到的方法。

我们需要处理的图像如下: 在这里插入图片描述

完整代码

首先我们先放出完整代码,然后根据整个分割流程介绍用到的函数和方法,完整代码如下:

import cv2
import numpy as np
import matplotlib.pyplot as plt

class barCodes:
    def __init__(self, img_path, col_scale=800):
        self.image = cv2.imread(img_path)  # BGR图像
        self.gray_image = cv2.cvtColor(self.image.copy(), cv2.COLOR_RGB2GRAY)  # 灰度图
        self.col_scale = col_scale  # 根据列等比缩放图片
        self.scale = col_scale / self.image.shape[1]  # 缩放后图像宽与当前实际图像的宽的比值
        self.unscale = 1.0 / self.scale  # self.scale的倒值,用于还原缩放
        self.boxs = []  # 用于存储分割出来的框

    def scaleImage(self, image):
        # 根据比值来缩放图像
        im = image.copy()
        scale=self.scale
        im = cv2.resize(im, (int(im.shape[1] * scale), int(im.shape[0] * scale)))
        return im

    def blackHat(self, image, kernel = np.ones((1, 3), np.uint8)):
        # 黑帽操作
        im = image.copy()
        im = cv2.morphologyEx(im, cv2.MORPH_BLACKHAT, kernel=kernel, anchor=(1, 0))
        return im

    def threshold(self, image, low=10, heigh=255):
        # 根据阈值二值化
        im = image.copy()
        thresh, im = cv2.threshold(im, low, heigh, cv2.THRESH_BINARY)
        return im

    def morph_close(self, image, kernel=np.ones((1, 5), np.uint8), iterations=2):
        # 闭操作
        im = image.copy()
        im = cv2.morphologyEx(im, cv2.MORPH_CLOSE, kernel, iterations=iterations)
        return im

    def dilate(self, image, kernel = np.ones((1,3),np.uint8)):
        # 腐蚀操作
        im = image.copy()
        im = cv2.dilate(im, kernel)
        return im

    def getContours(self, image):
        # 获取边框
        im = image.copy()
        im_out = self.image.copy()
        unscale = self.unscale
        contours, hierarchy = cv2.findContours(im, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
        if contours != None:
            for contour in contours:
                if cv2.contourArea(contour) < 2000:
                    continue
                rect = cv2.minAreaRect(contour)
                rect = ((int(rect[0][0] * unscale), int(rect[0][1] * unscale)), \
                       (int(rect[1][0] * unscale), int(rect[1][1] * unscale)),
                        rect[2]
                       )
        
                box = np.int0(cv2.boxPoints(rect))
                self.boxs.append(box)
                cv2.drawContours(im_out, [box], 0, (0, 255, 0), thickness=2)
        return im_out

    def forward(self, image, process_lst):
        im = image.copy()
        for i, process in enumerate(process_lst):
            im = process(im)
            self.plot_show(im, cmap=plt.cm.gray)

        return im

    def plot_show(self, image, cmap=None):
        # 显示图像
        if cmap is None:
            plt.imshow(image)
        else:
            plt.imshow(image, cmap)
        plt.show()
        
    def getBarRect(self):
        self.plot_show(self.image[:,:,::-1])
        process_lst = [self.scaleImage, self.blackHat, self.threshold, self.morph_close, self.dilate, self.getContours]
        image = self.forward(self.gray_image, process_lst)
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        self.plot_show(image)

barcode = barCodes('test.png')
barcode.getBarRect()

函数详解

初始化

def __init__(self, img_path, col_scale=800):
	self.image = cv2.imread(img_path)  # BGR图像
	self.gray_image = cv2.cvtColor(self.image.copy(), cv2.COLOR_RGB2GRAY)  # 灰度图
	self.col_scale = col_scale  # 根据列等比缩放图片
	self.scale = col_scale / self.image.shape[1]  # 缩放后图像宽与当前实际图像的宽的比值
	self.unscale = 1.0 / self.scale  # self.scale的倒值,用于还原缩放
	self.boxs = []  # 用于存储分割出来的框

在OpenCV中使用cv2.imread从文件中读入图像

语法:
	img = cv2.imread(filename[, flags])
参数:
	filename:要读取的图像的文件名或文件路径。
	flags(可选):读取图像时的标志。常用的标志有:	
		cv2.IMREAD_COLOR:以彩色模式加载图像。这是默认值。
		cv2.IMREAD_GRAYSCALE:以灰度模式加载图像。
		cv2.IMREAD_UNCHANGED:包括图像的 alpha 通道(如果存在)

为了方便显示,我们首先选择cv2.IMREAD_COLOR读取彩色BGR图片,因为随后的处理过程需要用到灰度图,所以我们需要将彩色图片转换成灰度图。

cv2.cvtColor 用于颜色空间转换。将图像从一个颜色空间转换到另一个颜色空间。

语法:
	dst = cv2.cvtColor(src, code[, dst[, dstCn]])
参数:
	src:输入图像,即要进行颜色空间转换的原始图像。
	code:转换类型。OpenCV 提供了多种预定义的颜色空间转换类型
		cv2.COLOR_BGR2GRAY:BGR 到灰度图。
		cv2.COLOR_BGR2HSV:BGR 到 HSV(色相、饱和度、亮度)。
		cv2.COLOR_BGR2HLS:BGR 到 HLS(色相、亮度、饱和度,与 HSV 类似,但计算方式不同)。
		cv2.COLOR_HSV2BGR:HSV 到 BGR。
		cv2.COLOR_GRAY2BGR:灰度图到 BGR(结果图像的每个通道都将具有相同的灰度值)
	dst(可选):输出图像,即颜色空间转换后的图像。
	dstCn(可选):目标图像的通道。

有时我们需要处理的图像过大,会影响到处理的速度,所以我们一般需要对图像进行缩放。但是如果直接对图像reshape,会影响到图像的比例导致图像失真,所以我们最好根据比例进行缩放。

我们设置了一个名为scale的变量,来保存我们需要的宽度与图像实际宽度的比,后续可以通过scale与图像的实际宽高相乘获取到等比例缩小后的长和宽。

显示图像

def plot_show(self, image, cmap=None):
	# 显示图像
    if cmap is None:
        plt.imshow(image)
    else:
        plt.imshow(image, cmap)
    plt.show()

在本篇中使用matplotlib显示图像。

按比例缩放

def scaleImage(self, image):
	# 根据比值来缩放图像
	im = image.copy()
	scale=self.scale
	im = cv2.resize(im, (int(im.shape[1] * scale), int(im.shape[0] * scale)))
	return im

这个功能还是很简单的,获取到缩小后的长和宽之后调用cv2.resize对图像比例缩放。

语法:
	dst = cv2.resize(src, dsize[, fx[, fy[, interpolation]]])
参数:
	src:输入图像。
	dsize:目标图像的大小(宽度,高度)。
	fx(可选):水平轴(宽度)的缩放因子。当 dsize 为(0, 0)时,它才有效。
	fy(可选):垂直轴(高度)的缩放因子。当 dsize 为(0, 0) 时,它才有效。
		通常设置为与 fx 相同,以保持图像的纵横比。
	interpolation(可选):插值方法。常用的插值方法有:
		cv2.INTER_LINEAR:线性插值(默认)。
		cv2.INTER_NEAREST:最近邻插值。
		cv2.INTER_AREA:区域插值(当缩小图像时使用)。
		cv2.INTER_CUBIC:三次样条插值(当放大图像时使用)。
		cv2.INTER_LANCZOS4:Lanczos 插值。

黑帽操作

def blackHat(self, image, kernel = np.ones((1, 3), np.uint8)):
	# 黑帽操作
	im = image.copy()
	im = cv2.morphologyEx(im, cv2.MORPH_BLACKHAT, kernel=kernel, anchor=(1, 0))
	return im
黑帽操作(Black Hat)是形态学操作中的一种常见操作。
原理:	
	闭运算结果与原始图像之间的差别。
	闭运算:先膨胀,再腐蚀。
	膨胀:白色像素占据黑色像素
	腐蚀:黑色像素占据白色像素
作用:
	黑帽操作可以得到图像中较亮的小区域,而不考虑更暗的大区域。
	通常用于提取图像中的大型结构、背景或者光照不均匀造成的影响。

在黑帽操作中,第一步是先膨胀再腐蚀然后与原始图像相减,这个步骤使得处理后的图像白色区域比黑色区域更多,即黑色区域被白色区域侵蚀;第二步是与原本图像相减,经过这个步骤,处理的结果中就只剩下了边缘轮廓的线条,而条形码区域的边缘轮廓线条较为密集,经过这一步骤可以除去图像中的大部分背景。

在OpenCV中cv2.morphologyEx用于执行形态学操作,我们可以通过cv2.morphologyEx实现黑帽操作

语法:
	dst = cv2.morphologyEx(src, op, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]])

参数
	src:源图像,必须是单通道的灰度图像。
	op:形态学操作的类型,可以是以下几种之一:
		cv2.MORPH_ERODE:腐蚀操作。
		cv2.MORPH_DILATE:膨胀操作。
		cv2.MORPH_OPEN:开运算(先腐蚀后膨胀)。
		cv2.MORPH_CLOSE:闭运算(先膨胀后腐蚀)。
		cv2.MORPH_GRADIENT:形态学梯度(膨胀后减去腐蚀)。
		cv2.MORPH_TOPHAT:顶帽运算(原图像减去开运算结果)。
		cv2.MORPH_BLACKHAT:黑帽运算(闭运算结果减去原图像)。
		kernel:结构元素或卷积核,用于定义形态学操作的结构和大小。
			它通常是一个奇数大小的二维数组(矩阵)。
		dst(可选):输出图像,如果未指定,则函数会创建一个新的输出图像。
		anchor(可选):锚点位置,指定了卷积核的基准点,
			默认为(-1, -1),表示位于卷积核的中心。
		iterations(可选):操作迭代的次数,默认为1。
			对于腐蚀和膨胀操作,增加迭代次数可以加大效果。
		borderType(可选):像素外推法选择标志,默认为cv2.BORDER_CONSTANT。
		borderValue(可选):当使用cv2.BORDER_CONSTANT时,此值指定了边界像素的值。

经过这一过程后图像变为 在这里插入图片描述

阈值二值化

def threshold(self, image, low=10, heigh=255):
	# 根据阈值二值化
	im = image.copy()
	thresh, im = cv2.threshold(im, low, heigh, cv2.THRESH_BINARY)
	return im

经过黑帽操作后的图像显然还不够清晰,我们可以通过阈值二值化操作将图像的像素点变为纯黑和纯白两种颜色。

在opencv中可以使用cv2.threshold完成这一操作

语法:
	retval, dst = cv2.threshold(src, thresh, maxval, type[, dst])
参数:
	src:源图像(必须是灰度图像)。
	thresh:阈值。
	maxval:用于替换超过阈值的像素的最大值。
	type:阈值处理的类型。OpenCV 提供了几种不同的类型,
		其中最常见的有:
		cv2.THRESH_BINARY:二值化阈值处理(0~maxval)
		cv2.THRESH_BINARY_INV:反二值化阈值处理(0~maxval)
		cv2.THRESH_TRUNC:截断阈值处理,如果像素值大于阈值,则像素值设置为阈值。
		cv2.THRESH_TOZERO:低阈值归零处理,如果像素值小于或等于阈值,则像素值设置为0。
		cv2.THRESH_TOZERO_INV:高阈值归零处理,如果像素值大于阈值,则像素值设置为0。
		cv2.THRESH_OTSU(结合此值,函数会计算并选择一个最佳的全局阈值)
		cv2.THRESH_TRIANGLE(结合此值,函数会计算并选择一个基于三角形方法的最佳全局阈值)
	dst(可选):输出图像。如果省略,则使用一个新的图像。
返回值:
	retval:分割的阈值
	dst:阈值处理后的图像。

经过这一操作,图像变为 在这里插入图片描述

闭操作

def morph_close(self, image, kernel=np.ones((1, 5), np.uint8), iterations=2):
	# 闭操作
	im = image.copy()
	im = cv2.morphologyEx(im, cv2.MORPH_CLOSE, kernel, iterations=iterations)
	return im

通过闭操作,先膨胀(白色像素吞噬黑色像素)后腐蚀(黑色像素吞噬白色像素),可以使得条形码分散的各部分合并在一起。

经过操作后图像效果如下 在这里插入图片描述

腐蚀操作

def dilate(self, image, kernel = np.ones((1,3),np.uint8)):
	# 腐蚀操作
	im = image.copy()
	im = cv2.dilate(im, kernel)
    return im

通过腐蚀操作(黑色像素吞噬白色像素)可以进一步去除非条形码的线条干扰。

经过操作后图像效果如下,在这里效果并不明显,可以再多尝试一些参数组合。 在这里插入图片描述

获取边框

 def getContours(self, image):
 	# 获取边框
 	im = image.copy()
 	im_out = self.image.copy()
 	unscale = self.unscale
 	contours, hierarchy = cv2.findContours(im, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
 	if contours != None:
 		for contour in contours:
 			if cv2.contourArea(contour) < 2000:
 				continue
 			rect = cv2.minAreaRect(contour)
 			rect = ((int(rect[0][0] * unscale), int(rect[0][1] * unscale)), \
                       (int(rect[1][0] * unscale), int(rect[1][1] * unscale)),
                        rect[2]
                       )
        	box = np.int0(cv2.boxPoints(rect))
        	self.boxs.append(box)
        	cv2.drawContours(im_out, [box], 0, (0, 255, 0), thickness=2)
	return im_out

根据之前的处理结果已经可以看到一些雏形了,接下来我们将直接使用 cv2.findContours直接获取条形码轮廓区域。 cv2.findContours的用法:

语法:
	contours, hierarchy = cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])
参数:
	image: 源图像,必须是8位单通道二值图像。
	mode: 轮廓检索模式。可选参数:
		cv2.RETR_EXTERNAL: 只检索最外层的轮廓。
		cv2.RETR_LIST: 检索所有的轮廓,但不建立父子关系。
		cv2.RETR_CCOMP: 检索所有的轮廓,并建立两个级别的关系
			(即,谁是谁的外部轮廓,谁是谁的孔)。
		cv2.RETR_TREE: 检索所有的轮廓,并重新建立嵌套的轮廓关系。
	method: 轮廓近似方法。可以是以下之一:
		cv2.CHAIN_APPROX_NONE: 存储轮廓的每个点。
		cv2.CHAIN_APPROX_SIMPLE: 压缩水平、垂直和对角线段,只留下它们的端点。
		cv2.CHAIN_APPROX_TC89_L1, cv2.CHAIN_APPROX_TC89_KCOS: 使用 Teh-Chinl 的近似算法。
	contours: 检测到的轮廓。每个轮廓都是图像边界点的一个点集。
	hierarchy: 可选的输出向量,其中包含了有关图像拓扑的信息。
返回值:
	contours和hierarchy。

上述代码中首先获取了条形码所有的轮廓区域,随后遍历所有轮廓区域,通过cv2.contourArea检测轮廓面积大小,当轮廓面积过小时,很显然并不是我们所需的条形码区域,所以我们直接跳过处理。如果大于设定的轮廓面积,使用cv2.minAreaRect获取符合轮廓形状的最小矩形,获取的矩形就是条形码的区域。随后就可以画图了。

cv2.drawContours用法

语法:
	cv2.drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]])
参数:
	image:源图像,可以是二值图像或彩色图像。
		注意,cv2.drawContours 会在图像上直接绘制轮廓。
	contours:要绘制的轮廓。
	contourIdx:要绘制的轮廓的索引。如果为 -1,则绘制所有轮廓。
	color:轮廓的颜色,是一个三元组,表示 BGR颜色。
	thickness:线条的粗细。如果为负数,则轮廓将被填充。
	lineType:线条类型。默认cv2.LINE_8(8连通线)。
	hierarchy:轮廓的层次关系,是一个 NumPy 数组。通常这个参数可以省略。
	maxLevel:最多绘制的轮廓层次级别。如果未指定,则绘制所有级别。
	offset:轮廓点相对于图像原点的偏移量。通常这个参数可以省略。

这一步骤后图像效果是蓝色的,因为opencv是BGR格式而matplotlib是RGB格式的 在这里插入图片描述

如果BGR转RGB,结果如下

在这里插入图片描述

流程化

根据之前的步骤我们可以看出,执行的过程是线性的,所以我们可以将代码流程化

def forward(self, image, process_lst):
	im = image.copy()
    for i, process in enumerate(process_lst):
        im = process(im)
        self.plot_show(im, cmap=plt.cm.gray)
    return im

其中process_lst是执行过程组成的列表,image是图像,每步执行后调用plot_show显示结果。

def getBarRect(self):
	self.plot_show(self.image[:,:,::-1])
	process_lst = [self.scaleImage, self.blackHat, self.threshold, self.morph_close, self.dilate, self.getContours]
	image = self.forward(self.gray_image, process_lst)
	image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
	self.plot_show(image)

在使用时传入图像路径后调用getBarRect即可完成条码区域区分功能

barcode = barCodes('test.png')
barcode.getBarRect()