OpenCV-Python实战(9)——OpenCV用于图像分割的阈值技术

0. 前言

图像分割是许多计算机视觉应用中的关键处理步骤,通常用于将图像划分为不同的区域,这些区域常常对应于真实世界的对象。因此,图像分割是图像识别和内容分析的重要步骤。图像阈值是一种简单、有效的图像分割方法,其中像素根据其强度值进行分区。在本文中,将介绍OpenCV所提供的主要阈值技术,可以将这些技术用作计算机视觉应用程序中图像分割的关键部分。

1. 阈值技术简介

阈值处理是一种简单、有效的将图像划分为前景和背景的方法。图像分割通常用于根据对象的某些属性(例如,颜色、边缘或直方图)从背景中提取对象。最简单的阈值方法会利用预定义常数(阈值),如果像素强度小于阈值,则用黑色像素替换,如果像素强度大于阈值,则用白色像素替换。OpenCV 提供了cv2.threshold()函数来对图像进行阈值处理。
为了测试cv2.threshold()函数,首次创建测试图像,其包含一些填充了不同的灰色调的大小相同的区域,利用build_sample_image()函数构建此测试图像:

def build_sample_image():
    """创建填充了不同的灰色调的大小相同的区域,作为测试图像"""
    # 定义不同区域
    tones = np.arange(start=50, stop=300, step=50)
    # 初始化
    result = np.zeros((50, 50, 3), dtype="uint8")

    for tone in tones:
        img = np.ones((50, 50, 3), dtype="uint8") * tone
        # 沿轴连接数组
        result = np.concatenate((result, img), axis=1)
    return result

接下来将使用不同的预定义阈值: 0 、 50 、 100 、 150 、 200 和 250 调用cv2.threshold()函数,以查看不同预定义阈值对阈值图像影响。例如,使用阈值thresh = 50对图像进行阈值处理:

ret1, thresh1 = cv2.threshold(gray_image, 50, 255, cv2.THRESH_BINARY)

其中,thresh1是仅包含黑白色的阈值图像。源图像gray_image中灰色强度小于 50 的像素为黑色,强度大于 50 的像素为白色。
使用几个不同的阈值对图像进行阈值处理:

# 可视化函数
def show_img_with_matplotlib(color_img, title, pos):
    img_RGB = color_img[:, :, ::-1]

    ax = plt.subplot(7, 1, pos)
    plt.imshow(img_RGB)
    plt.title(title, fontsize=8)
    plt.axis('off')
# 使用 build_sample_image() 函数构建测试图像
image = build_sample_image()
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
for i in range(6):
    # 使用多个不同阈值对图像进行阈值处理
    ret, thresh = cv2.threshold(gray_image, 50 * i, 255, cv2.THRESH_BINARY)
    # 可视化
    show_img_with_matplotlib(cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR), "threshold = {}".format(i * 50), i + 2)
# 可视化测试图像
show_img_with_matplotlib(cv2.cvtColor(gray_image, cv2.COLOR_GRAY2BGR), "img with tones of gray - left to right: (0,50,100,150,200,250)", 1)
# 图像进行阈值处理后,常见的输出是黑白图像
# 因此,为了更好的可视化效果,修改背景颜色
fig.patch.set_facecolor('silver')

plt.show()

阈值技术简介

从上图可以看出,根据阈值与样本图像灰度值的差异,阈值化后生成的黑白图像的变化。

2. 简单的阈值技术

上一节中,我们已经简单介绍过了OpenCV中提供的简单阈值处理函数——cv2.threshold(),该函数用法如下:

cv2.threshold(src, thresh, maxval, type, dst=None) -> retval, dst

cv2.threshold()函数对src输入数组(可以为单通道或多通道图像)应用预定义常数thresh设置的阈值;type参数用于设置阈值类型,阈值类型的可选值如下:cv2.THRESH_BINARY、cv2.THRESH_BINARY_INV、cv2.THRESH_TRUNC、cv2.THRESH_TOZERO、cv2.THRESH_TOZERO_INV、cv2.THRESH_OTSU和cv2.THRESH_TRIANGLE。
maxval参数用于设置最大值,其仅在阈值类型为cv2.THRESH_BINARY和cv2.THRESH_BINARY_INV时有效;需要注意的是,在阈值类型为cv2.THRESH_OTSU和cv2.THRESH_TRIANGLE时,输入图像src应为为单通道。

2.1 阈值类型

为了更好的了解阈值操作的不同类型,接下来给出每种阈值类型的具体公式。符号说明:src是源(原始)图像,dst对应于阈值化后的目标(结果)图像,因此,src(x, y)对应于源图像像素(x, y)处的强度,而dst(x, y)对应于目标图像像素(x, y)处的强度。
阈值类型cv2.THRESH_BINARY公式如下:
dst%28x%2Cy%29%20%3D%20%5Cbegin%7Bcases%7D%20maxval%2C%20%26%20src%28x%2Cy%29%3Ethresh%20%5C%5C%5B2ex%5D%200%2C%20%26%20src%28x%2Cy%29%5Cleq%20thresh%20%5Cend%7Bcases%7D
其表示,如果像素src(x, y)的强度高于thresh,则目标图像像素强度dst(x,y)将被设为maxval;否则,设为0。
阈值类型cv2.THRESH_BINARY_INV公式如下:
dst%28x%2Cy%29%20%3D%20%5Cbegin%7Bcases%7D%200%2C%20%26%20src%28x%2Cy%29%3Ethresh%20%5C%5C%5B2ex%5D%20maxval%2C%20%26%20src%28x%2Cy%29%5Cleq%20thresh%20%5Cend%7Bcases%7D
其表示,如果像素src(x, y)的强度高于thresh,则目标图像像素强度dst(x,y)将被设为0;否则,设为maxval。
阈值类型cv2.THRESH_TRUNC公式如下:
dst%28x%2Cy%29%20%3D%20%5Cbegin%7Bcases%7D%20threshold%2C%20%26%20src%28x%2Cy%29%3Ethresh%20%5C%5C%5B2ex%5D%20src%28x%2Cy%29%2C%20%26%20src%28x%2Cy%29%5Cleq%20thresh%20%5Cend%7Bcases%7D
其表示,如果像素src(x, y)的强度高于thresh,则目标图像像素强度设置为threshold;否则,设为src(x, y)。
阈值类型cv2.THRESH_TOZERO公式如下:
dst%28x%2Cy%29%20%3D%20%5Cbegin%7Bcases%7D%20src%28x%2Cy%29%2C%20%26%20src%28x%2Cy%29%3Ethresh%20%5C%5C%5B2ex%5D%200%2C%20%26%20src%28x%2Cy%29%5Cleq%20thresh%20%5Cend%7Bcases%7D
其表示,如果像素src(x, y)的强度高于thresh,则目标图像像素值将设置为src(x, y);否则,设置为0。
阈值类型cv2.THRESH_TOZERO_INV公式如下:
dst%28x%2Cy%29%20%3D%20%5Cbegin%7Bcases%7D%200%2C%20%26%20src%28x%2Cy%29%3Ethresh%20%5C%5C%5B2ex%5D%20src%28x%2Cy%29%2C%20%26%20src%28x%2Cy%29%5Cleq%20thresh%20%5Cend%7Bcases%7D
其表示,如果像素src(x, y)的强度大于thresh,则目标图像像素值将设置为0;否则,设置为src(x, y)。
而cv2.THRESH_OTSU和cv2.THRESH_TRIANGLE属于特殊的阈值类型,它们可以与上述阈值类型(cv2.THRESH_BINARY、cv2.THRESH_BINARY_INV、cv2.THRESH_TRUNC、cv2.THRESH_TOZERO和cv2.THRESH_TOZERO_INV)进行组合。组合后,阈值处理函数cv2.threshold()将只能处理单通道图像,且计算并返回最佳阈值,而非指定阈值。
接下来,使用不同的阈值类型对同一张测试图像进​​行阈值化,观察不同阈值化的效果:

ret1, thresh1 = cv2.threshold(gray_image, 100, 255, cv2.THRESH_BINARY)
ret2, thresh2 = cv2.threshold(gray_image, 100, 220, cv2.THRESH_BINARY)
ret3, thresh3 = cv2.threshold(gray_image, 100, 255, cv2.THRESH_BINARY_INV)
ret4, thresh4 = cv2.threshold(gray_image, 100, 220, cv2.THRESH_BINARY_INV)
ret5, thresh5 = cv2.threshold(gray_image, 100, 255, cv2.THRESH_TRUNC)
ret6, thresh6 = cv2.threshold(gray_image, 100, 255, cv2.THRESH_TOZERO)
ret7, thresh7 = cv2.threshold(gray_image,100,255, cv2.THRESH_TOZERO_INV)
# 可视化
show_img_with_matplotlib(cv2.cvtColor(thresh1, cv2.COLOR_GRAY2BGR), "THRESH_BINARY - thresh = 100 & maxValue = 255", 2)
show_img_with_matplotlib(cv2.cvtColor(thresh2, cv2.COLOR_GRAY2BGR), "THRESH_BINARY - thresh = 100 & maxValue = 220", 3)
show_img_with_matplotlib(cv2.cvtColor(thresh3, cv2.COLOR_GRAY2BGR), "THRESH_BINARY_INV - thresh = 100", 4)
# 其他图像可视化方法类似,不再赘述
# ...

不同阈值类型
如上图所示,maxval参数仅在使用cv2.THRESH_BINARY和cv2.THRESH_BINARY_INV阈值类型时有效,上例中将cv2.THRESH_BINARY和cv2.THRESH_BINARY_INV类型的maxval值设置为255及220,以便查看阈值图像在这两种情况下的变化情况。

2.2 简单阈值技术的实际应用

了解cv2.threshold()不同参数的工作原理后,我们将cv2.threshold()应用于真实图像,并使用不同的阈值:

# 加载图像
image = cv2.imread('example.png')
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# 绘制灰度图像
show_img_with_matplotlib(cv2.cvtColor(gray_image, cv2.COLOR_GRAY2BGR), "img", 1)

# 使用不同的阈值调用 cv2.threshold() 并进行可视化
for i in range(8):
    ret, thresh = cv2.threshold(gray_image, 130 + i * 10, 255, cv2.THRESH_BINARY)
    show_img_with_matplotlib(cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR), "threshold = {}".format(130 + i * 10), i + 2)

阈值类型示例
如上图所示,阈值在使用cv2.threshold()对图像进行阈值处理时起着至关重要的作用。假设图像处理算法用于识别图像中的对象,如果阈值较低,则阈值图像中缺少一些信息,而如果阈值较高,则有一些信息会被黑色像素遮挡。因此,为整个图像找到一个全局最优阈值是相当困难的,特别是如果图像受到不同光照条件的影响,找到全局最优阈值几乎是不可能的。这就是为什么我们需要应用其他自适应阈值算法来对图像进行阈值处理的原因。

3. 自适应阈值算法

当由于图像不同区域的光照条件不同时,为了获取更好的阈值结果,可以尝试使用自适应阈值。在OpenCV中,自适应阈值由cv2.adapativeThreshold()函数实现,其用法如下:

adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C[, dst]) -> dst

此函数将自适应阈值应用于src数组( 8 位单通道图像)。maxValue参数用于设置dst图像中满足条件的像素值。adaptiveMethod参数设置要使用的自适应阈值算法,有以下两个可选值:

  1. cv2.ADAPTIVE_THRESH_MEAN_C:T(x, y)阈值为(x, y)的blockSize x blockSize邻域的平均值减去参数C
  2. cv2.ADAPTIVE_THRESH_GAUSSIAN_C:T(x, y)阈值为(x, y)的blockSize x blockSize邻域的加权均值减去参数C

blockSize 参数用于计算像素阈值的邻域区域的大小,C 参数是均值或加权均值需要减去的常数(取决于由adaptiveMethod参数设置的自适应方法),thresholdType参数可选值包括cv2.THRESH_BINARY和cv2.THRESH_BINARY_INV。
综上所属,cv2.adapativeThreshold()函数的计算公式如下,其中T(x, y)是为每个像素计算的阈值:
当thresholdType参数为cv2.THRESH_BINARY时:
dst%28x%2Cy%29%20%3D%20%5Cbegin%7Bcases%7D%20maxval%2C%20%26%20src%28x%2Cy%29%3ET%28x%2Cy%29%20%5C%5C%5B2ex%5D%200%2C%20%26%20src%28x%2Cy%29%5Cleq%20thresh%20%5Cend%7Bcases%7D
当thresholdType参数为cv2.THRESH_BINARY_INV时:
dst%28x%2Cy%29%20%3D%20%5Cbegin%7Bcases%7D%200%2C%20%26%20src%28x%2Cy%29%3ET%28x%2Cy%29%20%5C%5C%5B2ex%5D%20maxval%2C%20%26%20src%28x%2Cy%29%5Cleq%20thresh%20%5Cend%7Bcases%7D

应用不同的自适应阈值方法进行图像处理:

# 加载图像
image = cv2.imread('example.png')
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 应用自适应阈值处理
thresh1 = cv2.adaptiveThreshold(gray_image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2)
thresh2 = cv2.adaptiveThreshold(gray_image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 31, 3)
thresh3 = cv2.adaptiveThreshold(gray_image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
thresh4 = cv2.adaptiveThreshold(gray_image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 3)
# 可视化
show_img_with_matplotlib(cv2.cvtColor(gray_image, cv2.COLOR_GRAY2BGR), "gray img", 1)
show_img_with_matplotlib(cv2.cvtColor(thresh1, cv2.COLOR_GRAY2BGR), "method=THRESH_MEAN_C, blockSize=11, C=2", 2)
# 其他图像可视化方法类似,不再赘述
# ...

自适应阈值算法
在上图中,可以看到应用具有不同参数的cv2.adaptiveThreshold()处理后的输出,自适应阈值可以提供更好的阈值图像。但是,图像中也会出现了很多噪点。为了减少噪点,可以应用一些平滑操作,例如双边滤波,因为它会在去除噪声的同时保持锐利边缘。因此,我们在对图像进行阈值处理之前首先应用OpenCV提供的双边滤波函数cv2.bilateralFilter():

# 加载图像
image = cv2.imread('example.png')
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 双边滤波
gray_image = cv2.bilateralFilter(gray_image, 15, 25, 25)
# 应用自适应阈值处理
thresh1 = cv2.adaptiveThreshold(gray_image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2)
# 之后代码与上一示例想同,不再赘述
# ...

自适应阈值算法
从上图中可以看出,在应用平滑滤波器后,噪声量大大减少。

4. Otsu 阈值算法

简单阈值算法应用同一全局阈值,因此我们需要尝试不同的阈值并查看阈值图像,以满足我们的需要。但是,这种方法需要大量尝试,一种改进的方法是使用OpenCV中的cv2.adapativeThreshold()函数计算自适应阈值。但是,计算自适应阈值需要两个合适的参数:blockSize和C;另一种改进方法是使用Otsu阈值算法,这在处理双峰图像时是非常有效(双峰图像其直方图包含两个峰值)。Otsu算法通过最大化两类像素之间的方差来自动计算将两个峰值分开的最佳阈值。其等效于最佳阈值最小化类内方差。Otsu阈值算法是一种统计方法,因为它依赖于从直方图获得的统计信息(例如,均值、方差或熵)。在OpenCV中使用cv2.threshold()函数计算Otsu阈值的方法如下:

ret, th = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

使用Otsu阈值算法,不需要设置阈值,因为Otsu阈值算法会计算最佳阈值,因此参数thresh = 0,cv2.THRESH_OTSU标志表示将应用Otsu算法,此标志可以与cv2.THRESH_BINARY、cv2.THRESH_BINARY_INV、cv2.THRESH_TRUNC、cv2.THRESH_TOZERO以及cv2.THRESH_TOZERO_INV结合使用,函数返回阈值图像th和阈值ret。
将此算法应用于实际图像,并绘制一条线可视化阈值,确定阈值th坐标:

def show_hist_with_matplotlib_gray(hist, title, pos, color, t=-1):
    ax = plt.subplot(2, 2, pos)
    plt.xlabel("bins")
    plt.ylabel("number of pixels")
    plt.xlim([0, 256])
    # 可视化阈值
    plt.axvline(x=t, color='m', linestyle='--')
    plt.plot(hist, color=color)
# 加载图像
image = cv2.imread('example.png')
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 直方图
hist = cv2.calcHist([gray_image], [0], None, [256], [0, 256])
# otsu 阈值算法
ret1, th1 = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

# 可视化
show_img_with_matplotlib(image, "image", 1)
show_img_with_matplotlib(cv2.cvtColor(gray_image, cv2.COLOR_GRAY2BGR), "gray img", 2)
show_hist_with_matplotlib_gray(hist, "grayscale histogram", 3, 'm', ret1)
show_img_with_matplotlib(cv2.cvtColor(th1, cv2.COLOR_GRAY2BGR), "Otsu's binarization", 4)

plt.show()

Otsu 阈值算法
在上图图中,可以看到源图像中没有噪声,因此算法可以正常工作,接下来,我们手动为图像添加噪声,以观察噪声对Otsu阈值算法的影响,然后利用高斯滤波消除部分噪声,以查看阈值图像变化情况:

import numpy as np
def gasuss_noise(image, mean=0, var=0.001):
    ''' 
        添加高斯噪声
        mean : 均值 
        var : 方差
    '''
    image = np.array(image/255, dtype=float)
    noise = np.random.normal(mean, var ** 0.5, image.shape)
    out = image + noise
    if out.min() < 0:
        low_clip = -1.
    else:
        low_clip = 0.
    out = np.clip(out, low_clip, 1.0)
    out = np.uint8(out*255)
    return out
# 加载图像、添加噪声,并将其转换为灰度图像
image = cv2.imread('903183h98p0.png')
image = gasuss_noise(image,var=0.05)
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 计算直方图
hist = cv2.calcHist([gray_image], [0], None, [256], [0, 256])
# 应用 Otsu 阈值算法
ret1, th1 = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 高斯滤波
gray_image_blurred = cv2.GaussianBlur(gray_image, (25, 25), 0)
# 计算直方图
hist2 = cv2.calcHist([gray_image_blurred], [0], None, [256], [0, 256])
# 高斯滤波后的图像,应用 Otsu 阈值算法
ret2, th2 = cv2.threshold(gray_image_blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

Otsu 阈值算法
如上图所示,如果不应用平滑滤波器,阈值图像也会充满噪声。应用高斯滤波器后,可以正确滤除噪声,可以看出滤波后得到的图像是双峰的。

5. Triangle 阈值算法

还有另一种自动阈值算法称为Triangle阈值算法,它是一种基于形状的方法,因为它分析直方图的结构或形状(例如,谷、峰和其他直方图形状特征)。该算法可以分为三步:第一步,计算直方图灰度轴上最大值b_%7Bmax%7D和灰度轴上最小值b_%7Bmin%7D之间的直线;第二步,计算b%20%5Bb_%7Bmin%7D%2C%20b_%7Bmax%7D%5D范围内直线(在第一步中计算)到直方图的距离;最后,选择直方图与直线距离最大的水平作为阈值。
OpenCV中Triangle阈值算法的使用方式与Otsu的算法非常相似,只需要将cv2.THRESH_OTSU标志修改为cv2.THRESH_TRIANGLE:

# 加载图像
image = cv2.imread('example.png')
image = gasuss_noise(image,var=0.05)
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

hist = cv2.calcHist([gray_image], [0], None, [256], [0, 256])
ret1, th1 = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_TRIANGLE)
# 高斯滤波
gray_image_blurred = cv2.GaussianBlur(gray_image, (25, 25), 0)

hist2 = cv2.calcHist([gray_image_blurred], [0], None, [256], [0, 256])
ret2, th2 = cv2.threshold(gray_image_blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_TRIANGLE)

在下图中,可以看到将Triangle阈值算法应用于噪声图像时的输出:

Triangle 阈值算法

6. 对彩色图像进行阈值处理

cv2.threshold()函数也可以应用于多通道图像,在此情况下,cv2.threshold()函数在 BGR 图像的每个通道中应用阈值操作:

image = cv2.imread('example.png')
ret1, thresh1 = cv2.threshold(image, 150, 255, cv2.THRESH_BINARY)

这与在每个通道中应用cv2.threshold()函数并合并阈值通道相同的结果:

(b, g, r) = cv2.split(image)
ret2, thresh2 = cv2.threshold(b, 150, 255, cv2.THRESH_BINARY)
ret3, thresh3 = cv2.threshold(g, 150, 255, cv2.THRESH_BINARY)
ret4, thresh4 = cv2.threshold(r, 150, 255, cv2.THRESH_BINARY)
bgr_thresh = cv2.merge((thresh2, thresh3, thresh4))

为了比较,我们还使用其他阈值类型对彩色图像进行了阈值处理:

ret5, thresh5 = cv2.threshold(image, 120, 255, cv2.THRESH_TOZERO)

# Apply cv2.threshold():
(b, g, r) = cv2.split(image)
ret2, thresh2 = cv2.threshold(b, 120, 255, cv2.THRESH_TOZERO)
ret3, thresh3 = cv2.threshold(g, 120, 255, cv2.THRESH_TOZERO)
ret4, thresh4 = cv2.threshold(r, 120, 255, cv2.THRESH_TOZERO)
bgr_thresh = cv2.merge((thresh2, thresh3, thresh4))

对彩色图像进行阈值处理
对彩色图像进行阈值处理

概括

阈值技术可用于许多计算机视觉任务(例如,文本识别和图像分割等)。在本文中,我们介绍了可用于对图像进行阈值处理的主要阈值技术。介绍了简单和自适应阈值技术,同时也了解了如何应用Otsu阈值算法和triangle阈值算法来自动选择一个全局阈值来对图像进行阈值处理。

系列链接

OpenCV-Python实战(1)——OpenCV简介与图像处理基础
OpenCV-Python实战(2)——图像与视频文件的处理
OpenCV-Python实战(3)——OpenCV中绘制图形与文本
OpenCV-Python实战(4)——OpenCV常见图像处理技术
OpenCV-Python实战(5)——OpenCV图像运算
OpenCV-Python实战(6)——OpenCV中的色彩空间和色彩映射
OpenCV-Python实战(7)——直方图详解
OpenCV-Python实战(8)——直方图均衡化
OpenCV-Python实战(10)——OpenCV轮廓检测
OpenCV-Python实战(11)——OpenCV轮廓检测相关应用
OpenCV-Python实战(12)——一文详解AR增强现实
OpenCV-Python实战(13)——OpenCV与机器学习的碰撞
OpenCV-Python实战(14)——人脸检测详解

版权声明:本文为博主盼小辉丶原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/LOVEmy134611/article/details/120069509

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
xiaoxingxing的头像xiaoxingxing管理团队
上一篇 2022年2月18日 下午2:07
下一篇 2022年2月18日 下午2:34

相关推荐