0

I've converted two images into numpy arrays

image1Array

Image 1

image2Array

Image 2

Both images have been converted to grayscale, so there are only 0 or 255 values.

In both examples, there are many rows of white at the top (and bottom):

[255 255 255 255 .... 255 255 255 255]

I believe what i'm referring to as 'row' is really an array. I'm new to using Numpy. So, there is an array for every line in the image, and every pixel in that line is represented with a 0 or 255.

How do I find the first row that contains a black 0 pixel and the last row that contains a black 0 pixel? I should be able to use that to calculate the height. In these examples, this should be approximately the same number.

I believe numpy.where(image1Array == 0)[0] is returning the row of every black pixel; min() and max() of that seems to be what i'm looking for, but i'm not sure yet.

Conversely, how do I find the width of each image? In these examples, Image 2 should have a larger width number than Image 1

EDIT

I think all I need is something like this:

Height (the difference between the first row with a black pixel and the last row with a black pixel):

(max(numpy.where(image1Array == 0)[0])) - (min(numpy.where(image1Array == 0)[0]))

Width (the difference between the lowest column value with a black pixel and the highest column value with a black pixel):

(max(numpy.where(image1Array == 0)[1])) - (min(numpy.where(image1Array == 0)[1]))

So far, my testing is showing this is correct. Comparing the two images in the example above, their heights are equal while image2Array's width is double that of image1Array.

yodish
  • 733
  • 2
  • 10
  • 28
  • 1
    You might be interested in [boolean array indexing](https://docs.scipy.org/doc/numpy-1.15.1/reference/arrays.indexing.html#boolean-array-indexing), [`np.any`](https://docs.scipy.org/doc/numpy-1.15.1/reference/generated/numpy.any.html#numpy-any) and [`np.where`](https://stackoverflow.com/questions/8218032/how-to-turn-a-boolean-array-into-index-array-in-numpy). – Georgy Oct 22 '18 at 14:10
  • 1
    @Rock Li, i'm looking for the size of the shape inside of the 'box' There is white space on all sides that I need to eliminate from the equation. – yodish Oct 22 '18 at 14:19
  • Sorry. I get what you're trying to do now. I think what you can do is `x, y = np.where(array==0)` and then `shape = (np.amax(x) - np.amin(x) + 1, same for y + 1)` – Rocky Li Oct 22 '18 at 14:30
  • So you want the size of the white margin (top, right, bottom, left), the size of the black square (width and height) or an array containing the black square? – jdehesa Oct 22 '18 at 14:38
  • @Rocky Li, i'm not sure that's it. I'm returning pairs of the same number. (55, 55) and (81,81). When it seems that second one should be something like (55, 110) – yodish Oct 22 '18 at 14:39
  • @jdehesa, no; I don't care about the white space. I'm trying to create a 'bounding box' around the black shape. – yodish Oct 22 '18 at 14:40

2 Answers2

2

You would want something like this:

mask = x == 0  # or `x != 255` where x is your array

columns_indices = np.where(np.any(mask, axis=0))[0]
rows_indices = np.where(np.any(mask, axis=1))[0]

first_column_index, last_column_index = columns_indices[0], columns_indices[-1]
first_row_index, last_row_index = rows_indices[0], rows_indices[-1]

Explanation:

Let's create an example array using np.pad1

import numpy as np


x = np.pad(array=np.zeros((3, 4)), 
           pad_width=((1, 2), (3, 4)),
           mode='constant',
           constant_values=255)
print(x)

[[255. 255. 255. 255. 255. 255. 255. 255. 255. 255. 255.]
 [255. 255. 255.   0.   0.   0.   0. 255. 255. 255. 255.]
 [255. 255. 255.   0.   0.   0.   0. 255. 255. 255. 255.]
 [255. 255. 255.   0.   0.   0.   0. 255. 255. 255. 255.]
 [255. 255. 255. 255. 255. 255. 255. 255. 255. 255. 255.]
 [255. 255. 255. 255. 255. 255. 255. 255. 255. 255. 255.]]

From here we can get a boolean mask array of zero elements as simple as that:

mask = x == 0
print(mask)

[[False False False False False False False False False False False]
 [False False False  True  True  True  True False False False False]
 [False False False  True  True  True  True False False False False]
 [False False False  True  True  True  True False False False False]
 [False False False False False False False False False False False]
 [False False False False False False False False False False False]]

Now we could use np.any to get those rows where there is at least one zero-element.
For columns:

print(np.any(mask, axis=0))
>>> [False False False  True  True  True  True False False False False]

and for rows:

print(np.any(mask, axis=1))
>>> [False  True  True  True False False]

Now we only have to convert boolean arrays to arrays of indices2:

columns_indices = np.where(np.any(mask, axis=0))[0]
print(columns_indices)
>>> [3 4 5 6]

rows_indices = np.where(np.any(mask, axis=1))[0]
print(rows_indices)
>>> [1 2 3]

Getting the first and the last rows/columns indices from here is pretty easy:

first_column_index, last_column_index = columns_indices[0], columns_indices[-1]
first_row_index, last_row_index = rows_indices[0], rows_indices[-1]

Timings:
I used the following code to calculate timings and plot them: Plot timings for a range of inputs.
Comparing my version with your version but refactored as this:

indices = np.where(x == 0)
first_row_index, last_row_index = indices[0][0], indices[0][-1]
first_column_index, last_column_index = indices[1][0], indices[1][-1]
plot_times([georgy_solution, yodish_solution],
           np.arange(10, 200, 5), 
           repeats=500)

enter image description here

plot_times([georgy_solution, yodish_solution],
           np.arange(200, 10000, 800), 
           repeats=1)

enter image description here


1 See How to pad numpy array with zeros for nice examples.
2 How to turn a boolean array into index array in numpy.

Georgy
  • 12,464
  • 7
  • 65
  • 73
  • thank you for this. I appreciate the step-through. I'm curious, do you see any obvious flaws with the method I describe in my edit to the original post? If so, I will definitely give this a try instead. – yodish Oct 22 '18 at 18:20
  • In your example you calculate `numpy.where(image1Array == 0)` four times, which is quite inefficient and violates [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). Also, instead of searching for `min` and `max` you could take the first and the last indices as well. After refactoring, your example would look like this: `indices = np.where(image1Array == 0); first_row_index, last_row_index = indices[0][0], indices[0][-1]; first_column_index, last_column_index = indices[1][0], indices[1][-1]`... – Georgy Oct 22 '18 at 20:09
  • ... In fact this would be faster for smaller arrays than my solution. But for bigger arrays the solution with `np.any` will win. I will try to provide some proof. – Georgy Oct 22 '18 at 20:11
  • 1
    @yodish added some plots as a proof – Georgy Oct 22 '18 at 20:57
  • much appreciated, I believe my way is also prone to error. I'm running into issues when the shape is not filled in (like in the example). – yodish Oct 22 '18 at 21:47
1

Here's a script that should work with python 2 or python 3. If you use IPython or jupyter, the "magic" %pylab does all the importing for you, and you don't need all the from... statements. After these statements, we create an image like the one you posted.

from __future__ import print_function # makes script work with python 2 and python 3
from matplotlib.pyplot import show, imshow, imread
from matplotlib.mlab import find
from numpy import zeros, int8, sum

img1 = zeros((256, 256), dtype=int8)
img1[50:200, 100:150] = 100
imshow(img1) 
show() # You don't need this call if you are using ipython or jupyter
# You now see a figure like the first one you posted
print('Axis 0 blob limits', find(sum(img1, axis=0) != 0)[[0, -1]]) 
print('Axis 1 blob limits', find(sum(img1, axis=1) != 0)[[0, -1]])

Using the sum function with an explicit specification for axis makes it return the sum along the given direction. the find function returns an array of all the indexes where the condition is true, in this case "where is the column or row sum zero?" Finally, slicing [0, -1] selects the first and last columns found.

If your image does not have any rows or any columns with all zeros, find returns an empty array, and the indexing attempt [0, -1] raises an IndexError. You can pretty the error condition up if you wrap a try...except block around it.

Frank M
  • 1,550
  • 15
  • 15
  • [*The find function was deprecated in Matplotlib 2.2 and will be removed in 3.1.*](https://matplotlib.org/api/mlab_api.html#matplotlib.mlab.find) – Georgy Oct 22 '18 at 14:40
  • Interesting about `find`,... I don't have any deprecation warnings in either Python2 or Python3. What is the replacement for it? – Frank M Oct 22 '18 at 14:42
  • @FrankM whats `numpy.__version__` return? You won't get the warning unless it's 2.2 or greater – Aaron Oct 22 '18 at 14:46
  • I appreciate it, i'm trying to avoid matplotlib and figure out a simpler solution with numpy only. – yodish Oct 22 '18 at 14:59
  • @Aaron: My ubuntu is at 1.13.1 for python3, 1.11.1 for python2. Ubuntu is somewhat conservative, so it's often a few versions behind. – Frank M Oct 23 '18 at 16:01
  • 1
    @yodish: you can use "nonzero" from the numpy library instead of "find". – Frank M Oct 23 '18 at 16:05