作者 | 王天庆,长期从事分布式系统、数据科学与工程、人工智能等方面的研究与开发,在人脸识别方面有丰富的实践经验。现就职某世界100强企业的数据实验室,从事数据科学相关技术领域的预研工作。
来源 | 大数据(ID:hzdashuju)
【导读】OpenCV是一个以BSD许可证开源的、跨平台的计算机视觉库。它提供了Python、C++、Java、Matlab等多种编程语言接口。它集成了很多计算机视觉算法,具有非常强大的功能,是计算机视觉中最为著名的一个库。在本文中,我们将要介绍OpenCV的一些基本用法。
01 OpenCV介绍
OpenCV是使用C++进行编写的、以BSD许可证开放源代码的、跨平台的计算机视觉库。它提供了上百种计算机视觉、机器学习、图像处理等相关算法,新版本的OpenCV支持Tensorflow、Caffe等深度学习框架。
OpenCV的底层优化处理得很好,能够支持多核处理,能够利用硬件实现加速。由于该库是以BSD许可证进行开源的,因此可以被免费应用在科学研究与商业应用中。
OpenCV库是由英特尔公司下属的俄罗斯技术团队发起的,由于优异的性能、免费、开源、算法丰富、使用简单等优点,自从项目被发起后便得到迅猛发展,越来越多的组织和个人加入到源代码的贡献队伍中,这也在客观进一步促进了OpenCV库的发展。
OpenCV在诸多领域得到了广泛的应用,例如物体检测、图像识别、运动跟踪、增强现实(AR)、机器人等场景。我们在本书中需要对图像进行处理时,需要用到OpenCV库。
OpenCV的安装也比较简单,在Python中,通过pip包管理工具就可以实现安装:
pip install opencv-python
如果在anaconda环境中安装OpenCV,则通过下面的方法进行安装:
conda install opencv
安装完毕OpenCV后,可以通过下述方法来查看是否安装成功:
# 查看引入OpenCV库时是否报错
import cv2
# 查看安装的版本
cv2.__version__
# 我这里显示的版本信息是 '3.4.1'
以下介绍OpenCV基本操作。
02 存取数据
OpenCV中的图片以RGB的形式存储,只不过再OpenCV中的颜色通道顺序不是RGB而是BGR。这可以归结为一个历史遗留原因。因为OpenCV库的研发历史比较“悠久”,在那个时代,BGR是数码相机设备的主流表示形式。这一点伴随着OpenCV的发展一直没有被改变,我们在后面编写代码的时候应该注意到通道顺序的问题。
我们看一下OpenCV中存储图片的形式,图4-2是按照BGR顺序存储的RGB颜色模型的图片,对于相同的数据,我们也可以将其分别拆分为蓝色、绿色、红色的颜色矩阵,如图4-3所示。
▲图4-2 OpenCV中以BGR形式存储的彩色图片
▲图4-3 将彩色图片拆分成三个颜色通道存储的形式
通过图4-2和图4-3,我们知道了OpenCV存储图片的形式。那么在Python环境中的OpenCV库底层存储这颜色的数据结构就是array类型。OpenCV将图片读取进来,经过解码后以array形式存储。通过下面的例子,我们看一下OpenCV中图片的读取和存储方法。
-
代码清单① OpenCV中图片的读取和存储
import cv2
import numpy as np
# 使用imread()方法读入一个图片
image = cv2.imread("lena.jpg")
# 看一下数据的存储维度
image.shape
# 返回:(121, 121, 3)
# 将读入的数据image打印出来
print(image)
'''
如果读入数据失败,返回值将不是一个array类型,而是None
我们可以看到图片数据的存储形式:
[[[113 152 227]
[109 153 230]
[104 152 230]
...,
[ 58 93 166]
[119 156 212]
[149 182 232]]
[[107 149 224]
[103 149 226]
[ 97 149 225]
...,
[ 79 112 175]
[ 77 108 159]
[ 65 91 137]]
[[101 148 222]
[ 96 146 222]
[ 91 146 221]
...,
[ 56 80 132]
[ 3 22 65]
[ 0 3 40]]
...,
[[ 21 40 45]
[ 24 37 45]
[ 34 41 50]
...,
[ 20 34 57]
[ 7 24 50]
[ 8 27 54]]
[[ 17 35 36]
[ 20 32 38]
[ 22 29 38]
...,
[ 13 29 52]
[ 28 45 72]
[ 41 59 90]]
[[ 15 31 30]
[ 19 31 35]
[ 21 28 37]
...,
[ 13 29 52]
[ 48 64 93]
[ 71 90 123]]]
'''
# 将存储图片数据的image变量写到磁盘中,写出的文件名为lena.bak.jpg
# 其返回值结果为True代表写入成功,反之代表失败
cv2.imwrite("lena.bak.jpg",image)
这里面有一个问题需要注意:OpenCV判断图片的格式是通过扩展名来实现的,如果我们写出的文件名为 lena.jpg.bak 那么可能会报错:
could not find a writer for the specified extension in function cv::imwrite_
所以,我们在使用OpenCV时候要注意图片文件的扩展名。
OpenCV相比于其他的库,最大的特点是对图像的处理功能非常完备。OpenCV能够实现对图片颜色和形状的变换。
03 颜色变换
图片的颜色变换可以有很多种类,譬如可以对彩色图片进行灰度化处理,调节图片的亮度和对比度,将图片转换成负片的形式等。这些操作都是表现在对图片的颜色处理上,下面我们介绍几种图片的常用颜色变换。
1. 灰度化
我们在平时接触到的图片大多都是彩色图片,存储的颜色模型一般也都是RGB模型。对于彩色图片我们前面提到过它的存储形式,相当于三个颜色通道分别用各自的颜色矩阵来记录数据。对于灰度图像来讲,它自然没有三个通道的说法,它的表现形式是一张矩阵,没有RGB三个不同的颜色通道。
彩色图片是可以转换为灰度图像的,虽然在转换为灰度图像的过程中丢失了颜色信息,但是却保留了图片的纹理、线条、轮廓等特征,这些特征往往比颜色特征更重要。
将彩色图片转换为灰度图片后,存储的数据量自然而然也随之减少,这样就会带来一个明显的好处:对图片进行处理时的计算量也将会减少很多,这一点在工程实践中非常重要,大家会在后面的内容中进一步体会。下面我们简述一下在OpenCV中将彩色图片转换为灰度图片的过程。
-
代码清单② 使用OpenCV将图片灰度化处理
import numpy as np
import cv2
img = cv2.imread("lena.jpg")
print(img.shape)
# (121, 121, 3)
# 使用cv2.cvtColor() 方法将彩色图片转换为灰度图片
gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
print(gray_img.shape)
# (121, 121)
# 将转换后的灰度图片回复成BGR形式
img2 = cv2.cvtColor(gray_img,cv2.COLOR_GRAY2BGR)
print(img2.shape)
# (121, 121, 3)
# 输出彩色图片img的内容
print(img)
'''
[[[113 152 227]
[109 153 230]
[104 152 230]
...,
[ 58 93 166]
[119 156 212]
[149 182 232]]
[[107 149 224]
[103 149 226]
[ 97 149 225]
...,
[ 79 112 175]
[ 77 108 159]
[ 65 91 137]]
[[101 148 222]
[ 96 146 222]
[ 91 146 221]
...,
[ 56 80 132]
[ 3 22 65]
[ 0 3 40]]
...,
[[ 21 40 45]
[ 24 37 45]
[ 34 41 50]
...,
[ 20 34 57]
[ 7 24 50]
[ 8 27 54]]
[[ 17 35 36]
[ 20 32 38]
[ 22 29 38]
...,
[ 13 29 52]
[ 28 45 72]
[ 41 59 90]]
[[ 15 31 30]
[ 19 31 35]
[ 21 28 37]
...,
[ 13 29 52]
[ 48 64 93]
[ 71 90 123]]]
'''
# 输出将灰度图片重新转换为BGR形式图片后的内容
print(img2)
'''
[[[170 170 170]
[171 171 171]
[170 170 170]
...,
[111 111 111]
[169 169 169]
[193 193 193]]
[[167 167 167]
[167 167 167]
[166 166 166]
...,
[127 127 127]
[120 120 120]
[102 102 102]]
[[165 165 165]
[163 163 163]
[162 162 162]
...,
[ 93 93 93]
[ 33 33 33]
[ 14 14 14]]
...,
[[ 39 39 39]
[ 38 38 38]
[ 43 43 43]
...,
[ 39 39 39]
[ 30 30 30]
[ 33 33 33]]
[[ 33 33 33]
[ 32 32 32]
[ 31 31 31]
...,
[ 34 34 34]
[ 51 51 51]
[ 66 66 66]]
[[ 29 29 29]
[ 31 31 31]
[ 30 30 30]
...,
[ 34 34 34]
[ 71 71 71]
[ 98 98 98]]]
'''
在上面的例子中,我们看到使用cvtColor() 函数可以将彩色图片转换为灰度图片,经过转换后的图片shape属性减少了一个维度,所以这个过程也可以看作是一个降维的过程。
在cvtColor() 函数取convert color 之意,第二个参数表示的是转换操作的类别。这里我们是将BGR形式的图片转换为灰度图片,所以使用 cv2.COLOR_BGR2GRAY 常量来表示,当然如果将灰度图片转换为BGR形式的图片,也可以传入cv2.COLOR_GRAY2BGR 常量。
在代码清单②中做了一个实验:尝试将灰度图片gray_img 再次转换为BGR形式的彩色图片,发现转换后的图片无法恢复原先不同颜色通道的数值,OpenCV所采用的方法是将所有的颜色通道全都置成相同的数值,这个数值就是该点的灰度值。
这也说明了从彩色图片转换到灰度图片的计算是单向的,使用简单的算法将灰度图片恢复为彩色图片是很难的,OpenCV中所采用的转换过程只是形式上的转换,并不是真正将灰度图片转换为彩色形式。目前有效果比较好的将灰度图片转换为彩色图片的算法多是结合机器学习的方法来实现的。
2. 负片转换
负片是摄影中会经常接触到的一个词语,在最早的胶卷照片冲印中是指经曝光和显影加工后得到的影像。负片操作在很多图像处理软件中也叫反色,其明暗与原图像相反,其色彩则为原图像的补色。
例如颜色值A与颜色值B互为补色,则其数值的和为255。即RGB图像中的某点颜色为(0,0,255) 则其补色为 (255,255,0)。
由于负片的操作过程比较简单,OpenCV并没有单独封装负片函数,这里我们需要将一张图片拆分为各个颜色通道矩阵,然后分别对每一个颜色通道矩阵进行处理,最后再将其重新组合为一张图片,示例代码如下。
-
代码清单③ 负片功能实现
import numpy as np
import cv2
# 读入图片
img = cv2.imread("lena.jpg")
# 获取高度和宽度,注意索引是高度在前,宽度在后
height = img.shape[0]
width = img.shape[1]
# 生成一个空的三维张量,用于存放后续三个通道的数据
negative_file = np.zeros((height,width,3))
# 将BGR形式存储的彩色图片拆分成三个颜色通道,注意颜色通道的顺序是蓝、绿、红
b,g,r = cv2.split(img)
# 进行负片化处理,求每个通道颜色的补色
r = 255 - r
b = 255 - b
g = 255 - g
# 将处理后的结果赋值到前面生成的三维张量中
negative_file[:,:,0] = b
negative_file[:,:,1] = g
negative_file[:,:,2] = r
# 看一下生成图片的数据
negative_file
'''
array([[[ 142., 103., 28.],
[ 146., 102., 25.],
[ 151., 103., 25.],
...,
[ 197., 162., 89.],
[ 136., 99., 43.],
[ 106., 73., 23.]],
[[ 148., 106., 31.],
[ 152., 106., 29.],
[ 158., 106., 30.],
...,
[ 176., 143., 80.],
[ 178., 147., 96.],
[ 190., 164., 118.]],
[[ 154., 107., 33.],
[ 159., 109., 33.],
[ 164., 109., 34.],
...,
[ 199., 175., 123.],
[ 252., 233., 190.],
[ 255., 252., 215.]],
...,
[[ 234., 215., 210.],
[ 231., 218., 210.],
[ 221., 214., 205.],
...,
[ 235., 221., 198.],
[ 248., 231., 205.],
[ 247., 228., 201.]],
[[ 238., 220., 219.],
[ 235., 223., 217.],
[ 233., 226., 217.],
...,
[ 242., 226., 203.],
[ 227., 210., 183.],
[ 214., 196., 165.]],
[[ 240., 224., 225.],
[ 236., 224., 220.],
[ 234., 227., 218.],
...,
[ 242., 226., 203.],
[ 207., 191., 162.],
[ 184., 165., 132.]]])
'''
# 将生成的图片保存起来,注意存储图片文件名中的扩展名
cv2.imwrite("negative_lena.jpg",negative_file)
经过上述代码的对图像的处理,我们可以看到经过处理的图像如图4-4b所示,原始图像如图4-4a所示。
▲图4-4 原始图像与经过负片处理后的图像
使用负片对图像进行处理,就是将图片的颜色进行反转的过程,这是一个线性变换过程。在图像处理中可以增强暗色区域中的白色或灰色细节。在这个例子中,我们应该同时熟悉对彩色图片中三个不同颜色通道的拆分以及重新构建图像的方法。
3. 亮度与对比度转换
一般来说,图像处理算子是将一幅或多幅图像作为输入数据,产生一幅输出图像的函数。图像变换可分为以下两种。
-
点算子:基于像素变换,在这一类图像变换中,仅仅根据输入像素值(有时可加上某些额外信息)计算相应的输出像素值。
-
邻域算子:基于图像区域进行变换。
两种常用的点算子,是用常数对点的像素值进行乘法或加法运算,可以表示为:
g(i, j)=α·f(i, j)+β
其中,图像中点的位置为(i, j), α值代表增益,β值代表偏置。对图像进行亮度和对比度的变换就是一种点算子,这两个参数分别可以用来控制对比度与亮度。
熟悉这个原理之后,我们就可以通过调节这两个参数的值,来对图片进行对比度或亮度的调节。即将原图像中的每一个像素点都加上一个偏置常数,则可以使图片的亮度变大,类似地,可以将原图片中的像素点乘上一个增益系数,来调整图片的对比度。
但是要注意,图片像素点的像素值取值范围是[0,255],一定不应该让其溢出,否则图片将不是我们想要的效果。
代码清单④分别演示了实现对图片的像素点进行计算的两种方法。
-
代码清单④ 对图片亮度与对比度转换演示
import cv2
import numpy as np
# 方法1:通过addWeighted()函数来实现
def convert_img1(img, alpha, beta):
blank = np.zeros(img.shape, img.dtype) # dtpye is uint8
return cv2.addWeighted(img, alpha, blank, 0, beta)
# 方法2: 通过for循环手动实现,与addWeighted()函数内部实现原理一样
def convert_img2(img, alpha, beta):
rows, cols, channel = img.shape
new_img = np.zeros(img.shape, img.dtype)
for i in range(0,rows):
for j in range(0,cols):
for k in range(0,channel):
# np.clip() 将数值限制在[0,255]区间,防止数字溢出
new_img[i,j,k] = np.clip(
alpha * img[i,j,k] + beta,0,255)
return new_img
img = cv2.imread('lena.jpg')
cv2.imwrite('converted_lena_1.jpg', convert_img1(img,2.2,50))
cv2.imwrite('converted_lena_2.jpg', convert_img2(img,2.2,50))
在上述代码中,convert_img1() 函数的addWeighted() 函数的参数列表分别为:
[img1, alpha, img2, beta, gamma]
代表将两个图片进行如下计算:
new_img=alpha·img1+beta·img2+gamma
因此,函数convert_img2() 实现的过程,就是通过for循环修改原始图片的像素值,与convert_img1() 函数的过程是一样的,只不过convert_img1() 函数调用addWeighted() 函数的img2参数中图片的像素值都是0罢了。
可以得到变换前的图片如图4-5a所示,变换后的图片如图4-5b所示。
▲图4-5 图片亮度与对比度转换示例
04 几何变换
图像的几何变换是指对图像中的图像像素点的位置进行变换的一种操作,它将一幅图像中的坐标位置映射到新的坐标位置,也就是是改变像素点的空间位置,同时也要估算新空间位置上的像素值。
经过几何变换的图片,直观来看就是其图像的形态发生了变化,例如常见的图像缩放、平移、旋转等都属于几何变换。
1. 图像裁剪
图像的裁剪实现起来相对容易,即在图像数据的矩阵中裁剪出部分矩阵作为新的图像数据,从而实现对图像的裁剪。例如下面的代码段落实现了对图片的裁剪。
-
代码清单⑤ 图像裁剪演示
import cv2
import numpy as np
img = cv2.imread('lena.jpg')
print(img.shape)
# (121, 121, 3)
new_img = img[20:120,20:120]
cv2.imwrite('new_img.jpg',new_img)
上述代码实现的过程是将原始的图像从第(20,20) 个像素点的位置,裁剪到(120,120) 处,裁剪的形状是矩形的。原始图像如图4-6a所示,裁剪后的图像如图4-6b所示,图像尺寸明显变小了。
▲图4-6 图像裁剪示例
2. 图像尺寸变换
修改图像的尺寸也就是修改图像的大小,OpenCV的resize() 函数可以实现这样的功能。对图像进行尺寸变换时,必然会丢失或者增加一些像素点,这些像素点怎么丢弃或者增加呢?
这就需要插值算法了,resize() 函数提供了指定插值算法的参数。在缩放时建议使用区域插值cv2.INTER_AREA, 可以避免波纹出现;在放大时建议使用三次样条插值cv2.INTER_CUBIC, 但是其计算速度相对较,或者线性插值cv2.INTER_LINEAR, 默认情况下所有改变图像尺寸大小的操作使用的是插值法都是线性插值。
我们可以通过设置缩放因子或者直接给出变换后图像的尺寸,则resize() 函数就可以为我们自动生成变换后的图像了。
-
代码清单⑥ 使用OpenCV变换图像尺寸
import cv2
import numpy as np
img = cv2.imread('lena.jpg')
print(img.shape)
# (121, 121, 3)
new_img = cv2.resize(img,(40,40),interpolation=cv2.INTER_AREA)
cv2.imwrite('new_img1.jpg', new_img)
print(new_img.shape)
# (40, 40, 3)
new_img2 = cv2.resize(img,None,fx=0.5, fy=0.6,interpolation=cv2.INTER_AREA)
print(new_img2.shape)
# 注意图像的宽对应的是列数,高对应的是行数
# (73, 60, 3)
cv2.imwrite('new_img2.jpg',new_img2)
如图4-7所示,原图如图4-7a所示,new_img1与new_img2分别如图4-7b与图4-7c所示。
▲图4-7 图像尺寸变换示例
3. 图像旋转
我们在前面介绍过图像的旋转原理,OpenCV为我们提供了图像的这种操作,旋转通过getRotationMatrix2D() 函数来实现。
-
代码清单⑦ 使用OpenCV实现图像旋转
import cv2
import numpy as np
img = cv2.imread('lena.jpg')
rows, cols, _ = img.shape
# 第一个参数为旋转中心,第二个为旋转角度,第三个为旋转后的缩放因子
rotated_img = cv2.getRotationMatrix2D((cols/2,rows/2),45,0.4)
cv2.imwrite('dst.jpg',dst)
原图如图 4-7a 所示,经过旋转后的图像如图4-8所示。
▲图4-8 经过旋转后的图像
05 图像噪声处理
我们曾在前面介绍过噪声,与信号相比,噪声是我们不希望得到的,噪声量越少则表明图像质量越高。由于图像采集设备的性能不同,有的采集设备获得的噪声少,有的则会很多,这可能会干扰到图像的处理。
因此,我们在这里介绍一下噪声的消减方法,可以用在图像的预处理上。与此同时,对训练数据添加适量噪声,可以使训练后的模型更加鲁棒,对模型的性能提升有一定帮助。因此,为图像添加噪声可以起到数据增强的作用。
1. 添加噪声
下面我们演示一下对图像添加两种常用噪声的方法,一种是椒盐噪声,另一种是高斯噪声,它们的实现代码如代码清单⑧所示。
-
代码清单⑧ 为图像添加噪声
import cv2
import numpy as np
import random
# 添加椒盐噪声
def salt_and_pepper_noise(img, percentage):
rows, cols = img.shape
num = int(percentage * rows * cols)
for i in range(num):
x = random.randint(0,rows - 1)
y = random.randint(0,cols - 1)
if random.randint(0,1) == 0:
img[x,y] = 0
else:
img[x,y] = 255
return img
# 添加高斯随机噪声
def gaussian_noise(img, mu, sigma, k):
rows, cols = img.shape
for i in range(rows):
for j in range(cols):
# 生成高斯分布的随机数,与原始数据相加后要取整
value = int(img[i,j] + k * random.gauss(mu=mu,
sigma=sigma))
value = np.clip(a_max=255,a_min=0,a=value)
img[i,j] = value
return img
img = cv2.imread('lena.jpg')
# 转换为灰度图像
gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
cv2.imwrite('gray_lena.jpg',gray_img)
# 需要复制一份,不然是对图像的引用,后面的操作会重叠
gray_img2 = gray_img.copy()
# 保存椒盐噪声图像
cv2.imwrite('salt_and_pepper.jpg',
salt_and_pepper_noise(gray_img,0.3))
# 保存高斯噪声图像
cv2.imwrite('gaussian.jpg',
gaussian_noise(gray_img2, 0, 1, 32))
▲图4-9 为图像添加噪声示例
在图4-9中,图4-9b是椒盐噪声处理后的图像,图4-9b是高斯噪声处理后的图像。
2. 模糊与滤波
OpenCV为我们提供了几种滤波方法,诸如中值滤波、双边滤波、高斯模糊、二维卷积等,这些操作的基本方法如代码清单⑨所示。
-
代码清单⑨ 图像滤波演示
import cv2
import numpy as np
import random
salt_and_pepper_img = cv2.imread('salt_and_pepper.jpg')
gaussian_img = cv2.imread('gaussian.jpg')
# 2维卷积
# 得到一个5*5大小的矩阵作为卷积核,矩阵中的每个值都为0.04
kernel = np.ones((5,5),np.float32) / 25
conv_2d_img = cv2.filter2D(salt_and_pepper_img, -1, kernel)
cv2.imwrite('filter_2d_img.jpg', conv_2d_img)
# 中值滤波
# 参数5表示选择附近5*5区域的像素值进行计算
median_blur_img = cv2.medianBlur(salt_and_pepper_img,5)
cv2.imwrite('median_blur_img.jpg', median_blur_img)
# 高斯模糊
# 标准差参数设置为0是指根据窗口大小(5,5)来自行计算高斯函数标准差
gaussian_blur_img = cv2.GaussianBlur(gaussian_img, (5,5), 0)
cv2.imwrite('gaussian_blur_img.jpg', gaussian_blur_img)
# 双边滤波
# cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace)
# 9 代表邻域直径,两个参数75分别代表值域与空域标准差
bilateral_filter_img = cv2.bilateralFilter(gaussian_img, 9, 75, 75)
cv2.imwrite('bilateral_filter_img.jpg', bilateral_filter_img)
上述操作加入过噪声的原始图像如图4-9所示,这两个带有噪声的图像经过滤波处理的结果如图4-10所示。
-
对添加过椒盐噪声图片经过二维卷积滤波后的结果
-
对添加过椒盐噪声图片进行中值滤波后的结果
-
对经过高斯噪声污染后的图片进行高斯滤波后的结果
-
对经过高斯噪声污染后的图片进行双边滤波后的结果
▲图4-10 带有噪声的图像经过滤波处理后的结果
06 小结
OpenCV是一个非常优秀且使用广泛的开源计算机视觉库,该库核心代码采用C++编写,提供了多种语言接口。在本文中,我们学习了OpenCV的Python接口使用方法,学习使用OpenCV对图像进行操作的基本方法。
(*本文为AI科技大本营转载文章,转载请联系作者)
推荐阅读
一份职位信息的精准推荐之旅,从AI底层架构说起
Uber提出损失变化分配方法LCA,揭秘神经网络“黑盒”
阿里云智能运维的自动化三剑客
掌握这些步骤,机器学习模型问题药到病除
6张拓扑图揭秘中心化交易所的5种行为, 原来中心化比你想象的重要!
分布式存储春天已来Storj首登top10; Cardano排名上升; 以太坊比特币活跃地址双下降 | 数据周榜
华为愿出售5G技术渴望对手;苹果将向印度投资10亿美元;华为全联接大会首发计算战略;腾讯自研轻量级物联网操作系统正式开源……
我愿出 2 倍工资,挖这个被裁的程序员!
厉害!接班马云的为何是张勇?
你点的每个“在看”,我都认真当成了喜欢“
转载:https://blog.csdn.net/dQCFKyQDXYm3F8rB0/article/details/101179568
查看评论