微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

填补分类数据漏洞的高效方法?

如何解决填补分类数据漏洞的高效方法?

我们有 3D 分割掩码,其中每个类都有自己的标签/ID。 对于每个类,我们都希望填补分割中的漏洞。

以下面的矩阵为例:

template <typename U>
std::suspend_always yield_value(const std::ranges::subrange<U,std::default_sentinel_t>& r) noexcept {
  /** ... **/
  return {};
}

应该导致

[
  [
    [ 1,1,2,2 ],[ 1,[ 0,3,4,0 ],[ 3,4 ],],[
    [ 1,]

唯一填充的孔是中间切片中的 1 和 3。 2 型向侧面敞开,4 型向后敞开。 类之间的 0 应该保持不变。

我使用现有的 scipy.ndimage.morphology.binary_fill_holes 函数(或其实现)和 [ [ [ 1,] 实现了 7 个版本。这是迄今为止最好的两个版本:

numpy

我通过以下方式测量了性能(与我真实世界的数据分布相匹配):

import numpy as np
from scipy.ndimage.morphology import binary_fill_holes,label,generate_binary_structure,binary_dilation

def fill_holes6(img: np.ndarray,applied_labels: np.ndarray) -> np.ndarray:
    output = np.zeros_like(img)
    for i in applied_labels:
        output[binary_fill_holes(img == i)] = i

    return output

def fill_holes7(img: np.ndarray,applied_labels: np.ndarray) -> np.ndarray:
    output = np.zeros(img.shape,dtype=int)
    for i in applied_labels:
        tmp = np.zeros(img.shape,dtype=bool)
        binary_dilation(tmp,structure=None,iterations=-1,mask=img != i,origin=0,border_value=1,output=tmp)
        output[np.logical_not(tmp)] = i
        
    return output

# EDIT: Added the following method:
def fill_holes8(img: np.ndarray,applied_labels: np.ndarray) -> np.ndarray:
    connectivity = 1
    footprint = generate_binary_structure(img.ndim,connectivity)
    background_mask = img == 0
    components,num_components = label(background_mask,structure=footprint)
    filled_holes = np.zeros_like(img)
    for component_label in range(1,num_components + 1):
        component_mask = components == component_label
        component_neighborhood = np.pad(img,constant_values=-1)[binary_dilation(np.pad(component_mask,1),structure=footprint)]

        neighbor_labels = np.unique(component_neighborhood)
        if len(neighbor_labels) == 2 and -1 not in neighbor_labels:
            neighbor_label = neighbor_labels[1]
            filled_holes[component_mask] = neighbor_label

    return img + filled_holes

对于我的第一个实现,我得到了以下执行时间 (t=100):

fill_holes1 fill_holes2 fill_holes3
分钟 6.4s 6.9s 6.2s
最大 83.7s 96.0s 80.4s
意思 32.9s 37.3s 31.6s
标准 17.3s 20.1s 16.5

这很慢。 最后一个实现fill_holes7只比fill_holes3快1.27倍。

有没有更高效的方法来做到这一点?

首先在 scipy 项目上打开了一个功能请求,但被要求先转到 stackoverflow:https://github.com/scipy/scipy/issues/14504

编辑:

我还对 MONAI 项目提出了一个功能请求。见#2678 为此,我使用迭代侵蚀解决方案 (import time import pandas as pd def measure(funs,t): res = [] for _ in range(t): ra = np.random.randint(10,40) sh = np.random.randint(200,400,3) img = np.random.randint(0,ra,sh) applied_labels = np.unique(img)[1:] fun_res = [] for fun in funs: start = time.time() fun(img,applied_labels) end = time.time() fun_res.append(end - start) res.append(fun_res) return np.min(res,axis=0),np.max(res,np.mean(res,np.std(res,axis=0) print(measure([fill_holes6,fill_holes7],t=10)) ) 打开了一个拉取请求。 您可以在此处找到文档:monai.transforms.FillHoles

在此期间,我还实现了基于连接组件标记 (ccl) 的版本。 请参阅 MONAI here 中的实现。 我在上面添加fill_holes7,这基本上就是那个实现。

MONAI 包很乐意接受任何可提高此方法性能的拉取请求。随意去那里,打开一个问题和一个拉取请求。

解决方法

binary_fill_holes 的实现不是很有效:它似乎没有使用 SIMD 指令并且没有并行化。它还基于非常密集的算法(迭代侵蚀)。由于此函数针对每个标签运行,因此您的最终实现的计算量非常大。解决性能问题的一种解决方案是重新设计算法

第一步是保持对标签的迭代并找到一种更有效的方法来填补空洞。一种有效的解决方案是在边界的每个尚未填充的单元格上使用 flood-fill 算法,然后查找未填充的单元格。这些剩余的单元格应该是已使用当前标签设置的孔或单元格。这样的算法应该相当快。然而,在 Python 中高效地实现它并不容易。 Python中有一些flood-fill的实现(例如在skimage.morphology中),但是对于大多数边界单元格从Python调用该函数的成本太高了。

另一种解决方案是使用标签算法来查找数组中相互连接的所有区域。这可以使用 labelskimage.measure 轻松完成。标记后,标记的边界区域可以设置为不是孔。剩下的作为孔或区域已经设置了正确的标签。此解决方案更加密集,尤其是当标签数量很大时(根据您的示例,只要每个标签都是单独计算的,这似乎很少见)。这是一个实现:

from skimage.measure import label

def getBorderLabels(img):
    # Detection
    lab0 = np.unique(img[:,:,0])
    lab1 = np.unique(img[:,-1])
    lab2 = np.unique(img[:,:])
    lab3 = np.unique(img[:,-1,:])
    lab4 = np.unique(img[0,:])
    lab5 = np.unique(img[-1,:])

    # Reduction
    lab0 = np.union1d(lab0,lab1)
    lab2 = np.union1d(lab2,lab3)
    lab4 = np.union1d(lab4,lab5)
    return np.union1d(np.union1d(lab0,lab2),lab4)

def getHoleLabels(borderLabels,labelCount):
    return np.setdiff1d(np.arange(1,labelCount+1,dtype=int),borderLabels,assume_unique=True)

def fill_holes8(img: np.ndarray,applied_labels: np.ndarray) -> np.ndarray:
    output = img.copy()
    for i in applied_labels:
        labelized,labelCount = label(img==i,background=True,return_num=True,connectivity=1)
        holeLabels = getHoleLabels(getBorderLabels(labelized),labelCount)
        if len(holeLabels) > 0:
            output[np.isin(labelized,holeLabels)] = i
    return output

此实现在我的机器上大约快 3 倍

请注意,可以通过同时处理多个标签来并行化算法(例如,使用多个进程)。但是,应该注意不要使用过多的内存,也不要以正确的顺序写入output(类似于顺序算法)。


减速的最大来源来自每个标签的单独计算。一旦可以调整洪水填充算法以编写您需要的自定义优化实现拟合,尽管这似乎很难做到。或者,可以调整基于标签的实现来做同样的事情。第二种方法更简单,但也不容易。在复杂的情况下会出现许多问题:当具有给定标签 L1 的单元格形成一个包含一个洞的边界时,会发生什么?如果边界彼此部分重叠怎么办?这种情况可能吗?是否应该对它们进行调查,如果是,可接受的输出集是什么?

只要标记的边界没有形成棘手的情况,就有一种非常有效的算法可以用正确的标签跟踪和填充漏洞。我不确定它是否总是有效,但这是一个想法:

  • 使用标签算法查找所有连接区域
  • 构建一组包含所有标签的标签
  • 从集合中移除与边界区域相关的标签
  • 删除与已标记单元格(即非零单元格)相关联的标签
  • 到目前为止,剩下的标签要么是漏洞,要么是假漏洞(或假设不存在的棘手情况)。假孔是未标记的单元格,周围是带有多个不同标签的标记单元格(例如示例中间的 0 单元格)。
  • 检查每个标记区域边界上的单元格标签。如果标记区域仅被具有相同标签 L3 的单元格包围,则它是一个必须用 L3 标签单元格填充的孔。否则,这要么是一个假洞(或一个棘手的案例)。

生成的算法应该比参考实现和前一个算法快得多。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。