Навигация мобильного робота для сбора клубники
В предыдущих материалах я рассказывал о разработке робототехнических систем и отдельных узлов управления. В этот раз задача — разработка мобильной платформы для робота-сборщика клубники.
Предполагается, что такой робот перемещается вдоль рядов растений, останавливается в рабочих позициях для сбора ягод и должен уметь реагировать на препятствия, появляющиеся на пути.
Полноценный робот-сборщик — достаточно большая система. В неё должны входить как минимум:
мобильная платформа +
система навигации +
лидар / датчики +
техническое зрение +
поиск спелой клубники +
манипулятор +
захват ягоды
Поэтому разработку я решил разбить на отдельные модули.
В этой статье рассматривается навигационная часть: робот должен самостоятельно проехать из начальной точки к заданной позиции, а при появлении препятствия определить занятую область и построить другой маршрут.
Для разработки используются ROS 2, Python и Webots.
Постановка задачи
На первом этапе движение робота по теплице сильно упрощено.
Рабочая область представлена сеткой 5×5:
20 21 22 23 24
15 16 17 18 19
10 11 12 13 14 5 6 7 8 9 0 1 2 3 4
Каждый номер соответствует возможной позиции мобильной платформы.
Такое представление пока не является полноценной картой теплицы. Оно нужно для отработки основных алгоритмов:
где находится робот? ↓
куда ему нужно приехать? ↓
какой маршрут короче? ↓
как выполнить этот маршрут? ↓
что делать, если дорога перекрыта?
Например:
START = 0
TARGET = 22
Робот должен построить маршрут от 0 до 22, выполнить его и остановиться в целевой позиции.
После этого в симуляции появляется препятствие.
Это может имитировать ситуацию, характерную для реальной эксплуатации в теплице: проход частично перекрыт ящиком, оборудованием или другим объектом.
Робот получает данные лидара, определяет занятую область и при возвращении уже не должен использовать её.
В результате эксперимент выглядит следующим образом:
старт ↓
построение маршрута ↓
движение по теплице ↓
рабочая позиция ↓
обнаружение нового препятствия ↓
изменение доступной карты ↓
перепланирование ↓
возвращение другим маршрутом
Что реализовано
Программа разделена на четыре основных компонента:
Graph
│
├── карта поля
├── координаты маркеров
└── соседние позиции
Planner
│
├── BFS
├── кратчайший маршрут
└── минимизация количества поворотов
Motion
│
├── Odometry
├── управление скоростью
└── движение между маркерами
ObstacleDetector
│
├── LaserScan
├── сравнение сканов
└── определение занятой клетки
Главный модуль связывает эти части в единый сценарий.
[Далее основная часть статьи: описание Graph, BFS, минимизации поворотов, Motion, Odometry, Twist, LaserScan и определения препятствия.]
Как это связано со сбором клубники
На данном этапе робот ещё не собирает ягоды.
Сейчас решается задача нижнего уровня — мобильная платформа должна надёжно доставлять роботизированную систему к рабочим позициям.
В дальнейшем архитектура может выглядеть следующим образом:
РОБОТ-СБОРЩИК │ ┌──────────────┼──────────────┐ │ │ │ ↓ ↓ ↓ Навигация Тех. зрение Манипулятор │ │ │ ↓ ↓ ↓ куда ехать где клубника как сорвать
Навигационный модуль отвечает за перемещение платформы.
После прибытия в очередную рабочую позицию управление сможет передаваться системе технического зрения.
Камера определяет ягоды:
изображение ↓
поиск клубники ↓
определение спелости ↓
координаты ягоды
После этого координаты передаются манипулятору:
координаты ягоды ↓
планирование движения ↓
подвод захвата ↓
срыв ↓
укладка ягоды
После завершения сбора в данной позиции мобильная платформа переезжает дальше.
То есть в перспективе полный рабочий цикл должен выглядеть так:
построить маршрут ↓
приехать к растениям ↓
остановиться ↓
найти спелые ягоды ↓
передать координаты манипулятору ↓
собрать ягоды ↓
проверить проход ↓
перейти к следующей позиции
Текущая работа закрывает первую часть этой цепочки.
Результат
На текущем этапе реализован прототип навигационного модуля робота-сборщика клубники.
Робот умеет:
работать с дискретной картой 5×5;
определять соседние доступные позиции;
искать кратчайший маршрут;
при одинаковой длине маршрутов выбирать вариант с меньшим количеством поворотов;
двигаться по маршруту с использованием одометрии;
управляться через
Twist;получать данные 2D-лидара;
обнаруживать появление нового препятствия;
определять занятую клетку;
исключать её из дальнейшего планирования;
строить новый маршрут.
При этом алгоритмы навигации специально реализованы самостоятельно, без использования готового Nav2. Для первого этапа это позволяет разобраться в базовой механике планирования и управления движением.
Исходный код
Весь пример разделён на несколько Python-файлов:
module_b_navigation/
│
├── __init__.py
├── main.py
├── graph.py
├── planner.py
├── motion_controller.py
└── obstacle_detector.py
graph.py содержит модель поля 5×5, координаты 25 маркеров и получение доступных соседних клеток.
planner.py содержит BFS-планировщик. Сначала выбирается минимальная длина маршрута, затем минимальное количество поворотов.
motion_controller.py отвечает за фактическое движение мобильной платформы. Положение получается из:
/RMC2/odometry
а команды отправляются в:
/RMC2/cmd_vel
obstacle_detector.py получает:
/RMC2/scan
сохраняет исходный LaserScan, сравнивает его с новым сканом и определяет, возле какого маркера появилось препятствие.
main.py объединяет всё в один эксперимент:
построение маршрута
→ движение
→ сохранение лидара
→ появление препятствия
→ определение занятого маркера
→ перепланирование
→ возвращение
Запуск
Стартовый и конечный маркеры передаются параметрами ROS 2.
Например:
ros2 run <package_name> <executable_name> --ros-args \ -p start_id:=0 \ -p target_id:=22
После запуска робот строит маршрут и начинает движение.
В целевой точке программа предложит установить препятствие в Webots:
РОБОТ ОСТАНОВИЛСЯ В ЦЕЛЕВОЙ ТОЧКЕ
Поставьте ОДНО препятствие в Webots.
После этого введите y и нажмите Enter.
Препятствие установлено? [y]:
После ввода:
y
получается новый скан лидара, определяется занятый маркер и строится обратный маршрут.
Репозиторий
Полный исходный код проекта я выложил на GitHub:
[ЗДЕСЬ ССЫЛКА НА РЕПОЗИТОРИЙ]
В репозитории находятся исходники навигационного модуля и инструкции по запуску проекта в ROS 2/Webots.
Что дальше
Следующий этап — приблизить модель к реальной задаче сбора клубники.
В первую очередь я планирую добавить анализ лидара не после остановки, а непосредственно во время движения:
робот движется ↓
постоянно работает lidar ↓
обнаружено препятствие? ↙ ↘ нет да ↓ ↓
едем дальше остановка ↓ обновление карты ↓ новый маршрут
После этого навигационный модуль можно объединять с системой технического зрения и манипулятором.
Отдельной интересной задачей станет уже непосредственно зрение для клубники: определение ягоды, оценка её спелости и получение пространственных координат для манипулятора.
В итоге хотелось бы получить полный экспериментальный цикл:
НАВИГАЦИЯ ↓
робот приехал к кусту ↓
ТЕХНИЧЕСКОЕ ЗРЕНИЕ ↓
найдена спелая клубника ↓
МАНИПУЛЯТОР ↓
ягода собрана ↓
НАВИГАЦИЯ ↓
следующая рабочая позиция
Таким образом, текущая система навигации — это не отдельный учебный пример, а один из базовых модулей будущего мобильного робота для автоматизированного сбора клубники.
Полный код примера
Ниже приведу полный код навигационной части прототипа робота для сбора клубники.
Проект разделён на несколько небольших модулей:
module_b_navigation/
│
├── __init__.py
├── main.py
├── graph.py
├── planner.py
├── motion_controller.py
└── obstacle_detector.py
Такое разделение позволяет независимо изменять представление карты, алгоритм поиска пути, управление мобильной платформой и обработку лидара.
graph.py
Первый модуль описывает наше условное поле теплицы.
import math
class Graph: """ Простая карта поля 5x5. Нумерация маркеров: 20 21 22 23 24 15 16 17 18 19 10 11 12 13 14 5 6 7 8 9 0 1 2 3 4 Расстояние между соседними маркерами принимаем равным 1 метру. """ SIZE = 5 def valid(self, marker): """Проверяет, существует ли такой маркер.""" return 0 <= marker < 25 def position(self, marker): """ Возвращает координаты маркера. Например: marker = 12 x = 12 % 5 = 2 y = 12 // 5 = 2 Значит маркер 12 находится в точке (2, 2). """ x = marker % self.SIZE y = marker // self.SIZE return float(x), float(y) def neighbors(self, marker, blocked=None): """ Возвращает соседние клетки. Порядок: вправо -> вверх -> влево -> вниз. """ if blocked is None: blocked = set() row = marker // self.SIZE col = marker % self.SIZE result = [] # Вправо if col < self.SIZE - 1: result.append(marker + 1) # Вверх if row < self.SIZE - 1: result.append(marker + self.SIZE) # Влево if col > 0: result.append(marker - 1) # Вниз if row > 0: result.append(marker - self.SIZE) # Убираем занятые клетки return [m for m in result if m not in blocked] def direction(self, a, b): """ Возвращает направление движения между двумя соседними маркерами. Нужно только для подсчёта поворотов. """ ax, ay = self.position(a) bx, by = self.position(b) if bx > ax: return "RIGHT" if bx < ax: return "LEFT" if by > ay: return "UP" if by < ay: return "DOWN" def nearest_marker( self, x, y, max_distance=0.75, excluded=None ): """ По координатам точки ищет ближайший маркер. Это используется для лидара: лидар дал координату препятствия -> определяем номер клетки. """ if excluded is None: excluded = set() best_marker = None best_distance = 999999.0 for marker in range(25): if marker in excluded: continue mx, my = self.position(marker) distance = math.hypot( x - mx, y - my ) if distance < best_distance: best_distance = distance best_marker = marker if best_distance > max_distance: return None return best_marker
planner.py
Следующий модуль занимается поиском маршрута.
Здесь используется BFS. Но если найдено несколько одинаково коротких маршрутов, выбирается маршрут с меньшим количеством поворотов.
from collections import deque
class Planner: """ Строит маршрут по клеткам. Критерии: 1. Сначала ищем самый короткий маршрут. 2. Если коротких маршрутов несколько, выбираем маршрут с меньшим количеством поворотов. 3. Если и это одинаково, выбираем маршрут с меньшими ID маркеров. """ def __init__(self, graph): self.graph = graph def count_turns(self, route): """Считает количество поворотов в маршруте.""" if len(route) < 3: return 0 turns = 0 old_direction = self.graph.direction( route[0], route[1] ) for i in range(1, len(route) - 1): new_direction = self.graph.direction( route[i], route[i + 1] ) if new_direction != old_direction: turns += 1 old_direction = new_direction return turns def plan(self, start, target, blocked=None): """ Возвращает: route, distance, turns Например: [0, 1, 2, 7, 12, 17, 22], 6, 1 """ if blocked is None: blocked = set() if not self.graph.valid(start): raise ValueError( "Неверный стартовый маркер" ) if not self.graph.valid(target): raise ValueError( "Неверный целевой маркер" ) if start in blocked: raise ValueError( "Стартовая клетка занята" ) if target in blocked: raise ValueError( "Целевая клетка занята" ) if start == target: return [start], 0, 0 # В очереди лежат целые маршруты. queue = deque() queue.append([start]) # Здесь будем хранить все самые короткие маршруты до цели. found_routes = [] shortest_length = None while queue: route = queue.popleft() # Если уже нашли более короткий маршрут, # этот маршрут продолжать бессмысленно. if shortest_length is not None: if len(route) > shortest_length: break current = route[-1] if current == target: shortest_length = len(route) found_routes.append(route) continue for next_marker in self.graph.neighbors( current, blocked ): # Не заходим второй раз в клетку, # которая уже есть в этом маршруте. if next_marker in route: continue new_route = route + [next_marker] queue.append(new_route) if not found_routes: raise RuntimeError( "Маршрут не найден" ) # Из всех кратчайших маршрутов выбираем: # сначала по количеству поворотов, # потом по самим ID маркеров. best_route = min( found_routes, key=lambda route: ( self.count_turns(route), route ) ) distance = len(best_route) - 1 turns = self.count_turns(best_route) return best_route, distance, turns
motion_controller.py
Теперь нужен контроллер, который заставит мобильную платформу физически выполнить полученный маршрут в Webots.
Он получает положение из одометрии и отправляет команды скорости через Twist.
import math
from geometry_msgs.msg import Twist
from nav_msgs.msg import Odometry
def angle_to_pi(angle): """ Приводит угол к диапазону от -pi до +pi. """ while angle > math.pi: angle -= 2 * math.pi while angle < -math.pi: angle += 2 * math.pi return angle
def quaternion_to_yaw(q): """ ROS хранит ориентацию как quaternion. Для движения по плоскости нам нужен только угол вокруг Z (yaw). """ sin_yaw = 2 * ( q.w * q.z + q.x * q.y ) cos_yaw = 1 - 2 * ( q.y * q.y + q.z * q.z ) return math.atan2( sin_yaw, cos_yaw )
class Motion: """ Простое движение робота по готовому маршруту. Класс: - получает odometry; - знает положение робота; - поворачивает робота к следующему маркеру; - едет к нему; - после достижения переходит к следующему. """ def __init__(self, node, graph): self.node = node self.graph = graph # Команды скорости робота self.publisher = node.create_publisher( Twist, "/RMC2/cmd_vel", 10 ) # Положение робота node.create_subscription( Odometry, "/RMC2/odometry", self.odom_callback, 10 ) # Управление вызывается каждые 0.05 секунды node.create_timer( 0.05, self.control ) self.x = None self.y = None self.yaw = None self.route = [] self.index = 0 # Пока active == True, # робот выполняет маршрут. self.active = False # Скорости self.linear_speed = 0.20 self.angular_speed = 0.60 # Допуски self.distance_tolerance = 0.12 self.angle_tolerance = math.radians(5) def odom_callback(self, msg): """Получаем текущее положение робота.""" self.x = msg.pose.pose.position.x self.y = msg.pose.pose.position.y self.yaw = quaternion_to_yaw( msg.pose.pose.orientation ) def start(self, route): """Запускает движение по маршруту.""" self.route = route # route[0] — клетка, # где робот уже находится. # Поэтому начинаем с route[1]. self.index = 1 if len(route) < 2: self.active = False return self.active = True self.node.get_logger().info( f"movement_start route={route}" ) def stop(self): """Посылает нулевую скорость.""" self.publisher.publish( Twist() ) def finish(self): """Останавливает робот и завершает маршрут.""" self.stop() self.active = False self.node.get_logger().info( "movement_stop" ) def control(self): """ Основной алгоритм движения. 1. Берём следующий маркер. 2. Вычисляем, где он находится. 3. Если робот смотрит не туда — поворачиваемся. 4. Если смотрит туда — едем прямо. 5. Если приехали — берём следующий маркер. """ if not self.active: return # Ждём первую odometry. if self.x is None: return # Если маршрут закончился. if self.index >= len(self.route): self.finish() return target = self.route[self.index] target_x, target_y = self.graph.position( target ) dx = target_x - self.x dy = target_y - self.y distance = math.hypot( dx, dy ) # Угол, куда нужно смотреть. target_yaw = math.atan2( dy, dx ) # Ошибка по углу. error = angle_to_pi( target_yaw - self.yaw ) # Уже приехали к маркеру. if distance < self.distance_tolerance: self.node.get_logger().info( f"marker_reached: {target}" ) self.stop() self.index += 1 if self.index >= len(self.route): self.finish() return command = Twist() # Сначала поворачиваемся. if abs(error) > self.angle_tolerance: command.linear.x = 0.0 if error > 0: command.angular.z = ( self.angular_speed ) else: command.angular.z = ( -self.angular_speed ) # Потом едем прямо. else: command.linear.x = self.linear_speed # Небольшая коррекция курса. command.angular.z = 1.2 * error # Ограничиваем скорость поворота. if command.angular.z > 0.30: command.angular.z = 0.30 if command.angular.z < -0.30: command.angular.z = -0.30 self.publisher.publish( command )
obstacle_detector.py
Этот модуль отвечает за лидар.
Перед появлением препятствия сохраняется исходный скан. После установки объекта новый скан сравнивается с исходным.
import math
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import LaserScan
class ObstacleDetector: """ Ищет ОДНО новое препятствие. Идея: 1. До появления препятствия запоминаем lidar. 2. Пользователь ставит препятствие. 3. Получаем новый lidar. 4. Ищем лучи, у которых расстояние сильно уменьшилось. 5. Переводим эти точки в координаты мира. 6. Смотрим, возле какого маркера больше всего таких точек. """ def __init__(self, node, graph): self.node = node self.graph = graph # Последнее сообщение лидара. self.scan = None # Расстояния до появления препятствия. self.baseline = None node.create_subscription( LaserScan, "/RMC2/scan", self.scan_callback, qos_profile_sensor_data ) # Если расстояние уменьшилось минимум на 20 см, # считаем, что появился новый объект. self.min_change = 0.20 # Насколько далеко точка может быть # от центра клетки. self.max_marker_distance = 0.75 # Минимум lidar-точек, # чтобы поверить результату. self.min_votes = 3 def scan_callback(self, msg): """Просто сохраняем самый свежий scan.""" self.scan = msg def save_baseline(self): """ Запоминаем lidar до появления препятствия. """ if self.scan is None: return False self.baseline = list( self.scan.ranges ) return True def find_obstacle( self, robot_x, robot_y, robot_yaw, excluded=None ): """ Возвращает номер занятой клетки. Если препятствие не найдено — возвращает None. """ if excluded is None: excluded = set() if self.scan is None: return None if self.baseline is None: return None if len(self.baseline) != len( self.scan.ranges ): return None # votes: # ключ = номер маркера # значение = сколько lidar-точек # попало возле него votes = {} # Для лога сохраняем координаты точек. points = {} for i in range( len(self.scan.ranges) ): old_range = self.baseline[i] new_range = self.scan.ranges[i] # Текущая дальность должна быть нормальной. if not math.isfinite(new_range): continue if new_range < self.scan.range_min: continue if new_range > self.scan.range_max: continue # Проверяем, появился ли здесь новый объект. new_object = False # Раньше лидар ничего не видел, # а теперь увидел объект. if not math.isfinite(old_range): new_object = True # Или объект стал минимум # на 20 см ближе. elif ( old_range - new_range >= self.min_change ): new_object = True if not new_object: continue # Угол конкретного луча лидара. angle = ( self.scan.angle_min + i * self.scan.angle_increment ) # Координаты относительно робота. local_x = ( new_range * math.cos(angle) ) local_y = ( new_range * math.sin(angle) ) # Переводим точку # в мировые координаты. world_x = robot_x + ( local_x * math.cos(robot_yaw) - local_y * math.sin(robot_yaw) ) world_y = robot_y + ( local_x * math.sin(robot_yaw) + local_y * math.cos(robot_yaw) ) # Определяем ближайший маркер. marker = self.graph.nearest_marker( world_x, world_y, self.max_marker_distance, excluded ) if marker is None: continue # Голосуем за этот маркер. votes[marker] = ( votes.get(marker, 0) + 1 ) if marker not in points: points[marker] = [] points[marker].append( (world_x, world_y) ) if not votes: return None # Маркер с максимальным количеством голосов. best_marker = max( votes, key=votes.get ) self.node.get_logger().info( f"obstacle_votes=" f"{dict(sorted(votes.items()))}" ) if votes[best_marker] < self.min_votes: return None # Средняя координата lidar-точек, # проголосовавших за победивший маркер. best_points = points[best_marker] avg_x = ( sum(p[0] for p in best_points) / len(best_points) ) avg_y = ( sum(p[1] for p in best_points) / len(best_points) ) self.node.get_logger().info( f"obstacle_world_position≈" f"({avg_x:.3f}, {avg_y:.3f})" ) return best_marker
main.py
И наконец главный файл.
Он объединяет планировщик, контроллер движения и лидар в один эксперимент.
import rclpy
from rclpy.node import Node
from module_b_navigation.graph import Graph
from module_b_navigation.planner import Planner
from module_b_navigation.motion_controller import Motion
from module_b_navigation.obstacle_detector import ( ObstacleDetector
)
def wait_robot(node, motion): """ Пока робот едет, даём ROS обрабатывать сообщения. """ while motion.active and rclpy.ok(): rclpy.spin_once( node, timeout_sec=0.1 )
def wait_for_new_scan(node): """ После установки препятствия несколько раз обрабатываем ROS-сообщения, чтобы получить свежий lidar scan. """ for i in range(10): rclpy.spin_once( node, timeout_sec=0.1 )
def print_route( title, start, target, blocked, route, distance, turns
): """Печатает найденный маршрут.""" print() print("========================================") print(title) print("========================================") print("Start marker :", start) print("Target marker:", target) print("Blocked :", sorted(blocked)) print() print("ROUTE :", route) print("DISTANCE :", distance) print("TURNS :", turns) print("========================================") print()
def main(args=None): # Запускаем ROS. rclpy.init(args=args) node = Node( "module_b_navigation" ) # Параметры запуска. node.declare_parameter( "start_id", 0 ) node.declare_parameter( "target_id", 22 ) start = node.get_parameter( "start_id" ).value target = node.get_parameter( "target_id" ).value # Создаём основные части программы. graph = Graph() planner = Planner( graph ) motion = Motion( node, graph ) detector = ObstacleDetector( node, graph ) try: # ============================================== # 1. СТРОИМ МАРШРУТ ТУДА # ============================================== blocked = set() node.get_logger().info( "OUTBOUND_PLANNING_START" ) route, distance, turns = planner.plan( start, target, blocked ) print_route( "OUTBOUND ROUTE", start, target, blocked, route, distance, turns ) # ============================================== # 2. ЕДЕМ К ЦЕЛИ # ============================================== node.get_logger().info( "OUTBOUND_MOVEMENT_START" ) motion.start( route ) wait_robot( node, motion ) node.get_logger().info( "OUTBOUND_MOVEMENT_STOP" ) # ============================================== # 3. ЗАПОМИНАЕМ ЛИДАР # ============================================== if not detector.save_baseline(): node.get_logger().error( "BASELINE_SCAN_NOT_AVAILABLE" ) return node.get_logger().info( "BASELINE_SCAN_CAPTURED" ) # ============================================== # 4. ПОЛЬЗОВАТЕЛЬ СТАВИТ ПРЕПЯТСТВИЕ # ============================================== print() print("========================================") print("РОБОТ ОСТАНОВИЛСЯ В ЦЕЛЕВОЙ ТОЧКЕ") print("========================================") print( "Поставьте ОДНО препятствие в Webots." ) print( "После этого введите y и нажмите Enter." ) print("========================================") while True: answer = input( "Препятствие установлено? [y]: " ) if answer.lower().strip() == "y": break # ============================================== # 5. ПОЛУЧАЕМ СВЕЖИЙ ЛИДАР # ============================================== wait_for_new_scan( node ) # ============================================== # 6. ИЩЕМ ЗАНЯТУЮ КЛЕТКУ # ============================================== node.get_logger().info( "OBSTACLE_SCAN_START" ) blocked_marker = detector.find_obstacle( motion.x, motion.y, motion.yaw, excluded={ start, target } ) if blocked_marker is None: print() print( "Препятствие не найдено." ) print() return print() print("========================================") print( "OBSTACLE DETECTED ON MARKER", blocked_marker ) print("========================================") print() # Эта клетка теперь запрещена. blocked = { blocked_marker } # ============================================== # 7. СТРОИМ ОБРАТНЫЙ МАРШРУТ # ============================================== node.get_logger().info( "RETURN_PLANNING_START" ) return_route, distance, turns = ( planner.plan( target, start, blocked ) ) print_route( "RETURN ROUTE", target, start, blocked, return_route, distance, turns ) # ============================================== # 8. ЕДЕМ ОБРАТНО # ============================================== node.get_logger().info( "RETURN_MOVEMENT_START" ) motion.start( return_route ) wait_robot( node, motion ) node.get_logger().info( "RETURN_MOVEMENT_STOP" ) node.get_logger().info( "MODULE_B_DONE" ) print() print("========================================") print("MODULE B COMPLETE") print( "Robot returned to the start marker." ) print("========================================") print() except KeyboardInterrupt: pass finally: # На всякий случай # всегда останавливаем робот. motion.stop() node.destroy_node() rclpy.shutdown()
if __name__ == "__main__": main()
Что происходит после запуска
Весь пример можно свести к следующей последовательности:
робот для сбора клубники ↓ START = 0 ↓ BFS ↓ маршрут к растениям ↓ движение ↓ TARGET = 22 ↓ сохранение LaserScan ↓ появление препятствия ↓ сравнение двух сканов ↓
определение занятой клетки ↓ blocked = {marker} ↓ повторный BFS ↓ обход препятствия ↓
возвращение к START
Конечно, это пока только навигационная часть будущего робота-сборщика. Здесь ещё нет распознавания ягод, определения спелости и управления манипулятором.
Но уже на этом этапе отдельно отрабатываются планирование маршрута, управление мобильной платформой, работа с одометрией и лидаром, обнаружение препятствия и перепланирование.
Следующим этапом эту систему можно связать с техническим зрением и манипулятором:
НАВИГАЦИЯ ↓
приехали к растениям ↓
ТЕХНИЧЕСКОЕ ЗРЕНИЕ ↓
нашли спелую клубнику ↓
получили координаты ягоды ↓
МАНИПУЛЯТОР ↓
сорвали ягоду ↓
НАВИГАЦИЯ ↓
следующая рабочая позиция
Таким образом, приведённый пример является первым самостоятельным модулем более крупного проекта — мобильного роботизированного комплекса для автоматизированного сбора клубники.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.