【医学影像数据处理】2D/3D patch的crop和merge操作汇总

news2024/9/23 19:17:40

在做3D分割任务中,多数的方法多采用整体缩放,或裁剪成一个个小的patch操作,这样做的一个主要原因是内存问题。

相较于整体缩放,采用裁剪成patch的方法,对于小目标会更加的鲁棒,这也是大多数3D分割任务中常选取的方式。尤其是针对医学影像的器官分割任务,CT结节诊断等等,对于细节的要求是非常高的。采用缩小的方式,反而会使得目标的像素区域在输入阶段,就损失较多。

后面,就针对2D和3D的图像和MR数据进行有重叠的crop操作和merge操作,帮助对其中的细节进行理解,下面开始吧。

一、2D crop and merge

对于一个[10, 10]大小的示例图像,采用patch大小为[3, 3]的进行裁剪,每次patch与patch之间,在x和y方向重叠1个像素,无法构成一个patch的部分,选择丢弃,如下所示:
示意图

分析过程:

  1. 首先,决定行列可以有多少个patch,是左上角第一个patch中右下角的那个红色点,因为他是第一个在水平和竖直方向都会需要重叠的点;
  2. 反应在行和列上面,可以移动的区域也就是width - patch_width + 1,和height - patch_height + 1,因为对于左上角第一个patch,只有右下角的坐标,是参与到遍历里面的,可参与遍历的区域就是红色曲线区域;
  3. 对于行、列的每一步,都会重叠overlap_size个像素区域,所以可走的步长,是patch_width - overlap_width,和patch_height - overlap_height
  4. 最后以左上角的坐标点为准,x:x+patch_widthy:y+patch_height就表示了一个区块patch的像素范围,被裁剪下来。

1.1、crop

下面是实现上述步骤的代码,主要就是要理解几个点:

  1. 在水平和数值方向,能取到多少个patch?
  2. patch的滑动选取,一次可以移动多大的步长?
  3. 最后取像素块,就简单了许多。
import numpy as np

def crop_volume(volume, patch_size=[96, 96], overlap_size=[4, 4]):
    """
    Crop a 2D volume into patches with specified size and overlap.
    Args:
        volume (np.ndarray): the 3D volume to be cropped, with shape [width, height]
        patch_size (tuple or list): the size of patch, with format [patch_width, patch_height]
        overlap_size (tuple or list): the size of overlap between adjacent patches, with format [overlap_width, overlap_height]
    Returns:
        np.ndarray: the cropped patches, with shape [num_patches, patch_width, patch_height]
    """
    width, height = volume.shape
    patch_width, patch_height = patch_size

    overlap_width, overlap_height = overlap_size
    patches = []
    # 不够一个patch,就丢弃
    for x in range(0, width - patch_width + 1, patch_width - overlap_width):
        for y in range(0, height - patch_height + 1, patch_height - overlap_height):
            print(x, y)
            patch = volume[x:x+patch_width, y:y+patch_height]
            patches.append(patch)
        print('\n')
    patches = np.asarray(patches)

    return patches


# 生成一个[10, 10]大小的示例图像
imgs = np.random.rand(10, 10)
patch_size=[3, 3]
overlap_size = [1, 1]
# print('img shape:', imgs.shape)

patches = crop_volume(imgs, patch_size, overlap_size)
print('patches shape:', patches.shape)

验证了前面我们的猜想,后面我们直接取一个图片,来验证下我们的思路。如果上述的思路是对的,那么,在裁剪后保存的图像,就该是一个具体部分重叠区域,但是,还能够反映全貌的一个个小图。下面就是:

import os
import itk
import cv2
from matplotlib import pylab as plt
from PIL import Image
path = os.path.join(r'F:\tmp\results2', '10.png')
imgs = cv2.imread(path, 0)
volume_size = imgs.shape
patch_size=[96, 96]
overlap_size = [4, 4]
print('img shape:', imgs.shape)

patches = crop_volume(imgs, patch_size, overlap_size)
print('patches shape:', patches.shape)

for i in range(0, patches.shape[0], 1):
    one_patch = patches[i, :, :]
    print(i, one_patch.shape)
    width_p, height_p = one_patch.shape

    img_Image = Image.fromarray(one_patch)
    img_Image = img_Image.convert("L")

    img_Image.save(r"F:\tmp\results1/" + str(i) + ".png")

如下,是读取的原始图像,和crop后的一个个散落的小图。尽管一个个小图在我们展示的时候,他们之间使用间隙的,但并不影响我们看到他的全貌。

还是一种图的样子,区别在与彩色图像成了灰度图,最右侧和最下侧的像素像是少了一些。这是因为不足一个patch,被丢弃的原因。基于此,我们也能给猜出来,在后续merge阶段,可能会还原回去的图像存在些许的差异。

二维裁剪

1.2、merge

mergecrop的一个逆过程。当时我们怎么拆的,现在就原路给拼接回去。拼接回去的图像尺寸和crop前的图像尺寸是一致的,当时被忽略的部分,都是0。

  1. 移动的步长还是一致的,在行列方向分别还是:patch_width - overlap_width,和patch_height - overlap_height
  2. 还要需要知道在行列方向,分别crop了多少次。以行为例,width - patch_width就是把第一个patch块去除掉的行长度,再除以(patch_width - overlap_width),得到剩余部分可以走多少步,再+1,把第一个块的数量补上,也就得到了在行方向上,有多少个patch了;
  3. 以左上角的坐标点为准,x:x+patch_widthy:y+patch_height就表示了一个区块patch了,现在就把对应的patch像素,给赋值给原始图了。
  4. 因为overlap重叠区域,被多次覆盖,这部分需要求均值,巧妙的采用了一个新的像素块,专门记录被赋值的次数,最后除以对应的次数,就可能实现求均值的过程了。

为什么先需要把第一个patch块的行列方向都先去掉呢?

因为第一个块是最特殊的,它被重叠的区域,只有overlap_size_w行和overlap_size_h例,而其余的patch块,重叠区域都会是2行和2列,都遵循步长的节奏。

width - patch_width把第一个patch块去除掉后的行长度,还能准确反映有多少个patch吗?

答案是可以的,这是因为减去一个patch,无非就是少了一个overlap_size_h的长度,去掉一个overlap_size_h的长度,如果恰好整除,那么加上这个长度,也是多余的,无法再次构成一个新的patch;即便有剩余,也是无法组成一个新的patch的。

下面是上面图像crop阶段裁剪得到的patch,加上merge后的操作,如下:

def merge_patches(patches, volume_size, overlap_size):
    """
    Merge the cropped patches into a complete 2D volume.
    Args:
        patches (np.ndarray): the cropped patches, with shape [num_patches, patch_width, patch_height]
        volume_size (tuple or list): the size of the complete volume, with format [width, height]
        overlap_size (tuple or list): the size of overlap between adjacent patches, with format [overlap_width, overlap_height]
    Returns:
        np.ndarray: the merged volume, with shape [width, height]
    """
    width, height = volume_size
    patch_width, patch_height = patches.shape[1:]
    overlap_width, overlap_height = overlap_size
    num_patches_x = (width - patch_width) // (patch_width - overlap_width) + 1
    num_patches_y = (height - patch_height) // (patch_height - overlap_height) + 1
    print('merge:', num_patches_x, num_patches_y)

    merged_volume = np.zeros(volume_size)
    weight_volume = np.zeros(volume_size)   # weight_volume的目的是用于记录每个像素在裁剪过程中被遍历的次数,最后用于求平均值
    idx = 0
    for x in range(num_patches_x):
        for y in range(num_patches_y):
            x_start = x * (patch_width - overlap_width)
            y_start = y * (patch_height - overlap_height)
            merged_volume[x_start:x_start+patch_width, y_start:y_start+patch_height] += patches[idx]
            weight_volume[x_start:x_start+patch_width, y_start:y_start+patch_height] += 1
            idx += 1
    merged_volume /= weight_volume
    return merged_volume

path = os.path.join(r'F:\tmp\results2', '10.png')
imgs = cv2.imread(path, 0)
volume_size = imgs.shape
patch_size=[96, 96]
overlap_size = [4, 4]
print('img shape:', imgs.shape)

patches = crop_volume(imgs, patch_size, overlap_size)
print('patches shape:', patches.shape)

merged_volume = merge_patches(patches, volume_size, overlap_size)
print('merged_volume shape:', merged_volume.shape)
merged_volume = Image.fromarray(merged_volume)
merged_volume = merged_volume.convert("L")
merged_volume.save(r"F:\tmp\results2/" + "merged_volume.png")

如下,果然和我们前面猜想的一样,在merge后的图像,相比于原图,在右侧和下侧,都少了部分,这个问题后面在3D时候,再细细的探讨。

nerge

二、3D crop and merge

3D部分相比于2D,无非就是多了一个深度信息,也就是z轴信息。所以在crop阶段和merge阶段,只需要按照2D行列的方式,增加一个维度的信息即可。代码如下所示:

import numpy as np

def crop_volume(volume, patch_size=[96, 96, 96], overlap_size=[4, 4, 4]):
    """
    Crop a 3D volume into patches with specified size and overlap.
    Args:
        volume (np.ndarray): the 3D volume to be cropped, with shape [width, height, depth]
        patch_size (tuple or list): the size of patch, with format [patch_width, patch_height, patch_depth]
        overlap_size (tuple or list): the size of overlap between adjacent patches, with format [overlap_width, overlap_height, overlap_depth]
    Returns:
        np.ndarray: the cropped patches, with shape [num_patches, patch_width, patch_height, patch_depth]
    """
    width, height, depth = volume.shape
    patch_width, patch_height, patch_depth = patch_size

    overlap_width, overlap_height, overlap_depth = overlap_size
    patches = []
    for z in range(0, depth - patch_depth + 1, patch_depth - overlap_depth):
        for y in range(0, height - patch_height + 1, patch_height - overlap_height):
            for x in range(0, width - patch_width + 1, patch_width - overlap_width):
                patch = volume[x:x+patch_width, y:y+patch_height, z:z+patch_depth]
                patches.append(patch)
    patches = np.asarray(patches)
    return patches

def merge_patches(patches, volume_size, overlap_size):
    """
    Merge the cropped patches into a complete 3D volume.
    Args:
        patches (np.ndarray): the cropped patches, with shape [num_patches, patch_width, patch_height, patch_depth]
        volume_size (tuple or list): the size of the complete volume, with format [width, height, depth]
        overlap_size (tuple or list): the size of overlap between adjacent patches, with format [overlap_width, overlap_height, overlap_depth]
    Returns:
        np.ndarray: the merged volume, with shape [width, height, depth]
    """
    width, height, depth = volume_size
    patch_width, patch_height, patch_depth = patches.shape[1:]
    overlap_width, overlap_height, overlap_depth = overlap_size
    num_patches_x = (width - patch_width) // (patch_width - overlap_width) + 1
    num_patches_y = (height - patch_height) // (patch_height - overlap_height) + 1
    num_patches_z = (depth - patch_depth) // (patch_depth - overlap_depth) + 1
    print('merge:', num_patches_x, num_patches_y, num_patches_z)
    merged_volume = np.zeros(volume_size)
    weight_volume = np.zeros(volume_size)   # weight_volume的目的是用于记录每个像素在裁剪过程中被遍历的次数,最后用于求平均值
    idx = 0
    for z in range(num_patches_z):
        for y in range(num_patches_y):
            for x in range(num_patches_x):
                x_start = x * (patch_width - overlap_width)
                y_start = y * (patch_height - overlap_height)
                z_start = z * (patch_depth - overlap_depth)
                merged_volume[x_start:x_start+patch_width, y_start:y_start+patch_height, z_start:z_start+patch_depth] += patches[idx]
                weight_volume[x_start:x_start+patch_width, y_start:y_start+patch_height, z_start:z_start+patch_depth] += 1
                idx += 1
    merged_volume /= weight_volume
    return merged_volume


import os
import itk

nii_path = os.path.join(r'F:\tmp\results2', 'brain.nii.gz')
imgs = itk.array_from_image(itk.imread(nii_path))
imgs /= np.max(imgs)
volume_size = imgs.shape
patch_size = [96, 96, 96]
overlap_size = [32, 32, 32]
print('img shape:', imgs.shape)

# crop
patches = crop_volume(imgs, patch_size, overlap_size)
print('patches shape:', patches.shape)

print(patches.shape)
for d in range(0, patches.shape[0], 1):
    one_patch = patches[d, :, :, :]*255
    print(d, one_patch.shape)
    width_p, height_p, depth_p = one_patch.shape

    one_patch = itk.image_from_array(one_patch)

    itk.imwrite(one_patch, os.path.join(r'F:\tmp\results2\patch', str(d) + ".nii.gz"))  # 保存nii文件

# merge
merged_volume = merge_patches(patches, volume_size, overlap_size)
print('merged_volume shape:', merged_volume.shape)
merged_volume = itk.image_from_array(merged_volume)
itk.imwrite(merged_volume, r'F:\tmp\results2\merged_volume.nii.gz')

原始的图像,如下:

1

crop一个块图像,如下:

在这里插入图片描述

patch_size = [96, 96, 96],overlap_size = [4, 4, 4] 时,merge后的图像,发现丢失像素区域较多,如下:

3

patch_size = [96, 96, 96],overlap_size = [32, 32, 32] 时,merge后的图像,发现丢失像素区域相对较少,如下:

4

对于大的patch size ,需要采用较大的overlap size,否则会导致右侧和下侧丢弃的像素区域过多,在合并的时候,会丢失较多信息。所以说为了避免这个问题,需要使得两者之间达到一个相对的平衡。

究竟选择的patch有多大,这个还需要综合来考虑。包括原始图像的大小和GPU的内存大小。太小了,对于空间相对位置信息的获取,肯定是不利的。太大了也可能内存占用太高,不太经济。

三、总结

这次就是一次对3D图像大尺寸的一次处理的记录,之前对这块内容涉猎较少,也没有好好的理解。比如说对于脑补MR的处理中,就选择把无关区域去除,只留下可能存在目标区域,用于后续的预测。这也是用全图训练的一种取巧的方式。

至于新的patch方法,可以与老方法进行相互的补充。尤其是对于尺寸较大的输入,patch的方法,就更加的取巧了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/498818.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Leetcode448. 找到所有数组中消失的数字

Every day a leetcode 题目来源:448. 找到所有数组中消失的数字 解法1:STL set set 是一个集合类型的容器,里面的元素具有唯一性,并且所有元素都会根据元素的键值自动被排序,以红黑树为底层数据结构。 我们使用集合…

git上传大大大文件项目好折磨人

本来想把unity项目的源码上传上gitee啊,但是那个项目有1个多G,还是个半成品,要是写完,都不知道行不行 正常的上传 所用到的命令: 1、 git init 初始化,创建本地仓库 2、 git add . 添加到本地仓库 3、 git…

【C++】打开C++大门,踏入C++世界

文章目录 ☑️前言一. 浅看【C】关键字二. 命名空间1. 命名空间的定义👌2. 命名空间的使用👌 三. 【C】输入输出(IO)四. 缺省参数1. 缺省参数的概念🏆2. 缺省参数的分类🏆 五. 函数重载1. 函数重载的概念✌️2. 【C】支持函数重载…

【计算机专业漫谈】【计算机系统基础学习笔记】W2-2-2 模运算系统和补码表示

利用空档期时间学习一下计算机系统基础,以前对这些知识只停留在应试层面,今天终于能详细理解一下了。参考课程为南京大学袁春风老师的计算机系统基础MOOC,参考书籍也是袁老师的教材,这是我的听课自查资料整理后的笔记 补码表示法…

实验9 分类问题

1. 实验目的 ①掌握逻辑回归的基本原理,实现分类器,完成多分类任务; ②掌握逻辑回归中的平方损失函数、交叉熵损失函数以及平均交叉熵损失函数。 2. 实验内容 ①能够使用TensorFlow计算Sigmoid函数、准确率、交叉熵损失函数等,…

GEE:基于变异系数法的遥感环境风险评估指数(RSEI)计算

作者:CSDN @ _养乐多_ 本文记录了基于变异系数法计算 Risk-Screening Environmental Indicators (RSEI) 的方法和代码。使用 变异系数法计算权重来代替PCA方法计算权重,根据权重计算综合指标。本文也记录了使用landsat-8数据计算LST、NDVI、NDBSI、WET四个指标的代码。 结果…

密码学:其他常见密码学应用.

密码学:其他常见密码学应用. 密码学是研究编制密码和破译密码的技术科学。研究密码变化的客观规律,应用于编制密码以保守通信秘密的,称为编码学;应用于破译密码以获取通信情报的,称为破译学,总称密码学. 目…

二叉树基础概念详解

文章目录 前言1. 树的基本概念2. 二叉树的基本概念3. 特殊二叉树🍑 满二叉树🍑 完全二叉树🍑 斜树 4. 二叉树的性质🍑 性质一🍑 性质二🍑 性质三🍑 性质四🍑 性质五🍑 性…

华为EC6108V9E/EC6108V9I_rk3228_安卓4.4.4_通刷_卡刷固件包

华为EC6108V9E/EC6108V9I_rk3228_安卓4.4.4_通刷_卡刷固件包-内有教程 特点: 1、适用于对应型号的电视盒子刷机; 2、开放原厂固件屏蔽的市场安装和u盘安装apk; 3、修改dns,三网通用; 4、大量精简内置的…

【Java】线程池--ThreadPoolExecutor底层原理源码的理解

文章目录 一、根据代码查看jdk提供的3种线程池创建:二、3种方式源码分析1、Executors.newCachedThreadPool()2、Executors.newFixedThreadPool(10)3、Executors.newSingleThreadExecutor() 三、自定义方式执行提交优先级执行优先级 一、根据代码查看jdk提供的3种线程…

第2章Elasticsearch入门

1.正排索引和倒排索引 正排索引: 正排索引(Forward Index)是一种用于搜索引擎和文本检索系统的索引结构,它将文档中的每个单词都映射到该单词在文档中出现的位置。正排索引可以帮助快速定位、检索和渲染文档内容,但它需要消耗大…

AI 工具合辑盘点(十一)持续更新 之 AI 学术研究工具

许多学生和研究人员已经在利用人工智能进行研究。它可以让你更容易地了解最新研究成果,并帮助你组织和正确引用你最喜爱的研究论文。 从生成长篇研究论文摘要到通知你领域内的新趋势,研究中的AI工具节省了大量时间和精力。如果你在学术界,那…

数据库系统

目录 第三章、数据库系统1、数据库模式1.1、三级模式--两级映射1.2、数据库设计过程 2、ER模型3、关系代数与元组演算4、规范化理论4.1、函数依赖4.2、价值与用途4.3、键4.4、范式4.5、模式分解 5、并发控制6、数据库完整性约束7、数据备份8、数控故障与恢复9、数据仓库与数据挖…

网络安全合规-数据安全风险评估

一、法律依据: 依据《数据安全法》第三十条的规定,重要数据的处理者应当按照规定对其数据处理活动定期开展风险评估,并向有关主管部门报送风险评估报告。 依据《网络数据安全管理条例》(征求意见稿) 第三十二条规定&am…

实时通信的服务器推送机制 EventSource(SSE) 简介,附 go 实现示例

简介 不知道大家有没有见过 Content-Type:text/event-stream 的请求头,这是 HTML5 中的 EventSource 是一项强大的 API,通过服务器推送实现实时通信。 与 WebSocket 相比,EventSource 提供了一种简单而可靠的单向通信机制(服务器…

《Linux 内核设计与实现》03. 进程管理

文章目录 进程描述符及任务结构分配进程描述符进程描述符的存放进程状态设置当前进程状态进程上下文进程家族树 进程创建线程在 Linux 中的实现创建线程内核线程 进程终结删除进程描述符孤儿进程 进程描述符及任务结构 内核把进程存放在任务队列(task list&#xf…

MySQL高级--锁

一、锁 锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU、RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题…

新手必看:腾讯云轻量服务器使用限制

腾讯云轻量应用服务器和云服务器CVM相比具有一些限制,比如轻量服务器不支持更换内网IP地址,轻量服务器只能套餐整体升级且不支持降配,轻量不支持用户自定义配置私有网络VPC,还有如实例配额、云硬盘配额、备案限制和内网连通性等限…

整理一下最近了解到的AIGC工具

AIGC工具的一点整理 前言AIGC类型图像生成类Stable diffusionMidjourneyDALLE 2三种工具比较DeepFloyd IF 文本生成语音生成So-vits-svc 4.0 结尾 前言 好久没有写csdn博客了,突然不知道写点什么,最近AIGC真的很火,有一种三天不看就跟不上发…

计算机系统-异常控制流

例行前言: 本篇不是学习课程时的笔记,是重看这本书时的简记。对于学习本课程的同学,未涉及的内容不代表考试不涉及。核心内容是信号部分。本章内容介绍了较多的信号处理函数,需要在实验中巩固本章所学内容及相关问题的处理(并发&…