求问关于图像重采样的问题

最近需要使用这个分割模型(SHIVA_PVS)用3D T1分割血管周围间隙。

README里面提供了图像分辨率是160*214*176,1*1*1 mm^3。但我的T1像不是这个分辨率,需要重采样到这个分辨率。之前要么是配准到模版,要么是软件会自动重采样,所以对这个问题的解决不是很了解。我的问题主要是:应该采用什么工具重采样到要求的分辨率比较好,最好能使得后续还原到个体原始空间也比较方便。

按照我的认知,似乎SPM的Coregister (Reslice)可以实现这个功能,我看了胡老师的这篇教程使用SPM12改变图像的分辨率和一些其他博客运用spm中的coregister改变图像大小(图像重采样),尤其是这篇博客图像重采样/插值原理与其在MRI脑影像分辨率修改中的应用——将尺寸为1mm标准模板修改成体素尺寸为3、6、8mm标准模板(FSL、SPM12、NIfTI_20140122、dpabi、nilearn),我对里面的这句话不是很理解,

这里可以“随便”选取一个空间尺寸符合的nii图像是否是因为:这篇博客里涉及的是两个模板间的重采样,已经都在MNI空间了,所以只用选取尺寸符合的图像就可以了。

而对于我的图像来说,我选择的是参考图像是一个符合目标空间尺寸的另一被试的T1像(在Problem Running on Mac这个issue中由提问者提供的),要重采样的图像是自己的T1像。

但是这样得到的结果虽然空间尺寸一致了,但图像明显被扭曲了,并且是往参考图像配准的?(我不知道这样描述是否准确,因为确实不懂算法的原理。是否是体现了SPM的说明:Reslice images to match voxel-for-voxel with an image defining some space中voxel-for-voxel的意思)

于是我又尝试在python中进行重采样

import nibabel as nib
import numpy as np
from scipy.ndimage import zoom


def resample_image(img, new_shape=(160, 214, 176), order=1):
    original_shape = img.shape[:3]
    zoom_factors = np.array(new_shape) / np.array(original_shape)

    new_affine = np.diag([1, 1, 1, 1])
    new_affine[:3, 3] = -np.array(new_shape) / 2 + 0.5  # 将原点设置在图像中心

    resampled_data = zoom(img.get_fdata(), zoom_factors, order=order)
    return resampled_data, new_affine

def main():
    img = nib.load(r'D:\Download\sent\T1.nii')

    resampled_data, new_affine = resample_image(img)

    new_img = nib.Nifti1Image(resampled_data, new_affine)
    nib.save(new_img, r'D:\Download\sent\reslice_T1_2.nii')


if __name__ == '__main__':
    main()

这样得到的结果似乎要正确一些。但是这里就有我的第二个疑惑,为了SPM Coregister (Reslice)得到的结果图像明显被扭曲了,但用MRIcron overlay到原始T1像却能够对齐,而后面python代码得到的重采样图像无法被MRIcron对齐,这个是什么原理呢?是头文件里的哪些信息导致了这种差异?


所以,目前我的解决办法就是用SPM Coregister (Reslice)把原始T1.nii重采样到后面得到的reslice_T1_2.nii空间,后面的标准化和分割似乎也没有什么问题。但这样操作感觉是不太对的,希望各位老师能够解答一下我的困惑,谢谢!

  1. SPM的coregister模块有两个部分,一个是将一个图像配准到另一个图像(Estimate),一个是把图像重采样到另一个图像(Reslice)。一般这两步是合在一起使用,用于同一个被试不同模态图像的配准,比如将T1和fMRI对齐。如果只用Reslice这个部分,那么就要求这两个图像本身已经是在同一个空间了,或者本身已经对齐了,只是分辨率不同而已。如果你直接在网上找一个图像来用,因为网上的图像和你的图像并不在一个空间,我不太清楚结果是怎么样的。
  2. 至于为什么用SPM coregister方法插值后还能和原始图像对齐,而你用python代码后就不行了。我个人觉得用python后不能对齐是显然的,因为你对图像进行了缩放。而SPM的方法能够保持,可能是在插值前后的映射关系保留在了头信息中(可以检查一下头信息里的sform和qform),而刚好MRIcron可以利用这部分映射关系。
  3. 我感觉比较简单的做法是直接对图像进行裁切和补0来实现,并不需要插值,比如你的原始图像是192x256x160,目标尺寸是160x214x176,所以对于第一个和第二个维度就切掉一部分,对于第三个维度就补充一部分0。要实现这个操作最简单的是用AFNI的3dZeropad,不过我没有测试过。也许自己编程实现也行,但是要对应修改头信息。
  4. 我前面写的小教程里的SPM Deformations模块应该也可以实现这个目的,你需要修改Bounding Box这个参数来调整维度,因为Bounding Box和Voxel Size(就是1x1x1)一起决定维度。

感谢老师回复!上个月一直在准备考试,抱歉没有及时回复。

为了先解决我的问题,我主要参照了第三点。同时看到网上类似的情况,确实一般都是通过裁切的方式统一到同一分辨率的(例如https://blog.csdn.net/gefeng1209/article/details/90414604、https://blog.csdn.net/HuaCode/article/details/89222573

我目前找到的比较简单的解决办法是先裁切/补齐到指定分辨率、得到分割结果、补齐/裁切回原分辨率、对齐和原始图像的中心。目前得到的结果没有发现有什么问题。

import os
import numpy as np
import nibabel as nib

### 裁切/补齐到指定分辨率、得到分割结果、补齐/裁切回原分辨率
def crop_or_pad_image(original_path, cropped_image_path, target_shape):
    # Load the NIfTI image
    img = nib.load(original_path)
    data = img.get_fdata()
    affine = img.affine
    header = img.header.copy()

    current_shape = data.shape
    new_data = np.zeros(target_shape, dtype=data.dtype)  # Create a zero array with the target shape

    # Calculate start indices for slicing
    start_idx = [(cs - ts) // 2 if cs > ts else 0 for cs, ts in zip(current_shape, target_shape)]
    end_idx = [si + min(ts, cs) for si, ts, cs in zip(start_idx, target_shape, current_shape)]

    # Calculate the start and end indices for inserting the crop into the new_data array
    new_start_idx = [(ts - min(ts, cs)) // 2 for ts, cs in zip(target_shape, current_shape)]
    new_end_idx = [nsi + min(ts, cs) for nsi, ts, cs in zip(new_start_idx, target_shape, current_shape)]

    # Crop or pad data
    slices_from = tuple(slice(si, ei) for si, ei in zip(start_idx, end_idx))
    slices_to = tuple(slice(nsi, nei) for nsi, nei in zip(new_start_idx, new_end_idx))
    new_data[slices_to] = data[slices_from]

    # Adjust the affine transformation matrix to account for the new data offset
    new_affine = affine.copy()
    offset_diff = [((nsi - si) * aff if cs > ts else (si - nsi) * aff)
                   for nsi, si, cs, ts, aff in zip(new_start_idx, start_idx, current_shape, target_shape, np.diag(affine))]
    new_affine[:3, 3] += offset_diff

    # Save the new NIfTI image
    cropped_img = nib.Nifti1Image(new_data, new_affine, header)
    nib.save(cropped_img, cropped_image_path)

# Define input and output paths
original_path = r'D:\Download\sent\T1.nii'
cropped_image_path = r'D:\Download\sent\crop_T1.nii'
target_shape = (160, 214, 176)

# Crop the image
crop_or_pad_image(original_path, cropped_image_path, target_shape)

# 用crop_T1得到分割结果outimage_v1.nii

# Revert the image back to the original shape
cropped_image_path = r'D:\Download\sent\outimage_v1.nii'
output_path = r'D:\Download\sent\reverted_outimage_v1.nii'
original_shape = (192, 256, 160)
crop_or_pad_image(cropped_image_path, output_path, original_shape)

### 对齐和原始图像的中心
def update_translation_for_alignment(original_path, adjusted_path, output_path):
    original_img = nib.load(original_path)
    adjusted_img = nib.load(adjusted_path)

    original_affine = original_img.affine
    adjusted_affine = adjusted_img.affine

    # Calculate centers of images in voxel indices
    original_center = np.array(original_img.shape) / 2
    adjusted_center = np.array(adjusted_img.shape) / 2

    # Convert voxel indices to physical space using affine matrices
    original_center_phys = original_affine.dot(np.append(original_center, 1))[:-1]
    adjusted_center_phys = adjusted_affine.dot(np.append(adjusted_center, 1))[:-1]

    # Calculate required shift
    shift = original_center_phys - adjusted_center_phys

    # Update the translation component of the affine matrix
    new_affine = adjusted_affine.copy()
    new_affine[:3, 3] += shift

    # Save the adjusted image with the new affine matrix
    new_img = nib.Nifti1Image(adjusted_img.get_fdata(), new_affine, adjusted_img.header)
    nib.save(new_img, output_path)

# Define paths
original_path = r'D:\Download\sent\T1.nii'
adjusted_path = r'D:\Download\sent\reverted_outimage_v1.nii'
output_path = r'D:\Download\sent\original_outimage_v1.nii'

# Adjust the image alignment
update_translation_for_alignment(original_path, adjusted_path, output_path)


这里就是AFNI 3dinfo查看发现R-to-L等三个方向的物理位置不一样,对齐中心后就能解决。理论上在裁剪时就控制图像的中心位置不变应当更好。

我也尝试了AFNI的3dZeropad

裁剪到指定的分辨率并分割没有问题。但裁剪的这一步会提示图像是倾斜的,而且后续通过对应的逆操作还原分割结果到原来的分辨率后没有办法和原始T1对齐,不知道和这个警告是否有关,暂时放弃了用AFNI。

其它的几条建议还在学习中,再次感谢!

1 个赞