Estimating WiFi signal strength (in an area with obstacles) in Python

Part 2

Book Lailert
Python in Plain English

--

Ever curious on how strong the WiFi signals will be in different places around your house if you were to move your WiFi access points around? In this article I will describe how we can find out just that using Python.

Before we begin, this article will refer to steps that I’ve mentioned in my previous article. Make sure you read that first here.

First thing we’ll need to do is provide the code with the map of where the walls/obstacles are in the area. It is important that it is to scale as well as we will be calculating the RSSI using the distance in metres.

The scale I’ve chosen is 10 pixels per metres (you can choose the scale based on your needs), now go ahead and measure your area.

Before drawing the map, we also have to understand that not all walls are created equal, some walls will block more signals than others, so we must also tell our code how much each wall will block the signals using colour codes. There’s quite a wide range of thickness of wall in the area I will be testing this on so I will be choosing these colour codes:

Red: -8dB
Green: -6dB
Blue: -4dB
Light Blue: -2dB
[Black: Don’t calculate here]

Adjust the colour codes to your needs and start drawing the map.

Here’s my finished map:

Save the image file as file type you want, I saved mine as bitmap.

Now we can move on to the coding bit. If you haven’t already, you will have to install these modules to your Python interpreter:

PIL (Pillow)
Matplotlib

Then import these along with a few other built-in modules that we’ll need

from PIL import Image
import matplotlib.pyplot as plt
from math import sqrt, sin, cos, atan, fabs, pi, e

You will see what we need these for later

Now the first thing we need to do is load the map. We’ll use Pillow to do so like this:

map_dir = "path_to_file"
obstacle_map = Image.open(map_dir)

We will also want to know the position of the access point we will be calculating, we will put that in variables ap_x and ap_y where they will correspond to the pixel of the obstacle map where the access point is located.

ap_x = 111
ap_y = 95

What we will then need to do is get the dimensions of the image.

width, height = obstacle_map.size

As we also need to be able to read the pixel RGB values, we will convert the image to an RGB image:

rgb_map = obstacle_map.convert("RGB")

We’ll want to store the expected RSSI for each position, so let’s create a dictionary for that

rssi_map = {}

We will then loop through all the pixels of the image.

In my case, the black colour code will tell the code to not calculate that area. To make the code skip the black area, we first get the r, g, b value of the current pixel, then if they’re all zeros, we will append the signal strength of -1.

for x in range(width):
for y in range(height):
r, g, b = rgb_map.getpixel((x, y))
if r == g == b == 0:
rssi_map[(x, y)] = -1
continue

After that, we use Pythagorean theorem to find the distance between that pixel and the access point:

        dy = x - ap_x
dx = y - ap_y
distance = sqrt(dy**2 + dx**2) / 10

Note that my scale is 10 pixels per metres, hence the / 10

At this point, we want to find the estimated RSSI from the distance, so what I will do is use the inverse function of what I’ve found in this article.

        raw_rssi = e**(-0.232584*distance)*(87.4389 - 81*e**(0.232584*distance)) # TODO: Replace with your own equation

This is the RSSI we would expect at that point on the map assuming that there’s no obstacle between it and the access point. But of course, we need to apply corrections caused by the signal being blocked by the obstacles. To do that, we will be creating a new function :get_path_obstacle(rgb_map, x1, y1, x2, y2) .

get_path_obstacle

get_path_obstacle trace a line between each point and the access point, then check each point on that line for its colour. As there may be multiple obstacles on each path, we will create a variable rssi_subtract which will contain how the RSSI would have reduced due to the obstacles

def get_path_obstacle(rgb_map, x1, y1, x2, y2):
rssi_subtract = 0

We will also need to calculate the distance in pixels (we don’t need to divide it in this case as we want the actual distance in pixels not in metres):

    dx = x - ap_x
dy = y - ap_y
px_distance = sqrt(dy**2 + dx**2)

Furthermore, we would also need the path angle (from the positive x axis of the map)

    path_angle = fabs(atan(dy/dx))

However, if the point is directly above or below the access point, the code will crash as dx will be zero. In order to fix this, we will add a short if statement after the statement.

    path_angle = fabs(atan(dy/dx)) if not dx == 0 else pi/2

This means that this line will return pi/2 (90 degrees) when dx = 0.

Another thing that we need to keep in mind is that the output of arctan will be will still 0 < arctan(x) < (pi/2) even when the actual angle is larger than that (dx or dy is negative) which can break out path finding algorithm as it will be going the wrong way. Therefore, we must keep track if dy or dx is negative (or both).

    x_neg = dx < 0
y_neg = dy < 0

Now we will be taking a reading at each pixel along the path. We will do this by iterating through px_distance and use that to find the x and y position on the map using the path_angle we found above.

    for path_distance in range(int(px_distance)):
path_x = path_distance * cos(path_angle)
path_y = path_distance * sin(path_angle)

However, as I mentioned path_x and path_y are always positive, so we must make it negative depending on whether dx or dy was negative.

        path_x = path_distance * cos(path_angle) * (-1 if x_neg else 1)
path_y = path_distance * sin(path_angle) * (-1 if y_neg else 1)

Right now, path_x and path_y are relative to the point, to get the absolute pixel, we have to add the position of the point to them.

       path_x = path_x + x1
path_y = path_y + y1

Now that we have the absolute position, we can get the RGB value at that position of the map.

       r, g, b = rgb_map.getpixel((path_x, path_y))

And use the RGB values to determine the colour which we then match with our colour code to work how much we will need to reduce the RSSI by.

        # TODO: replace with your own colour code
if b - r > 30 and g - r > 30: # Light Blue
rssi_subtract += 2
elif r - g > 30 and r - b > 30: # Red
rssi_subtract += 8
elif g - r > 30 and g - b > 30: # Green
rssi_subtract += 6
elif b - r > 30 and b - g > 30: # Blue
rssi_subtract += 4

Note than instead of just doing if b > r , I’m using if b — r > 30 as there can sometimes be error in the process of saving the map causing the RGB value to not be exact.

Now we can return rssi_subtract

     return rssi_subtract

— — — — —

In the main code, we then subtract raw_rssi with the value from this function

       adjusted_rssi = raw_rssi - get_path_obstacle(rgb_map, x, y, ap_x, ap_y)

We can then append this value to the dictionary we created earlier

        rssi_map[(x, y)] = adjusted_rssi

Now that we have a dictionary containing the estimated RSSI at each position of the map. We would want to put it in a way which is easier for us to read. For this we will create another function: draw_heat_map(rssi_map, min_val, max_val, width, height)

draw_heat_map

The first thing that the function needs to do is create an empty image with the size we want. This will be the canvas of our signal heat map.

def draw_heat_map(rssi_map, min_val, max_val, width, height):
heat_map = Image.new("RGB", size=(width, height))

Then create a list instance of heat_map

    pixels = heat_map.load()

And then calculate the range which we will use to determine the colour intensity

    val_range = max_val - min_val

Then we iterate through all of the values of rssi_map and extract the x , y and rssi from it

    for pos, val in rssi_map.items():
x = pos[0]
y = pos[1]

And use that to calculate the colour intensity (out of 255)

        colour_intensity = ((val - min_val)/val_range)*255

We can then set that pixel to that colour on the map. I chose Red, but you can choose whatever you want by putting int(colour_intensity) in a different element of the tuple.

        pixels[x, y] = (int(colour_intensity), 0, 0)

We then call matplotlib to show the image

    plt.imshow(heat_map)
plt.show()

— — — —

Now in the main code call this function and provide it with parameters.

draw_heat_map(rssi_map, -90, -30, width, height)

We’re done! Now if you run the code, you should see a window open with an image like this.

Here’s the complete code for this project:

from PIL import Image
import matplotlib.pyplot as plt
from math import sqrt, sin, cos, atan, fabs, pi, e


def get_path_obstacle(rgb_map, x1, y1, x2, y2):
rssi_subtract = 0
dx = x2 - x1
dy = y2 - y1
px_distance = sqrt(dy ** 2 + dx ** 2)
path_angle = fabs(atan(dy/dx)) if not dx == 0 else pi/2
x_neg = dx < 0
y_neg = dy < 0
for path_distance in range(int(px_distance)):
path_x = path_distance * cos(path_angle) * (-1 if x_neg else 1)
path_y = path_distance * sin(path_angle) * (-1 if y_neg else 1)
path_x = path_x + x1
path_y = path_y + y1
r, g, b = rgb_map.getpixel((path_x, path_y))
if b - r > 30 and g - r > 30:
rssi_subtract += 2
elif r - g > 30 and r - b > 30:
rssi_subtract += 8
elif g - r > 30 and g - b > 30:
rssi_subtract += 6
elif b - r > 30 and b - g > 30:
rssi_subtract += 4
return rssi_subtract


def draw_heat_map(rssi_map, min_val, max_val, width, height):
heat_map = Image.new("RGB", size=(width, height))
pixels = heat_map.load()
val_range = max_val - min_val
for pos, val in rssi_map.items():
x = pos[0]
y = pos[1]
colour_intensity = ((val - min_val) / val_range) * 255
pixels[x, y] = (int(colour_intensity), 0, 0)
plt.imshow(heat_map)
plt.show()


def create_map(map_dir, ap_x, ap_y):
obstacle_map = Image.open(map_dir)
width, height = obstacle_map.size
rgb_map = obstacle_map.convert("RGB")
rssi_map = {}
for x in range(width):
for y in range(height):
r, g, b = rgb_map.getpixel((x, y))
if r == g == b == 0:
rssi_map[(x, y)] = -1
continue
dy = x - ap_x
dx = y - ap_y
distance = sqrt(dy**2 + dx**2) / 10
raw_rssi = e ** (-0.232584 * distance) * (87.4389 - 81 * e ** (0.232584 * distance)) # TODO: Replace with your own equation
adjusted_rssi = raw_rssi - get_path_obstacle(rgb_map, x, y, ap_x, ap_y)
rssi_map[(x, y)] = adjusted_rssi
draw_heat_map(rssi_map, -90, -30, width, height)


create_map("maps/0.bmp", 111, 95)

--

--

A-Level student in Bangkok, Thailand. Interested in Computer Science and Programming.