Related
We have 3D segmentation masks where every class has its own label / ID.
For every class we would like to fill holes in the segmentation.
For an example, the following matrix:
[
[
[ 1, 1, 1, 2, 2, 2 ],
[ 1, 1, 1, 2, 2, 2 ],
[ 1, 1, 1, 2, 2, 2 ],
[ 0, 3, 0, 0, 4, 0 ],
[ 3, 3, 3, 4, 0, 4 ],
[ 0, 3, 0, 0, 4, 0 ],
],
[
[ 1, 1, 1, 2, 2, 2 ],
[ 1, 0, 1, 2, 0, 0 ],
[ 1, 1, 1, 2, 2, 2 ],
[ 0, 3, 0, 0, 4, 0 ],
[ 3, 0, 3, 4, 0, 4 ],
[ 0, 3, 0, 0, 4, 0 ],
],
[
[ 1, 1, 1, 2, 2, 2 ],
[ 1, 1, 1, 2, 2, 2 ],
[ 1, 1, 1, 2, 2, 2 ],
[ 0, 3, 0, 0, 4, 0 ],
[ 3, 3, 3, 4, 4, 4 ],
[ 0, 3, 0, 0, 4, 0 ],
],
]
Should result in
[
[
[ 1, 1, 1, 2, 2, 2 ],
[ 1, 1, 1, 2, 2, 2 ],
[ 1, 1, 1, 2, 2, 2 ],
[ 0, 3, 0, 0, 4, 0 ],
[ 3, 3, 3, 4, 0, 4 ],
[ 0, 3, 0, 0, 4, 0 ],
],
[
[ 1, 1, 1, 2, 2, 2 ],
[ 1, 1, 1, 2, 0, 0 ],
[ 1, 1, 1, 2, 2, 2 ],
[ 0, 3, 0, 0, 4, 0 ],
[ 3, 3, 3, 4, 0, 4 ],
[ 0, 3, 0, 0, 4, 0 ],
],
[
[ 1, 1, 1, 2, 2, 2 ],
[ 1, 1, 1, 2, 2, 2 ],
[ 1, 1, 1, 2, 2, 2 ],
[ 0, 3, 0, 0, 4, 0 ],
[ 3, 3, 3, 4, 4, 4 ],
[ 0, 3, 0, 0, 4, 0 ],
],
]
The only filled holes are the 1 and 3 in the middle slice.
The 2 shape is open to the side and the 4 is open to the back.
The 0 between the classes should stay untouched.
I implemented 7 versions using the existing scipy.ndimage.morphology.binary_fill_holes function (or its implementation) and numpy. Here the two best versions so far:
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, 1, 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
I measured the performance the following way (matching my real world data distribution):
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, axis=0), np.mean(res, axis=0), np.std(res, axis=0)
print(measure([fill_holes6, fill_holes7], t=10))
For my first implementations I got the following execution times (t=100):
fill_holes1
fill_holes2
fill_holes3
min
6.4s
6.9s
6.2s
max
83.7s
96.0s
80.4s
mean
32.9s
37.3s
31.6s
std
17.3s
20.1s
16.5
This is very slow.
The last implementation fill_holes7 is only 1.27 times faster than fill_holes3.
Is there a more performant way of doing this?
Opened a feature request on the scipy project first but was asked to go to stackoverflow first: https://github.com/scipy/scipy/issues/14504
EDIT:
I also opened a feature request on the MONAI project. See #2678
For this I opened a pull request with the iterative erosion solution (fill_holes7).
You can find the documentation here: monai.transforms.FillHoles
During this I also implemented a connected component labeling (CCL) based version.
See the implementation in MONAI here.
I added fill_holes8 above, which is basically that implementation.
The MONAI package is happy for any pull request improving the performance of this method. Feel free to go there, open an issue and a pull request.
binary_fill_holes is not very efficiently implemented: it seems to not make use of SIMD instruction and is not parallelized. It is also based on a pretty intensive algorithm (iterative erosion). Since this function is run for each label, your final implementation is very computationally intensive. One solution to fix the performance issue is to redesign your algorithm.
A first step is to keep the iteration over the label and find a more efficient way to fill hole. One efficient solution is to use a flood-fill algorithm on each cell of the border not yet filled and then look for the unfilled cells. These remaining cells should be either holes or cell already set with the current label. Such an algorithm should be quite fast. However, it is not easy to implement it efficiently in Python. There are some implementations of flood-fill in Python (eg. in skimage.morphology), but the cost of calling the function from Python for most border cell would be too high.
An alternative solution is to use a label algorithm to find all the region of the array that are connected each other. This can easily be done using label of skimage.measure. Once labeled, the labeled border regions can be set as not being holes. The remaining one as either holes or regions already set with the right label. This solution is more intensive, especially when the number of label is big (which seems quite rare based on your example and as long as each labels are computed separately). Here is an implementation:
from skimage.measure import label
def getBorderLabels(img):
# Detection
lab0 = np.unique(img[:,:,0])
lab1 = np.unique(img[:,:,-1])
lab2 = np.unique(img[:,0,:])
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
This implementation is about 3 times faster on my machine.
Note that it is possible to parallelize the algorithm (eg. using multiple processes) by working on multiple label at the same time. However, one should care to not use too much memory and not write in output in the correct order (similar to the sequential algorithm).
The biggest source of slow-down comes from the separate computation of each label. Once can tune the flood-fill algorithm to write a custom well-optimized implementation fitting you need although this appear to be pretty hard to do. Alternatively, one can tune the label-based implementation to do the same. The second approach is simpler, but not easy either. Many question arise in complex cases: what should happen when cells with a given label L1 form a boundary containing a hole itself containing other cells with a given label L2 forming a boundary containing another hole? What if the boundaries overlap partially each other? Are such cases possible? Should they be investigated, and if yes, what would be the set of accepted outputs?
As long as the labeled boundaries are not forming tricky cases, there is a quite efficient algorithm to track and fill holes with the right labels. I am not sure it always work but here is the idea:
Use a label algorithm to find all connected regions
Build a set of label containing all the labels
Remove the labels associated with border regions from the set
Remove the labels associated with cells already labelled (ie. non-zero cells)
So far, the remaining labels are either holes, fake-holes (or tricky cases assumed not present). Fake-holes are unlabelled cells surrounded by labelled cells with multiple different labels (like the 0 cells in the middle of your example).
Check the label of the cell on the boundaries of each labelled regions. If a labelled region is only surrounded by cells with the same label L3, then it is a hole that must be filled with L3-label cells. Otherwise, this is either a fake-hole (or a tricky case).
The resulting algorithm should be much faster than the reference implementation and the previous one.
I have a quite big listed number that includes negatives, 2nd placed decimal numbers. For example, (10348.94, -984.23, 9429.92). I want to find the sum of a number that adds up from one in one of the list. Also the number in the list can be repeated, and the given sum can be negative.
Here is what I got so far, the repetition and the decimal seems to work but when I try to do a negative numbers both in the list and the given sum it wouldn't work.
def Find(goal, VarienceNum):
variance = [[Listed] for Listed in VarienceNum]
newList = []
result = []
while variance:
for holder in variance:
s = sum(holder)
for Listed in VarienceNum:
if Listed >= holder[-1]:
if s + Listed < goal:
newList.append(holder + [Listed])
elif s + Listed == goal:
result.append(holder + [Listed])
variance = newList
newList = []
return result
goal=float(input("please enter your goal: "))
VarienceNum=list(map(float,input("please enter the list: ").split()))
print(Find(goal,VarienceNum))
here's the output
Get all subsets of the list, check the sum of each subset, and when that sum finally matches the target value return that subset!
def inc_bool_array(arr, ind=0):
if (ind >= len(arr)): return;
if (arr[ind] == 0):
arr[ind] = 1;
else:
arr[ind] = 0;
inc_bool_array(arr, ind + 1);
def find_subset_sum(target, arr):
size = len(arr);
pick = [ 0 for n in arr ];
num_subsets = 2 ** size;
'''
Loop through every possible subset until we find one such that
`sum(subset) == target`
'''
for n in range(num_subsets):
''' Subset is determined by the current boolean values in `pick` '''
subset = [ arr[ind] for ind in range(size) if pick[ind] == 1 ];
if sum(subset) == target: return subset;
''' Update `pick` to the next set of booleans '''
inc_bool_array(pick);
return None;
print(find_subset_sum(3, [ 1, 2, 3 ]));
print(find_subset_sum(5, [ 1, 2, 3 ]));
print(find_subset_sum(6, [ 1, 2, 3 ]));
print(find_subset_sum(7, [ 1, 2, 3 ]));
print(find_subset_sum(3, [ -1, 5, 8 ]));
print(find_subset_sum(4, [ -1, 5, 8 ]));
print(find_subset_sum(5, [ -1, 5, 8 ]));
print(find_subset_sum(6, [ -1, 5, 8 ]));
print(find_subset_sum(7, [ -1, 5, 8 ]));
print(find_subset_sum(8, [ -1, 5, 8 ]));
print(find_subset_sum(12, [ -1, 5, 8 ]));
print(find_subset_sum(13, [ -1, 5, 8 ]));
The hard part here is getting all possible subsets of the list. Getting all subsets is a matter of choosing "include" or "exclude" for every item in the list (2 options per element results in 2^n possible choices, and 2^n possible subsets).
In order to enumerate all these choices I use a simple array called pick which is composed of boolean values; one boolean value for each value in the source array. Each boolean represents an include/exclude choice for its corresponding value in the source array. The array starts full of only 0, representing the choice of "exclude" for each item. Then a function called inc_bool_array is used to update pick to the next set of values. This means pick will take on these values over time:
Step 1: [ 0, 0, 0, 0, 0, ... ]
Step 2: [ 1, 0, 0, 0, 0, ... ]
Step 3: [ 0, 1, 0, 0, 0, ... ]
Step 4: [ 1, 1, 0, 0, 0, ... ]
Step 5: [ 0, 0, 1, 0, 0, ... ]
Step 6: [ 1, 0, 1, 0, 0, ... ]
Step 7: [ 0, 1, 1, 0, 0, ... ]
Step 8: [ 1, 1, 1, 0, 0, ... ]
Step 9: [ 0, 0, 0, 1, 0, ... ]
.
.
.
Gradually every possible combination of 0s and 1s will occur. Then pick is used to generate a subset which only contains values corresponding to a 1, simply using a generator with an if condition:
subset = [ arr[ind] for ind in range(len(arr)) if pick[ind] == 1 ]
I have a potentially long array of one's and zero's that looks like this:
a = [0,0,1,0,1,0,.....]
I want to translate each consecutive pair of values to an integer between 0 & 3 as shown below:
0,0 -> 0
0,1 -> 1
1,0 -> 3
1,1 -> 2
I'm looking for a nice clean efficient way to create the array b (example output below) using the mapping above:
b = [0, 3, 3,...]
Is a dict a good idea? I can't figure out how to do it though.
Try:
x = np.reshape(a, (-1,2))
b = x[:,0]*2 + (x[:,0] ^ x[:,1])
If you want to use a dict with an explicit mapping between corresponding decimal numbers, you could try this:
# Convert to a (-1,2) matrix and decimal numbers first
a = np.reshape(a, (-1,2))
a = np.sum(np.array([2,1])*a, axis=1)
# Define dictionary with mapping
D = {0: 0, 1: 1, 2: 3, 3: 2}
# Apply dictionary
a = [D[x] for x in a]
You could use the successive pairs as indices to perform a look-up, for getting the translated value:
master = np.array([[0, 1],[3, 2]])
b = master[a[::2], a[1::2]]
Test input:
[1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1]
Output:
[2, 1, 2, 2, 1, 1, 0, 3]
I have a pytorch tensor A like below:
A =
tensor([[ 4, 3, 3, ..., 0, 0, 0],
[ 13, 4, 13, ..., 0, 0, 0],
[707, 707, 4, ..., 0, 0, 0],
...,
[ 7, 7, 7, ..., 0, 0, 0],
[ 0, 0, 0, ..., 0, 0, 0],
[195, 195, 195, ..., 0, 0, 0]], dtype=torch.int32)
I would like to:
identify all the columns whose all of its entries are equal to 0
delete only those columns that has all of their entries equal to 0
I can imagine doing:
zero_list = []
for j in range(A.size()[1]):
if torch.sum(A[:,j]) == 0:
zero_list = zero_list.append(j)
to identify the columns that only has 0 for its elements
but I am not sure how to delete such columns filled with 0 from the original tensor.
How can I delete the columns with zero from a pytorch tensor based on the index number?
Thank you,
Identify all the columns whose all of its entries are equal to 0
non_empty_mask = A.abs().sum(dim=0).bool()
This sums up over the absolute values of each column and then converts the result to a boolean, i.e. False if the sum is zero and True otherwise.
Delete only those columns that has all of their entries equal to 0
A[:,non_empty_mask]
This simply applies the mask to the original tensor, i.e. it keeps the rows where non_empty_mask is True.
It makes more sense to index the columns you want to keep instead of what you want to delete.
valid_cols = []
for col_idx in range(A.size(1)):
if not torch.all(A[:, col_idx] == 0):
valid_cols.append(col_idx)
A = A[:, valid_cols]
Or a little more cryptically
valid_cols = [col_idx for col_idx, col in enumerate(torch.split(A, 1, dim=1)) if not torch.all(col == 0)]
A = A[:, valid_cols]
Consider a sequence of coin tosses: 1, 0, 0, 1, 0, 1 where tail = 0 and head = 1.
The desired output is the sequence: 0, 1, 2, 0, 1, 0
Each element of the output sequence counts the number of tails since the last head.
I have tried a naive method:
def timer(seq):
if seq[0] == 1: time = [0]
if seq[0] == 0: time = [1]
for x in seq[1:]:
if x == 0: time.append(time[-1] + 1)
if x == 1: time.append(0)
return time
Question: Is there a better method?
Using NumPy:
import numpy as np
seq = np.array([1,0,0,1,0,1,0,0,0,0,1,0])
arr = np.arange(len(seq))
result = arr - np.maximum.accumulate(arr * seq)
print(result)
yields
[0 1 2 0 1 0 1 2 3 4 0 1]
Why arr - np.maximum.accumulate(arr * seq)? The desired output seemed related to a simple progression of integers:
arr = np.arange(len(seq))
So the natural question is, if seq = np.array([1, 0, 0, 1, 0, 1]) and the expected result is expected = np.array([0, 1, 2, 0, 1, 0]), then what value of x makes
arr + x = expected
Since
In [220]: expected - arr
Out[220]: array([ 0, 0, 0, -3, -3, -5])
it looks like x should be the cumulative max of arr * seq:
In [234]: arr * seq
Out[234]: array([0, 0, 0, 3, 0, 5])
In [235]: np.maximum.accumulate(arr * seq)
Out[235]: array([0, 0, 0, 3, 3, 5])
Step 1: Invert l:
In [311]: l = [1, 0, 0, 1, 0, 1]
In [312]: out = [int(not i) for i in l]; out
Out[312]: [0, 1, 1, 0, 1, 0]
Step 2: List comp; add previous value to current value if current value is 1.
In [319]: [out[0]] + [x + y if y else y for x, y in zip(out[:-1], out[1:])]
Out[319]: [0, 1, 2, 0, 1, 0]
This gets rid of windy ifs by zipping adjacent elements.
Using itertools.accumulate:
>>> a = [1, 0, 0, 1, 0, 1]
>>> b = [1 - x for x in a]
>>> list(accumulate(b, lambda total,e: total+1 if e==1 else 0))
[0, 1, 2, 0, 1, 0]
accumulate is only defined in Python 3. There's the equivalent Python code in the above documentation, though, if you want to use it in Python 2.
It's required to invert a because the first element returned by accumulate is the first list element, independently from the accumulator function:
>>> list(accumulate(a, lambda total,e: 0))
[1, 0, 0, 0, 0, 0]
The required output is an array with the same length as the input and none of the values are equal to the input. Therefore, the algorithm must be at least O(n) to form the new output array. Furthermore for this specific problem, you would also need to scan all the values for the input array. All these operations are O(n) and it will not get any more efficient. Constants may differ but your method is already in O(n) and will not go any lower.
Using reduce:
time = reduce(lambda l, r: l + [(l[-1]+1)*(not r)], seq, [0])[1:]
I try to be clear in the following code and differ from the original in using an explicit accumulator.
>>> s = [1,0,0,1,0,1,0,0,0,0,1,0]
>>> def zero_run_length_or_zero(seq):
"Return the run length of zeroes so far in the sequnece or zero"
accumulator, answer = 0, []
for item in seq:
accumulator = 0 if item == 1 else accumulator + 1
answer.append(accumulator)
return answer
>>> zero_run_length_or_zero(s)
[0, 1, 2, 0, 1, 0, 1, 2, 3, 4, 0, 1]
>>>