第4章:色彩空间类型转换

色彩空间称为颜色空间、彩色空间、颜色模型、彩色系统、彩色模型、色彩模型等。

常见的色彩空间类型有:

  • RGB色彩空间(最常见)
  • GRAY色彩空间(灰度图像)
  • XYZ色彩空间
  • YCrCb色彩空间
  • HSV色彩空间
  • HLS色彩空间
  • CIEL* a * b* 色彩空间
  • CIEL * u * v* 色彩空间
  • Bayer色彩空间

每个色彩空间都有处理问题的领域,因此当我们处理具体问题时,就需要用的色彩空间类型转换。

one. 色彩空间基础知识:

1. GRAY色彩空间:

GRAY(灰度图像)通常指8位灰度图,其具有256个灰度级,像素值的范围是[0, 255]。当图像由RGB色彩空间转换为GRAY色彩空间时,其处理方式如下:

  • GRAY = 0.299 * R + 0.587 * G + 0.114 * B

上面的转换公式是标准转换公式,也是OpenCV中使用的转换方式。有时也可以简化成:

  • $GRAY = \cfrac { R + G +B }{ 3 } $

当图像由GRAY色彩空间转换为RGB色彩空间时,最终所有的通道值都将是相同的, 其处理方式如下:

  • R = GRAY
  • G = GRAY
  • B = GRAY

2. XYZ色彩空间

XYZ色彩空间是有CIE定义的,是一种更加便于计算的色彩空间,可以与RGB色彩空间进行相互转换。

  • 将RGB色彩空间转换为XYZ色彩空间:

image-20211006132125441

  • 将XYZ色彩空间转换为RGB色彩空间

image-20211006132148911

3. YCrCb色彩空间

​ 人眼视觉系统(HSV)对颜色的敏感度要低于对亮度的敏感度。在传统RGB色彩空间中,RGB三原色具有相同的重要性,但是忽略了亮度信息。

​ 在YCrCb色彩空间中,Y代表光源亮度,色彩信息保存在Cr和Cb中,其中,Cr表示红色分量信息,Cb表示蓝色分量信息。

​ 亮度给出了颜色的亮暗程度信息,该信息通过照明强度的加权和来计算。在RGB光源中,绿色分量影响最大,蓝色分量影响最小。

  • RGB 色彩空间转换为YCrCb色彩空间:

    • Y = 0.299 * R + 0.578 * G + 0.114 * B
    • Cr = ( R - Y ) × 0.713 + delta
    • Cb = (B - Y ) × 0.546 + delta

    式中delta的值为: { 128 , 8 位 图 像 32768 , 16 位 图 像 0.5 , 单 精 度 图 像 \begin{cases} 128, \quad 8位图像 \\ 32768, \quad 16位图像 \\ 0.5, \quad 单精度图像 \end{cases} 128832768,160.5,

  • YCrCb色彩空间转为RGB色彩空间:

    • R = Y + 1.403 * (Cr - delta)
    • G = Y - 0.714 * (Cr - delta) - 0.344 * (Cb - delta)
    • B = Y + 1.733 * (Cr - delta)

    式中delta的值与上面公式中的delta的值相同。

3. HSV色彩空间

RGB是从硬件的角度提出颜色模型,在与人眼匹配的过程中可能存在一定的差异。但HSV色彩空间不同,是一种面向视觉感知的颜色模型。HSV色彩空间从心理学和视觉角度出发,指出了人眼的色彩知觉主要包含三要素:色调(Hue)、饱和度(Saturation)、亮度(Value),色调指光的颜色,饱和度指色彩深浅程度、亮度之人眼感受到的光的明暗程度。

  • 色调:色调与混合光谱中主要光波长相关,例如"赤橙黄绿青蓝紫"分别表示不同的色调。如果从波长的角度考虑,不同的波长的光表现为不同的颜色,实际上它们体现的是色调的差异。
  • 饱和度:指相对纯净度,或一种颜色混合白光的数量。纯普色是全饱和的,像深红色(红加白)和淡紫色(紫加白)这样的彩色是欠饱和的,饱和度与所加白光的数量成反比。
  • 亮度:反映的是人眼感受到的光的明暗程度,该指标与物体的反射度相关。对于色彩来讲,如果期中参入的白色越多,则亮度越高;如果其中掺入的黑色越多,则其亮度越低。

在具体实现上,我们将物理空间的颜色分布在圆周上,不同的角度代表不同的颜色。因此,通过调整色调值就能选取不同的颜色,色调的取值区间是[0, 360]。色调取不同值时,所代表的的颜色不同,两个角度之间的角度对应两个颜色之间的过渡色。

image-20211009104245556

​ 饱和度为一比例值,范围是[0, 1],具体为所选颜色的纯度值和该颜色最大纯度值之间的比值。饱和度为0时,只有灰度。亮度表示色彩的明亮程度,取值范围也是[0, 1]。

​ 在HSV色彩模型中,取色变得更加直观。例如,取值"色调=0, 饱和度=1, 亮度= 1",则当前颜色为深红色,而且颜色较亮。取值"色调=120,饱和度=0.3,亮度=0.4",则当前颜色为浅绿色,而且颜色较暗。

​ 在从RGB色彩空间转换到HSV色彩空间之前,需要先将RGB色彩空间的值转换到[0, 1]之间,然后再进行处理。具体处理方法为:

image-20211009105224843

计算结果可能存在H<0的情况,如果出现这种情况,则需要对H进行进一步计算,如下

H = { H + 360 , H < 360 H , ( 其 他 情 况 ) H = \begin{cases} H + 360, \quad H<360 \\ H, \quad (其他情况) \end{cases} H={ H+360,H<360H,()

由上述公式可知:

  • S ∈ [ 0 , 1 ] S \in [0, 1] S[0,1]
  • V ∈ [ 0 , 1 ] V \in [0, 1] V[0,1]
  • H ∈ [ 0 , 360 ] H \in [0, 360] H[0,360]

4. HLS 色彩空间

HLS色彩空间包含三要素:色调H(Hue)、光亮度\明度L(Lightness)、饱和度S(Saturation)与HSV色彩空间类似,只是HLS色彩空间用“光亮度/明度L(lightness)”替换了“亮度(Value)”。

  • 色调:表示人眼所能感知的颜色,在 HLS 模型中,所有的颜色分布在一个平面的色调环上,整个色调环为360度的圆心角,不同的角度代表不同的颜色。
  • 光亮度/明度:用来控制色彩的明暗变化,它的取值范围也是[0,1]。我们通过光亮度/明度的大小来衡量有多少光线从物体表面反射出来。光亮度/明度对于眼睛感知颜色很重要,因为当一个具有色彩的物体处于光线太强或者光线太暗的地方时,眼睛是无法准确感知物体颜色的。
  • 饱和度:使用[0,1]的值描述相同色调、相同光亮度/明度下的色彩纯度变化。饱和度的值越大,表示颜色的纯度越高,颜色越鲜艳;反之,饱和度的值越小,色彩的纯度越低,颜色越暗沉。通常用该属性表示颜色的深浅,比如深绿色、浅绿色

5. CIEL * a * b *色彩空间

​ CIEL * a * b 色彩空间是均匀色彩空间模型,它是面向视觉感知的颜色模型。从视觉感知均匀的角度来讲,人所感知到的两种颜色的区别程度,应该与这两种颜色在色彩空间中的距离成正比。在某个色彩空间中,如果人所观察到的两种颜色的区别程度,与这两种颜色在该色彩空间中对应的点之间的欧式距离成正比,则称该色彩空间为均匀色彩空间。
​ CIEL * a * b 色彩空间中的L分量用于表示像素的亮度,取值范围是[0,100],表示从纯黑到纯白;a * 分量表示从红色到绿色的范围,取值范围是[-127,127]; b * 分量表示从黄色到蓝色的范围,取值范围是[-127,127]。
​ 在从RGB色彩空间转换到CIEL * a * b * 色彩空间之前,需要先将RGB色彩空间的值转换到[0,1]之间,然后再进行处理。
​ 由于CIEL * a * b * 色彩空间是在CIE的XYZ色彩空间的基础上发展起来的,在具体处理时,需要先将RGB转换为XYZ色彩空间,再将其转换到CIEL
a * b *色彩空间。具体实现方法为:

image-20211009111854298

式中:

f ( t ) = { t 1 3 , t > 0.0008856 7.787 t + 16 116 , 其 他 情 况 f(t) = \begin{cases} t^\cfrac { 1 }{ 3 }, \quad t > 0.0008856 \\ 7.787t + \cfrac { 16 }{ 116 }, \quad 其他情况 \end{cases} f(t)=t31,t>0.00088567.787t+11616,

d e l t a = { 128 , 8 位 图 像 0 , 单 精 度 图 像 delta = \begin{cases} 128, \quad 8位图像 \\ 0, \quad 单精度图像 \end{cases} delta={ 128,80

所得结果中各个值的取值范围为:

  • L ∈ [ 0 , 100 ] L \in [0, 100] L[0,100]
  • a ∈ [ − 127 , 127 ] a \in [-127, 127] a[127,127]
  • b ∈ [ − 127 , 127 ] b \in [-127, 127] b[127,127]

6. CIEL * u * v *色彩空间

CIEL * u * v * 色彩空间同 CIEL * a * b * 色彩空间一样,都是均匀的颜色模型。CIEL * u * v * 色彩空间与设备无关,适用于显示器显示和根据加色原理进行组合的场合,该模型中比较强调对红色的表示,即对红色的变化比较敏感,但对蓝色的变化不太敏感。
下面的公式给出了从RGB色彩空间到CIEL * u * v *色彩空间的转换公式。

从RGB色彩空间到XYZ色彩空间的转换:

image-20211009114509255

从XYZ色彩空间到CIEL * u * v *色彩空间的转换:

image-20211009114622048

所得结果中各个值的取值范围为:

  • L ∈ [ 0 , 100 ] L \in [0, 100] L[0,100]
  • u ∈ [ − 127 , 127 ] u \in [-127, 127] u[127,127]
  • v ∈ [ − 127 , 127 ] v \in [-127, 127] v[127,127]

7. Bayer色彩空间

​ Bayer色彩空间(Bayer模型)被广泛地应用在CCD和CMOS相机中。它能够从如图所示的单平面R、G、B交错表内获取彩色图像。

image-20211009114825841

​ 输出的RGB图像的像素点值,是根据当前点的1个、2个或4个邻域像素点的相同颜色的像素值获得的。上述模式能够通过移动一个左边的像素或者上方的像素来完成修改。在函数cv2.cvtColor()的色彩空间转换参数中,通常使用两个特定的参数x和y来表示特定的模式。该模式组成通过图第二行中的第2列与第3列的值来指定。图就是典型的“BG”模式。

​ 常见的模式还有很多,例如 cv2.COLOR_BayerBG2BGR、cv2.COLOR_BayerGB2BGR、cv2.COLOR_BayerRG2BGR、cv2.COLOR_BayerGR2BGR、cv2.COLOR_BayerBG2RGB、cv2.COLOR_BayerGB2RGB、cv2.COLOR_BayerRG2RGB、cv2.COLOR_BayerGR2RGB等。

two. 类型转换函数:

在OpenCV中,我们使用cv2.cvtColor()函数进行色彩空间的转换。该函数可以实现多个色彩空间的转换。

  • dst = cv2.cvtColor(src, code [, dstCn ] )

    dst:表示输出图像,与原始输入图像具有同样的数据类型和深度。

    src:表示原始输入图像。可以是8位无符号图像、16位无符号图像,或者单精度浮点数等。

    code:是色彩空间转换码。

    dstCn:是目标图像的通道数。如果参数为默认的0,则通道数自动通过原始输入图像和code得到。

注意: 图像深度是指存储每个像素所用的位数,也用于量度图像的色彩分辨率。如8位无符号图像 指的是图像深度。https://blog.csdn.net/qq_40041064/article/details/102971585

image-20211009143102590

image-20211009142358176

image-20211009142432255

image-20211009142508665

image-20211009142554657

image-20211009142834892

注意:BGR色彩空间与传统的RGB色彩空间不同。对于一个标准的24位位图,BGR色彩空中第1个字节存放的是蓝色组成的信息,第2个字节存放的是绿色组成的信息,第3个字节存放的是红色组成的的信息。

颜色空间的转换都用到了如下约定:

  • 8位图像值的范围是[0,255]。
  • 16位图像值的范围是[0,65 535]。
  • 浮点数图像值的范围是[0.0~1.0]。

​ 对于线性转换来说,这些取值范围是无关紧要的。但是对于非线性转换来说,输入的RGB图像必须归一化到其对应的取值范围内,才能获取正确的转换结果。

​ 例如,对于8位图,其能够表示的灰度级有 2 8 2^8 28=256个,也就是说,在8位图中,最多能表示256个状态,通常是[0,255]之间的值。但是,在很多色彩空间中,值的范围并不恰好在[0,255]范围内,这时,就需要将该值映射到[0,255]内。

​ 例如,在HSV或HLS色彩空间中,色调值通常在[0,360)范围内,在8位图中转换到上述色彩空间后,色调值要除以2,让其值范围变为[0,180),以满足存储范围,即让值的分布位于8位图能够表示的范围[0,255]内。又例如,在CIEL * a * b * 色彩空间中,a通道和b通道的值范围是[−127,127],为了使其适应[0,255]的范围,每个值都要加上127。不过需要注意,由于计算过程存在四舍五入,所以转换过程并不是精准可逆的。

three. 类型转换实例

1、将图像在BGR模式和灰度图像之间相互转换。

import cv2

lena = cv2.imread('../lena512color.tiff')
gray = cv2.cvtColor(lena, cv2.COLOR_BGR2GRAY)
rgb = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB)
print(lena.shape)
print(gray.shape)
print(rgb.shape)
cv2.imshow('lena', lena)
cv2.imshow('gray', gray)
cv2.imshow('rgb', rgb)
cv2.waitKey()
cv2.destroyAllWindows()

image-20211009145825898

注意:通过“rgb=cv2.cvtColor(gray,cv2.COLOR_GRAY2BGR)”得到的 RGB 图像中,B 通道、G 通道、R通道的值都是一样的,所以其看起来仍是灰度图像。

2、将图像从BGR模式转成RGB模式。

import cv2

bgr = cv2.imread('../lena512color.tiff')
rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
cv2.imshow('bgr', bgr)
cv2.imshow('rgb', rgb)
cv2.waitKey()
cv2.destroyAllWindows()

four. HSV色彩空间讨论:

​ RGB色彩空间是一种被广泛接受的色彩空间,但是该色彩空间过于抽象,我们不能够直接通过其值感知具体的色彩。我们更习惯使用直观的方式来感知颜色,HSV色彩空间提供了这样的方式。通过HSV色彩空间,我们能够更加方便地通过色调、饱和度和亮度来感知颜色。

HSV色彩空间从心理学和视觉的角度出发,提出人眼的色彩知觉主要包含三要素:

  • H:色调(Hue,也称为色相)。
  • S:饱和度(Saturation)。
  • V:亮度(Value)。

**1. 色调: **

​ 在HSV色彩空间中,色调H的取值范围是[0,360]。8位图像内每个像素点所能表示的灰度级有 2 8 2^8 28=256个,所以在8位图像内表示HSV图像时,要把色调的角度值映射到[0,255]范围内。在OpenCV中,可以直接把色调的角度值除以2,得到[0,180]之间的值,以适应8位二进制(256个灰度级)的存储和表示范围。具体如表所示:

image-20211009152209129

在OpenCV中,将色调值除以2之后,会得到如表所示的色调值与对应的颜色,以下是映射后的色调值。

image-20211009152314816

​ 确定值范围后,就可以直接在图像的H通道内查找对应的值,从而找到特定的颜色。例如,在HSV图像中,H通道内值为120的像素点对应蓝色。查找H通道内值为120的像素点,找到的就是蓝色像素点。

​ 在上述基础上,通过分析各种不同对象对应的 HSV 值,便可以查找不同的对象。例如,通过分析得到肤色的HSV值,就可以直接在图像内根据肤色的HSV值来查找人脸(等皮肤)区域。

2. 饱和度:

饱和度值的范围是[0,1],所以针对饱和度,需要说明以下问题:

  • 灰度颜色所包含的R、G、B的成分是相等的,相当于一种极不饱和的颜色。所以,灰度颜色的饱和度值是0。
  • 作为灰度图像显示时,较亮区域对应的颜色具有较高的饱和度。
  • 如果颜色的饱和度很低,那么它计算所得色调就不可靠。
  • 同样要将饱和度S的值从[0,1]范围映射到[0,255]范围内

3. 亮度:

​ 亮度的范围与饱和度的范围一致,都是[0,1]。同样,亮度值在OpenCV内也将值映射到[0,255]范围内。

​ 亮度值越大,图像越亮;亮度值越低,图像越暗。当亮度值为0时,图像是纯黑色。

1. 获取指定颜色:

​ 可以通过多种方式获取RGB色彩空间的颜色值在HSV色彩空间内所对应的值。例如,可以通过图像编辑软件或者在线网站获取RGB值所对应的HSV值。

​ 需要注意,在从RGB/BGR色彩空间转换到HSV色彩空间时,OpenCV为了满足8位图的要求,对HSV空间的值进行了映射处理。所以,通过软件或者网站获取的HSV值还需要被进一步映射,才能与OpenCV中的HSV值一致。

例:在OpenCV中,测试RGB色彩空间中不同颜色的值转换到HSV色彩空间后的对应值。

import cv2
import numpy as np

img_blue = np.zeros([1, 1, 3], dtype=np.uint8)
img_blue[0, 0, 0] = 255
blue = img_blue
blue_hsv = cv2.cvtColor(blue, cv2.COLOR_BGR2HSV)
print('blue=\n', blue)
print('blue_hsv=\n', blue_hsv)

img_green = np.zeros([1, 1, 3], dtype=np.uint8)
img_green[0, 0, 1] = 255
green = img_green
green_hsv = cv2.cvtColor(green, cv2.COLOR_BGR2HSV)
print('green=\n', green)
print('green_hsv=\n', green_hsv)

img_red = np.zeros([1, 1, 3], dtype=np.uint8)
img_red[0, 0, 2] = 255
red = img_red
red_hsv = cv2.cvtColor(red, cv2.COLOR_BGR2HSV)
print('red=\n', red)
print('red_hsv=\n', red_hsv)

# 输出结果
blue=
 [[[255   0   0]]]
blue_hsv=
 [[[120 255 255]]]
green=
 [[[  0 255   0]]]
green_hsv=
 [[[ 60 255 255]]]
red=
 [[[  0   0 255]]]
red_hsv=
 [[[  0 255 255]]]

2. 标记指定颜色:

​ 在HSV色彩空间中,H通道(饱和度Hue通道)对应不同的颜色。或者换个角度理解,颜色的差异主要体现在H通道值的不同上。所以,通过对H通道值进行筛选,便能够筛选出特定的颜色。例如,在一幅HSV图像中,如果通过控制仅仅将H通道内值为240(在OpenCV内被调整为120)的像素显示出来,那么图像中就会仅仅显示蓝色部分。

  • 通过inRange()函数锁定特定值

    OpenCV中通过函数cv2.inRange()判断图像内像素点的像素值是否在指定的范围内,其语法为:

    • dst=cv2.inRange(src,lowerb,upperb)

      dst:表示输出结果,大小和src一致。

      src:表示要检查的数组或图像。

      lowerb:表示范围下界。

      upperb:表示范围上界。

    • 返回值dst与src等大小,其值取决于src中对应位置上的值是否处于区间[lowerb,upperb]内

      • 如果src值处于该指定区间内,则dst中对应位置上的值为255。
      • 如果src值不处于该指定区间内,则dst中对应位置上的值为0。

    例:使用函数cv2.inRange()将某个图像内的在[100,200]内的值标注出来。

    import cv2
    import numpy as np
    
    img = np.random.randint(0, 256, size=[5, 5], dtype=np.uint8)
    min = 100
    max = 200
    mask = cv2.inRange(img, min, max)
    print('img=\n', img)
    print('mask=\n', mask)
    
    # 输出结果
    img=
     [[170 181 245  13 255]
     [ 79  72 133 113 138]
     [112  72 169  87  47]
     [142 210 139  94 165]
     [ 32  16   9  89  95]]
    mask=
     [[255 255   0   0   0]
     [  0   0 255 255 255]
     [255   0 255   0   0]
     [255   0 255   0 255]
     [  0   0   0   0   0]]
    
  • 通过基于掩码的按位与操作显示ROI:

    例:正常显示某个图像内的感兴趣区域(ROI),而将其余区域显示为黑色。

    import cv2
    import numpy as np
    
    img = np.ones([5, 5], dtype=np.uint8) * 9
    mask = np.zeros([5, 5], dtype=np.uint8)
    mask[0:3, 0] = 1
    mask[2:5, 2:4] = 1
    
    roi = cv2.bitwise_and(img, img, mask=mask)
    print('img=\n', img)
    print('mask=\n', mask)
    print('roi=\n', roi)
    
    # 输出结果
    img=
     [[9 9 9 9 9]
     [9 9 9 9 9]
     [9 9 9 9 9]
     [9 9 9 9 9]
     [9 9 9 9 9]]
    mask=
     [[1 0 0 0 0]
     [1 0 0 0 0]
     [1 0 1 1 0]
     [0 0 1 1 0]
     [0 0 1 1 0]]
    roi=
     [[9 0 0 0 0]
     [9 0 0 0 0]
     [9 0 9 9 0]
     [0 0 9 9 0]
     [0 0 9 9 0]]
    
  • 显示特定颜色值:

    例:使用OpenCV,提取图片中不同颜色。

    ​ 需要注意的是,在实际提取颜色时,往往不是提取一个特定的值,而是提取一个颜色区间。例如,在OpenCV中的HSV模式内,蓝色在H通道内的值是120。在提取蓝色时,通常将“蓝色值120”附近的一个区间的值作为提取范围。该区间的半径通常为10左右,例如通常提取[120−10,120+10]范围内的值来指定蓝色。

    ​ 相比之下,HSV模式中S通道、V通道的值的取值范围一般是[100,255]。这主要是因为,当饱和度和亮度太低时,计算出来的色调可能就不可靠了。

    import cv2
    import numpy as np
    
    
    img = cv2.imread('../pepper.tiff')
    cv2.imshow('img', img)
    print(img)
    img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    cv2.imshow('img_hsv', img_hsv)
    
    min_red = np.array([0, 50, 50])
    max_red = np.array([20, 255, 255])
    mask = cv2.inRange(img_hsv, min_red, max_red)
    img_red = cv2.bitwise_and(img_hsv, img_hsv, mask=mask)
    cv2.imshow('img_red', img_red)
    
    min_green = np.array([40, 50, 50])
    max_green = np.array([70, 255, 255])
    mask = cv2.inRange(img_hsv, min_green, max_green)
    img_green = cv2.bitwise_and(img_hsv, img_hsv, mask=mask)
    cv2.imshow('img_green', img_green)
    
    min_yellow = np.array([20, 50, 50])
    max_yellow = np.array([40, 255, 255])
    mask = cv2.inRange(img_hsv, min_yellow, max_yellow)
    img_yellow = cv2.bitwise_and(img_hsv, img_hsv, mask=mask)
    cv2.imshow('img_yellow', img_yellow)
    
    cv2.waitKey()
    cv2.destroyAllWindows()
    

five. alpha通道:

​ 在RGB色彩空间三个通道的基础上,还可以加上一个A通道,也叫alpha通道,表示透明度。这种4个通道的色彩空间被称为RGBA色彩空间,PNG图像是一种典型的4通道图像。alpha通道的赋值范围是[0,1],或者[0,255],表示从透明到不透明。

例:编写一个程序,对图像的alpha通道进行处理。

import cv2

img = cv2.imread('../lena512color.tiff')
bgra = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
b, g, r, a = cv2.split(bgra)
a[:, :] = 125
bgra_125 = cv2.merge([b, g, r, a])
a[:, :] = 0
bgra_0 = cv2.merge([b, g, r, a])

cv2.imshow('img', img)
cv2.imshow('bgra', bgra)
cv2.imshow('bgra_125', bgra_125)
cv2.imshow('bgra_0', bgra_0)

cv2.imwrite('../bgra_125.png', bgra_125)
cv2.imwrite('../bgra_0.png', bgra_0)

cv2.waitKey()
cv2.destroyAllWindows()

image-20211010135327358

上面显示了bgra_125和bgra_0图片,注意:如果只用cv2.imshow显示图像,各个图像的alpha通道值虽然不同,但是在显示时是没有差别的。 如果用cv2.imwrite()将其保存到本地文件中看,是可以发现其不同的。

猜你喜欢

转载自blog.csdn.net/weixin_57440207/article/details/122646984