【问题标题】:Efficiently mask an image with a label mask使用标签蒙版有效地蒙版图像
【发布时间】:2022-11-23 19:53:36
【问题描述】:

我有一张我用tifffile.imread读入的图像,它变成了一个3D矩阵,第一维代表Y坐标,第二维代表X,第三维代表图像的通道(这些图像不是RGB等可以有任意数量的通道)。

这些图像中的每一个都有一个标签掩码,它是一个二维数组,指示图像中对象的位置。在标签掩码中,值为 0 的像素不属于任何对象,值为 1 的像素属于第一个对象,值为 2 的像素属于第二个对象,依此类推。

我想计算的是每个对象和图像的每个通道,我想知道通道的平均值、中值、标准差、最小值和最大值。因此,例如,我想知道对象 10 中像素的第一个通道的平均值、中值标准差、最小值和最大值。

我已经编写了代码来执行此操作,但它非常慢(如下所示),我想知道人们是否有更好的方法或知道一个可能有助于我更快/更有效地执行此操作的程序包。 (此处“污点”一词与通道的含义相同)

sample = imread(input_img)
label_mask = np.load(input_mask)

n_stains = sample.shape[2]
n_labels = np.max(label_mask)

#Create empty dataframe to store intensity measurements
intensity_measurements = pd.DataFrame(columns = ['sample', 'label', 'stain', 'mean', 'median', 'std', 'min', 'max'])

for label in range(1, n_labels+1):
    for stain in range(n_stains):
        #Extract stain and label
        stain_label = sample[:,:,stain][label_mask == label]

        #Calculate intensity measurements
        mean = np.mean(stain_label)
        median = np.median(stain_label)
        std = np.std(stain_label)
        min = np.min(stain_label)
        max = np.max(stain_label)

        #Add intensity measurements to dataframe
        intensity_measurements = intensity_measurements.append({'sample' : args.input_img, 'label': label, 'stain': stain, 'mean': mean, 'median': median, 'std': std, 'min': min, 'max': max}, ignore_index=True)

【问题讨论】:

  • 请问图像的尺寸以及通道和对象的数量是多少?您的代码需要多长时间?
  • 输入可以是任何大小,我们的图像目前范围在 128x128 到 150000x80000 之间,通道数在 2 到 5 之间。具有 3 个通道的大图像的当前运行时间和此代码 >48hrs
  • 只是好奇你是否尝试过我提出的方法 :) 特别是在巨大的图像上。我猜它需要大量的内存。
  • 由于 Python 仅使用单核/线程,您可以考虑使用多处理,因为现在几乎所有 CPU 都有 4 个或更多核。为每个流程分配标签或其他粒度。或者,考虑使用 numba 对 5 行(np.mean()、np.median() ... np.max())进行编码
  • @Raibek 我已经运行了你的算法,但它似乎比原始帖子中的算法花费的时间明显更长。不幸的是,我们的集群现在正在进行维护,但一旦它恢复正常,我希望能带回一些基准测试

标签: python image-processing


【解决方案1】:

您的代码很慢,因为您为每个标签遍历了整个图像。对于 n 个像素和 k 个标签,这是 O(n k) 的操作。您可以改为遍历图像,并为每个像素检查标签,然后使用像素值更新该标签的测量值。这是一个 O(n) 的操作。您将为每个标签和每个测量值保留一个累加器(标准偏差需要累加平方和以及总和,但您已经为均值累积的总和)。唯一不能以这种方式计算的度量是中位数,因为它需要对完整值列表进行部分排序。

这显然是一个成本低得多的操作,除了 Python 是一种缓慢的解释型语言这一事实​​,并且在 Python 中循环遍历每个像素会导致程序非常缓慢。在编译语言中,您将以这种方式实现它。

请参阅this answer,了解使用 NumPy 功能有效实现这一点的方法。


使用 DIPlib 库(披露:我是作者),您可以按如下方式应用操作(中位数未实现)。其他图像处理库具有类似的功能,但在通道数量方面可能不那么灵活。

import diplib as dip

# sample = imread(input_img)
# label_mask = np.load(input_mask)
# Alternative random data so that I can run the code for testing:
sample = imageio.imread("../images/trui_c.tif")
label_mask = np.random.randint(0, 20, sample.shape[:2], dtype=np.uint32)

sample = dip.Image(sample, tensor_axis=2)
msr = dip.MeasurementTool.Measure(label_mask, sample, features=["Mean", "StandardDeviation", "MinVal", "MaxVal"])
print(msr)

这打印出来:

   |                                 Mean |                    StandardDeviation |                               MinVal |                               MaxVal |
-- | ------------------------------------ | ------------------------------------ | ------------------------------------ | ------------------------------------ |
   |      chan0 |      chan1 |      chan2 |      chan0 |      chan1 |      chan2 |      chan0 |      chan1 |      chan2 |      chan0 |      chan1 |      chan2 |
   |            |            |            |            |            |            |            |            |            |            |            |            |
-- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- |
 1 |      82.26 |      41.30 |      24.77 |      57.77 |      52.16 |      48.22 |      5.000 |      3.000 |      1.000 |      255.0 |      255.0 |      255.0 |
 2 |      82.02 |      41.18 |      24.85 |      52.16 |      48.22 |      48.33 |      3.000 |      1.000 |      1.000 |      255.0 |      255.0 |      255.0 |
 3 |      82.39 |      41.17 |      24.93 |      48.22 |      48.33 |      48.48 |      1.000 |      1.000 |      1.000 |      255.0 |      255.0 |      255.0 |
 4 |      82.14 |      41.62 |      25.03 |      48.33 |      48.48 |      48.47 |      1.000 |      1.000 |      0.000 |      255.0 |      255.0 |      255.0 |
 5 |      82.89 |      41.45 |      24.94 |      48.48 |      48.47 |      48.54 |      1.000 |      0.000 |      1.000 |      255.0 |      255.0 |      255.0 |
 6 |      82.83 |      41.60 |      25.26 |      48.47 |      48.54 |      48.65 |      0.000 |      1.000 |      1.000 |      255.0 |      255.0 |      255.0 |
 7 |      81.95 |      41.77 |      25.51 |      48.54 |      48.65 |      48.22 |      1.000 |      1.000 |      2.000 |      255.0 |      255.0 |      255.0 |
 8 |      82.93 |      41.36 |      25.19 |      48.65 |      48.22 |      48.11 |      1.000 |      2.000 |      1.000 |      255.0 |      255.0 |      255.0 |
 9 |      81.88 |      41.70 |      25.07 |      48.22 |      48.11 |      47.69 |      2.000 |      1.000 |      1.000 |      255.0 |      255.0 |      255.0 |
10 |      81.46 |      41.40 |      24.82 |      48.11 |      47.69 |      48.32 |      1.000 |      1.000 |      2.000 |      255.0 |      255.0 |      255.0 |
11 |      81.33 |      40.98 |      24.76 |      47.69 |      48.32 |      48.85 |      1.000 |      2.000 |      1.000 |      255.0 |      255.0 |      255.0 |
12 |      82.30 |      41.55 |      25.12 |      48.32 |      48.85 |      48.75 |      2.000 |      1.000 |      1.000 |      255.0 |      255.0 |      255.0 |
13 |      82.43 |      41.50 |      25.15 |      48.85 |      48.75 |      48.89 |      1.000 |      1.000 |      1.000 |      255.0 |      255.0 |      255.0 |
14 |      83.29 |      42.11 |      25.65 |      48.75 |      48.89 |      48.32 |      1.000 |      1.000 |      1.000 |      255.0 |      255.0 |      255.0 |
15 |      83.20 |      41.64 |      25.28 |      48.89 |      48.32 |      48.13 |      1.000 |      1.000 |      1.000 |      255.0 |      255.0 |      255.0 |
16 |      81.51 |      40.92 |      24.76 |      48.32 |      48.13 |      48.73 |      1.000 |      1.000 |      1.000 |      255.0 |      255.0 |      255.0 |
17 |      81.81 |      41.31 |      24.71 |      48.13 |      48.73 |      48.49 |      1.000 |      1.000 |      0.000 |      255.0 |      255.0 |      255.0 |
18 |      83.58 |      41.85 |      25.25 |      48.73 |      48.49 |      32.20 |      1.000 |      0.000 |      1.000 |      255.0 |      255.0 |      212.0 |
19 |      82.12 |      41.24 |      25.06 |      48.49 |      32.20 |      24.44 |      0.000 |      1.000 |      1.000 |      255.0 |      212.0 |      145.0 |

我没有中位数的有效解决方案。您必须将图像拆分为每个标签的单独数组,然后在其上运行中位数。这与上面的一样有效,但会占用更多的内存。

【讨论】:

  • 这看起来真的很好谢谢。我还没有设法让 diplib 工作(我会继续尝试,如果我失败了,我会提出一个问题)但是 numpy_groupies 似乎在工作。我对此还有另一个问题,但会在那个线程而不是这个线程上提出它。
  • 为了向任何试图解决相同问题的人澄清,我使用链接中的 ng.aggregate 加快了我的计算速度,我只是问我是否可以在每次迭代中计算多个函数
【解决方案2】:

下面提出的方法利用矩阵乘法来加速计算。
它建立在两个重要的 Numpy 工具之上:

  1. https://numpy.org/doc/stable/reference/generated/numpy.einsum.html?highlight=einsum#numpy.einsum

    评估操作数的爱因斯坦求和约定。

    1. https://numpy.org/doc/stable/reference/maskedarray.html

    屏蔽数组是可能包含缺失或无效条目的数组。 numpy.ma 模块为支持带掩码的数据数组的 numpy 提供了几乎类似工作的替代品。

    屏蔽数组更新:https://stackoverflow.com/users/7328782/cris-luengo 发现我的初始代码中存在错误后,使用掩码数组更新了初始代码。

    这会将给定标签的所有未选择像素替换为 0 值,并将所有这些零包含在测量中。

    现在我们在测量计算之前屏蔽未选择的像素。

    import numpy as np
    import numpy.ma as ma
    import pandas as pd
    
    sample = imread(input_img)
    label_mask = np.load(input_mask)
    
    n_labels = np.max(label_mask)
    
    # let's create boolean label masks for each label 
    # producing 3D matrix where 1st axis is label
    label_mask_unraveled = np.equal.outer(label_mask, np.arange(1, n_labels +1))
    
    # now we can apply these boolean label masks simultaniously
    # to all the sample channels with help of 'einsum' producing 4D matrix, 
    # where the 1st axis is channel/stain and the 2nd axis is label
    sample_label_masks_applied = np.einsum("ijk,ijl->klij", sample, label_mask_unraveled)
    
    # in order to exclude the non-selected pixels 
    # from meausurement calculations, we mask the pixels first
    non_selected_pixels_mask = np.moveaxis(~label_mask_unraveled, -1, 0)[np.newaxis, :, :, :]
    non_selected_pixels_mask = np.repeat(non_selected_pixels_mask, sample.shape[2], axis=0)
    
    sample_label_masks_applied = ma.masked_array(sample_label_masks_applied, non_selected_pixels_mask)    
    
    # intensity measurement calculations
    # embedded into pd.DataFrame initialization
    intensity_measurements = pd.DataFrame(
        {
            "sample": args.input_img,
            "label": sample.shape[2] * list(range(1, n_labels+1)),
            "stain": n_labels * list(range(sample.shape[2])),
            "mean": ma.mean(sample_label_masks_applied, axis=(2, 3)).flatten(),
            "median": ma.median(sample_label_masks_applied, axis=(2, 3)).flatten(),
            "std": ma.std(sample_label_masks_applied, axis=(2, 3)).flatten(),
            "min": ma.min(sample_label_masks_applied, axis=(2, 3)).flatten(),
            "max": ma.max(sample_label_masks_applied, axis=(2, 3)).flatten() 
        }
    )
    

【讨论】:

  • 这会将给定标签的所有未选择像素替换为 0 值,并将所有这些零包含在测量中。最小值始终为 0,除非图像中存在负值。对于均值和标准差,您可以使用此方法,但改为计算总和和平方和,并计算每个标签的值的数量。
  • 非常感谢,我现在明白了。会尝试解决这个问题:)
【解决方案3】:

我找到了一个很好的解决方案,可以使用 scikit 图像,特别是 regionprops 函数。

import numpy as np
import pandas as pd
from skimage.measure import regionprops, regionprops_table
np.random.seed(42)

这是该图像的随机“图像”和标签掩码

img = np.random.randint(0, 255, size=(100, 100, 3))
mask = np.zeros((100, 100)).astype(np.uint8)
mask[20:50, 20:50] = 1
mask[65:70, 65:70] = 2

已经有一个内置函数可以非常快速地测量每个通道的平均强度

pd.DataFrame(regionprops_table(mask, img, properties=['label', 'mean_intensity']))

您还可以传递采用二进制掩码和一个通道强度图像到regionprops_table

def my_mean_func(mask, img):
    return np.mean(img[mask])

pd.DataFrame(regionprops_table(mask, img, properties=['label'], extra_properties=[my_mean_func]))

这很快,因为传递给自定义函数的二进制蒙版和强度图像是蒙版的最小边界框。因此,计算速度更快,因为它们在更小的区域上运行。

这只允许用户计算每个通道的值,但是有一个概括会返回所选区域的 3D 矩阵,以便在通道测量之间(或可以进行任何您喜欢的测量)。

props = regionprops(mask, img)

for prop in props:
    print("Region ", prop['label'], ":")
    print("Mean intensity: ", prop['mean_intensity'])
    print()

这只是非常基本的功能的一个示例。

我没有时间对上述任何算法进行基准测试,但这个答案中使用的算法确实非常非常快,我用它们来快速处理非常大的图像。然而,这里需要注意的是,这对我来说这么快的原因之一是因为我希望每个对象(具有相同值的标签掩码的每个条目)仅位于非常小的一部分图片。因此,regionprops 返回的最小边界框表示比原始图像小得多,大大加快了计算速度。

非常感谢大家的帮助。

【讨论】:

    猜你喜欢
    • 2020-05-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-20
    • 2011-05-10
    • 2020-05-05
    • 1970-01-01
    相关资源
    最近更新 更多