图像锐化
kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], np.float32) #定义一个核 dst = cv2.filter2D(img, -1, kernel=kernel) plt.imshow(dst)
边缘检测
- Canny边缘检测的简单概念
- OpenCV函数:
cv2.Canny()
Canny边缘检测方法常被誉为边缘检测的最优方法:
cv2.Canny()
进行边缘检测,参数2、3表示最低、高阈值,下面来解释下具体原理。
经验之谈:之前我们用低通滤波的方式模糊了图片,那反过来,想得到物体的边缘,就需要用到高通滤波。
Canny边缘检测
Canny边缘提取的具体步骤如下:
- 使用5×5高斯滤波消除噪声:
边缘检测本身属于锐化操作,对噪点比较敏感,所以需要进行平滑处理。
K=1256[1464141624164624362464162416414641]K=\frac{1}{256}\left[ \begin{matrix} 1 & 4 & 6 & 4 & 1 \newline 4 & 16 & 24 & 16 & 4 \newline 6 & 24 & 36 & 24 & 6 \newline 4 & 16 & 24 & 16 & 4 \newline 1 & 4 & 6 & 4 & 1 \end{matrix} \right]K=2561[1464141624164624362464162416414641]
- 计算图像梯度的方向:
首先使用Sobel算子计算两个方向上的梯度GxG_xGx和GyG_yGy,然后算出梯度的方向:
θ=arctan(GyGx)\theta=\arctan(\frac{G_y}{G_x})θ=arctan(GxGy)
保留这四个方向的梯度:0°/45°/90°/135°,有什么用呢?我们接着看。
- 取局部极大值:
梯度其实已经表示了轮廓,但为了进一步筛选,可以在上面的四个角度方向上再取局部极大值
- 滞后阈值:
经过前面三步,就只剩下0和可能的边缘梯度值了,为了最终确定下来,需要设定高低阈值:
- 像素点的值大于最高阈值,那肯定是边缘
- 同理像素值小于最低阈值,那肯定不是边缘
- 像素值介于两者之间,如果与高于最高阈值的点连接,也算边缘,所以上图中C算,B不算
Canny推荐的高低阈值比在2:1到3:1之间。
img = cv2.imread('lena.jpg') img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) edges = cv2.Canny(img, 30, 70) # canny边缘检测 plt.imshow(edges)
先阈值分割后检测
其实很多情况下,阈值分割后再检测边缘,效果会更好。
_, thresh = cv2.threshold(img, 124, 255, cv2.THRESH_BINARY) edges = cv2.Canny(thresh, 30, 70) plt.imshow(edges)
小结
- Canny是用的最多的边缘检测算法,用
cv2.Canny()
实现。
腐蚀与膨胀
- 了解形态学操作的概念
- 学习膨胀、腐蚀、开运算和闭运算等形态学操作
- OpenCV函数:
cv2.erode()
,cv2.dilate()
,cv2.morphologyEx()
啥叫形态学操作
形态学操作其实就是改变物体的形状,比如腐蚀就是"变瘦",膨胀就是"变胖"。
经验之谈:形态学操作一般作用于二值化图,来连接相邻的元素或分离成独立的元素。腐蚀和膨胀是针对图片中的白色部分!
腐蚀
腐蚀的效果是把图片"变瘦",其原理是在原图的小区域内取局部最小值。因为是二值化图,只有0和255,所以小区域内有一个是0该像素点就为0。
这样原图中边缘地方就会变成0,达到了瘦身目的
OpenCV中用cv2.erode()
函数进行腐蚀,只需要指定核的大小就行:
img = cv2.imread('lena.jpg') img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) kernel = np.ones((5, 5), np.uint8) erosion = cv2.erode(img, kernel) # 腐蚀
这个核也叫结构元素,因为形态学操作其实也是应用卷积来实现的。结构元素可以是矩形/椭圆/十字形,可以用
cv2.getStructuringElement()
来生成不同形状的结构元素,比如:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 矩形结构 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) # 椭圆结构 kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5)) # 十字形结构
膨胀
膨胀与腐蚀相反,取的是局部最大值,效果是把图片"变胖":
dilation = cv2.dilate(img, kernel) # 膨胀 plt.imshow(dilation)
开/闭运算
先腐蚀后膨胀叫开运算(因为先腐蚀会分开物体,这样容易记住),其作用是:分离物体,消除小区域。这类形态学操作用cv2.morphologyEx()
函数实现:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 定义结构元素 opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel) # 开运算 plt.imshow(opening) 复制代码
闭运算则相反:先膨胀后腐蚀(先膨胀会使白色的部分扩张,以至于消除/"闭合"物体里面的小黑洞,所以叫闭运算)
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel) # 闭运算 plt.imshow(closing) 复制代码
经验之谈:很多人对开闭运算的作用不是很清楚,但看上图↑,不用怕:如果我们的目标物体外面有很多无关的小区域,就用开运算去除掉;如果物体内部有很多小黑洞,就用闭运算填充掉。
使用OpenCV摄像头与加载视频
学习打开摄像头捕获照片、播放本地视频、录制视频等。
- 打开摄像头并捕获照片
- 播放本地视频,录制视频
- OpenCV函数:
cv2.VideoCapture()
,cv2.VideoWriter()
import IPython # 加载视频 IPython.display.Video('demo_video.mp4') 复制代码
Your browser does not support the video
element.
打开摄像头
要使用摄像头,需要使用cv2.VideoCapture(0)
创建VideoCapture对象,参数0指的是摄像头的编号,如果你电脑上有两个摄像头的话,访问第2个摄像头就可以传入1,依此类推。
# 打开摄像头并灰度化显示 import cv2 capture = cv2.VideoCapture(0) while(True): # 获取一帧 ret, frame = capture.read() # 将这帧转换为灰度图 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) cv2.imshow('frame', gray) if cv2.waitKey(1) == ord('q'): break 复制代码
capture.read()
函数返回的第1个参数ret(return value缩写)是一个布尔值,表示当前这一帧是否获取正确。cv2.cvtColor()
用来转换颜色,这里将彩色图转成灰度图。
另外,通过cap.get(propId)
可以获取摄像头的一些属性,比如捕获的分辨率,亮度和对比度等。propId是从0~18的数字,代表不同的属性,完整的属性列表可以参考:VideoCaptureProperties。也可以使用cap.set(propId,value)
来修改属性值。比如说,我们在while之前添加下面的代码:
# 获取捕获的分辨率 # propId可以直接写数字,也可以用OpenCV的符号表示 width, height = capture.get(3), capture.get(4) print(width, height) # 以原分辨率的一倍来捕获 capture.set(cv2.CAP_PROP_FRAME_WIDTH, width * 2) capture.set(cv2.CAP_PROP_FRAME_HEIGHT, height * 2) 复制代码
经验之谈:某些摄像头设定分辨率等参数时会无效,因为它有固定的分辨率大小支持,一般可在摄像头的资料页中找到。
播放本地视频
跟打开摄像头一样,如果把摄像头的编号换成视频的路径就可以播放本地视频了。回想一下cv2.waitKey()
,它的参数表示暂停时间,所以这个值越大,视频播放速度越慢,反之,播放速度越快,通常设置为25或30。
# 播放本地视频 capture = cv2.VideoCapture('demo_video.mp4') while(capture.isOpened()): ret, frame = capture.read() gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) cv2.imshow('frame', gray) if cv2.waitKey(30) == ord('q'): break 复制代码
录制视频
之前我们保存图片用的是cv2.imwrite()
,要保存视频,我们需要创建一个VideoWriter
的对象,需要给它传入四个参数:
FourCC是用来指定视频编码方式的四字节码,所有的编码可参考Video Codecs。如MJPG编码可以这样写: cv2.VideoWriter_fourcc(*'MJPG')
或cv2.VideoWriter_fourcc('M','J','P','G')
capture = cv2.VideoCapture(0) # 定义编码方式并创建VideoWriter对象 fourcc = cv2.VideoWriter_fourcc(*'MJPG') outfile = cv2.VideoWriter('output.avi', fourcc, 25., (640, 480)) while(capture.isOpened()): ret, frame = capture.read() if ret: outfile.write(frame) # 写入文件 cv2.imshow('frame', frame) if cv2.waitKey(1) == ord('q'): break else: break 复制代码
小结
- 使用
cv2.VideoCapture()
创建视频对象,然后在循环中一帧帧显示图像。参数传入数字时,代表打开摄像头,传入本地视频路径时,表示播放本地视频。 cap.get(propId)
获取视频属性,cap.set(propId,value)
设置视频属性。cv2.VideoWriter()
创建视频写入对象,用来录制/保存视频。
3. 图像分类任务概念导入
计算机视觉中的图像分类任务
图像分类的基本任务
对人类来说,识别猫和狗是件非常容易的事。但对计算机来说,即使是一个精通编程的高手,也很难轻松写出具有通用性的程序(比如:假设程序认为体型大的是狗,体型小的是猫,但由于拍摄角度不同,可能一张图片上猫占据的像素比狗还多)。
在早期的图像分类任务中,通常是先人工提取图像特征,再用机器学习算法对这些特征进行分类,分类的结果强依赖于特征提取方法,往往只有经验丰富的研究者才能完成。
在这种背景下,基于神经网络的特征提取方法应运而生。Yann LeCun是最早将卷积神经网络应用到图像识别领域的,其主要逻辑是使用卷积神经网络提取图像特征,并对图像所属类别进行预测,通过训练数据不断调整网络参数,最终形成一套能自动提取图像特征并对这些特征进行分类的网络。
人工智能与深度学习
图源《如何创造可信的AI》
计算机视觉的子任务
- Image Classification: 图像分类,用于识别图像中物体的类别(如:bottle、cup、cube)。
- Object Localization: 目标检测,用于检测图像中每个物体的类别,并准确标出它们的位置。
- Semantic Segmentation: 图像语义分割,用于标出图像中每个像素点所属的类别,属于同一类别的像素点用一个颜色标识。
- Instance Segmentation: 实例分割,值得注意的是,目标检测任务只需要标注出物体位置,实例分割任务不仅要标注出物体位置,还需要标注出物体的外形轮廓。
图像分类问题的经典数据集
MNIST手写数字识别
MNIST是一个手写体数字的图片数据集,该数据集来由美国国家标准与技术研究所(National Institute of Standards and Technology (NIST))发起整理,一共统计了来自250个不同的人手写数字图片,其中50%是高中生,50%来自人口普查局的工作人员。该数据集的收集目的是希望通过算法,实现对手写数字的识别。
%matplotlib inline import numpy as np import cv2 import matplotlib.pyplot as plt import paddle
print('视觉相关数据集:', paddle.vision.datasets.__all__)
from paddle.vision.datasets import MNIST mnist = MNIST(mode='test') for i in range(len(mnist)): if i == 0: sample = mnist[i] print(sample[0].size, sample[1])
# 查看测试集第一个数字 plt.imshow(mnist[0][0]) print('手写数字是:', mnist[0][1])
# 查看测试集第11个数字 plt.imshow(mnist[10][0]) print('手写数字是:', mnist[10][1])
Cifar数据集
CIFAR-10
CIFAR-10数据集由10个类的60000个32x32彩色图像组成,每个类有6000个图像。有50000个训练图像和10000个测试图像。
数据集分为五个训练批次和一个测试批次,每个批次有10000个图像。测试批次包含来自每个类别的恰好1000个随机选择的图像。训练批次以随机顺序包含剩余图像,但一些训练批次可能包含来自一个类别的图像比另一个更多。总体来说,五个训练集之和包含来自每个类的正好5000张图像。
以下是数据集中的类,以及来自每个类的10个随机图像:
这些类完全相互排斥。汽车和卡车之间没有重叠。“汽车”包括轿车,SUV,这类东西。“卡车”只包括大卡车。都不包括皮卡车。
CIFAR-100
CIFAR-100数据集就像CIFAR-10,除了它有100个类,每个类包含600个图像。,每类各有500个训练图像和100个测试图像。CIFAR-100中的100个类被分成20个超类。每个图像都带有一个“精细”标签(它所属的类)和一个“粗糙”标签(它所属的超类)
以下是CIFAR-100中的类别列表:
超类 | 类别 |
水生哺乳动物 | 海狸,海豚,水獭,海豹,鲸鱼 |
鱼 | 水族馆的鱼,比目鱼,射线,鲨鱼,鳟鱼 |
花卉 | 兰花,罂粟花,玫瑰,向日葵,郁金香 |
食品容器 | 瓶子,碗,罐子,杯子,盘子 |
水果和蔬菜 | 苹果,蘑菇,橘子,梨,甜椒 |
家用电器 | 时钟,电脑键盘,台灯,电话机,电视机 |
家用家具 | 床,椅子,沙发,桌子,衣柜 |
昆虫 | 蜜蜂,甲虫,蝴蝶,毛虫,蟑螂 |
大型食肉动物 | 熊,豹,狮子,老虎,狼 |
大型人造户外用品 | 桥,城堡,房子,路,摩天大楼 |
大自然的户外场景 | 云,森林,山,平原,海 |
大杂食动物和食草动物 | 骆驼,牛,黑猩猩,大象,袋鼠 |
中型哺乳动物 | 狐狸,豪猪,负鼠,浣熊,臭鼬 |
非昆虫无脊椎动物 | 螃蟹,龙虾,蜗牛,蜘蛛,蠕虫 |
人 | 宝贝,男孩,女孩,男人,女人 |
爬行动物 | 鳄鱼,恐龙,蜥蜴,蛇,乌龟 |
小型哺乳动物 | 仓鼠,老鼠,兔子,母老虎,松鼠 |
树木 | 枫树,橡树,棕榈,松树,柳树 |
车辆1 | 自行车,公共汽车,摩托车,皮卡车,火车 |
车辆2 | 割草机,火箭,有轨电车,坦克,拖拉机 |
from paddle.vision.datasets import Cifar10 cifar10 = Cifar10(mode='test') # 查看测试集第2张图 plt.imshow(cifar10[1][0]) print('图片类别编码是:', cifar10[1][1])
Cache file /home/aistudio/.cache/paddle/dataset/cifar/cifar-10-python.tar.gz not found, downloading https://dataset.bj.bcebos.com/cifar/cifar-10-python.tar.gz Begin to download Download finished
ImageNet数据集
- ImageNet数据集是一个计算机视觉数据集,是由斯坦福大学的李飞飞教授带领创建。该数据集包合 14,197,122张图片和21,841个Synset索引。Synset是WordNet层次结构中的一个节点,它又是一组同义词集合。ImageNet数据集一直是评估图像分类算法性能的基准。
- ImageNet 数据集是为了促进计算机图像识别技术的发展而设立的一个大型图像数据集。2016 年ImageNet 数据集中已经超过干万张图片,每一张图片都被手工标定好类别。ImageNet 数据集中的图片涵盖了大部分生活中会看到的图片类别。ImageNet最初是拥有超过100万张图像的数据集。如图下图所示,它包含了各种各样的图像,并且每张图像都被关联了标签(类别名)。每年都会举办使用这个巨大数据集的ILSVRC图像识别大赛。
4. PaddleClas数据增强代码解析与实战
# 创建一副图片 img = cv2.imread('lena.jpg') img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
class RandFlipImage(object): """ random flip image 随机翻转图片 flip_code: 1: Flipped Horizontally 水平翻转 0: Flipped Vertically 上下翻转 -1: Flipped Horizontally & Vertically 水平、上下翻转 """ def __init__(self, flip_code=1): # 设置一个翻转参数,1、0或-1 assert flip_code in [-1, 0, 1 ], "flip_code should be a value in [-1, 0, 1]" self.flip_code = flip_code def __call__(self, img): # 随机生成0或1(即是否翻转) if random.randint(0, 1) == 1: return cv2.flip(img, self.flip_code) else:
# 初始化实例,默认随机水平翻转 flip = RandFlipImage() plt.imshow(flip(img))
# 默认随机上下翻转 flip = RandFlipImage(0) plt.imshow(flip(img))
# 默认既水平翻转又上下翻转 flip = RandFlipImage(-1) plt.imshow(flip(img))
class RandCropImage(object): """ random crop image """ """ 随机裁剪图片 """ def __init__(self, size, scale=None, ratio=None, interpolation=-1): self.interpolation = interpolation if interpolation >= 0 else None if type(size) is int: self.size = (size, size) # (h, w) else: self.size = size self.scale = [0.08, 1.0] if scale is None else scale self.ratio = [3. / 4., 4. / 3.] if ratio is None else ratio def __call__(self, img): size = self.size scale = self.scale ratio = self.ratio aspect_ratio = math.sqrt(random.uniform(*ratio)) w = 1. * aspect_ratio h = 1. / aspect_ratio img_h, img_w = img.shape[:2] bound = min((float(img_w) / img_h) / (w**2), (float(img_h) / img_w) / (h**2)) scale_max = min(scale[1], bound) scale_min = min(scale[0], bound) target_area = img_w * img_h * random.uniform(scale_min, scale_max) target_size = math.sqrt(target_area) w = int(target_size * w) h = int(target_size * h) i = random.randint(0, img_w - w) j = random.randint(0, img_h - h) img = img[j:j + h, i:i + w, :] if self.interpolation is None: return cv2.resize(img, size) else: return cv2.resize(img, size, interpolation=self.interpolation) 复制代码
crop = RandCropImage(350) plt.imshow(crop(img)) 复制代码
class RandomErasing(object): def __init__(self, EPSILON=0.5, sl=0.02, sh=0.4, r1=0.3, mean=[0., 0., 0.]): self.EPSILON = EPSILON self.mean = mean self.sl = sl self.sh = sh self.r1 = r1 def __call__(self, img): if random.uniform(0, 1) > self.EPSILON: return img for attempt in range(100): area = img.shape[1] * img.shape[2] target_area = random.uniform(self.sl, self.sh) * area aspect_ratio = random.uniform(self.r1, 1 / self.r1) h = int(round(math.sqrt(target_area * aspect_ratio))) w = int(round(math.sqrt(target_area / aspect_ratio))) if w < img.shape[2] and h < img.shape[1]: x1 = random.randint(0, img.shape[1] - h) y1 = random.randint(0, img.shape[2] - w) if img.shape[0] == 3: img[0, x1:x1 + h, y1:y1 + w] = self.mean[0] img[1, x1:x1 + h, y1:y1 + w] = self.mean[1] img[2, x1:x1 + h, y1:y1 + w] = self.mean[2] else: img[0, x1:x1 + h, y1:y1 + w] = self.mean[1] return img return img
erase = RandomErasing() plt.imshow(erase(img))