Skip to content

Задача одновременной локализации и построения карты. Fast SLAM.

astSlam — один из подходов решения задач SLAM (англ. Simultaneous Localization And Mapping). В основе алгоритма лежит так называемый фильтр частиц и применение Байесовской сети. В FastSLAM одна большая карта рассматривается как совокупность локальных подкарт, что позволяет убрать зависимость ориентиров друг от друга и таким образом значительно сократить время пересчета оценки состояния системы[1]. Метод был разработан в 2002 году студентами Карнеги — Меллона и Стэнфордского университетов.

Вот подробное объяснение работы FastSLAM и пример реализации на Python с использованием объектно-ориентированного программирования (ООП):

Основные компоненты FastSLAM

  1. Частицы (Particles): Набор гипотез о возможных состояниях робота (местоположении и ориентации).
  2. Фильтр Калмана (Kalman Filter): Используется для обновления оценки местоположения и карты для каждой частицы.
  3. Вес частицы (Particle Weight): Оценивает, насколько хорошо частица соответствует наблюдениям.

Основные шаги алгоритма FastSLAM

  1. Инициализация: Создание начального набора частиц с равномерным весом.
  2. Предсказание (Prediction): Применение модели движения к каждой частице для получения предсказанного состояния.
  3. Обновление (Update): Обновление состояния частиц на основе наблюдений с использованием фильтра Калмана.
  4. Пересэмплинг (Resampling): Выбор новых частиц на основе их весов для устранения частиц с малым весом и размножения частиц с большим весом.

Пример реализации на Python

1. Определение классов

import numpy as np

class Particle:
    def __init__(self, x, y, theta, weight):
        self.x = x
        self.y = y
        self.theta = theta
        self.weight = weight
        self.landmarks = {}  # key: landmark id, value: [mean, covariance]

    def move(self, delta_x, delta_y, delta_theta):
        self.x += delta_x
        self.y += delta_y
        self.theta += delta_theta

    def update_landmark(self, landmark_id, z, R):
        if landmark_id not in self.landmarks:
            self.landmarks[landmark_id] = [z, R]
        else:
            mean, covariance = self.landmarks[landmark_id]

            # Фильтр Калмана: прогноз
            predicted_mean = mean
            predicted_covariance = covariance + R

            # Фильтр Калмана: обновление
            innovation = z - predicted_mean
            innovation_covariance = predicted_covariance + R

            kalman_gain = np.dot(predicted_covariance, np.linalg.inv(innovation_covariance))
            new_mean = predicted_mean + np.dot(kalman_gain, innovation)
            new_covariance = np.dot(np.eye(2) - kalman_gain, predicted_covariance)

            self.landmarks[landmark_id] = [new_mean, new_covariance]

class FastSLAM:
    def __init__(self, num_particles):
        self.particles = [Particle(np.random.uniform(-1, 1), 
                                   np.random.uniform(-1, 1), 
                                   np.random.uniform(-np.pi, np.pi), 
                                   1.0/num_particles) for _ in range(num_particles)]

    def predict(self, control):
        for particle in self.particles:
            delta_x, delta_y, delta_theta = control
            particle.move(delta_x, delta_y, delta_theta)

    def update(self, observations):
        for particle in self.particles:
            for obs in observations:
                landmark_id, obs_x, obs_y = obs
                z = np.array([obs_x, obs_y])
                R = np.array([[0.1, 0], [0, 0.1]])  # Допущение: одинаковая ковариация для всех измерений
                particle.update_landmark(landmark_id, z, R)

                # Обновление веса частицы
                mean, covariance = particle.landmarks[landmark_id]
                innovation = z - mean
                innovation_covariance = covariance + R

                weight = np.exp(-0.5 * np.dot(np.dot(innovation.T, np.linalg.inv(innovation_covariance)), innovation))
                particle.weight *= weight

        # Нормализация весов частиц
        total_weight = sum(particle.weight for particle in self.particles)
        for particle in self.particles:
            particle.weight /= total_weight

    def resample(self):
        weights = [particle.weight for particle in self.particles]
        new_particles_indices = np.random.choice(len(self.particles), size=len(self.particles), p=weights)
        self.particles = [self.particles[i] for i in new_particles_indices]

    def get_best_particle(self):
        best_particle = max(self.particles, key=lambda p: p.weight)
        return best_particle

# Пример использования:
num_particles = 100
fast_slam = FastSLAM(num_particles)

# Пример команд управления (delta_x, delta_y, delta_theta)
control = (0.1, 0.0, 0.05)
fast_slam.predict(control)

# Пример наблюдений [(id_landmark, x_obs, y_obs)]
observations = [(1, 2.0, 1.0), (2, -1.0, 3.0)]
fast_slam.update(observations)

# Пересэмплинг
fast_slam.resample()

# Получение наилучшей частицы
best_particle = fast_slam.get_best_particle()
print(f"Best particle: x={best_particle.x}, y={best_particle.y}, theta={best_particle.theta}, weight={best_particle.weight}")

Объяснение примера

  1. Класс Particle: Представляет частицу с положением (x, y, theta) и весом. Метод move обновляет положение частицы на основе модели движения.

  2. Класс FastSLAM: Содержит набор частиц и методы для выполнения основных шагов алгоритма.

    • __init__: Инициализация набора частиц с случайным начальным положением.
    • predict: Применяет команду управления к каждой частице.
    • update: Обновляет вес каждой частицы на основе наблюдений.
    • resample: Пересэмплинг частиц на основе их весов.
    • get_best_particle: Возвращает частицу с наибольшим весом.

Этот пример показывает базовую структуру алгоритма FastSLAM. В реальной реализации обновление состояния частиц с использованием фильтра Калмана и работа с наблюдениями будет более сложной и требует точных математических вычислений.

И пример на более сложном Python

Реализация алгоритма FastSLAM на Python с применением объектно-ориентированного подхода и принципов чистого кода (Clean Code) включает несколько ключевых компонентов: класс робота, класс карты, класс частиц и сам алгоритм FastSLAM. Также мы будем использовать декораторы для логгирования и проверки типов данных.

Основные классы и декораторы

  1. Декоратор для логгирования: Этот декоратор будет использоваться для логгирования вызовов методов и их результатов.
  2. Декоратор для проверки типов: Этот декоратор проверит типы данных входных параметров функций.
  3. Класс Particle: Представляет одну частицу в фильтре частиц.
  4. Класс Map: Хранит карту окружения.
  5. Класс Robot: Представляет робота, который перемещается и собирает данные.
  6. Класс FastSLAM: Основной класс, реализующий алгоритм FastSLAM.

Декораторы

import logging
from typing import Callable
from functools import wraps

logging.basicConfig(level=logging.INFO)

def log_method_call(func: Callable) -> Callable:
    @wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
        result = func(*args, **kwargs)
        logging.info(f"{func.__name__} returned {result}")
        return result
    return wrapper

def type_check(func: Callable) -> Callable:
    @wraps(func)
    def wrapper(*args, **kwargs):
        annotations = func.__annotations__
        for arg_name, arg_type in annotations.items():
            if arg_name in kwargs:
                assert isinstance(kwargs[arg_name], arg_type), f"Argument {arg_name} is not of type {arg_type}"
            else:
                idx = list(func.__code__.co_varnames).index(arg_name)
                assert isinstance(args[idx], arg_type), f"Argument {arg_name} is not of type {arg_type}"
        return func(*args, **kwargs)
    return wrapper

Классы

import numpy as np

class Particle:
    def __init__(self, pose, weight=1.0):
        self.pose = pose  # [x, y, theta]
        self.weight = weight
        self.map = {}

    @log_method_call
    def update_pose(self, new_pose):
        self.pose = new_pose

    @log_method_call
    def update_weight(self, new_weight):
        self.weight = new_weight

    @log_method_call
    def add_landmark(self, landmark_id, position):
        self.map[landmark_id] = position

class Map:
    def __init__(self):
        self.landmarks = {}

    @log_method_call
    def add_landmark(self, landmark_id, position):
        self.landmarks[landmark_id] = position

    @log_method_call
    def get_landmark(self, landmark_id):
        return self.landmarks.get(landmark_id, None)

class Robot:
    def __init__(self, initial_pose):
        self.pose = initial_pose  # [x, y, theta]

    @log_method_call
    def move(self, delta_pose):
        dx, dy, dtheta = delta_pose
        self.pose[0] += dx
        self.pose[1] += dy
        self.pose[2] += dtheta

    @log_method_call
    def sense(self):
        # This is a stub for sensing landmarks.
        # In practice, this would use actual sensor data.
        return [(1, [2.0, 3.0]), (2, [1.0, 1.5])]

class FastSLAM:
    def __init__(self, num_particles, initial_pose):
        self.particles = [Particle(initial_pose) for _ in range(num_particles)]
        self.robot = Robot(initial_pose)

    @log_method_call
    def move_robot(self, delta_pose):
        self.robot.move(delta_pose)
        for particle in self.particles:
            particle.update_pose(self.robot.pose)

    @log_method_call
    def update(self, measurements):
        for particle in self.particles:
            for landmark_id, position in measurements:
                particle.add_landmark(landmark_id, position)
                particle.update_weight(1.0)  # Simplified weight update
        self.resample_particles()

    @log_method_call
    def resample_particles(self):
        weights = [particle.weight for particle in self.particles]
        new_particles = np.random.choice(self.particles, len(self.particles), p=weights/np.sum(weights))
        self.particles = [Particle(particle.pose) for particle in new_particles]

# Example usage
initial_pose = [0, 0, 0]
fast_slam = FastSLAM(num_particles=10, initial_pose=initial_pose)

# Simulate movement
fast_slam.move_robot([1, 0, 0.1])

# Simulate sensing
measurements = fast_slam.robot.sense()
fast_slam.update(measurements)

Объяснение

  1. Декораторы:
  2. log_method_call: Логгирует вызовы методов и их результаты.
  3. type_check: Проверяет типы аргументов функций.

  4. Классы:

  5. Particle: Представляет частицу с состоянием робота и картой.
  6. Map: Представляет карту с методами для добавления и получения ориентиров.
  7. Robot: Управляет движением робота и получением данных от сенсоров.
  8. FastSLAM: Основной класс, управляющий частицами и выполняющий алгоритм SLAM.

  9. Использование:

  10. Создаем объект FastSLAM с начальными параметрами.
  11. Симулируем движение робота и обновляем состояние частиц.
  12. Симулируем получение данных от сенсоров и обновляем карты частиц.

Этот код представляет упрощенную версию алгоритма FastSLAM, демонстрируя основные принципы его работы с использованием декораторов для логгирования и проверки типов данных.