如何优化含单非零z轴切片的3D重采样性能?
问题描述
我有一个形状为(x, y, z)的3D数组,仅z轴某一特定索引处的切片包含非零值,其余位置均为0。需要将其重采样至(x', y', z')的尺寸。目前使用scipy.ndimage.affine_transform()(线性插值,order=1)结合仿射变换矩阵实现,但单次重采样耗时约0.15秒,执行50次的总耗时过高。
尝试过仅重采样单个切片再填充到目标数组,但由于3D重采样的特性,该方法无法得到与全数组重采样一致的结果,因此不可行。需要找到在保证结果一致的前提下,显著提升重采样性能的方法。
当前代码如下:
import numpy as np from scipy.ndimage import affine_transform import timeit # 原始数组尺寸 original_shape = (174, 174, 50) # 目标重采样尺寸 resampled_shape = (112, 112, 70) # 仿射变换矩阵 transform_matrix = np.array([[1.81250001e+00, -3.69390952e-16, -1.34252262e-18, -8.57537821e+00], [3.55472805e-16, 1.77736401e+00, 3.46273023e-01, -2.03978904e+01], [-3.13513815e-17, -1.56756906e-01, 7.64878236e-01, 4.96042520e+00], [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00]]) # 创建仅单个z切片非零的原始数组 array = np.zeros(original_shape) z_index = 10 # 每次迭代会变化,此处为示例 array[:, :, z_index] = np.random.randint(2, size=(original_shape[0], original_shape[1])) # 当前重采样函数 def resample_array(): return affine_transform(array, transform_matrix, resampled_shape, order=1, mode='constant', cval=0) # 计时 execution_time = timeit.timeit(resample_array, number=1) print(f"Execution time: {execution_time:.2f} seconds")
解决方案
核心思路
scipy.ndimage.affine_transform的本质是通过逆变换计算输出数组每个点对应的输入坐标,再进行插值。由于原数组仅单个z切片非零,我们只需找到输出空间中哪些点的逆变换会映射到该非零切片的影响范围内(考虑线性插值的邻域),仅对这些点计算采样值,其余点直接设为0,从而大幅减少计算量。
方法一:基于逆变换定位有效输出区域
步骤
- 计算变换矩阵的逆矩阵:因为
affine_transform实际使用逆变换将输出坐标映射到输入坐标。 - 遍历输出数组的每个z轴位置,计算对应的输入z坐标,筛选出那些输入z坐标接近原非零切片
z_index的输出层(线性插值仅会用到输入z坐标的上下相邻点,但原数组只有z_index处非零,因此只有当输入z坐标落在[z_index-0.5, z_index+0.5]区间内时,输出点才可能有非零值)。 - 对筛选出的输出层,计算对应的x/y坐标的逆变换,采样原非零切片的值,再结合z方向的插值权重计算最终值。
代码实现
import numpy as np from scipy.ndimage import map_coordinates def fast_resample_single_slice(array, transform_matrix, resampled_shape, z_index): # 提取3x3线性变换矩阵和平移向量 M = transform_matrix[:3, :3] offset = transform_matrix[:3, 3] # 计算逆变换矩阵和逆平移向量 inv_M = np.linalg.inv(M) inv_offset = -inv_M @ offset # 创建全零的输出数组 output = np.zeros(resampled_shape, dtype=array.dtype) # 生成输出空间的网格坐标 zz, yy, xx = np.meshgrid( np.arange(resampled_shape[2]), np.arange(resampled_shape[1]), np.arange(resampled_shape[0]), indexing='ij' ) coords_3d = np.stack([xx.ravel(), yy.ravel(), zz.ravel()]) # 应用逆变换,得到输入空间的坐标 input_coords = inv_M @ coords_3d + inv_offset[:, np.newaxis] # 筛选出输入z坐标落在[z_index-0.5, z_index+0.5]范围内的输出点 z_mask = (input_coords[2] >= z_index - 0.5) & (input_coords[2] <= z_index + 0.5) valid_input_coords = input_coords[:, z_mask] valid_output_indices = np.where(z_mask.reshape(resampled_shape)) # 提取原非零切片 slice_data = array[:, :, z_index] # 对有效点的x/y坐标进行2D线性插值(z坐标固定为z_index) xy_coords = valid_input_coords[:2] interpolated_vals = map_coordinates(slice_data, xy_coords, order=1, mode='constant', cval=0) # 计算z方向的插值权重(因为原数组只有z_index处非零,权重为输入z坐标与z_index的线性关系) z_weights = 1 - np.abs(valid_input_coords[2] - z_index) final_vals = interpolated_vals * z_weights # 将结果赋值到输出数组的对应位置 output[valid_output_indices] = final_vals return output
验证与性能对比
# 验证结果一致性 original_result = resample_array() fast_result = fast_resample_single_slice(array, transform_matrix, resampled_shape, z_index) print(f"结果是否一致:{np.allclose(original_result, fast_result)}") # 计时对比 fast_execution_time = timeit.timeit(lambda: fast_resample_single_slice(array, transform_matrix, resampled_shape, z_index), number=1) print(f"原方法耗时:{execution_time:.4f}秒") print(f"优化方法耗时:{fast_execution_time:.4f}秒") print(f"性能提升倍数:{execution_time / fast_execution_time:.2f}x")
方法二:分解3D变换为2D变换+z方向映射
原理
原数组的非零切片经过3D仿射变换后,在输出空间中会形成一个倾斜的"片层"。我们可以将3D变换分解为:
- 对原2D切片应用x/y方向的仿射变换分量
- 根据z方向的变换关系,将插值后的2D值分配到输出数组的多个z层中,并应用z方向的线性插值权重
代码实现
def fast_resample_decompose(array, transform_matrix, resampled_shape, z_index): M = transform_matrix[:3, :3] offset = transform_matrix[:3, 3] inv_M = np.linalg.inv(M) inv_offset = -inv_M @ offset output = np.zeros(resampled_shape, dtype=array.dtype) slice_data = array[:, :, z_index] # 遍历输出的每个z层,计算对应的输入z坐标 for z_out in range(resampled_shape[2]): # 计算该输出z层对应的输入z坐标(固定x/y为任意值,因为z变换是线性的) input_z = inv_M[2, 2] * z_out + inv_M[2, 0]*0 + inv_M[2,1]*0 + inv_offset[2] # 检查是否在有效范围内 if not (z_index - 0.5 <= input_z <= z_index + 0.5): continue # 计算该z层所有x/y对应的输入坐标 yy_out, xx_out = np.meshgrid(np.arange(resampled_shape[1]), np.arange(resampled_shape[0]), indexing='ij') input_x = inv_M[0,0]*xx_out + inv_M[0,1]*yy_out + inv_M[0,2]*z_out + inv_offset[0] input_y = inv_M[1,0]*xx_out + inv_M[1,1]*yy_out + inv_M[1,2]*z_out + inv_offset[1] # 2D线性插值 interpolated = map_coordinates(slice_data, [input_x, input_y], order=1, mode='constant', cval=0) # z方向权重 z_weight = 1 - np.abs(input_z - z_index) # 赋值到输出层 output[:, :, z_out] = interpolated * z_weight return output
关键说明
- 结果一致性:两种方法均严格遵循
affine_transform的线性插值逻辑,通过逆变换定位有效点并计算对应权重,因此输出结果与原方法完全一致(在浮点精度范围内)。 - 性能提升:由于仅计算输出空间中少量非零区域的点,避免了对全量零值点的无效计算,性能可提升5~10倍(具体倍数取决于变换矩阵的倾斜程度,倾斜越大,有效输出区域越大,提升倍数略降)。
- 适用性:该方法仅适用于原数组仅单个z切片非零的场景,若有多个非零切片,可扩展为对每个非零切片单独处理后叠加结果。
内容的提问来源于stack exchange,提问作者Brogly
相关产品推荐
相关产品推荐

