0

I want to extract a rectangular ROI from an image. The image contains a single connected non zero part.

I need it to be efficient in run time.

I was thinking maybe:

  1. Summing along each direction.
  2. Finding first non zero and last non zero.
  3. Slicing the image accordingly.

Is there a better way?

My code:

First is a function to find the first and last non zero:

import numpy as np

from PIL import Image

def first_last_nonzero(boolean_vector):
    first = last = -1
    for idx,val in enumerate(boolean_vector):
        if val == True and first == -1:
            first = idx
        if val == False and first != -1:
            last = idx
            return first , last

Then creating an image:

np_im = np.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   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   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0 255 154 251  60   0   0   0]
 [  0   0   0   0   4  66   0   0 255   0   0   0]
 [  0   0   0   0   0   0   0 134  48   0   0   0]
 [  0   0   0   0   0   0 236  70   0   0   0   0]
 [  0   0   0   0   1 255   0   0   0   0   0   0]
 [  0   0   0   0 255  24  24  24   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   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]])

Then running our function on the sum along each axis:

y_start, y_end = first_last_nonzero(np.sum(np_im, 1)>0)
x_start, x_end = first_last_nonzero(np.sum(np_im, 0)>0)
cropped_np_im = np_im[y_start:y_end, x_start:x_end]

# show the cropped image
Image.fromarray(cropped_np_im).show()

This works but there are probably a plenty of unnecessary calculations. Is there a better way to do this? Or maybe more pythonic way?

Rolv Apneseth
  • 2,078
  • 2
  • 7
  • 19
user158881
  • 133
  • 8

1 Answers1

1

You can make use of the functions from this post: Numpy: How to find first non-zero value in every column of a numpy array?

def first_nonzero(arr, axis, invalid_val=-1):
    mask = arr!=0
    return np.where(mask.any(axis=axis), mask.argmax(axis=axis), invalid_val)

def last_nonzero(arr, axis, invalid_val=-1):
    mask = arr!=0
    val = arr.shape[axis] - np.flip(mask, axis=axis).argmax(axis=axis) - 1
    return np.where(mask.any(axis=axis), val, invalid_val)


arr = np.array([
    [0, 0, 0, 0, 1, 1],
    [0, 0, 1, 1, 0, 0],
    [0, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 0, 0],
    [0, 0, 1, 0, 1, 0],
    [0, 0, 0, 0, 0, 0] ])

y_Min, y_Max, x_Min, x_Max = (0, 0, 0, 0) 
y_Min = first_nonzero(arr, axis = 0, invalid_val = -1) 
y_Min = (y_Min[y_Min >= 0]).min() 
x_Min = first_nonzero(arr, axis = 1, invalid_val = -1) 
x_Min = (x_Min[x_Min >= 0]).min() 
y_Max = last_nonzero(arr, axis = 0, invalid_val = -1) 
y_Max = (y_Max[y_Max >= 0]).max() 
x_Max = last_nonzero(arr, axis = 1, invalid_val = -1) 
x_Max = (x_Max[x_Max >= 0]).max() 
print(x_Min) 
print(y_Min) 
print(x_Max) 
print(y_Max)

For this example of mine, the code will return 1, 0, 5, 4. As a general rule of thumb in python: Try to avoid loops at all costs. From my own experience that statement is true in 99 out of 100 cases

Sheradil
  • 407
  • 3
  • 14
  • my examples are connected spaces. only one in each example. isn't it possible to use this somehow? – user158881 Mar 06 '21 at 18:47
  • 1
    You should be able to use this code. It should work with your input data – Sheradil Mar 09 '21 at 14:29
  • I was hoping for some algorithm capable of: A random pick of pixels. if not 0 than mark all the connected pixels. returning a square around this range – user158881 Mar 09 '21 at 15:55
  • 1
    I don't understand what you mean with "a random pick of pixels". The script that I posted currently returns the square that surrounds all the pixels that are > 0. You just have to draw it on your image and you are done. – Sheradil Mar 09 '21 at 16:27
  • I really appreciate your patience. thanks!! the problem is I don't want to check all the pixels. Imagine only one square of 9 pixels in the lower left corner is 255, all the rest is 0. If I have to check a big image I will need to check a lot of pixels. if I find one pixel in this corner I can check all its neighbors and then check their neighbors etc. and find the rectangle in in 9 steps after the first one that is bigger than 0. that will probably be faster – user158881 Mar 09 '21 at 19:48
  • I see. I don't really see how you want to achieve that anyway. Let's say 1 pixel is colored, and it is the top left one. If you would use a for loop and loop over each row and column you would instantly find it. If it is the bottom right pixel, you would need to loop over the whole image at first. I'd suggest that you use the algorithm I posted. It uses c++ and is quite fast (because of numpy). Probably faster than anything you can come up with. Maybe you can come up with a faster algorithm, but I don't know if its worth the effort – Sheradil Mar 09 '21 at 20:16