细胞核的分割与分类模型·HoVer-Net|动手实操

小罗碎碎念

上一期推文已经介绍了hover net的背景和代码仓库情况,这一期则是根据作者提供的示例代码进行分析,详细你看完这一期推文,应该就能大致掌握这些套路了。如果觉得意犹未尽,那就等待下一期吧,哈哈。


一、编程环境配置

按照前期在交流群内提供的链接,你应该能熟练掌握使用Jupyter了,现在登录Jupyter,找到你之前导入的仓库。

image-20240622164141319

为什么要用Jupyter呢?你打开文件夹看文件的后缀名就明白了。

image-20240622164232441


现在开始正式介绍如何使用HoVer Net。

前面已经介绍过了,HoVer Net是一个用于在苏木精-伊红染色(H&E)全切片图像(WSIs)中分割细胞核的模型。

它提供了两种推理模式:瓦片处理模式和全切片图像处理模式。在瓦片处理模式中,输入图像必须是标准格式,如.jpg或.png。而在全切片图像处理模式中,输入图像必须是OpenSlide支持的全切片图像格式。

两种处理模式均输出一个.json文件,该文件包含以下信息:

  • 每个细胞核的边界框坐标
  • 每个细胞核的中心点坐标
  • 每个细胞核的轮廓坐标
  • 细胞核类型的预测
  • 每个细胞核的类别概率(可选)

瓦片模式还输出一个.mat文件和覆盖层文件。.mat文件包含:

  • 网络的原始输出(可选)
  • 包含从0到N的值的实例图,其中N是细胞核的数量
  • 包含每个细胞核预测的N长度列表

全切片图像模式还生成了一个低分辨率缩略图和一个组织掩模。


二、数据集分析

前面已经介绍过了,在细胞核分割和分类任务中,每个不同的细胞核类型都被赋予了一个唯一的标识符或类别编号

在不同的医学图像分析任务中,根据数据集的特点和研究目的,细胞核的分类标准可能会有所不同。使用在特定数据集上训练的模型可以更好地适应该数据集的特点,从而提高分类的准确性。在实际应用中,了解这些分类标准对于解释模型的输出和评估模型的性能至关重要。同时,这也表明在将模型从一个数据集迁移到另一个数据集时,可能需要进行适当的调整或重新训练。

2-1:不同数据集的类别编号和含义

  • CoNSeP数据集
    • 1:上皮细胞(Epithelial)
    • 2:炎症细胞(Inflammatory)
    • 3:梭形细胞(Spindle-Shaped)
    • 4:其他(Miscellaneous)
  • PanNuke数据集
    • 1:肿瘤细胞(Neoplastic)
    • 2:炎症细胞(Inflammatory)
    • 3:结缔组织细胞(Connective)
    • 4:死亡细胞(Dead)
    • 5:非肿瘤上皮细胞(Non-Neoplastic Epithelial)
  • MoNuSAC数据集
    • 1:上皮细胞(Epithelial)
    • 2:淋巴细胞(Lymphocyte)
    • 3:巨噬细胞(Macrophage)
    • 4:中性粒细胞(Neutrophil)

2-2:使用的模型

这里使用的是一个在PanNuke数据集上训练的检查点(checkpoint)。检查点通常指的是在深度学习模型训练过程中的某个特定时刻保存的模型参数,可以用来恢复训练过程或进行进一步的推理。

2-3:分析

通过观察不同数据集中的类别编号和含义,我们可以了解到每个数据集都关注不同类型的细胞核,并且这些类型在不同的数据集中可能有不同的分类方式。例如,CoNSeP和MoNuSAC都将上皮细胞作为类别1,但在PanNuke中,上皮细胞被进一步细分为肿瘤上皮细胞和非肿瘤上皮细胞。


三、Jupyter设置

进入之前创建的虚拟环境

conda activate hovernet

安装Jupyter

conda install jupyter

设置密码

jupyter notebook --generate -config
jupyter notebook password

image-20240622172232679

启动jupyter

jupyter notebook --no-browser

打开终端

输入如下代码

ssh -N -L localhost:8888:localhost:8888 name@IP

image-20240622172353081


四、代码解析

4-1:导入库

这段代码是一个Python脚本的开头部分,主要作用是导入所需的库并设置一些环境变量,以便后续代码能够正常运行。

# load the libraries

import sys
sys.path.append('../')

import numpy as np
import os
import glob
import matplotlib.pyplot as plt
import scipy.io as sio
import cv2
import json
import openslide

from misc.wsi_handler import get_file_handler
from misc.viz_utils import visualize_instances_dict

下面是对每行代码的解释:

  1. # load the libraries:这是一个注释行,说明接下来的代码将加载所需的库。

  2. import sys:导入Python的系统库(sys),它提供了与Python解释器和它的环境有关的函数和变量。

  3. sys.path.append('../'):将上级目录(../)添加到模块搜索路径的末尾。这样做通常是为了让Python能够找到上级目录中的模块或包。

  4. import numpy as np:导入NumPy库,并将其别名设置为np。NumPy是一个广泛使用的科学计算库,提供了大量的数学函数和对多维数组的支持。

  5. import os:导入os库,这个库提供了与操作系统交互的功能,如文件路径操作等。

  6. import glob:导入glob库,它提供了对文件名模式匹配的功能,可以用来查找符合特定规则的文件路径。

  7. import matplotlib.pyplot as plt:导入matplotlib.pyplot模块,并将其别名设置为plt。这是一个用于创建图表和可视化的库。

  8. import scipy.io as sio:导入scipy.io模块,并将其别名设置为sio。这个模块提供了一些输入输出功能,比如读取和写入文件。

  9. import cv2:导入OpenCV库(cv2),这是一个用于计算机视觉的库,提供了大量的图像处理功能。

  10. import json:导入json库,它用于处理JSON数据格式,可以解析JSON字符串为Python字典,或者将Python字典转换为JSON字符串。

  11. import openslide:导入OpenSlide库,这是一个用于读取和处理全切片图像(Whole Slide Images, WSIs)的库。

  12. from misc.wsi_handler import get_file_handler:从misc模块的wsi_handler中导入get_file_handler函数。用于处理特定的文件操作。

  13. from misc.viz_utils import visualize_instances_dict:从misc模块的viz_utils中导入visualize_instances_dict函数。用于可视化实例的字典。

整体来看,这段代码主要是为了设置Python脚本的运行环境,导入了一系列用于数据处理、图像处理、可视化和文件操作的库和模块。这些库和模块将在脚本的后续部分中被用来执行特定的任务。


4-2:设置文件路径变量

# first, we shall set the image tile, WSI and output paths.

tile_path = '../dataset/sample_tiles/imgs/'
tile_json_path = '../dataset/sample_tiles/pred/json/'
tile_mat_path = '../dataset/sample_tiles/pred/mat/'
tile_overlay_path = '../dataset/sample_tiles/pred/overlay/'
wsi_path = '../dataset/sample_wsis/wsi/'
wsi_json_path = '../dataset/sample_wsis/out/'

这段代码的主要作用是设置文件路径变量,这些变量用于指定图像瓦片、全切片图像(WSI)以及输出文件的存储位置。

  1. # first, we shall set the image tile, WSI and output paths.:这是一条注释,说明接下来的代码将设置与图像瓦片、全切片图像以及输出路径相关的变量。

  2. tile_path = '../dataset/sample_tiles/imgs/':设置一个变量tile_path,其值为图像瓦片的存储路径。这个路径是相对于当前脚本文件的上级目录下的dataset/sample_tiles/imgs/文件夹。

  3. tile_json_path = '../dataset/sample_tiles/pred/json/':设置一个变量tile_json_path,用于存储与图像瓦片相关的预测结果的JSON文件。这些文件将被保存在dataset/sample_tiles/pred/json/目录下。

  4. tile_mat_path = '../dataset/sample_tiles/pred/mat/':设置一个变量tile_mat_path,用于存储与图像瓦片相关的预测结果的MAT文件。MAT文件通常用于存储矩阵数据,这里可能用于保存更详细的预测信息。

  5. tile_overlay_path = '../dataset/sample_tiles/pred/overlay/':设置一个变量tile_overlay_path,用于存储图像瓦片的覆盖层文件。

  6. wsi_path = '../dataset/sample_wsis/wsi/':设置一个变量wsi_path,用于指定全切片图像(WSI)的存储路径。这些图像通常很大,包含整个组织切片的详细信息。

  7. wsi_json_path = '../dataset/sample_wsis/out/':设置一个变量wsi_json_path,用于存储全切片图像的输出结果,这些结果以JSON格式保存在dataset/sample_wsis/out/目录下。

通过设置这些路径,脚本能够知道在哪里查找输入数据,以及在哪里存储处理后的结果。这些路径可以根据实际的项目结构和需求进行调整。


4-3:图像处理和分析

# load the original image, the `.mat` file and the overlay

image_list = glob.glob(tile_path + '*.tif')
image_list.sort()

# get a random image 
rand_nr = np.random.randint(0,len(image_list))
image_file = image_list[rand_nr]

basename = os.path.basename(image_file)
image_ext = basename.split('.')[-1]
basename = basename[:-(len(image_ext)+1)]

image = cv2.imread(image_file)
# convert from BGR to RGB
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# get the corresponding `.mat` file 
result_mat = sio.loadmat(tile_mat_path + basename + '.mat')

# get the overlay
overlay = cv2.imread(tile_overlay_path + basename + '.png')
overlay = cv2.cvtColor(overlay, cv2.COLOR_BGR2RGB)

这段代码主要执行了以下任务:

  1. 加载图像列表

    • 使用glob.glob函数,结合之前定义的tile_path变量,获取路径下所有以.tif结尾的图像文件的列表。
    • image_list.sort()将获取到的图像文件列表按照字母顺序进行排序。
  2. 随机选择一个图像文件

    • 使用np.random.randint函数从列表中随机选择一个图像文件的索引。
    • 根据索引获取随机选择的图像文件的路径。
  3. 提取基本文件名和扩展名

    • 使用os.path.basename从文件路径中提取基本的文件名。
    • 使用字符串的split方法和切片操作提取文件的扩展名,并去除扩展名,只保留基本的文件名部分。
  4. 读取图像

    • 使用cv2.imread函数读取被随机选中的图像文件。
    • 由于OpenCV默认以BGR格式读取图像,而大多数图像处理库使用RGB格式,因此使用cv2.cvtColor函数将图像从BGR转换为RGB格式。
  5. 读取对应的.mat文件

    • 构造.mat文件的路径,使用sio.loadmat函数从该路径加载.mat文件。这个文件可能包含了图像的某些分析结果或处理数据。
  6. 读取覆盖层图像

    • 构造覆盖层图像的路径,并使用cv2.imread函数读取覆盖层图像。
    • 同样地,将覆盖层图像从BGR转换为RGB格式。

代码的执行流程是:

  • 首先,列出所有图像文件。
  • 然后,随机选择一个图像文件进行处理。
  • 接着,读取这个图像文件,并将其颜色格式从BGR转换为RGB。
  • 之后,加载与该图像对应的.mat文件。
  • 最后,读取与该图像对应的覆盖层图像,并将其颜色格式从BGR转换为RGB。

这些步骤通常用于图像处理和分析,特别是在需要将图像数据与其他分析结果结合时。


4-4:细胞核识别和分类

# ** now, let's get the outputs and print some basic shape information
# 2D map where each nucleus has a unique ID assigned. 0 is background
inst_map = result_mat['inst_map'] 
# Nx2 length array denoting nuclear type predictions. N is the number of nuclei
# idx=0 denotes the corresponding inst_id in the `inst_map` and 
# idx=1 denotes the type of that inst_id
inst_type = result_mat['inst_type'] 

print('instance map shape', inst_map.shape)
# double check the number of instances is the same as the number of type predictions
print('number of instances', len(np.unique(inst_map)[1:].tolist()))
print('number of type predictions', len(np.unique(inst_type[:,1])))
print('overlay shape', overlay.shape)

这段代码的目的是处理和输出从图像分析中得到的一些基本结果信息。

  1. # ** now, let's get the outputs and print some basic shape information:这是一条多行注释的开始,说明接下来的代码将获取输出结果并打印一些基本的形状信息。

  2. inst_map = result_mat['inst_map']:从之前加载的.mat文件中获取名为inst_map的变量。这个变量是一个2D数组,每个细胞核都被分配了一个唯一的ID,背景被标记为0。

  3. inst_type = result_mat['inst_type']:从.mat文件中获取名为inst_type的变量。这是一个Nx2的数组,其中N是细胞核的数量。第一列(idx=0)表示inst_map中的相应inst_id,第二列(idx=1)表示该inst_id的类型。

  4. print('instance map shape', inst_map.shape):打印inst_map的形状。这通常是(高度, 宽度)的形式,表示图像的尺寸。

  5. print('number of instances', len(np.unique(inst_map)[1:].tolist())):计算并打印不同实例的数量。这里使用np.unique函数找出inst_map中所有不同的值(即不同的细胞核ID),然后排除背景值0(通过[1:]),并计算剩余值的数量。

  6. print('number of type predictions', len(np.unique(inst_type[:,1]))):计算并打印不同细胞核类型的预测数量。这里通过选择inst_type数组的第二列(即细胞核类型的列),然后使用np.unique找出所有不同的细胞核类型,并计算这些类型的数量。

  7. print('overlay shape', overlay.shape):打印覆盖层图像overlay的形状。这通常也是(高度, 宽度, 通道数)的形式,其中通道数对于RGB图像是3。

image-20240622175345556

这段代码通过打印输出,提供了对图像分析结果的基本理解,包括实例图的大小、不同实例的数量、不同类型预测的数量,以及覆盖层图像的尺寸。这些信息对于验证分析结果的一致性和正确性非常重要。


4-5:三图对比

# plot the original image, along with the instance map and the overlay

plt.figure(figsize=(40,20))

plt.subplot(1,3,1)
plt.imshow(image[:400,:400,:])
plt.axis('off')
plt.title('Image', fontsize=25)

plt.subplot(1,3,2)
plt.imshow(inst_map[:400,:400])
plt.axis('off')
plt.title('Instance Map', fontsize=25)

plt.subplot(1,3,3)
plt.imshow(overlay[:400,:400,:])
plt.axis('off')
plt
plt.title('Overlay', fontsize=25)

plt.show()

这段代码使用matplotlib.pyplot库来创建一个包含三个子图的图形,用于显示原始图像、实例图和覆盖层。

image-20240622175909884

  1. plt.figure(figsize=(40,20)):创建一个新的图形对象,并设置图形的大小为宽40英寸、高20英寸。

  2. plt.subplot(1,3,1):设置当前的子图网格为1行3列,并将当前活跃的子图设置为第1个,即左起第1个位置。

  3. plt.imshow(image[:400,:400,:]):在当前活跃的子图中显示原始图像的左上角400x400像素区域。使用imshow函数可以显示图像,这里通过切片操作来限制显示的区域大小。

  4. plt.axis('off'):关闭当前子图的坐标轴,使得图像显示时不显示坐标轴。

  5. plt.title('Image', fontsize=25):为当前子图设置标题为"Image",字体大小为25。

  6. plt.subplot(1,3,2):设置当前的子图网格为1行3列,并将当前活跃的子图设置为第2个,即左起第2个位置。

  7. plt.imshow(inst_map[:400,:400]):在当前子图中显示实例图的左上角400x400像素区域。

  8. plt.subplot(1,3,3):设置当前的子图网格为1行3列,并将当前活跃的子图设置为第3个,即左起第3个位置。

  9. plt.imshow(overlay[:400,:400,:]):在当前子图中显示覆盖层图像的左上角400x400像素区域。

  10. plt.axis('off'):同上,关闭当前子图的坐标轴。

  11. plt.title('Overlay', fontsize=25):为当前子图设置标题为"Overlay",字体大小为25。

  12. plt.show():显示所有子图组成的图形。这将打开一个窗口,展示所有的子图。

代码中使用了plt.subplot来安排子图的布局,plt.imshow来显示图像,plt.axis('off')来隐藏坐标轴,plt.title来设置每个子图的标题,最后通过plt.show()来展示整个图形。由于图像可能很大,这里通过切片操作[:400,:400,:]来只显示图像的一小部分,以便于观察和比较。


4-6:细胞核类型种类

# let's inspect the inst_type output

print(np.unique(inst_type[:,1]))

这行代码用于检查inst_type数组中第二列(即inst_type[:,1])的独一无二(unique)值。

  • inst_type:这是一个从.mat文件中加载的数组,其中包含了关于图像中每个细胞核类型的预测信息。根据之前的描述,inst_type是一个Nx2的数组,其中第一列(idx=0)表示实例ID,而第二列(idx=1)表示该实例ID对应的细胞核类型。

  • inst_type[:,1]:这个表达式使用了NumPy的切片操作来选择inst_type数组的第二列。:表示选择所有行,而1表示选择第二列。

  • np.unique(...):这是一个NumPy函数,用于找出数组中所有不同的元素。这个函数会返回一个包含所有唯一值的数组,并且这些值会按照升序排序。

  • print(np.unique(inst_type[:,1])):这行代码将打印出inst_type数组第二列中所有不同的细胞核类型。这有助于了解模型预测了哪些类型的细胞核,以及每种类型的数量。

例如,如果inst_type数组的第二列包含了一些标签,如[1, 2, 2, 3],那么np.unique(inst_type[:,1])将返回[1, 2, 3],并且print函数将输出这些唯一的类型标签。这对于验证模型的输出和理解不同细胞核类型的分布非常有用。


4-7:读取数据&分类存储

# load the json file and add the contents to corresponding lists

json_path = tile_json_path + basename + '.json'

bbox_list = []
centroid_list = []
contour_list = [] 
type_list = []

with open(json_path) as json_file:
    data = json.load(json_file)
    mag_info = data['mag']
    nuc_info = data['nuc']
    for inst in nuc_info:
        inst_info = nuc_info[inst]
        inst_centroid = inst_info['centroid']
        centroid_list.append(inst_centroid)
        inst_contour = inst_info['contour']
        contour_list.append(inst_contour)
        inst_bbox = inst_info['bbox']
        bbox_list.append(inst_bbox)
        inst_type = inst_info['type']
        type_list.append(inst_type)

这段代码是用于从JSON文件中读取数据,并将其分类存储到不同的列表中。

  1. json_path = tile_json_path + basename + '.json':构造JSON文件的完整路径。这里使用了之前定义的tile_json_path变量和从图像文件名中提取的basename,再加上.json扩展名来形成完整的文件路径。

  2. bbox_list = []:初始化一个空列表,用于存储边界框(bounding box)的坐标信息。

  3. centroid_list = []:初始化一个空列表,用于存储每个细胞核的质心(centroid)坐标信息。

  4. contour_list = []:初始化一个空列表,用于存储每个细胞核的轮廓(contour)坐标信息。

  5. type_list = []:初始化一个空列表,用于存储每个细胞核的类型(type)信息。

  6. with open(json_path) as json_file::使用with语句打开指定路径的JSON文件,并将其别名设置为json_filewith语句可以确保文件在操作完成后正确关闭。

  7. data = json.load(json_file):使用json.load函数从打开的文件中读取JSON数据,并将其存储在变量data中。

  8. mag_info = data['mag']:从读取的JSON数据中提取mag键对应的值,并将其存储在变量mag_info中。mag代表与图像相关的放大倍数信息。

  9. nuc_info = data['nuc']:从JSON数据中提取nuc键对应的值,这通常包含了关于细胞核的所有信息,并将其存储在变量nuc_info中。

  10. for inst in nuc_info::遍历nuc_info字典中的每个实例。这里的inst代表每个细胞核的实例ID。

  11. inst_info = nuc_info[inst]:获取当前实例的详细信息。

  12. inst_centroid = inst_info['centroid']:从当前实例的信息中提取质心坐标,并将其存储在inst_centroid变量中。

  13. centroid_list.append(inst_centroid):将当前细胞核的质心坐标添加到centroid_list列表中。

  14. inst_contour = inst_info['contour']:提取当前实例的轮廓坐标。

  15. contour_list.append(inst_contour):将当前细胞核的轮廓坐标添加到contour_list列表中。

  16. inst_bbox = inst_info['bbox']:提取当前实例的边界框坐标。

  17. bbox_list.append(inst_bbox):将当前细胞核的边界框坐标添加到bbox_list列表中。

  18. inst_type = inst_info['type']:提取当前实例的类型。

  19. type_list.append(inst_type):将当前细胞核的类型添加到type_list列表中。

通过这段代码,原始JSON文件中的数据被解析并根据其类型存储到不同的列表中,这有助于后续的数据处理和分析。


4-8:不同列表中的元素数量

# get the number of items in each list

print('Number of centroids', len(centroid_list))
print('Number of contours', len(contour_list))
print('Number of bounding boxes', len(bbox_list))

# each item is a list of coordinates - let's take a look!
print('-'*60)
print(centroid_list[0])
print('-'*60)
print(contour_list[0])
print('-'*60)
print(bbox_list[0])

这段代码的目的是打印出不同列表中的元素数量,并展示每个列表中第一个元素的内容。

  1. print('Number of centroids', len(centroid_list)):打印出centroid_list列表中的元素数量。列表centroid_list存储了从JSON文件中提取的所有细胞核的质心坐标。

  2. print('Number of contours', len(contour_list)):打印出contour_list列表中的元素数量。列表contour_list存储了所有细胞核的轮廓坐标。

  3. print('Number of bounding boxes', len(bbox_list)):打印出bbox_list列表中的元素数量。列表bbox_list存储了所有细胞核的边界框坐标。

  4. print('-'*60):打印出60个连字符(-),作为分隔线,以便于区分不同的输出部分。

  5. print(centroid_list[0]):打印出centroid_list列表中的第一个元素。这个元素是一个包含质心坐标的列表。

  6. print('-'*60):再次打印出分隔线。

  7. print(contour_list[0]):打印出contour_list列表中的第一个元素。这个元素是一个包含细胞核轮廓坐标的列表。

  8. print('-'*60):再次打印出分隔线。

  9. print(bbox_list[0]):打印出bbox_list列表中的第一个元素。这个元素是一个包含边界框坐标的列表。

通过这段代码,我们可以得到以下信息:

image-20240622181540517

  • 每个列表中存储的元素数量,即每种类型的数据点(质心、轮廓、边界框)有多少个。
  • 每个列表中第一个元素的具体内容,这有助于了解数据的结构和格式。例如,质心坐标是一个包含两个浮点数的列表(例如[x, y]),轮廓坐标是一个包含多个坐标点的列表(例如[[x1, y1], [x2, y2], ...]),边界框坐标是一个包含四个数值的列表,代表边界框的左上角和右下角坐标(例如[x_min, y_min, x_max, y_max])。

这样的输出对于验证数据的完整性和理解数据结构非常有用。


4-9:随机可视化

# get a single contour, bounding box and centroid and visualise

rand_nucleus = np.random.randint(0, len(centroid_list))
rand_centroid = centroid_list[rand_nucleus]
rand_bbox = bbox_list[rand_nucleus]
rand_contour = contour_list[rand_nucleus]

# draw the overlays
overlay = image.copy()
overlay = cv2.drawContours(overlay.astype('uint8'), [np.array(rand_contour)], -1, (255,255,0), 1)
overlay = cv2.circle(overlay.astype('uint8'),(np.round(rand_centroid[0]).astype('int'), np.round(rand_centroid[1]).astype('int')), 3, (0,255,0), -1)
overlay = cv2.rectangle(overlay.astype('uint8'), (rand_bbox[0][1], rand_bbox[0][0]), (rand_bbox[1][1], rand_bbox[1][0]), (255,0,0), 1)

这段代码的目的是随机选择一个细胞核的质心、轮廓和边界框,并在原始图像上绘制这些元素的可视化效果。

  1. rand_nucleus = np.random.randint(0, len(centroid_list)):使用NumPy的randint函数随机生成一个整数,范围从0到centroid_list列表的长度减1。这个整数将用作随机选择一个细胞核的索引。

  2. rand_centroid = centroid_list[rand_nucleus]:根据随机索引rand_nucleus,从centroid_list中选择一个质心坐标。

  3. rand_bbox = bbox_list[rand_nucleus]:根据随机索引,从bbox_list中选择一个边界框坐标。

  4. rand_contour = contour_list[rand_nucleus]:根据随机索引,从contour_list中选择一个轮廓坐标。

  5. overlay = image.copy():复制原始图像imageoverlay变量,以便在其上绘制图形,而不改变原始图像。

  6. overlay = cv2.drawContours(...:使用OpenCV的drawContours函数在overlay图像上绘制轮廓。参数解释如下:

    • overlay.astype('uint8'):确保overlay图像的数据类型为无符号8位整数,这是drawContours函数的要求。
    • [np.array(rand_contour)]:将轮廓坐标转换为NumPy数组,并放在列表中,因为函数需要一个轮廓列表。
    • -1:表示绘制单个轮廓,而不是轮廓列表。
    • (255,255,0):绘制轮廓的颜色为黄色(RGB颜色值)。
    • 1:轮廓线条的粗细。
  7. overlay = cv2.circle(...:使用OpenCV的circle函数在overlay图像上绘制一个圆,代表细胞核的质心。参数解释如下:

    • overlay.astype('uint8'):同上,确保数据类型正确。
    • (np.round(rand_centroid[0]).astype('int'), np.round(rand_centroid[1]).astype('int')):计算质心坐标的四舍五入值,并转换为整数,作为圆心坐标。
    • 3:圆的半径。
    • (0,255,0):圆的颜色为绿色。
    • -1:填充整个圆。
  8. overlay = cv2.rectangle(...:使用OpenCV的rectangle函数在overlay图像上绘制一个矩形,代表细胞核的边界框。参数解释如下:

    • overlay.astype('uint8'):同上,确保数据类型正确。
    • (rand_bbox[0][1], rand_bbox[0][0]):边界框左上角的坐标(先y后x)。
    • (rand_bbox[1][1], rand_bbox[1][0]):边界框右下角的坐标。
    • (255,0,0):矩形的颜色为蓝色。
    • 1:矩形线条的粗细。

通过这段代码,可以在原始图像上可视化单个细胞核的质心、轮廓和边界框,这对于图像分析和细胞核检测算法的验证非常有帮助。


# plot the cropped overlay

pad = 30
crop1 = rand_bbox[0][0]-pad
if crop1 < 0: 
    crop1 = 0
crop2 = rand_bbox[1][0]+pad
if crop2 > overlay.shape[0]: 
    crop2 = overlay.shape[0]
crop3 = rand_bbox[0][1]-pad
if crop3 < 0: 
    crop3 = 0
crop4 = rand_bbox[1][1]+pad
if crop4 > overlay.shape[1]: 
    crop4 = overlay.shape[1]
crop_overlay = overlay[crop1:crop2,crop3:crop4,:]
plt.figure(figsize=(10,10))

plt.imshow(crop_overlay)
plt.axis('off')
plt.title('Overlay', fontsize=25)
plt.show()

这段代码的目的是从一个较大的图像中裁剪出一个区域,并展示这个区域的可视化效果,通常这个区域会包含之前绘制的细胞核的质心、轮廓和边界框。

  1. pad = 30:定义一个变量pad,用于在边界框周围添加额外的像素。这样做可以确保在裁剪图像时,细胞核的某些部分不会因为边界框的精确边缘而被裁剪掉。

  2. crop1 = rand_bbox[0][0]-pad:计算裁剪区域的起始x坐标。这是边界框左上角的x坐标减去pad值。

  3. if crop1 < 0: crop1 = 0:如果计算出的起始x坐标小于0,则将其设置为0,以避免数组越界。

  4. crop2 = rand_bbox[1][0]+pad:计算裁剪区域的结束x坐标。这是边界框右下角的x坐标加上pad值。

  5. if crop2 > overlay.shape[0]: crop2 = overlay.shape[0]:如果结束x坐标大于图像的高度,则将其设置为图像的高度,以避免数组越界。

  6. crop3 = rand_bbox[0][1]-pad:计算裁剪区域的起始y坐标。这是边界框左上角的y坐标减去pad值。

  7. if crop3 < 0: crop3 = 0:如果计算出的起始y坐标小于0,则将其设置为0。

  8. crop4 = rand_bbox[1][1]+pad:计算裁剪区域的结束y坐标。这是边界框右下角的y坐标加上pad值。

  9. if crop4 > overlay.shape[1]: crop4 = overlay.shape[1]:如果结束y坐标大于图像的宽度,则将其设置为图像的宽度。

  10. crop_overlay = overlay[crop1:crop2,crop3:crop4,:]:根据计算出的坐标,从overlay图像中裁剪出一个子区域。这个子区域包含了原始边界框周围额外的pad像素。

  11. plt.figure(figsize=(10,10)):创建一个新的图形对象,并设置图形的大小为宽10英寸、高10英寸。

  12. plt.imshow(crop_overlay):在当前图形中显示裁剪后的覆盖层图像crop_overlay

  13. plt.axis('off'):关闭图形的坐标轴,使得图像显示时不显示坐标轴。

  14. plt.title('Overlay', fontsize=25):为图形设置标题为"Overlay",字体大小为25。

  15. plt.show():显示图形窗口,展示裁剪后的覆盖层图像。

image-20240622182109207

通过这段代码,可以更加清晰地查看和分析单个细胞核及其标注的详细信息,这对于图像处理和细胞核分析的验证非常有用。


五、全切片图像处理

  1. 输出格式:WSI处理的输出结果是JSON格式的文件,这意味着可以使用之前提到的技术来提取实例信息。
  2. 输出内容
    • 分割结果:处理输出包括了图像的分割结果,这通常指的是细胞核或其他感兴趣的区域的识别和标记。
    • 低分辨率缩略图:生成的输出中包含一张低分辨率的缩略图,这可以用于快速浏览WSI的概览。
    • 掩模:同时保存了一个掩模,这是一个标记了特定区域或特征的图像层,常用于进一步的分析或可视化。
  3. 技术应用:使用JSON格式的输出,可以方便地提取和处理数据,这是现代图像分析中常用的一种方法。通过结合低分辨率缩略图和掩模,可以快速地对WSI进行质量评估。
  4. 潜在的挑战:处理WSI时可能会遇到的挑战包括图像文件的大小、处理速度、内存使用等,因为WSI通常具有非常高的分辨率和庞大的数据量。

5-1:处理全切片图像

# get the list of all wsis
wsi_list = glob.glob(wsi_path + '*')

# get a random wsi from the list
rand_wsi = np.random.randint(0,len(wsi_list))
wsi_file = wsi_list[rand_wsi]
wsi_ext = '.svs'

wsi_basename = os.path.basename(wsi_file)
wsi_basename = wsi_basename[:-(len(wsi_ext))]

这段代码用于处理全切片图像(Whole Slide Images, WSIs),主要执行以下任务:

  1. wsi_list = glob.glob(wsi_path + '*'):使用glob模块的glob函数,结合之前定义的wsi_path变量,获取路径下所有文件的列表。星号*是通配符,代表任意字符的零次或多次出现,这里用来匹配所有文件。

  2. rand_wsi = np.random.randint(0,len(wsi_list)):使用np.random.randint函数从wsi_list中随机选择一个索引。这个索引用于后续获取列表中的一个全切片图像文件。

  3. wsi_file = wsi_list[rand_wsi]:根据随机生成的索引rand_wsi,从wsi_list中获取对应的全切片图像文件的路径。

  4. wsi_ext = '.svs':定义全切片图像文件的扩展名为.svs。这是一种常见的全切片图像格式。

  5. wsi_basename = os.path.basename(wsi_file):使用os.path.basename函数从全切片图像文件的完整路径中提取基本的文件名。

  6. wsi_basename = wsi_basename[:-(len(wsi_ext))]:去除文件名末尾的扩展名.svs,只保留基本的文件名部分。这里使用了字符串切片操作,[:-(len(wsi_ext))]表示从字符串的开头到扩展名前一个字符的切片。

通过这段代码,我们可以得到一个随机选择的全切片图像的基本文件名,这个基本文件名在后续的处理中会被用来生成输出文件的名称或作为其他操作的依据。例如,在处理WSI时,需要根据基本文件名来查找对应的JSON输出文件或掩模图像。


5-2:读片&绘图

# read the thumbnail and mask from file

mask_path_wsi = wsi_json_path + 'mask/' + wsi_basename + '.png'
thumb_path_wsi = wsi_json_path + 'thumb/' + wsi_basename + '.png'

thumb = cv2.cvtColor(cv2.imread(thumb_path_wsi), cv2.COLOR_BGR2RGB)
mask = cv2.cvtColor(cv2.imread(mask_path_wsi), cv2.COLOR_BGR2RGB)

# plot the low resolution thumbnail along with the tissue mask

plt.figure(figsize=(15,8))

plt.subplot(1,2,1)
plt.imshow(thumb)
plt.axis('off')
plt.title('Thumbnail', fontsize=25)

plt.subplot(1,2,2)
plt.imshow(mask)
plt.axis('off')
plt.title('Mask', fontsize=25)

plt.show()

这段代码的目的是读取全切片图像(WSI)的缩略图和掩模,并使用matplotlib库将它们显示出来。

  1. mask_path_wsi = wsi_json_path + 'mask/' + wsi_basename + '.png':构造掩模图像的完整文件路径。这里假设掩模被保存在wsi_json_path路径下的mask子目录中,并且文件名由基本文件名wsi_basename.png扩展名组成。

  2. thumb_path_wsi = wsi_json_path + 'thumb/' + wsi_basename + '.png':构造缩略图图像的完整文件路径。缩略图被保存在wsi_json_path路径下的thumb子目录中,文件命名方式与掩模相同。

  3. thumb = cv2.cvtColor(cv2.imread(thumb_path_wsi), cv2.COLOR_BGR2RGB):使用OpenCV的imread函数读取缩略图图像,然后使用cvtColor函数将图像的颜色空间从BGR转换为RGB。这是因为OpenCV默认使用BGR颜色空间,而大多数图像处理和可视化库使用RGB颜色空间。

  4. mask = cv2.cvtColor(cv2.imread(mask_path_wsi), cv2.COLOR_BGR2RGB):同上,读取掩模图像并将其颜色空间从BGR转换为RGB。

  5. plt.figure(figsize=(15,8)):创建一个新的matplotlib图形对象,并设置图形的大小为宽15英寸、高8英寸。

  6. plt.subplot(1,2,1):设置当前的子图网格为1行2列,并激活第1个子图(左起第1个位置)。

  7. plt.imshow(thumb):在当前激活的子图中显示缩略图图像。

  8. plt.axis('off'):关闭当前子图的坐标轴。

  9. plt.title('Thumbnail', fontsize=25):为当前子图设置标题为"Thumbnail",字体大小为25。

  10. plt.subplot(1,2,2):设置当前的子图网格为1行2列,并激活第2个子图(左起第2个位置)。

  11. plt.imshow(mask):在当前激活的子图中显示掩模图像。

  12. plt.axis('off'):同上,关闭当前子图的坐标轴。

  13. plt.title('Mask', fontsize=25):为当前子图设置标题为"Mask",字体大小为25。

  14. plt.show():显示所有子图组成的图形。这将打开一个窗口,展示缩略图和掩模图像。

通过这段代码,用户可以同时查看WSI的缩略图和掩模,这有助于快速评估WSI的整体结构和分割结果的质量。

image-20240622185456508


5-3:提取特定于WSI的分析结果

# load the json file (may take ~20 secs)

json_path_wsi = wsi_json_path + 'json/' + wsi_basename + '.json'

bbox_list_wsi = []
centroid_list_wsi = []
contour_list_wsi = [] 
type_list_wsi = []

# add results to individual lists
with open(json_path_wsi) as json_file:
    data = json.load(json_file)
    mag_info = data['mag']
    nuc_info = data['nuc']
    for inst in nuc_info:
        inst_info = nuc_info[inst]
        inst_centroid = inst_info['centroid']
        centroid_list_wsi.append(inst_centroid)
        inst_contour = inst_info['contour']
        contour_list_wsi.append(inst_contour)
        inst_bbox = inst_info['bbox']
        bbox_list_wsi.append(inst_bbox)
        inst_type = inst_info['type']
        type_list_wsi.append(inst_type)

这段代码是用于从JSON文件中提取特定于WSI(全切片图像)的分析结果,并将这些结果存储到不同的列表中。

  1. json_path_wsi = wsi_json_path + 'json/' + wsi_basename + '.json':构建存储WSI分析结果的JSON文件的完整路径。这里假设JSON文件位于wsi_json_path路径下的json子目录中,并且文件名由基本文件名wsi_basename和扩展名.json组成。

  2. bbox_list_wsi = []:初始化一个空列表,用于存储从JSON文件中提取的边界框(bounding box)坐标。

  3. centroid_list_wsi = []:初始化一个空列表,用于存储细胞核的质心(centroid)坐标。

  4. contour_list_wsi = []:初始化一个空列表,用于存储细胞核的轮廓(contour)坐标。

  5. type_list_wsi = []:初始化一个空列表,用于存储细胞核的类型(type)信息。

  6. with open(json_path_wsi) as json_file::使用with语句打开json_path_wsi路径指定的JSON文件,并将文件对象赋值给变量json_filewith语句可以确保文件在使用后正确关闭。

  7. data = json.load(json_file):使用json.load函数从打开的文件中解析JSON数据,并将解析后的数据存储在变量data中。

  8. mag_info = data['mag']:从解析后的JSON数据中提取键为'mag'的值,这包含了与图像放大倍数相关的信息,并将其存储在变量mag_info中。

  9. nuc_info = data['nuc']:从JSON数据中提取键为'nuc'的值,这个值包含了关于细胞核的详细信息,并将其存储在变量nuc_info中。

  10. for inst in nuc_info::遍历nuc_info字典中的每个条目,inst代表每个细胞核实例的键(可能是细胞核的ID)。

  11. inst_info = nuc_info[inst]:获取当前细胞核实例的详细信息。

  12. inst_centroid = inst_info['centroid']:从当前细胞核实例的信息中提取质心坐标。

  13. centroid_list_wsi.append(inst_centroid):将提取的质心坐标添加到centroid_list_wsi列表中。

  14. inst_contour = inst_info['contour']:提取当前细胞核实例的轮廓坐标。

  15. contour_list_wsi.append(inst_contour):将提取的轮廓坐标添加到contour_list_wsi列表中。

  16. inst_bbox = inst_info['bbox']:提取当前细胞核实例的边界框坐标。

  17. bbox_list_wsi.append(inst_bbox):将提取的边界框坐标添加到bbox_list_wsi列表中。

  18. inst_type = inst_info['type']:提取当前细胞核实例的类型。

  19. type_list_wsi.append(inst_type):将提取的细胞核类型添加到type_list_wsi列表中。

整体来看,这段代码通过解析JSON文件,提取了WSI分析结果中的关键信息,并将这些信息分类存储到不同的列表中,为进一步的分析或可视化做准备。注释中提到的“may take ~20 secs”暗示了这个文件非常大,解析它可能需要一些时间。


5-4:从全切片图像生成(提取)一个特定区域的瓦片

# let's generate a tile from the WSI

# define the region to select
x_tile = 30000
y_tile = 30000
w_tile = 1500
h_tile = 1500

# load the wsi object and read region
wsi_obj = get_file_handler(wsi_file, wsi_ext)
wsi_obj.prepare_reading(read_mag=mag_info)
wsi_tile = wsi_obj.read_region((x_tile,y_tile), (w_tile,h_tile))

这段代码的目的是从一个全切片图像(Whole Slide Image, WSI)中生成(提取)一个特定区域的瓦片(tile)。

  1. # let's generate a tile from the WSI:这是一个注释,说明接下来的代码将从WSI中生成一个瓦片。

  2. # define the region to select:这是另一个注释,指出接下来的代码将定义要从WSI中提取的区域的坐标和尺寸。

  3. x_tile = 30000:定义瓦片的x坐标起始点。这是瓦片左上角在WSI中的水平位置。

  4. y_tile = 30000:定义瓦片的y坐标起始点。这是瓦片左上角在WSI中的垂直位置。

  5. w_tile = 1500:定义瓦片的宽度。这是瓦片在水平方向上的像素数。

  6. h_tile = 1500:定义瓦片的高度。这是瓦片在垂直方向上的像素数。

  7. wsi_obj = get_file_handler(wsi_file, wsi_ext):调用get_file_handler函数来创建一个用于处理WSI文件的对象。这个函数接收WSI文件的路径和扩展名作为参数。

  8. wsi_obj.prepare_reading(read_mag=mag_info):调用WSI对象的prepare_reading方法来准备读取操作。这里使用mag_info作为读取放大倍数的参数,这可能是从JSON解析中得到的放大倍数信息。

  9. wsi_tile = wsi_obj.read_region((x_tile,y_tile), (w_tile,h_tile)):调用WSI对象的read_region方法来从指定的区域提取瓦片。这个方法接收两个参数:一个是定义区域左上角坐标的元组(x_tile, y_tile),另一个是定义区域大小的元组(w_tile, h_tile)。返回的瓦片存储在变量wsi_tile中。

这段代码通常用在数字病理学中,用于从WSI中提取特定区域以进行更详细的分析或可视化。提取的瓦片可以用于进一步的处理,如细胞核分割、特征提取或其他图像分析任务。


5-5:筛选重叠的细胞核轮廓

# only consider results that are within the tile

coords_xmin = x_tile
coords_xmax = x_tile + w_tile
coords_ymin = y_tile
coords_ymax = y_tile + h_tile

tile_info_dict = {}
count = 0
for idx, cnt in enumerate(contour_list_wsi):
    cnt_tmp = np.array(cnt)
    cnt_tmp = cnt_tmp[(cnt_tmp[:,0] >= coords_xmin) & (cnt_tmp[:,0] <= coords_xmax) & (cnt_tmp[:,1] >= coords_ymin) & (cnt_tmp[:,1] <= coords_ymax)] 
    label = str(type_list_wsi[idx])
    if cnt_tmp.shape[0] > 0:
        cnt_adj = np.round(cnt_tmp - np.array([x_tile,y_tile])).astype('int')
        tile_info_dict[idx] = {'contour': cnt_adj, 'type':label}
        count += 1

这段代码的目的是筛选出那些与从WSI中提取的瓦片区域重叠的细胞核轮廓信息,并存储这些信息到一个字典中。

  1. coords_xmin = x_tile:设置瓦片区域的最小x坐标,即瓦片左上角的x坐标。

  2. coords_xmax = x_tile + w_tile:设置瓦片区域的最大x坐标,即瓦片右下角的x坐标。

  3. coords_ymin = y_tile:设置瓦片区域的最小y坐标,即瓦片左上角的y坐标。

  4. coords_ymax = y_tile + h_tile:设置瓦片区域的最大y坐标,即瓦片右下角的y坐标。

  5. tile_info_dict = {}:初始化一个空字典,用于存储与瓦片重叠的细胞核轮廓信息。

  6. count = 0:初始化一个计数器,用于记录有多少个细胞核轮廓与瓦片重叠。

  7. for idx, cnt in enumerate(contour_list_wsi)::遍历contour_list_wsi列表,该列表包含了WSI中所有细胞核的轮廓坐标。enumerate函数用于同时获取索引(idx)和值(cnt)。

  8. cnt_tmp = np.array(cnt):将轮廓坐标列表转换为NumPy数组,以便于进行数学运算。

  9. cnt_tmp = cnt_tmp[(cnt_tmp[:,0] >= coords_xmin) & (cnt_tmp[:,0] <= coords_xmax) & (cnt_tmp[:,1] >= coords_ymin) & (cnt_tmp[:,1] <= coords_ymax)]:使用布尔索引筛选出那些位于瓦片区域内的轮廓点。这里cnt_tmp[:,0]cnt_tmp[:,1]分别代表轮廓点的x和y坐标。

  10. label = str(type_list_wsi[idx]):获取当前细胞核轮廓对应的类型,并将其转换为字符串。

  11. if cnt_tmp.shape[0] > 0::检查筛选后的轮廓点数组是否非空。

  12. cnt_adj = np.round(cnt_tmp - np.array([x_tile,y_tile])).astype('int'):计算筛选后的轮廓点相对于瓦片左上角的坐标,并四舍五入到最近的整数。这样做是为了将坐标系从WSI转换到瓦片的局部坐标系。

  13. tile_info_dict[idx] = {'contour': cnt_adj, 'type':label}:如果筛选后的轮廓点数组非空,将这些点及其类型作为键值对存入tile_info_dict字典中,其中idx作为键。

  14. count += 1:对于每个有效筛选的轮廓,增加计数器。

通过这段代码,我们能够提取出所有与特定瓦片区域相交的细胞核轮廓信息,并将这些信息存储在一个字典中,便于后续的处理和分析。


5-6:可视化

# plot the overlay

# the below dictionary is specific to PanNuke checkpoint - will need to modify depeending on categories used
type_info = {
    "0" : ["nolabe", [0  ,   0,   0]], 
    "1" : ["neopla", [255,   0,   0]], 
    "2" : ["inflam", [0  , 255,   0]], 
    "3" : ["connec", [0  ,   0, 255]], 
    "4" : ["necros", [255, 255,   0]], 
    "5" : ["no-neo", [255, 165,   0]] 
}

plt.figure(figsize=(18,15))
overlaid_output = visualize_instances_dict(wsi_tile, tile_info_dict, type_colour=type_info)
plt.imshow(overlaid_output)
plt.axis('off')
plt.title('Segmentation Overlay')
plt.show()

这段代码的目的是将从WSI中提取的瓦片与对应的细胞核轮廓信息进行可视化,通过在瓦片上绘制不同颜色的轮廓来表示不同类别的细胞核。

  1. type_info = {...}:定义一个字典type_info,其中包含不同细胞核类别的标签和颜色信息。这个字典是针对PanNuke数据集的检查点(checkpoint)特定的,如果使用其他数据集或类别,需要相应地修改这个字典。

    • 字典的键是类别的标识符(例如,“0”, “1”, "2"等)。
    • 每个键对应的值是一个列表,包含两个元素:类别的描述性标签(例如,“nolabe”, "neopla"等)和一个表示颜色的RGB列表(例如,[255, 0, 0]代表红色)。
  2. plt.figure(figsize=(18,15)):创建一个新的matplotlib图形对象,并设置图形的大小为宽18英寸、高15英寸。

  3. overlaid_output = visualize_instances_dict(...):调用visualize_instances_dict函数来在瓦片图像上绘制细胞核轮廓。这个函数接收以下参数:

    • wsi_tile:从WSI中提取的瓦片图像。
    • tile_info_dict:之前代码段中创建的包含细胞核轮廓信息的字典。
    • type_colour:定义了每种细胞核类别的颜色的字典。

    函数将根据tile_info_dict中的信息,在瓦片图像上绘制轮廓,并返回绘制后的图像。

  4. plt.imshow(overlaid_output):在当前图形中显示visualize_instances_dict函数返回的绘制了轮廓的图像。

  5. plt.axis('off'):关闭图形的坐标轴显示。

  6. plt.title('Segmentation Overlay'):为图形设置标题为"Segmentation Overlay"。

  7. plt.show():显示图形窗口,展示绘制了细胞核轮廓的瓦片图像。

通过这段代码,用户可以直观地看到WSI瓦片上的细胞核分割结果,其中不同颜色的轮廓代表不同类别的细胞核,有助于进行进一步的分析和验证。

下载

  • 10
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要进行Python细胞核分割,可以使用OpenCV库和U-net网络结构。首先,将图像转化成灰度图像,并进行二值化处理,通过设置合适的阈值来提取细胞核和细胞质的区域。接下来,可以使用开运算和边缘检测函数对图像进行处理,以进一步提取和分割细胞核的边界。最后,可以使用U-net网络结构进行细胞核分割。U-net是一种常用的医学影像分割网络结构,它通过Encoder部分逐步下采样,提取高层语义信息,然后通过Decoder部分逐步恢复原始图像的尺寸,并结合跳跃连接来获得更好的分割结果。通过计算细胞核和细胞质的大小比例,可以得到核质比的结果。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [Python计算细胞核与细胞质的面积比opencv或pil实验](https://blog.csdn.net/m0_46653805/article/details/125959673)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *3* [Pytorch实现U-net细胞分割](https://blog.csdn.net/lwf1881/article/details/121864565)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值