How to find lines in a 2d numpy array?

Viewed 148

I have a 2D numpy array and I want to find boundary points of both horizontal and vertical lines.

gray_img = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
                     [0, 255, 255, 255, 255, 255, 255, 255, 0],
                     [0, 255, 255, 255, 255, 255, 255, 255, 0],
                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                     [0, 255, 255, 0, 0, 255, 255, 255, 255],
                     [0, 255, 255, 0, 0, 255, 255, 255, 255],
                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                     [0, 255, 255, 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]])

desired_outcome = [ [[1,1],[10,1]],
                    [[1,2],[10,2]],
                    [[1,3],[2,3]], ...]

Here are the lines I want to find:

enter image description here enter image description here

Later, I want to remove the smaller lines to keep only those with more than 2 points distance.

4 Answers

vertical lines:

m = np.diff(gray_img, 1, 0) # get discrete difference along the 0-axis
m = np.argwhere(m != 0)     # get indices where value is not zero 
m = m[np.lexsort(m.T)]      # sort indices first 1-column then 0-column
m[::2,0] += 1               #

output:

[[ 1  1]
 [10  1]
 [ 1  2]
 [10  2]
 [ 1  3]
 [ 2  3]
 [ 1  4]
 [ 2  4]
 [ 1  5]
 [ 2  5]
 [ 6  5]
 [ 7  5]
 [ 1  6]
 [ 2  6]
 [ 6  6]
 [ 7  6]
 [ 1  7]
 [ 2  7]
 [ 6  7]
 [ 7  7]
 [ 6  8]
 [ 7  8]]

horizontal lines:

m = np.diff(gray_img, 1, 1, append=np.zeros((gray_img.shape[0], 1)))
m = np.argwhere(m != 0)
m[::2,1] += 1

output:

[[ 1  1]
 [ 1  7]
 [ 2  1]
 [ 2  7]
 [ 3  1]
 [ 3  2]
 [ 4  1]
 [ 4  2]
 [ 5  1]
 [ 5  2]
 [ 6  1]
 [ 6  2]
 [ 6  5]
 [ 6  8]
 [ 7  1]
 [ 7  2]
 [ 7  5]
 [ 7  8]
 [ 8  1]
 [ 8  2]
 [ 9  1]
 [ 9  2]
 [10  1]
 [10  2]]

Here's an algorithm for horizontal lines based on prefix sums:

# Do a prefix sum to get the lengths of each line.
gray_cumsum = np.cumsum(gray_img / 255, axis=1)
gray_cumsum[:, 1:] = gray_cumsum[:, 1:] * (gray_cumsum[:, 1:] != gray_cumsum[:, :-1])
# Reindex all the points so each line starts at 1.
start_num = gray_cumsum.copy()
a = start_num[:,1:-1] != 0
b = start_num[:,:-2] == 0
c = start_num[:,2:] != 0
start_num[:,1:-1] = start_num[:,1:-1] * np.logical_and(np.logical_and(a, b), c)
start_num[:, -1] = 0
start_num = np.maximum.accumulate(start_num, axis=1)
gray_cumsum = np.maximum(gray_cumsum - start_num, 0)
# Detect only the ends of each line.
gray_cumsum[:,:-1] = gray_cumsum[:,:-1] * (gray_cumsum[:,1:] == 0)
# Get the starting and endings points of each line.
end_points = np.stack(gray_cumsum.nonzero(), axis=-1)
lengths = gray_cumsum[gray_cumsum.nonzero()]
start_points = end_points.copy()
start_points[:, 1] = start_points[:, 1] - lengths
print(start_points)
print(end_points)

Just change the indexing to get vertical lines. You can use the lengths array to filter out which lines you want.

EDIT: Collecting both Horizontal and Vertical lines, also I reduced some superfluous comparisons that I used in the first draft: when scanning there is one direction, thus only the right/bottom end coordinates have to be updated, while the left/top are constant, equal to the beginning of the scanning of the current line segment. There is still some superfluous code that can be compressed.

EDIT2: Added the final formatting of the list as in the question.

It finds and lists first the horizontal lines, if the vertical have to come first as in your example output, then just put the second traversal on top.

    import numpy as np
                
    gray_img = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
                                     [0, 255, 255, 255, 255, 255, 255, 255, 0],
                                     [0, 255, 255, 255, 255, 255, 255, 255, 0],
                                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                                     [0, 255, 255, 0, 0, 255, 255, 255, 255],
                                     [0, 255, 255, 0, 0, 255, 255, 255, 255],
                                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                                     [0, 255, 255, 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]])
                
    bounds = []
    a = gray_img

    #SCANNING HORIZONTAL LINES
    for y in range(0,a.shape[0]):
      print("\nY=",y)  
      found = False #set to True when scanning a line
      left_x, right_x = 0,0
      top_y = y; bottom_y = y
      if a[y,0]==255:
        top_y = min(top_y, y)
        bottom_y = max(bottom_y,y)     
        found = True
      for x in range(0,a.shape[1]): #
        #while x < len(a[1]) or x!==0 #...
         if a[y,x]==255 and not found: #first item
           found = True
           right_x = x
           left_x = x
           #right_x = max(right_x, x)     
           #left_x = x # min(left_x, x)
           print("START",top_y, bottom_y, left_x, right_x)
         else:
           if a[y,x]==255 and found: #running line       
             right_x = max(right_x, x)     
             #left_x = min(left_x, x)
             #print(top_y, bottom_y, left_x, right_x)
         if a[y,x]==0 and found: #end of a running line
            bounds.append([top_y, left_x, bottom_y, right_x])
            print(a[y,x],end=",")
            found = False
      if found: #end of a running line matches the end of the dimension/line
         bounds.append([top_y, left_x, bottom_y, right_x])
         print(a[y,x],end=",")
         found = False                                       
    print("\n")
    print(bounds)
    print(a.shape)
    #print(f"LEN= {a.shape[0])}, {a.shape[1])}")  
    
    #SCANNING VERTICAL LINES
    for x in range(0,a.shape[1]):
      print("\nY=",y)  
      found = False #set to True when scanning a line
      left_x, right_x = x,x
      top_y = 0; bottom_y = 0
      if a[0,x]==255:
        top_y = min(top_y, y)
        bottom_y = max(bottom_y,y)     
        found = True
      for y in range(0,a.shape[0]): #
        #while x < len(a[1]) or x!==0 #...
         if a[y,x]==255 and not found: #first item
           found = True
           #right_x = max(right_x, x)     
           bottom_y = y       
           top_y = y
           left_x = x # min(left_x, x)
           print("START",top_y, bottom_y, left_x, right_x)
         else:
           if a[y,x]==255 and found: #running line       
             bottom_y = y #max(right_x, x)     
             #top_y = min(left_x, x)
             #print(top_y, bottom_y, left_x, right_x)
         if a[y,x]==0 and found: #end of a running line
            bounds.append([top_y, left_x, bottom_y, right_x])
            print(a[y,x],end=",")
            found = False
      if found: #end of a running line matches the end of the dimension/line
         bounds.append([top_y, left_x, bottom_y, right_x])
         print(a[y,x],end=",")
         found = False                                       
    print("\n")
    print(bounds)
    print(a.shape)   

# [[1, 1, 1, 7], [2, 1, 2, 7], [3, 1, 3, 2], [4, 1, 4, 2], [5, 1, 5, 2], [6, 1, 6, 2], [6, 5, 6, 8], [7, 1, 7, 2], [7, 5, 7, 8], [8, 1, 8, 2], [9, 1, 9, 2], [10, 1, 10, 2], [1, 1, 10, 1], [1, 2, 10, 2], [1, 3, 2, 3], [1, 4, 2, 4], [1, 5, 2, 5], [6, 5, 7, 5], [1, 6, 2, 6], [6, 6, 7, 6], [1, 7, 2, 7], [6, 7, 7, 7], [6, 8, 7, 8]]
#(15, 9)

#This list has to be additionally traversed in order to form [ [[1,1][1,7]], [...]] e.g.:

fm = []
for i in bounds:
  #i=[1,1,1,7] etc.
  f = [[i[0],i[1]],[i[2],i[3]]]
  fm.append(f)

print(fm) 

[[[1, 1], [1, 7]], [[2, 1], [2, 7]], [[3, 1], [3, 2]], [[4, 1], [4, 2]], [[5, 1], [5, 2]], [[6, 1], [6, 2]], [[6, 5], [6, 8]], [[7, 1], [7, 2]], [[7, 5], [7, 8]], [[8, 1], [8, 2]], [[9, 1], [9, 2]], [[10, 1], [10, 2]], [[1, 1], [10, 1]], [[1, 2], [10, 2]], [[1, 3], [2, 3]], [[1, 4], [2, 4]], [[1, 5], [2, 5]], [[6, 5], [7, 5]], [[1, 6], [2, 6]], [[6, 6], [7, 6]], [[1, 7], [2, 7]], [[6, 7], [7, 7]], [[6, 8], [7, 8]]]

Then you may traverse the result list and compute that distance (or you mean the lenght of the lines) and transfer only the longer lines to another list.

Is this what you want?

import pandas as pd
import numpy as np

a = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
                     [0, 255, 255, 255, 255, 255, 255, 255, 0],
                     [0, 255, 255, 255, 255, 255, 255, 255, 0],
                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                     [0, 255, 255, 0, 0, 255, 255, 255, 255],
                     [0, 255, 255, 0, 0, 255, 255, 255, 255],
                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                     [0, 255, 255, 0, 0, 0, 0, 0, 0],
                     [0, 255, 255, 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]])

a = a[a != 0]
list = a.tolist()
print(list)
Related