写点什么

昇腾 AI 行业案例(六):基于 PraNet 的医疗影像分割

作者:AI布道Mr.Jin
  • 2025-01-22
    上海
  • 本文字数:7523 字

    阅读完需:约 25 分钟

昇腾AI行业案例(六):基于 PraNet 的医疗影像分割

00 - 前言

欢迎学习《基于 PraNet 的医疗影像分割》实验。在本实验中,你将深入了解如何运用计算机视觉(CV)领域的 AI 模型,搭建一个高效精准的医疗影像分割系统,专注于息肉分割任务,并利用开源数据集对模型效果加以验证。


学习目标

在本课程中,您将学习一些与使用 AI 图像处理技术实现息肉影像分割有关的重要概念,包括:

  • 医疗影像数据的预处理方法

  • 采用 PraNet 模型对息肉区域进行分割的方法

  • 图像分割的后处理方法

  • 端到端深度学习工作流


目录

本实验分为四个核心部分。第一部分主要介绍案例的应用场景,阐述遥感影像地块分割的重要性及意义;第二部分会详细阐述端到端的解决方案,搭建起技术实现的整体框架;第三部分会手把手指导您完成代码编写与实现;最后一部分给出测试题,帮助您巩固学习内容。

  1. 场景介绍

  2. 解决方案

  3. 代码实战

  4. 课后测试

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 executeprint('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 npimport 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 = 0ACL_MEMCPY_HOST_TO_DEVICE = 1ACL_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 osimport globfrom pathlib import Pathfrom 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 依赖软件

本实验的依赖软件版本信息如下:


  1. Python:为了方便开发者进行学习,本课程采用 Python 代码实现,您可以在服务器上安装一个 Conda,用于创建 Python 环境,本实验使用的是 python 3.10

  2. pillow:Python 的图像处理库,本实验使用的是 11.0.0 版本;

  3. opencv-python:opencv-python 是 OpenCV 库的 Python 接口,它提供了对 OpenCV 功能的访问,包括图像处理、视频分析、计算机视觉算法和实时图像处理等,使得开发者能够在 Python 环境中轻松实现复杂的视觉任务,本实验使用的是 4.10.0.84 版本;

  4. numpy: 开源的 Python 科学计算库,用于进行大规模数值和矩阵运算,本实验使用的是 1.26.4 版本;

  5. CANN(Compute Architecture for Neural Networks):Ascend 芯片的使能软件,本实验使用的是 8.0.rc2 版本

04 课后测试

  1. 尝试调整置信度阈值 THRESHOLD_VALUE ,观察分割预测结果如何变化;

  2. 尝试用其他医疗影像数据进行预测,观察预测效果如何。


发布于: 刚刚阅读数: 3
用户头像

还未添加个人签名 2020-11-13 加入

还未添加个人简介

评论

发布
暂无评论
昇腾AI行业案例(六):基于 PraNet 的医疗影像分割_AI布道Mr.Jin_InfoQ写作社区