The project has been scripted in MATLAB because of it's amazing matrix math functions and simply because I had it installed on my laptop.

What I eventually would like to do is develop a SLAM (Simultaneous Localization and Mapping) algorithm to generate maps with only the use of a LiDAR and no other navigation aid (incl. encoders).

This project however only addresses the issue of localization of a robot in a known map - like the arena our robot is going to be in for the Trinity Fire Fighting Robot competition.


The key to the algorithm is calculating best-match scores for every possible linear and angular offsets for the pixelized LiDAR map on a pixelized known map. The best score is then obtained and the x and y indexes of the best score is used as the map offset. The LiDAR map is first rotated to make all lines vertical or horizontal - this leaves only for angular possibilities for the map-matching algorithm which significantly reduces the load on the processor. The rotation of the best match is inverted and the H/V rotation is also inverted to get the robot's position and heading.

For those who wish to use the algorithm in your own LiDAR-based mapping robot - here's my code:

(Hackaday could definitely use MATLAB or Octave syntax highlighting btw)

clear all;
close all;
clc;

% Hardware Specifications
LiDARRange = [20 600]; % [cm] LiDAR Range
LiDARdtheta = 1; % [deg] LiDAR Angular Resolution
LiDARdr = 0.2; % [cm] LiDAR Linear Resolution

map = [];

% Simulation Parameters
run = 1; % run simulation flag
tth = 1; % [cm] Thickness tolerance threshold
dt = 0.1; % [s] simulation time step
dd = 10; % [cm] Discontinuity check threshold
dr = 0.1; % Linearity check range threshold
da = 10; % [deg] Angularity tolerance
mR = 4; % Minimum repetition required to be valid
bounds = [0 244 0 244]; % [cm] Maze bounds (MATLAB axis)
RobotPos = [80 10]; % [cm] Position of Robot (x,y)
RobotHdg = 80; % [deg] Heading of Robot, with 0 deg pointing E, CCW +
LiDARMap = [transpose(0:359), zeros(360,1)+600];
res = 16; % [px] Map resolution

% Define Arena Walls
walls = [  0   0   0 244; % W Boundary
           0   0 244   0; % S Boundary
         244   0 244 244; % E Boundary
           0 244 244 244; % N Boundary
         126   0 126  45; % Room 1 W Wall    [M]
         126  91 198  91; % Room 1 N Wall
           0 103  72 103; % Room 2 N Wall
          72  56  72 103; % Room 2 E Wall
          46 157  72 157; % Room 3 S Wall
          72 157  72 244; % Room 3 E Wall
         126 147 126 198; % Room 4 W Wall
         126 147 196 147; % Room 4 S Wall    [M]
         196 147 196 198; % Room 4 E Wall
         126 198 150 198]; % Room 4 N Wall   [M]
     
% Localization STEP 1 - Pixelize Known map <40x40> [ONE TIME PROCESS]
pixMap_s = zeros(res); % Stored Pixel Map

ds = 244/res; 

for w = 1:size(walls,1)
    p1 = walls(w,1:2);
    p2 = walls(w,3:4);
    if p1(1) == p2(1) % V Line
        length = p2(2) - p1(2);
        for s = 0:round(length/ds);
            pixMap_s(1+round(p1(1)/ds),1+round(p1(2)/ds) + s) = 1;
        end
    else % H Line (There are only H and V Lines on this map)
        length = p2(1) - p1(1);
        for s = 0:round(length/ds);
            pixMap_s(1+round(p1(1)/ds) + s,1+round(p1(2)/ds)) = 1;
        end
    end
end
     
while run
    
    % clc;
    
    LiDARMap = [transpose(0:359), zeros(360,1)+600];
    
    % LiDAR Generate Range Map
    for theta = 0:359
        % Line12 - line from robot to LiDAR range along theta
        x1 = RobotPos(1);
        y1 = RobotPos(2);
        x2 = RobotPos(1) + LiDARRange(2)*cosd(RobotHdg + theta);
        y2 = RobotPos(2) + LiDARRange(2)*sind(RobotHdg + theta);
        
        for wall = 1:size(walls,1)            
            % Line34 - wall
            x3 = walls(wall,1);
            y3 = walls(wall,2);
            x4 = walls(wall,3);
            y4 = walls(wall,4);
            
            % Find Intersection point
            den = (x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4);
            numTerm1 = x1*y2 - y1*x2;
            numTerm2 = x3*y4 - y3*x4;
            Px = (numTerm1*(x3 - x4) - numTerm2*(x1 - x2))/den;
            Py = (numTerm1*(y3 - y4) - numTerm2*(y1 - y2))/den;
            
            % Check if intersection point is between bounds
            if ((Px >= x1-tth && Px <= x2+tth) || (Px >= x2-tth && Px <= x1+tth)) && ...
               ((Py >= y1-tth && Py <= y2+tth) || (Py >= y2-tth && Py <= y1+tth)) && ...
               ((Px >= x3-tth && Px <= x4+tth) || (Px >= x4-tth && Px <= x3+tth)) && ...
               ((Py >= y3-tth && Py <= y4+tth) || (Py >= y4-tth && Py <= y3+tth)) ;
                theta_corr = theta;
                if theta > 359
                    theta_corr = theta - 360;
                end
                d = round(sqrt((Px - RobotPos(1))^2 + (Py - RobotPos(2))^2)*5)/5;
                if d < LiDARMap(theta_corr+1,2)
%                     if d < LiDARRange(1)
%                         d = LiDARRange(1);
%                     end
                    LiDARMap(theta_corr+1,2) = d;
                end
            end
            
        end
    end
    
%     for theta = 0:359
%         d = LiDARMap(theta+1,2);
%         if(d>LiDARRange(1) && d<LiDARRange(2))
%             map = [map; RobotPos + d.*[cosd(RobotHdg+theta), sind(RobotHdg+theta)]];
%         end
%     end
    
%%%%%%%%%%%%%%%%%%%%%%%%%% ONBOARD PROCESSING %%%%%%%%%%%%%%%%%%%%%%%%%%%%%

    % Rotate LiDAR Map to H/V Lines
    
    % Start by identifying the general complimentary angles to find the
    % rotation angle
    
    m = []; % Vector of Slopes
    
    mapP = [];
    
    for theta = 1:358
        % Make sure the linearity check is not at a discontinuity
        if (abs(LiDARMap(theta+2,2) - LiDARMap(theta+1,2)) < dd && abs(LiDARMap(theta+1,2) - LiDARMap(theta,2)) < d)
            p0 = LiDARMap(theta,2).*[cosd(theta); sind(theta)];
            p1 = LiDARMap(theta+1, 2).*[cosd(theta+1); sind(theta+1)];
            u = (p1-p0)./norm(p1-p0);
            A = [[cosd(theta+1); sind(theta+1)] -u];
            x_vect = inv(A)*p1;
            r_exp = x_vect(1);
            r_tol = r_exp*dr;
            if abs(LiDARMap(theta+2,2) - r_exp) < r_tol
                % Grab angle and append to m vector
                m = [m; (180/pi)*atan2(u(2),u(1))];
                mapP = [mapP; p0' p1'];
            end
        end
    end
    
    % There should only be 4 angles in the data set but ofcourse,
    % instrument and sensor errors can cause deviations (let's say +/- 10
    % deg?) - so, I'm going to check if the next angle in within a certain
    % range, it's going to average the value.
    
    primAngles = [1, m(1)]; % [n AVG_ANGLE]
    
    for n = 2:size(m,1)
        foundMatch = 0;
        for p = 1:size(primAngles,1)
            if (abs(primAngles(p,2)-m(n)) < da)
                primAngles(p,2) = (primAngles(p,1)*primAngles(p,2) + m(n))/(primAngles(p,1)+1);
                primAngles(p,1) = primAngles(p,1)+1; % Increment n
                foundMatch = 1;
                break
            end
        end
        if ~foundMatch
            primAngles = [primAngles; 1 m(n)];
        end
    end
    
    % The bottom code is to further refine the rotation angle, but a simple
    % alternative gives a pretty awesome approximation that has worked
    % every single time for me so far
        
%     % Keep only most seen angles - filter out the non-repetitive angles
%     primAngles_filtered = [];
%     for n = 1:size(primAngles,1)
%         if (primAngles(n,1) > mR)
%             primAngles_filtered = [primAngles_filtered; primAngles(n,:)];
%         end
%     end
%     primAngles = sortrows(primAngles_filtered,-1);
%     
%     % Now, if an angle is complementary to the first one, rotate it to the
%     % original domain for further averaging (better angular determination)
%     rotAngle = primAngles(1,2);
%     rotN = primAngles(1,1);
%     
%     if rotAngle > 360
%         rotAngle = rotAngle - 360;
%     elseif rotAngle < 0
%         rotAngle = rotAngle + 360;
%     end
%     
%     if size(primAngles,1) > 1
%         for n = 2:size(primAngles,1)
%             for phi = 90:90:270
%                 thisAngle = primAngles(n,2) + phi;
%                 if thisAngle > 360
%                     thisAngle = thisAngle - 360;
%                 elseif thisAngle < 0
%                     thisAngle = thisAngle + 360;
%                 end
%                 if (abs(thisAngle-rotAngle) < da)
%                     rotAngle = ((rotN*rotAngle) + (primAngles(n,1)*thisAngle))/(rotN+primAngles(n,1));
%                     rotN = rotN + primAngles(n,1);
%                 end
%             end
%         end
%     end
    
    % Simple most popular average selection approximation for rot. angle
    primAngles = sortrows(primAngles,-1);
    rotAngle = primAngles(1,2);
    
    % Now that the a complimentary (90 offset) rotation angle has been,
    % generate a cartesian map of the LiDAR data offset by the rotation
    % angle.
    cartMap = zeros(360,2);
    for theta = 1:360
        cartMap(theta,:) = LiDARMap(theta,2).*[cosd(theta-rotAngle) sind(theta-rotAngle)];
    end
    
    xRange = max(cartMap(:,2)) - min(cartMap(:,2));
    yRange = max(cartMap(:,1)) - min(cartMap(:,1));
    
    pixMap_l = zeros(floor(yRange/ds), floor(xRange/ds)); % LiDAR Data Pixel Map
    
    for n = 1:size(cartMap,1)
        pixMap_l(1+round((cartMap(n,1) - min(cartMap(:,1)))/ds), 1+round((cartMap(n,2) - min(cartMap(:,2)))/ds)) = 1;
    end
    
    % Now that both the known and LiDAR maps have been pixelized to the
    % same scale, they can be compared (at +0, +90, +180 and +270 deg) at
    % every possible x and y offset to search for a match.
    
    % For each of these angular and linear offsets, a similarity score can
    % be calculated based on how many pixels overlap and the offset with
    % the highest score can be selected to find the robot's position with
    % relation to the map
    
    rotMaps = {0, 0, 0, 0};
    
    rotMaps{1} = pixMap_l;
    for n = 2:4
        rotMaps{n} = rot90(rotMaps{n-1});
    end
    
    xL = size(pixMap_l,2); % LiDAR pixmap map x range
    yL = size(pixMap_l,1); % LiDAR pixmap y range
    
    xO = (res - xL) + 2; % Possible number of x-offset values
    yO = (res - yL) + 2; % Possible number of y-offset values
    
    rotScores = {0, 0, 0, 0};
    rotMaxScores = zeros(1,4);

    for r = 1:4 % For each angular/rotationl offset
        % Pre-allocate scores array for processing time optimization
        % Get rotated x and y offset values (flipped for certain rotations)
        if (r == 1 || r == 3)
            xR = xO;
            yR = yO;
            xRL = xL;
            yRL = yL;
        else
            xR = yO;
            yR = xO;
            xRL = yL;
            yRL = xL;
        end
        rotScores{r} = zeros(yR, xR);
        for x = 1:xR
            for y = 1:yR
                % Match Score calculation algorithm
                mapSeg = pixMap_s(y:(y+yRL-1),x:(x+xRL-1));
                score = sum(sum(mapSeg & rotMaps{r}));

                % Add score to rest of the score for comparison
                rotScores{r}(x,y) = score;
            end
        end
        rotMaxScores(r) = max(max(rotScores{r}));
    end
    
    % Get the rotational and linear offsets used which yield the highest
    % match score.
    rMax = find(rotMaxScores==max(rotMaxScores),1);
    HDG_est = (rMax-1)*90 - rotAngle;
    
    if HDG_est > 360
        HDG_est = HDG_est - 360;
    elseif HDG_est < 0
        HDG_est = HDG_est + 360;
    end
    
    % Find the best score linear offset (x,y)
    index = find(rotScores{rMax}==max(max(rotScores{rMax})),1);
    xOffset = ceil(index/size(rotScores{rMax},1));
    yOffset = index - (xOffset-1)*size(rotScores{rMax},1);

    switch rMax
        case 1 % 0 OFF
            xPos_est = - min(cartMap(:,1)) + (xOffset-1)*ds;
            yPos_est = - min(cartMap(:,2)) + (yOffset-1)*ds;
        case 2 % 90 OFF
            xPos_est = xRange + min(cartMap(:,2)) + (xOffset-1)*ds;
            yPos_est = - min(cartMap(:,1)) + (yOffset-1)*ds;
        case 3 % 180 OFF
            xPos_est = xRange + min(cartMap(:,2)) - (xOffset-1)*ds;
            yPos_est = yRange + min(cartMap(:,1)) - (yOffset-1)*ds;
        case 4 % 270 OFF
            xPos_est = - min(cartMap(:,2)) + (yOffset-1)*ds;
            yPos_est = yRange + min(cartMap(:,1)) - (xOffset-1)*ds;
    end
    
%     disp([num2str(rMax) '  POS: <' num2str(xPos_est) ', ' num2str(yPos_est) '>  HDG: ' num2str(HDG_est)]);

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

    % PLOTS
    figure(1);
    clf;
    subplot(2,2,1); % ARENA | SIMULATION VISUALIZATION
    hold on;
    
    % Draw Robot Position and Heading    
    plot(RobotPos(1), RobotPos(2), 'bo'); % Plot Rover
    plot([RobotPos(1) RobotPos(1)+(10.*cosd(RobotHdg))], [RobotPos(2) RobotPos(2)+(10.*sind(RobotHdg))], 'b-');
    
%     plot(xPos_est, yPos_est, 'ro'); % Plot Rover
%     plot([xPos_est xPos_est+(10.*cosd(HDG_est))], [yPos_est yPos_est+(10.*sind(HDG_est))], 'r-');
%     
    
    % Draw Area Walls
    for wall = 1:size(walls,1)
        plot(walls(wall,[1 3]), walls(wall,[2 4]), 'Color', [0 0 0] ,'LineStyle', '-', 'LineWidth', 3);
    end
    
    hold off;
    grid on;
    axis(bounds);
    axis square;
    
    subplot(2,2,2); % LiDAR | VISIBLE RANGE MAP

    polar((LiDARMap(:,1) + 90).*(pi/180),LiDARMap(:,2),'r.');
    
    grid on;
    axis equal;
    
%     % MAPPING PLOT - NOT REQ.
%     plot(map(:,1), map(:,2), 'k.');
%     grid on;
%     axis(bounds);
%     axis square;

    subplot(2,2,3); % PIXELIZED MAP MATCHING VISUALIZATION
    hold on;
    for x = 0:res
        for y = 0:res
            if (pixMap_s(1+x,1+y) == 1)
                plot(x,y,'k*');
            end
        end
    end
    for x = 0:(size(rotMaps{rMax},1)-1)
        for y = 0:(size(rotMaps{rMax},2)-1)
            if (rotMaps{rMax}(1+x,1+y) == 1)
                plot(x+xOffset-1,y+yOffset-1,'r*');
            end
        end
    end
    
    hold off;
    axis([-1 res+1 -1 res+1]);
    axis square;
    grid on;
    
    subplot(2,2,4); % PIXELIZED MAP MATCHING VISUALIZATION
    % polar((LiDARMap(:,1) + 90 - rotAngle).*(pi/180),LiDARMap(:,2),'r.');
    hold on;
    for x = 0:(size(pixMap_l,1)-1)
        for y = 0:(size(pixMap_l,2)-1)
            if (pixMap_l(1+x,1+y) == 1)
                plot(x,y,'k*');
            end
        end
    end
    hold off;
    
    grid on;
    axis([-1 res+1 -1 res+1]);
    axis square;

    % AUTOMATIC CONTROL

    pause(dt);
    RobotPos = RobotPos + 2*[cosd(RobotHdg), sind(RobotHdg)];
    
    % MANUAL CONTROL
    
%     ch = getkey;
%     switch ch
%         case 30 % UP
%             RobotPos = RobotPos + 5.*[cosd(RobotHdg), sind(RobotHdg)];
%         case 31 % DOWN
%             RobotPos = RobotPos - 5.*[cosd(RobotHdg), sind(RobotHdg)];
%         case 28 % LEFT
%             RobotHdg = RobotHdg + 30;
%         case 29 % RIGHT
%             RobotHdg = RobotHdg - 30;
%         otherwise % EXIT
%             run = 0;
%     end
    
    % run = 0;
    
    if ((RobotPos(1) < 0) || (RobotPos(1) > 244) || (RobotPos(2) < 0) || (RobotPos(2) > 244))
        run = 0;
    end

end