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!
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()
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()
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
, col
umn 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
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
, col
umn]
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)
:
and we want to partition those 1
‘s into separate list
s (this example will be easier to convey using nested list
s inside a dict
ionary 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
, col
umn list
s; as a dict
ionary, each "new" drivable area will be inserted as a new list
in the order in which it is discriminated, and row
, col
umn values will be compared with all drivable areas (append
ed to pre-existing list
s 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 col
umns and row
is 2
; the white dashed lines represent iterating col
umns, 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()
We can refactor
some_lists_in_dict
; this function is similar to the is_part_of
function, but it compares one drivable area row
col
umn list
with every other drivable area list
, and instead of return
ing True
it return
s some_new_lists_in_dict
after combining two drivable areas–some_lists_in_dict[key]
append
s some_lists_in_dict[_key]
and some_lists_in_dict[_key]
is del
eted:
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]]}
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!
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()
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()
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
, col
umn 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
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
, col
umn]
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)
:
and we want to partition those 1
‘s into separate list
s (this example will be easier to convey using nested list
s inside a dict
ionary 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
, col
umn list
s; as a dict
ionary, each "new" drivable area will be inserted as a new list
in the order in which it is discriminated, and row
, col
umn values will be compared with all drivable areas (append
ed to pre-existing list
s 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 col
umns and row
is 2
; the white dashed lines represent iterating col
umns, 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()
We can refactor
some_lists_in_dict
; this function is similar to the is_part_of
function, but it compares one drivable area row
col
umn list
with every other drivable area list
, and instead of return
ing True
it return
s some_new_lists_in_dict
after combining two drivable areas–some_lists_in_dict[key]
append
s some_lists_in_dict[_key]
and some_lists_in_dict[_key]
is del
eted:
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]]}