00 - 前言
欢迎学习《基于 PraNet 的医疗影像分割》实验。在本实验中,你将深入了解如何运用计算机视觉(CV)领域的 AI 模型,搭建一个高效精准的医疗影像分割系统,专注于息肉分割任务,并利用开源数据集对模型效果加以验证。
学习目标
在本课程中,您将学习一些与使用 AI 图像处理技术实现息肉影像分割有关的重要概念,包括:
- 医疗影像数据的预处理方法
- 采用 PraNet 模型对息肉区域进行分割的方法
- 图像分割的后处理方法
- 端到端深度学习工作流
目录
本实验分为四个核心部分。第一部分主要介绍案例的应用场景,阐述遥感影像地块分割的重要性及意义;第二部分会详细阐述端到端的解决方案,搭建起技术实现的整体框架;第三部分会手把手指导您完成代码编写与实现;最后一部分给出测试题,帮助您巩固学习内容。
- 场景介绍
- 解决方案
- 代码实战
- 课后测试
JupyterLab
在本实操实验中,我们将使用 JupyterLab 管理我们的环境。JupyterLab 界面是一个控制面板,可供您访问交互式 iPython Notebook、所用环境的文件夹结构,以及用于进入 Ubuntu 操作系统的终端窗口,只需要点击菜单栏的小三角就可以运行代码。
尝试执行下方单元中的一条简单的 print(打印)语句。
# DO NOT CHANGE THIS CELL # activate this cell by selecting it with the mouse or arrow keys then use the keyboard shortcut [Shift+Enter] to execute print('This is just a simple print statement') This is just a simple print statement
01 场景介绍
在医疗领域,息肉分割是一项至关重要的任务。息肉是肠道等器官表面的异常生长组织,其早期发现对于预防和治疗相关疾病(如结直肠癌)具有重要意义。传统的息肉检测方法依赖于医生的肉眼观察,容易受到主观因素的影响,且在处理大量影像数据时效率较低。因此,利用 AI 技术实现自动化的息肉分割,能够提高诊断的准确性和效率,为医生提供有力的辅助决策支持。
02 解决方案
本实验教程使用的解决方案如下,首先对原始图像进行预处理,使得图像数据符合 AI 模型的输入要求,然后使用图像分割模型进行推理,预测每个像素点属于息肉组织的置信度,最后进行后处理,把分割的区域可视化标记出来。
以下分别介绍这几个核心模块。
2.1 图像预处理模块
原始的医疗影像是通过胃镜等设备在患者体内拍摄得到的,包含着各种复杂的信息,直接用于分割检测可能会导致较低的准确性。因此,我们首先对其进行预处理操作,其中一项重要的处理是将图像转换为 YUV 格式。
YUV 格式相较于常见的 RGB 格式,在颜色信息的表示上具有独特的优势。通过将亮度(Y)与色度(U 和 V)信息分离,能够更好地突出图像中的物体轮廓和细节特征(息肉检测的重点就是要把它的轮廓特征检测出来),为后续的模型处理提供更加清晰和易于分析的数据基础。在转换过程中,我们运用专业的图像转换算法,确保图像的色彩信息得以准确保留,同时优化了亮度和色度的分布,使得息肉与周围环境的对比度增强,从而有效提升了后续模型对息肉特征的提取能力,为精准分割奠定了坚实的基础。
2.2 图像分割模型
在数据预处理之后,我们借助先进的息肉分割模型 PraNet 进行推理操作,以实现对每个像素点的准确分类,这是整个解决方案的核心环节。
PraNet(Parallel Reverse Attention Network for Polyp Segmentation)是一种针对息肉分割任务需求设计的、名为并行反向注意力的深度神经网络。主要包括两个部分: - 并行的部分解码器(PPD):这个部分的作用是聚合图像的高级特征,形成一个初始的引导区域。简单来说,它就像是一个“粗略定位器”,能够大致确定息肉在图像中的位置。它通过分析图像的全局信息,找出息肉的大致轮廓,为后续的精确分割打下基础。 - 反向注意模块(RA):这个部分的作用是挖掘边界线索,通过反向传播的方式,逐步细化息肉的边界。它就像是一个“精细雕刻师”,在PPD确定的粗略区域上,进一步精确地勾勒出息肉的边缘。它能够捕捉到息肉与周围组织的细微差别,使得分割结果更加准确。
2.3 后处理模块
二值化处理:经过模型推理得到的结果,是每个像素点分类为息肉组织的置信度,为了把息肉组织和周边组织区分开,我们需要对分类结果进行二值化,得到一个新的分割图,这个分割图上息肉组织的像素点的值是255(白色),其他组织像素点是0(黑色),这样就能清楚地看出息肉组织的位置了。
对比展示:得到分割结果图后,我们可以把它和原图并排放在一起,确认检测结果是否和实际情况一致。
03 动手实验
3.1 实验准备
数据集
实验所用的开源图像集是kvasir-seg数据集,该数据集包含了1000张内镜影像。
模型权重
本实验采用的 PraNet
模型需要从这个链接下载,里面包含了 onnx 格式的模型,我们后续将会用到。
3.2 图像预处理
参考原项目的 pranet.aippconfig
文件,可以知道模型的输入图像格式为 YUV420SP_U8
,含义如下:
YUV:颜色编码方式,YUV 是一种颜色模型,它将颜色信息分为亮度(Y)和色度(U, V)两部分。这种编码方式在图像和视频压缩中非常常见,因为它可以更有效地表示颜色信息,并且允许单独压缩亮度和色度分量。
420:色度采样(Chroma Subsampling),420表示色度信息的采样率。在YUV420中,U和V分量的采样率是亮度分量的一半。具体来说,对于每四个Y值,只有一个U和一个V值。这种采样方式减少了色度信息的数量,可以在保持图像质量的同时减少数据量。
SP:Semi-Planar的缩写,指的是色度分量的存储方式。在YUV420SP格式中,U和V分量交错存储(交错是指在行或列上相邻存储),而不是分开存储。这意味着U和V分量不是完全独立的两个平面,而是交错在一起。
U8:数据类型,U8表示每个颜色分量(Y、U、V)使用8位无符号整数(0-255)来存储。这表示每个颜色值可以用一个字节来表示。
所以我们需要把原始的图像进行预处理,才能传给 AI 模型。 第一步,导入所需的三方库:
import numpy as np import cv2
接着,创建图像预处理函数:
def img_process(img_path): image = cv2.imread(img_path) # resize成模型的输入shape image = cv2.resize(image, (352, 352), interpolation=cv2.INTER_AREA) # 将图片从BGR颜色空间转换为YUV颜色空间 yuv_image = cv2.cvtColor(image, cv2.COLOR_BGR2YUV) # 获取图片的尺寸 height, width = yuv_image.shape[:2] # 分离Y, U, V分量 y, u, v = cv2.split(yuv_image) # 将U和V分量下采样到4:2:0 u = cv2.resize(u, (width // 2, height // 2), interpolation=cv2.INTER_LINEAR) v = cv2.resize(v, (width // 2, height // 2), interpolation=cv2.INTER_LINEAR) # 交错UV分量,形成NV12格式 uv = np.zeros((height // 2, width), dtype=yuv_image.dtype) uv[:, 0::2] = u uv[:, 1::2] = v # 将Y分量和交错的UV分量合并 image = np.concatenate((y, uv), axis=0) return image
完成数据类的定义后,我们可以对其进行测试,读取并预处理图像:
source = "./kvasir_test_pics/cju2zrojo9kcd0878ld2epejq.jpg" process_data = img_process(source) print(process_data.shape) (528, 352)
3.3 使用 PraNet 模型进行分割预测
对于一个长宽为 (w, h) 的图像,PraNet 模型的输出 shape 为 (1, w, h),代表每个像素点在分类为息肉组织的置信度,一般情况下,息肉组织对应的置信度大于0,其他正常组织的置信度小于0。
首先,我们需要把下载的 onnx 格式的模型转成能在昇腾硬件上运行的 om 格式的模型,命令如下:
# atc --model=./PraNet/PraNet-19.onnx --output=./PraNet-19_bs1 --framework=5 --input_shape="actual_input_1:1,3,352,352" --soc_version=Ascendxxx --input_format=NCHW --output_type=FP32 --insert_op_conf=./pranet.aippconfig # config 文件下载地址: https://gitee.com/ascend/mindxsdk-referenceapps/blob/master/contrib/PraNetSegmentation/model/pranet.aippconfig
然后我们构建 om 模型的推理类:
import acl ACL_MEM_MALLOC_HUGE_FIRST = 0 ACL_MEMCPY_HOST_TO_DEVICE = 1 ACL_MEMCPY_DEVICE_TO_HOST = 2 class OmModel: def __init__(self, model_path): # 初始化函数 self.device_id = 5 # step1: 初始化 ret = acl.init() # 指定运算的Device ret = acl.rt.set_device(self.device_id) # step2: 加载模型,本示例为pfld模型 # 加载离线模型文件,返回标识模型的ID self.model_id, ret = acl.mdl.load_from_file(model_path) # 创建空白模型描述信息,获取模型描述信息的指针地址 self.model_desc = acl.mdl.create_desc() # 通过模型的ID,将模型的描述信息填充到model_desc ret = acl.mdl.get_desc(self.model_desc, self.model_id) # step3:创建输入输出数据集 # 创建输入数据集 self.input_dataset, self.input_data = self.prepare_dataset('input') # 创建输出数据集 self.output_dataset, self.output_data = self.prepare_dataset('output') def prepare_dataset(self, io_type): # 准备数据集 if io_type == "input": # 获得模型输入的个数 io_num = acl.mdl.get_num_inputs(self.model_desc) acl_mdl_get_size_by_index = acl.mdl.get_input_size_by_index else: # 获得模型输出的个数 io_num = acl.mdl.get_num_outputs(self.model_desc) acl_mdl_get_size_by_index = acl.mdl.get_output_size_by_index # 创建aclmdlDataset类型的数据,描述模型推理的输入。 dataset = acl.mdl.create_dataset() datas = [] for i in range(io_num): # 获取所需的buffer内存大小 buffer_size = acl_mdl_get_size_by_index(self.model_desc, i) # 申请buffer内存 buffer, ret = acl.rt.malloc(buffer_size, ACL_MEM_MALLOC_HUGE_FIRST) # 从内存创建buffer数据 data_buffer = acl.create_data_buffer(buffer, buffer_size) # 将buffer数据添加到数据集 _, ret = acl.mdl.add_dataset_buffer(dataset, data_buffer) datas.append({"buffer": buffer, "data": data_buffer, "size": buffer_size}) return dataset, datas def forward(self, inputs): # 执行推理任务 # 遍历所有输入,拷贝到对应的buffer内存中 input_num = len(inputs) for i in range(input_num): bytes_data = inputs[i].tobytes() bytes_ptr = acl.util.bytes_to_ptr(bytes_data) # 将图片数据从Host传输到Device。 ret = acl.rt.memcpy(self.input_data[i]["buffer"], # 目标地址 device self.input_data[i]["size"], # 目标地址大小 bytes_ptr, # 源地址 host len(bytes_data), # 源地址大小 ACL_MEMCPY_HOST_TO_DEVICE) # 模式:从host到device # 执行模型推理。 ret = acl.mdl.execute(self.model_id, self.input_dataset, self.output_dataset) # 处理模型推理的输出数据,输出top5置信度的类别编号。 inference_result = [] for i, item in enumerate(self.output_data): buffer_host, ret = acl.rt.malloc_host(self.output_data[i]["size"]) # 将推理输出数据从Device传输到Host。 ret = acl.rt.memcpy(buffer_host, # 目标地址 host self.output_data[i]["size"], # 目标地址大小 self.output_data[i]["buffer"], # 源地址 device self.output_data[i]["size"], # 源地址大小 ACL_MEMCPY_DEVICE_TO_HOST) # 模式:从device到host # 从内存地址获取bytes对象 bytes_out = acl.util.ptr_to_bytes(buffer_host, self.output_data[i]["size"]) # 按照float32格式将数据转为numpy数组 data = np.frombuffer(bytes_out, dtype=np.float32) inference_result.append(data) return inference_result def __del__(self): # 析构函数 按照初始化资源的相反顺序释放资源。 # 销毁输入输出数据集 for dataset in [self.input_data, self.output_data]: while dataset: item = dataset.pop() ret = acl.destroy_data_buffer(item["data"]) # 销毁buffer数据 ret = acl.rt.free(item["buffer"]) # 释放buffer内存 ret = acl.mdl.destroy_dataset(self.input_dataset) # 销毁输入数据集 ret = acl.mdl.destroy_dataset(self.output_dataset) # 销毁输出数据集 # 销毁模型描述 ret = acl.mdl.destroy_desc(self.model_desc) # 卸载模型 ret = acl.mdl.unload(self.model_id) # 释放device ret = acl.rt.reset_device(self.device_id) # acl去初始化 ret = acl.finalize()
现在测试一下模型的推理结果
# 加载模型 pranet_om_model_path = "./PraNet-19_bs1.om" pranet_om_model = OmModel(pranet_om_model_path) # 推理 output_res = pranet_om_model.forward([process_data]) tensor_res = output_res[0].reshape(1, 1, 352, 352) # 模型的输出shape是 (1, 1, 352, 352) print(tensor_res.shape) (1, 1, 352, 352)
3.4 后处理函数
正如前面提到的,模型的输出结果是各个像素点分类为息肉组织的置信度,我们还需要把分类结果进行二值化,构建黑色和白色表示的区域分割图片。这一系列操作对应下面的后处理函数:
THRESHOLD_VALUE = 0.5 # 二值化置信度阈值,根据算法效果调试得到 def decode_seg_map(label_mask, save_path): # 获取2维的分类结果 segment_result = label_mask[0][0] # 新建一个0矩阵,表示二值分割像素矩阵 seg_map = np.zeros((segment_result.shape[0], segment_result.shape[1]), dtype=np.float32) # 二值化 seg_map[segment_result > THRESHOLD_VALUE] = 255 # 保存二值化结果 cv2.imwrite(save_path, seg_map) return
此外,为了比较原图和分割结果,还需要保存原图和分割结果的对比图:
def enable_contrast_output(arr): """ Enable comparison graph output Args: arr: arr[0] is the img one, arr[1] is the img two, arr[2] is the output directory of the comparison graph result Returns: null Output: a comparison graph result in arr[2] """ img1 = Image.open(arr[0]) img2 = Image.open(arr[1]) img2 = img2.resize(img1.size, Image.NEAREST) # create a new image, set the width and height toImage = Image.new('RGB', (img1.width + img2.width + 35, img2.height), 'white') # paste image1 to the new image, and set the position toImage.paste(img1, (0, 0)) # paste image2 to the new image, and set the position toImage.paste(img2, (img1.width + 35, 0)) # save the result image, and the quality is 100 toImage.save(arr[2], quality=100)
好了,完成这些后,我们就可以开始进行端到端的图像分割实验了!
3.5 整合代码实现端到端检测
我们把前面创建的预处理函数、模型推理代码和后处理函数组合起来,形成下面的流程:
import os import glob from pathlib import Path from PIL import Image img_dir = "./kvasir_test_pics/" save_path = "./result/" imgs_path = glob.glob(str(Path(img_dir).resolve()) + '/*.*') if os.path.exists(img_dir) != 1: print("The test image " + str(img_dir) + " does not exist.") exit() for img_path in imgs_path: img_name = img_path.split("/")[-1] segment_save_path = save_path + "seg_" + img_name compare_save_path = save_path + "compare_" + img_name image = img_process(img_path) # Get the result of the DANet model output_res_PraNet = pranet_om_model.forward([image]) if output_res_PraNet == []: continue # reshape the matrix to (1, 1, 352, 352) tensor_res_PraNet = output_res_PraNet[0].reshape(1, 1, 352, 352) # The result is mapped to a picture decode_seg_map(tensor_res_PraNet, segment_save_path) # Enable comparison graph output if true enable_contrast_output([img_path, segment_save_path, compare_save_path]) print('success!') success! success! success!
查看一张分割结果对比图,可以看出基本上把息肉组织分割出来了:
恭喜你!至此,你已经成功完成了基于 PraNet 的医疗影像分割的全部实验流程,希望你能够熟练掌握这套技术方案,并将其应用到实际医疗影像分割项目中去!
3.6 依赖软件
本实验的依赖软件版本信息如下:
- Python:为了方便开发者进行学习,本课程采用Python代码实现,您可以在服务器上安装一个Conda,用于创建Python环境,本实验使用的是
python 3.10
; - pillow:Python的图像处理库,本实验使用的是
11.0.0
版本; - opencv-python:opencv-python 是 OpenCV 库的 Python 接口,它提供了对 OpenCV 功能的访问,包括图像处理、视频分析、计算机视觉算法和实时图像处理等,使得开发者能够在 Python 环境中轻松实现复杂的视觉任务,本实验使用的是
4.10.0.84
版本; - numpy: 开源的Python科学计算库,用于进行大规模数值和矩阵运算,本实验使用的是
1.26.4
版本; - CANN(Compute Architecture for Neural Networks):Ascend芯片的使能软件,本实验使用的是
="https://www.hiascend.com/developer/download/community/result?module=cann&cann=8.0.RC2.alpha002">8.0.rc2 版本。
04 课后测试
- 尝试调整置信度阈值
THRESHOLD_VALUE
,观察分割预测结果如何变化; - 尝试用其他医疗影像数据进行预测,观察预测效果如何。