[英]Efficiently mask an image with a label mask
我有一張用tifffile.imread
讀入的圖像,它變成了一個 3D 矩陣,第一維代表 Y 坐標,第二維代表 X,第三維代表圖像的通道(這些圖像不是 RGB 等可以有任意數量的通道)。
這些圖像中的每一個都有一個 label 掩碼,它是一個二維數組,指示圖像中的 position 個對象。 在label掩碼中,值為0的像素不屬於任何object,值為1的像素屬於第一個object,值為2的像素屬於第二個object,依此類推。
我想計算的是每個 object 以及圖像的每個通道,我想知道通道的均值、中值、標准差、最小值和最大值。 因此,例如,我想知道 object 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)
您的代碼很慢,因為您為每個標簽遍歷了整個圖像。 對於 n 個像素和 k 個標簽,這是 O(nk) 的操作。 您可以改為遍歷圖像,並針對每個像素檢查 label,然后使用像素值更新該 label 的測量值。 這是一個 O(n) 的操作。 您將為每個 label 和每個測量保留一個累加器(標准偏差需要累加平方和以及總和,但您已經為均值累積的總和)。 唯一不能以這種方式計算的度量是中位數,因為它需要對完整值列表進行部分排序。
這顯然是一個更便宜的操作,除了 Python 是一種緩慢的解釋性語言這一事實,並且循環遍歷 Python 中的每個像素會導致一個非常慢的程序。 在編譯語言中,您將以這種方式實現它。
請參閱此答案,了解使用 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 |
我沒有中位數的有效解決方案。 您必須為每個 label 將圖像拆分為一個單獨的數組,然后在其上運行中值。 這與上面的方法一樣有效,但會消耗更多 memory。
下面提出的方法利用矩陣乘法來加速計算。
它建立在兩個關鍵的 Numpy 工具之上:
評估操作數的愛因斯坦求和約定。
屏蔽的 arrays 是 arrays,可能有缺失或無效的條目。 numpy.ma 模塊為 numpy 提供了幾乎類似的替代品,支持帶掩碼的數據 arrays。
屏蔽數組更新:在https://stackoverflow.com/users/7328782/cris-luengo在我的初始代碼中發現錯誤后,使用屏蔽數組更新了初始代碼。
這會將給定 label 的所有未選擇像素替換為 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()
}
)
我找到了一個很好的解決方案,可以使用 scikit 圖像,特別是 regionprops 函數。
import numpy as np
import pandas as pd
from skimage.measure import regionprops, regionprops_table
np.random.seed(42)
這是一個隨機的“圖像”和該圖像的 label 掩碼
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
已經有一個內置的 function 用於測量每個通道的平均強度,速度非常快
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]))
這很快,因為傳遞給自定義 function 的二進制蒙版和強度圖像是蒙版的最小邊界框。 因此,計算速度更快,因為它們在更小的區域上運行。
這只允許用戶計算每個通道的值,但有一個概括會返回所選區域的 3D 矩陣,以便在通道測量之間(或可以進行任何您喜歡的測量)。
props = regionprops(mask, img)
for prop in props:
print("Region ", prop['label'], ":")
print("Mean intensity: ", prop['mean_intensity'])
print()
這只是非常基本的功能的一個例子。
我沒有時間對上述任何算法進行基准測試,但這個答案中使用的算法確實非常非常快,我用它們來快速處理非常大的圖像。 但是,這里需要注意的是,這對我來說這么快的原因之一是因為我希望每個 object(具有相同值的 label 掩碼的每個條目)僅位於非常小的一部分圖片。 因此, regionprops
返回的最小邊界框表示比原始圖像小得多,並且大大加快了計算速度。
非常感謝大家的幫助。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.