How to group drivable areas in YOLOP

Question:

On YOLOP, I can detect the lanes and drivable areas without problem. The data comes out of a torch model and being stored as a 2D numpy array such as like that:

[[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]]

This 2D numpy array stores only 0’s and 1’s corresponding to not-a-drivable-area and drivable-area. And if I plot the output with pyplot, this is what we have.

My question kicks in here. I have to separate these 3 different lanes of drivable areas into separate groups (arrays) so I can use the required drivable area only. For example…

So when I decide to show only the drivableLane[0], i should get an output like this.

At first I’ve tried to iterate the whole 2D array one by one and store the drivable-area coordinates but it was a bad idea due to this operation being too slow. I’ve also came up with the DBSCAN and BIRCH clustering algorithms but at the end, i screwed.

I would appreciate to hear an idea!

Asked By: Jack

||

Answers:

Given the example image you provided:

from PIL import Image
import requests
from io import BytesIO
import numpy as np
import matplotlib.pyplot as plt

url = 'https://i.stack.imgur.com/zyzIJ.png'
response = requests.get(url)
img = Image.open(BytesIO(response.content))
img = np.array(img)
plt.imshow(img)
plt.show()

enter image description here

Where we have RGB(A) values of the image represented as a numpy matrix:

fig = plt.figure(figsize=(15, 5))
ax = plt.subplot(1, 3, 1)
plt.imshow(img[300:800, 300:600, :3])
ax.set_title('Zoom in')
ax = plt.subplot(1, 3, 2)
plt.imshow(img[400:600, 300:600, :3])
ax.set_title('Zoom in more')
ax = plt.subplot(1, 3, 3)
plt.imshow(img[450:550, 400:500, :3])
for r in range(10, img[450:550, 400:500, :3].shape[0] - 20, 20):
    for c in range(10, img[450:550, 400:500, :3].shape[1], 20):
        ax.text(r, c, str(np.round(np.mean(img[r, c, :]), decimals=0)))
ax.set_title('Perfect... now show the values')
plt.show()

enter image description here

Which you said is already 0‘s and 1‘s (which is great! even easier), we can make those matrices of 1‘s according to the row, column indices of drivable areas that don’t overlap.

Let’s visualize it here:

import numpy as np
import matplotlib.pyplot as plt
def draw_shape(ax, x, y, size, layers, layer_colors, layer_alpha, **kwargs):
    for layer in range(layers):
        for line in range(size + 1):
            ax.plot(np.ones(10)*x + line + layer, np.linspace(y + layer, y + size + layer, 10), color=[0, 0, 0], **kwargs)
            ax.plot(np.linspace(y + layer, y + size + layer, 10), np.ones(10)*x + line + layer, color=[0, 0, 0], **kwargs)
            if line < size:
                for row in range(size):
                    ax.text(x + line + layer + (size / 2.5) - 1.0, y + layer + (size / 2) - 1.0 + row, '[' + str(row + x) + ', ' + str(line) + ']')
        ax.fill_between(range(layer, size + layer + 1), x + layer, x + size + layer, color=layer_colors[layer], alpha=layer_alpha)

fig = plt.figure(figsize=(17, 5))
ax = plt.subplot(1, 3, 1)
draw_shape(ax, 0, 0, 3, 1, [[1, 1, 1]], 1.0, lw=2.5, ls='-')
draw_shape(ax, 2.0, 2.0, 3, 1, [[1, 1, 1]], 1.0, lw=2.5, ls='-')
ax.axis('off')
ax = plt.subplot(1, 3, 2)
draw_shape(ax, 0, 0, 3, 1, [[1, 1, 1]], 1.0, lw=2.5, ls='-')
draw_shape(ax, 2.5, 2.5, 3, 1, [[1, 1, 1]], 1.0, lw=2.5, ls='-')
ax.axis('off')
ax = plt.subplot(1, 3, 3)
draw_shape(ax, 0, 0, 3, 1, [[1, 1, 1]], 1.0, lw=2.5, ls='-')
draw_shape(ax, 3.1, 3.1, 3, 1, [[1, 1, 1]], 1.0, lw=2.5, ls='-')
ax.axis('off')
plt.show

enter image description here

We can discriminate between the boundaries by checking if neighboring locations in the image are also drivable areas (1‘s):

for row in range(img.shape[0]):
    for col in range(img.shape[1]):
        if img[row, col] == 1.0:
            # here is where the rules go:
            # example: if there's at least one neighboring '1.0':
            if any([img[max(0, row - 1), col], img[row, max(0, col - 1)], img[max(0, row - 1), max(0, col - 1)], img[min(img.shape[0] - 1, row + 1), col], img[row, min(img.shape[1] - 1, col + 1)], img[min(img.shape[0] - 1, row + 1), min(img.shape[1] - 1, col + 1)]]):
                ...

And we can also discriminate the border of the drivable areas (1‘s) in the image (or if you want to have the matrix for the "background" or non-drivable areas in the image):

for row in range(img.shape[0]):
    for col in range(img.shape[1]):
        if img[row, col] == 1.0:
            # here is where the rules go:
            # example: if there's at least one neighboring '1.0':
            if any([img[max(0, row - 1), col], img[row, max(0, col - 1)], img[max(0, row - 1), max(0, col - 1)], img[min(img.shape[0] - 1, row + 1), col], img[row, min(img.shape[1] - 1, col + 1)], img[min(img.shape[0] - 1, row + 1), min(img.shape[1] - 1, col + 1)]]):
                ...
        else:
            # get borders:
            if any([img[max(0, row - 1), col], img[row, max(0, col - 1)], img[max(0, row - 1), max(0, col - 1)], img[min(img.shape[0] - 1, row + 1), col], img[row, min(img.shape[1] - 1, col + 1)], img[min(img.shape[0] - 1, row + 1), min(img.shape[1] - 1, col + 1)]]):
                ...
            # get background:
            else:
                ...

For example, if we have a matrix of 0‘s and fill the top left corner with 1‘s:

import numpy as np
a = np.zeros((4, 4))
a[:2, :2] = 1.0

print(a):

[[1. 1. 0. 0.]
 [1. 1. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]

We can discriminate the locations of 1‘s and their neighbors (the 0‘s):

for row in range(a.shape[0]):
    for col in range(a.shape[1]):
        if a[row, col] == 1.0:
            if any([a[max(0, row - 1), col], a[row, max(0, col - 1)], a[max(0, row - 1), max(0, col - 1)], a[min(a.shape[0] - 1, row + 1), col], a[row, min(a.shape[1] - 1, col + 1)], a[min(a.shape[0] - 1, row + 1), min(a.shape[1] - 1, col + 1)]]):
                print('equals 1:', str(row), str(col))
        else:
            if any([a[max(0, row - 1), col], a[row, max(0, col - 1)], a[max(0, row - 1), max(0, col - 1)], a[min(a.shape[0] - 1, row + 1), col], a[row, min(a.shape[1] - 1, col + 1)], a[min(a.shape[0] - 1, row + 1), min(a.shape[1] - 1, col + 1)]]):
                print('Neighbor:', str(row), str(col))
            else:
                print('Background:', str(row), str(col))

Getting the locations [row, column] of this "square object" (of 1‘s) from a matrix (of 0‘s) or "image" array:

equals 1: 0 0
equals 1: 0 1
Neighbor: 0 2
Background: 0 3
equals 1: 1 0
equals 1: 1 1
Neighbor: 1 2
Background: 1 3
Neighbor: 2 0
Neighbor: 2 1
Neighbor: 2 2
Background: 2 3
Background: 3 0
Background: 3 1
Background: 3 2
Background: 3 3

Now if a has multiple drivable areas:

a = np.zeros((10, 20))
a[:, 2:4] = 1.0
a[:, -4:-2] = 1.0
a[:2, 4] = 1.0
a[:3, -5] = 1.0

which looks like print(a):

[[0. 0. 1. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 1. 0. 0.]
 [0. 0. 1. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 1. 0. 0.]
 [0. 0. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 1. 0. 0.]
 [0. 0. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 0. 0.]
 [0. 0. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 0. 0.]
 [0. 0. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 0. 0.]
 [0. 0. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 0. 0.]
 [0. 0. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 0. 0.]
 [0. 0. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 0. 0.]
 [0. 0. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 0. 0.]]

or plt.imshow(a):

enter image description here

and we want to partition those 1‘s into separate lists (this example will be easier to convey using nested lists inside a dictionary instead of matrices), we can make some_lists_in_dict which will contain each drivable area (numbered in order represented as keys) and its values as row, column lists; as a dictionary, each "new" drivable area will be inserted as a new list in the order in which it is discriminated, and row, column values will be compared with all drivable areas (appended to pre-existing lists if it is_part_of that drivable area, or made into a new list for a new drivable area):

some_lists_in_dict = {'0': []}

def is_part_of(x, y, some_list, neighbors=[-1, 0, 1]):
    for row in neighbors:
        for col in neighbors:
            if [x + row, y + col] in some_list:
                    return True
    return False

for row in range(a.shape[0]):
    for col in range(a.shape[1]):
        if a[row, col] == 1.0:
            if len(some_lists_in_dict[str(len(some_lists_in_dict.keys()) - 1)]):
                if any([is_part_of(row, col, some_lists_in_dict[key]) for key in some_lists_in_dict.keys()]):
                    some_lists_in_dict[[[key for key in some_lists_in_dict.keys() if is_part_of(row, col, some_lists_in_dict[key])]][0][0]].append([row, col])
                else:
                    some_lists_in_dict[str(len(some_lists_in_dict.keys()))] = []
                    some_lists_in_dict[str(len(some_lists_in_dict.keys()) - 1)].append([row, col])
            else:
                some_lists_in_dict[str(len(some_lists_in_dict.keys()) - 1)].append([row, col])

and print(some_lists_in_dict) shows us that it correctly discriminates the two drivable areas:

{'0': [[0, 2], [0, 3], [0, 4], [1, 2], [1, 3], [1, 4], [2, 2], [2, 3], [3, 2], [3, 3], [4, 2], [4, 3], [5, 2], [5, 3], [6, 2], [6, 3], [7, 2], [7, 3], [8, 2], [8, 3], [9, 2], [9, 3]], '1': [[0, 15], [0, 16], [0, 17], [1, 15], [1, 16], [1, 17], [2, 15], [2, 16], [2, 17], [3, 16], [3, 17], [4, 16], [4, 17], [5, 16], [5, 17], [6, 16], [6, 17], [7, 16], [7, 17], [8, 16], [8, 17], [9, 16], [9, 17]]}

Edge Cases

What about edge cases where a matrix has some "awkwardly" shaped drivable areas? We can see in this example where is_part_of will incorrectly discriminate a separate drivable area when iterating columns and row is 2; the white dashed lines represent iterating columns, and the red boxes represent the neighbors that the is_part_of function relies on:

import numpy as np
a = np.zeros((10, 20))
a[3:-1, 4] = 1.0
a[1, -3:-2] = 1.0
a[4:7, 7:10] = 1.0
a[4, 7:9] = 0.0
a[-5:-1, 1:4] = 1.0
a[-8:-2, -7:-4] = 1.0
a[-2, -10:-3] = 1.0
a[-8, -7:-3] = 1.0
from matplotlib import pyplot as plt
fig = plt.figure(figsize=(10, 5))
ax = plt.subplot(1, 2, 1)
ax.plot(range(18), np.ones(18), ls='--', lw=1.5, color=[1.0, 1.0, 1.0], zorder=2)
for y in range(3):
    ax.plot(range(16, 19), np.ones(3)*y, ls='--', lw=1.0, color=[1.0, 0.0, 0.0], zorder=1)
    ax.plot(np.ones(3)*(16 + y), range(0, 3), ls='--', lw=1.0, color=[1.0, 0.0, 0.0], zorder=1)
ax.imshow(a, zorder=0)
ax.set_title('"for col in..." when row is 1')
ax = plt.subplot(1, 2, 2)
ax.plot(range(18), np.ones(18), ls='--', lw=1.5, color=[1.0, 1.0, 1.0], zorder=2)
ax.plot(range(14), np.ones(14)*2, ls='--', lw=1.5, color=[1.0, 1.0, 1.0], zorder=2)
for y in range(3):
    ax.plot(range(16, 19), np.ones(3)*y, ls='--', lw=1.0, color=[1.0, 0.0, 0.0], zorder=1)
    ax.plot(np.ones(3)*(16 + y), range(0, 3), ls='--', lw=1.0, color=[1.0, 0.0, 0.0], zorder=1)
    ax.plot(range(12, 15), np.ones(3)*(y + 1), ls='--', lw=1.0, color=[1.0, 0.0, 0.0], zorder=1)
    ax.plot(np.ones(3)*(12 + y), range(1, 4), ls='--', lw=1.0, color=[1.0, 0.0, 0.0], zorder=1)
ax.imshow(a, zorder=0)
ax.set_title('"for col in..." when row is 2 and is_part_of() fails')
plt.show()

enter image description here

We can refactor some_lists_in_dict; this function is similar to the is_part_of function, but it compares one drivable area row column list with every other drivable area list, and instead of returning True it returns some_new_lists_in_dict after combining two drivable areas–some_lists_in_dict[key] appends some_lists_in_dict[_key] and some_lists_in_dict[_key] is deleted:

def refactor(some_lists_in_dict, some_new_lists_in_dict=some_lists_in_dict.copy(), neighbors=[-1, 0, 1]):
    for key in some_lists_in_dict.keys(): # list being compared; some_lists_in_dict[key] or xy;
        for _key in list(some_lists_in_dict.keys())[int(key) + 1:]: # comparison list(s); some_lists_in_dict[_key];
            if any([[xy[0] + row, xy[1] + col] in some_lists_in_dict[_key] for xy in some_lists_in_dict[key] for row in neighbors for col in neighbors]):
                some_lists_in_dict[key] += some_lists_in_dict[_key]
                del some_new_lists_in_dict[_key]
                new_keys = int(key)
                for old_keys in range(int(_key) + 1, len(some_lists_in_dict.keys())):
                    del some_new_lists_in_dict[str(old_keys)]
                    new_keys += 1
                    some_new_lists_in_dict[str(new_keys)] = some_lists_in_dict[str(old_keys)]
                return some_new_lists_in_dict

Since we may not know how many drivable areas there are beforehand, we can @keep_refactoring by using a while loop decorator that calls the refactor function until it can’t refactor some_lists_in_dict any further:

def keep_refactoring(some_function):
    def wrapper(some_lists_in_dict, some_new_lists_in_dict=some_lists_in_dict.copy()):
        while some_lists_in_dict is not None:
            some_lists_in_dict = some_function(some_new_lists_in_dict)
            some_new_lists_in_dict = some_lists_in_dict if some_lists_in_dict is not None else some_new_lists_in_dict
        return some_new_lists_in_dict
    return wrapper

@keep_refactoring
def refactor(some_lists_in_dict, some_new_lists_in_dict=some_lists_in_dict.copy(), neighbors=[-1, 0, 1]):
    for key in some_lists_in_dict.keys(): # list being compared; some_lists_in_dict[key] or xy;
        for _key in list(some_lists_in_dict.keys())[int(key) + 1:]: # comparison list(s); some_lists_in_dict[_key];
            if any([[xy[0] + row, xy[1] + col] in some_lists_in_dict[_key] for xy in some_lists_in_dict[key] for row in neighbors for col in neighbors]):
                some_lists_in_dict[key] += some_lists_in_dict[_key]
                del some_new_lists_in_dict[_key]
                new_keys = int(key)
                for old_keys in range(int(_key) + 1, len(some_lists_in_dict.keys())):
                    del some_new_lists_in_dict[str(old_keys)]
                    new_keys += 1
                    some_new_lists_in_dict[str(new_keys)] = some_lists_in_dict[str(old_keys)]
                return some_new_lists_in_dict

We can incorporate is_part_of and refactor functions together, or we can just refactor some_lists_in_dict afterwards:

import numpy as np
a = np.zeros((10, 20))
a[3:-1, 4] = 1.0
a[1, -3:-2] = 1.0
a[4:7, 7:10] = 1.0
a[4, 7:9] = 0.0
a[-5:-1, 1:4] = 1.0
a[-8:-2, -7:-4] = 1.0
a[-2, -10:-3] = 1.0
a[-8, -7:-3] = 1.0

some_lists_in_dict = {'0': []}

def is_part_of(x, y, some_list, neighbors=[-1, 0, 1]):
    for row in neighbors:
        for col in neighbors:
            if [x + row, y + col] in some_list:
                    return True
    return False

for row in range(a.shape[0]):
    for col in range(a.shape[1]):
        if a[row, col] == 1.0:
            if len(some_lists_in_dict[str(len(some_lists_in_dict.keys()) - 1)]):
                if any([is_part_of(row, col, some_lists_in_dict[key]) for key in some_lists_in_dict.keys()]):
                    some_lists_in_dict[[[key for key in some_lists_in_dict.keys() if is_part_of(row, col, some_lists_in_dict[key])]][0][0]].append([row, col])
                else:
                    some_lists_in_dict[str(len(some_lists_in_dict.keys()))] = []
                    some_lists_in_dict[str(len(some_lists_in_dict.keys()) - 1)].append([row, col])
            else:
                some_lists_in_dict[str(len(some_lists_in_dict.keys()) - 1)].append([row, col])

def keep_refactoring(some_function):
    def wrapper(some_lists_in_dict, some_new_lists_in_dict=some_lists_in_dict.copy()):
        while some_lists_in_dict is not None:
            some_lists_in_dict = some_function(some_new_lists_in_dict)
            some_new_lists_in_dict = some_lists_in_dict if some_lists_in_dict is not None else some_new_lists_in_dict
        return some_new_lists_in_dict
    return wrapper

@keep_refactoring
def refactor(some_lists_in_dict, some_new_lists_in_dict=some_lists_in_dict.copy(), neighbors=[-1, 0, 1]):
    for key in some_lists_in_dict.keys(): # list being compared; some_lists_in_dict[key] or xy;
        for _key in list(some_lists_in_dict.keys())[int(key) + 1:]: # comparison list(s); some_lists_in_dict[_key];
            if any([[xy[0] + row, xy[1] + col] in some_lists_in_dict[_key] for xy in some_lists_in_dict[key] for row in neighbors for col in neighbors]):
                some_lists_in_dict[key] += some_lists_in_dict[_key]
                del some_new_lists_in_dict[_key]
                new_keys = int(key)
                for old_keys in range(int(_key) + 1, len(some_lists_in_dict.keys())):
                    del some_new_lists_in_dict[str(old_keys)]
                    new_keys += 1
                    some_new_lists_in_dict[str(new_keys)] = some_lists_in_dict[str(old_keys)]
                return some_new_lists_in_dict

some_lists_in_dict = refactor(some_lists_in_dict)

and print(some_lists_in_dict):

{'0': [[1, 17], [2, 16], [3, 15], [4, 14], [4, 15], [5, 13], [5, 14], [5, 15], [6, 13], [6, 14], [6, 15], [7, 13], [7, 14], [7, 15], [8, 12], [8, 13], [8, 14], [8, 15], [8, 16], [2, 13], [2, 14], [2, 15], [3, 13], [3, 14], [4, 13], [8, 10], [8, 11]], '1': [[3, 4], [4, 4], [5, 3], [5, 4], [6, 2], [6, 3], [6, 4], [7, 1], [7, 2], [7, 3], [7, 4], [8, 1], [8, 2], [8, 3], [8, 4], [5, 1], [5, 2], [6, 1]], '2': [[4, 9], [5, 8], [5, 9], [6, 7], [6, 8], [6, 9], [5, 7]]}
Answered By: Ori Yarden
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.