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)
复制代码
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)
复制代码
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!')
复制代码
查看一张分割结果对比图,可以看出基本上把息肉组织分割出来了:
恭喜你!至此,你已经成功完成了基于 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 芯片的使能软件,本实验使用的是 8.0.rc2
版本。
04 课后测试
尝试调整置信度阈值 THRESHOLD_VALUE
,观察分割预测结果如何变化;
尝试用其他医疗影像数据进行预测,观察预测效果如何。
评论