Reputation: 718
I have a 2D array containing grayscale image created from .png
as follows:
import cv2
img = cv2.imread("./images/test.png")
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
What I would like to do is to extract a subarray containing only the rectangle containing the data - ignoring all zeros surrounding the picture.
For example, if input is:
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 175 0 0 0 71 0
0 0 0 12 8 54 0 0
0 0 0 0 255 0 0 0
0 0 0 2 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
then output should be:
175 0 0 0 71
0 12 8 54 0
0 0 255 0 0
0 2 0 0 0
I could iterate over the rows in forward direction to find the first nonzero row and then iterate over the rows backwards to find the last nonzero row remembering indices - and then repeat the same for the columns and then extract a subarray using that data but I am sure there are more appropriate ways of doing the same or there even might be a NumPy function designed for such a purpose.
If I were to choose between shortest code vs fastest execution I'd be more interested in fastest code execution.
EDIT:
I did not include the best example because there could be zero rows/columns in the middle as here:
Input:
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 175 0 0 0 71 0
0 0 0 12 8 54 0 0
0 0 0 0 255 0 0 0
0 0 0 0 0 0 0 0
0 0 0 2 0 0 0 0
0 0 0 0 0 0 0 0
Output:
175 0 0 0 71
0 12 8 54 0
0 0 255 0 0
0 0 0 0 0
0 2 0 0 0
Upvotes: 9
Views: 3461
Reputation: 14530
Update This simpler method using opencv functions actually faster, and probably faster than the other methods presented in the other answers here.
def crop_fastest(arr):
return cv2.boundingRect(cv2.findNonZero(arr))
This return the x, y, width, and the height of bounding box. On my desktop pc for my old code 1000 loops, best of 3: 562 µs per loop
while for this new code 10000 loops, best of 3: 179 µs per loop
.
Yet Another Update
As Chupo_cro pointed out simply calling cv2.boundingRect(arr)
returns the same result, and that seems to be due to the code in this function that does the conversion internally.
Previous answer
Probably there are faster methods for this. This simpler function is slightly faster.
from scipy import ndimage
def crop_fast(arr):
slice_x, slice_y = ndimage.find_objects(arr>0)[0]
return arr[slice_x, slice_y]
To compare the speeds of droooze's code and this one,
arr = np.zeros(shape=(50000,6), dtype=np.uint8)
arr[2] = [9,8,0,0,1,1]
arr[1] = [0,3,0,0,1,1]
Then %timeit crop(arr)
returns 1000 loops, best of 3: 1.62 ms per loop
and %timeit crop_fast(arr)
returns 1000 loops, best of 3: 979 µs per loop
on my laptop. That is, crop_fast()
takes about 60% of time by crop()
.
Upvotes: 5
Reputation: 3532
Note, not an OpenCV
solution - this will work for n-dimensional NumPy
or SciPy
arrays in general.
(Based on Divakar's answer, extended to n dimensions)
def crop_new(arr):
mask = arr != 0
n = mask.ndim
dims = range(n)
slices = [None]*n
for i in dims:
mask_i = mask.any(tuple(dims[:i] + dims[i+1:]))
slices[i] = (mask_i.argmax(), len(mask_i) - mask_i[::-1].argmax())
return arr[[slice(*s) for s in slices]]
Speed tests:
In [42]: np.random.seed(0)
In [43]: a = np.zeros((30, 30, 30, 20),dtype=np.uint8)
In [44]: a[2:-2, 2:-2, 2:-2, 2:-2] = np.random.randint(0,255,(26,26,26,16),dtype
=np.uint8)
In [45]: timeit crop(a) # Old solution
1 loop, best of 3: 181 ms per loop
In [46]: timeit crop_fast(a) # modified fireant's solution for n-dimensions
100 loops, best of 3: 5 ms per loop
In [48]: timeit crop_new(a) # modified Divakar's solution for n-dimensions
100 loops, best of 3: 1.91 ms per loop
Old solution
You can use np.nonzero
to get the indices of the array. The bounding box of this array are then contained entirely in the maximum and minimum values of the indices.
def _get_slice_bbox(arr):
nonzero = np.nonzero(arr)
return [(min(a), max(a)+1) for a in nonzero]
def crop(arr):
slice_bbox = _get_slice_bbox(arr)
return arr[[slice(*a) for a in slice_bbox]]
E.g.
>>> img = np.array([[ 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 175, 0, 0, 0, 71, 0],
[ 0, 0, 0, 12, 8, 54, 0, 0],
[ 0, 0, 0, 0, 255, 0, 0, 0],
[ 0, 0, 0, 2, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0]], dtype='uint8')
>>> print crop(img)
[[175 0 0 0 71]
[ 0 12 8 54 0]
[ 0 0 255 0 0]
[ 0 2 0 0 0]]
Upvotes: 11
Reputation: 221574
We could use argmax
to get the start, stop row and column indices, as discussed in some detail in this post
. We also intend to work with boolean arrays/masks for efficient processing. Thus, using these tools/ideas, we would have one vectorized solution, like so -
def remove_black_border(a):
# Mask of non-zeros
mask = a!=0 # Use a >tolerance for a tolerance defining black border
# Mask of non-zero rows and columns
mask_row = mask.any(1)
mask_col = mask.any(0)
# First, last indices among the non-zero rows
sr0,sr1 = mask_row.argmax(), len(mask_row) - mask_row[::-1].argmax()
# First, last indices among the non-zero columns
sc0,sc1 = mask_col.argmax(), len(mask_col) - mask_col[::-1].argmax()
# Finally slice along the rows & cols with the start and stop indices to get
# cropped image. Slicing helps for an efficient operation.
return a[sr0:sr1, sc0:sc1]
Sample run -
In [56]: a # Input image array
Out[56]:
array([[ 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 175, 0, 0, 0, 71],
[ 0, 0, 0, 0, 12, 8, 54, 0],
[ 0, 0, 0, 0, 0, 255, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 2, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0]])
In [57]: out = remove_black_border(a)
In [58]: out
Out[58]:
array([[175, 0, 0, 0, 71],
[ 0, 12, 8, 54, 0],
[ 0, 0, 255, 0, 0],
[ 0, 0, 0, 0, 0],
[ 0, 2, 0, 0, 0]])
Memory efficiency :
The output is a view into the input array, so no extra memory or copying is needed, which is helping on memory efficiency. Let's verify the view part -
In [59]: np.shares_memory(a, out)
Out[59]: True
Timings with all proposed approaches on bigger image
In [105]: # Setup for 1000x1000 2D image and 100 offsetted boundaries all across
...: np.random.seed(0)
...: a = np.zeros((1000,1000),dtype=np.uint8)
...: a[100:-100,100:-100] = np.random.randint(0,255,(800,800),dtype=np.uint8)
In [106]: %timeit crop_fast(a) # @fireant's soln
...: %timeit crop(a) # @droooze's soln
...: %timeit remove_black_border(a) # from this post
100 loops, best of 3: 4.58 ms per loop
10 loops, best of 3: 127 ms per loop
10000 loops, best of 3: 155 µs per loop
Upvotes: 8