I’m currently working on a wall-following program for Colin, my mobile robot. True to form, I’ve added an extra complication to the problem: Colin won’t respond directly to his sensor inputs. Instead, he’ll use his sensor inputs to create a simple model of his environment. Then he’ll decide how to move based on this model.
In order to follow a wall, Colin only needs to know the position of the wall relative to himself. To do this Colin will calculate the (x, y) coordinates of each obstacle detected by his rangefinders. Then he’ll fit a line to those points and assume that line represents the wall he’s supposed to be following.
I’ll discuss the actual wall following algorithm in detail in my next post. I’m laying out my method for fitting lines to Colin’s sensor readings in a separate post so I don’t get bogged down in the underlying maths when I’m presenting the wall-following algorithm. If you’re not interested in the maths feel free to skip this post; you can use my line fitting classes without understanding their inner workings. If you want to see what’s going on under the hood, read on.
Interpreting Range Readings
The first thing Colin does is take the range readings from his sonar sensors and convert them into (x, y) points in his local coordinate system. To do this Colin combines his the range readings from his sensors with the orientations of those sensors relative to his heading, which are constant. This turns his range readings into points in a polar coordinate system defined by (r, θ). Then converting these polar points to Cartesian coordinates is pretty easy. Colin calculates the x and y coordinates of each point as follows:
In the above two equations, is the range reading from sensor i and is the angle of sensor i relative to Colin’s heading.
My Point data structure does this conversion automatically. Its constructor takes the polar coordinates of the point as arguments and automatically calculates the Cartesian coordinates. It also stores r, as this is used to assign weight to the point later in the program.
Line Fitting
After converting his sensor readings to an array of Cartesian points, Colin fits a line to them. One of the simplest ways to do this is with least-squares linear regression, which works as follows. Assume the following equation describes all of the points:
Where and are coordinates of point i, a and b are the slope and intercept of the fitted line, and is the error between the line and point i. Least squares regression determines coefficients a and b such that the sum of squares of the errors, is minimized.
In terms of linear algebra, the line equation is represented by where A is an n by 2 matrix containing the x values of Colin’s points:
B is an n by 1 vector containing the y values of Colin’s points:
and x is a 2 by 1 vector containing the y intercept and slope of the fitted line.
Unfortunately, there is usually no x vector that satisfies for every entry in the A and B matrices because all of the points will not lie on the same line. What we want, then, is a vector that minimizes the sum of squares of the errors for each entry. The equation for is as follows:
This equation can be solved pretty simply by multiplying both sides by the inverse of :
The above equation calculates the slope and y intercept of the line that best fits all of Colin’s sensor readings. That brings us a step closer to our goal!
Also, the math presented above is only a very high-level overview. If you’d like to go more in-depth I’d recommend this very thorough textbook chapter from MIT.
Weighting the Points
You may have noticed a problem with the idea of using simple linear regression to model walls. Most of Colin’s sensors will either detect no obstacle or they will detect a far-away obstacle that is not part of the wall Colin is following. This means that most of the points are not relevant. At most three or four of these points will actually represent a wall.
Very distant points are likely to represent failed readings or obstacles that aren’t relevant to Colin’s decision making. So I decided that points that are more distant from Colin should be less significant and closer points should be more significant. Using this criteria, Colin uses a simple weighting function to assign significance to his points:
where is the weight assigned to point i, is the distance between Colin and point i, and c is a constant. The weighting function’s graph looks like this:
The function assigns a weight of 1 for a distance of 0. Varying the value of c adjusts the distance at which the weight decays to 0.
Weighted Linear Regression
Colin uses the above weighting function to create a weights matrix, W:
As you can see, W is a diagonal matrix of the weights assigned to each point. It fits into the linear regression equation as follows:
Similar to the unweighted regression, this is solved for by multiplying both sides by the inverse of :
Using the above function Colin calculates the equation of a line fitted to the obstacles that are closest to him and therefore most likely to be relevant to his decision making.
My class, LineFitter, stores an array of Points and automatically fits a line to those points using the above method for weighted linear regression. This makes it very easy to use in my wall following program, which I will present in my next post.
Alternate Methods
A more sophisticated method would incorporate RANSAC to determine which points actually represent the wall and then fit a line to those points, ignoring the others. It would be very interesting to try to implement this method and compare its performance to my simple weighted linear regression. However, I don’t believe Colin would benefit much from using the more sophisticated method in his current configuration.
This is because Colin is working with a very small data set of only 8 points. This means the both my method and RANSAC could easily find spurious relationships in the data. Further, RANSAC is probably overkill; a brute-force search for relationships between Colin’s points would probably be more efficient on such a small data set. Since both are workable and weighted regression is much easier to implement, that’s where I’m starting. This is a good excuse to do further research on RANSAC, though, and I may do a post on that topic in the future.