如何解决填补分类数据漏洞的高效方法?
我们有 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调用该函数的成本太高了。
另一种解决方案是使用标签算法来查找数组中相互连接的所有区域。这可以使用 label
的 skimage.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 举报,一经查实,本站将立刻删除。