3D-симуляция и управление движением с помощью PyBullet
Делиться

Вступление
PyBullet — это платформа моделирования с открытым исходным кодом, созданная Facebook и предназначенная для обучения физических агентов (например, роботов) в трёхмерной среде. Она предоставляет физический движок для моделирования как твёрдых, так и мягких тел. Она широко используется в робототехнике, искусственном интеллекте и разработке игр.
Роботизированные манипуляторы пользуются большой популярностью благодаря своей скорости, точности и способности работать в опасных условиях. Они используются для таких задач, как сварка, сборка и погрузка-разгрузка материалов, особенно в промышленных условиях (например, на производстве).
Робот может выполнить задачу двумя способами:
- Ручное управление — требует явного программирования каждого действия человеком. Лучше подходит для фиксированных задач, но имеет проблемы с неопределенностью и требует утомительной настройки параметров для каждого нового сценария.
- Искусственный интеллект позволяет роботу методом проб и ошибок изучать оптимальные действия для достижения цели. Таким образом, он может адаптироваться к изменяющимся условиям, обучаясь на основе поощрений и штрафов без заранее заданного плана (обучение с подкреплением).
В этой статье я покажу, как создать трёхмерную среду с помощью PyBullet для ручного управления роботизированной рукой . Я представлю несколько полезных фрагментов кода на Python, которые можно легко применить в других подобных случаях (просто скопируйте, вставьте и запустите), и прокомментирую каждую строку кода, чтобы вы могли повторить этот пример.
Настраивать
PyBullet можно легко установить с помощью одной из следующих команд (если pip не работает, conda определенно должен сработать):
pip install pybullet conda install -c conda-forge pybullet
PyBullet поставляется с набором предустановленных файлов URDF (Unified Robot Description Format), которые представляют собой XML-схемы, описывающие физическую структуру объектов в трехмерной среде (например, куба, сферы, робота).
импорт pybullet как p импорт pybullet_data время импорта ## настройка p.connect(p.GUI) p.resetSimulation() p.setGravity(gravX=0, gravY=0, gravZ=-9.8) p.setAdditionalSearchPath(path=pybullet_data.getDataPath()) ## загрузка URDF plane = p.loadURDF(«plane.urdf») cube = p.loadURDF(«cube.urdf», basePosition=[0,0,0.1], globalScaling=0.5, useFixedBase=True) obj = p.loadURDF(«cube_small.urdf», basePosition=[1,1,0.1], globalScaling=1.5) p.changeVisualShape(objectUniqueId=obj, linkIndex=-1, rgbaColor=[1,0,0,1]) #красный пока p.isConnected(): p.setRealTimeSimulation(True)

Давайте рассмотрим код выше:
- когда вы сможете подключиться к физическому движку, вам необходимо указать, хотите ли вы открыть графический интерфейс (p.GUI) или нет (p.DIRECT).
- Декартово пространство имеет 3 измерения : ось x (вперед/назад), ось y (влево/вправо), ось z (вверх/вниз).
- Обычно для имитации гравитации Земли устанавливают значение (x=0, y=0, z=-9,8), но его можно изменить в зависимости от цели моделирования.
- Обычно для создания пола требуется импортировать плоскость , иначе объекты будут падать бесконечно. Если вы хотите, чтобы объект был закреплён на полу, установите useFixedBase=True (по умолчанию False). Я импортировал объекты с basePosition=[0,0,0.1], что означает, что они находятся на высоте 10 см над землёй.
- Симуляция может быть визуализирована в реальном времени с помощью p.setRealTimeSimulation(True) или быстрее (с использованием процессорного времени) с помощью p.stepSimulation(). Другой способ установить режим реального времени:
import time for _ in range(240): #240 временной шаг, обычно используемый при разработке видеоигр p.stepSimulation() #добавление шага физики (скорость ЦП = 0,1 секунды) time.sleep(1/240) #замедление до реального времени (240 шагов × 1/240 секунды sleep = 1 секунда)
Робот
Сейчас наше трёхмерное окружение состоит из маленького объекта (маленького красного кубика) и стола (большого куба), закреплённого на полу (плоскости). Я добавлю роботизированную руку , которая будет поднимать меньший объект и класть его на стол.
В PyBullet по умолчанию используется роботизированная рука, созданная по образцу робота Franka Panda.
robo = p.loadURDF(«franka_panda/panda.urdf», basePosition=[1,0,0.1], useFixedBase=True)

Первое, что нужно сделать, это проанализировать связи (твердые части) и сочленения (соединения между двумя жёсткими частями) робота. Для этого можно просто использовать p.DIRECT, так как графический интерфейс не нужен.
import pybullet as p import pybullet_data ## настройка p.connect(p.DIRECT) p.resetSimulation() p.setGravity(gravX=0, gravY=0, gravZ=-9.8) p.setAdditionalSearchPath(path=pybullet_data.getDataPath()) ## загрузка URDF robo = p.loadURDF(«franka_panda/panda.urdf», basePosition=[1,0,0.1], useFixedBase=True) ## сочленения dic_info = { 0:»индекс сочленения», #начинается с 0 1:»имя сочленения», 2:»тип сочленения», #0=вращательное (вращательное), 1=призматическое (скользящее), 4=фиксированное 3:»индекс вектора состояния», 4:»индекс вектора скорости», 5:»флаги», #nvm всегда 0 6:»демпфирование сочленения», 7:»трение сочленения», #коэффициент 8:»нижнийпредел сочленения», #минимальный угол 9:»верхнийпредел сочленения», #максимальный угол 10:»макссила сочленения», #максимально допустимая сила 11:»максскорость сочленения», #максимальная скорость 12:»имя ссылки», #дочерняя ссылка, подключенная к этому сочленению 13:»ось сочленения», 14:»положение родительского кадра», #позиция 15:»положение родительского кадра», #ориентация 16:»индекс родительского кадра» #−1 = основание } for i in range(p.getNumJoints(bodyUniqueId=robo)): joint_info = p.getJointInfo(bodyUniqueId=robo, jointIndex=i) print(f»— СОЕДИНЕНИЕ {i} —«) print({dic_info[k]:joint_info[k] for k in dic_info.keys()}) ## links for i in range(p.getNumJoints(robo)): link_name = p.getJointInfo(robo, i)[12].decode('utf-8') #field 12=»Имя ссылки» dyn = p.getDynamicsInfo(robo, i) pos, orn, *_ = p.getLinkState(robo, i) dic_info = {«Масса»:dyn[0], «Трение»:dyn[1], «Положение»:pos, «Ориентация»:orn} print(f»— LINK {i}: {link_name} —«) print(dic_info)

У каждого робота есть « база » — корневая часть его тела, которая соединяет всё (например, позвоночник нашего скелета). Судя по результату кода выше, мы знаем, что у роботизированной руки 12 сочленений и 12 звеньев. Они соединены следующим образом:

Управление движением
Чтобы заставить робота что-то сделать, нужно двигать его суставы. Существует три основных типа управления, каждый из которых основан на законах движения Ньютона:
- Управление положением : по сути, вы говорите роботу «иди сюда». Технически, вы задаёте целевое положение , а затем прикладываете силу, чтобы постепенно перемещать сочленение из текущего положения к цели. По мере приближения к цели прилагаемая сила уменьшается, чтобы избежать перекоса, и в конечном итоге идеально уравновешивает силы сопротивления (например, трение и гравитацию), удерживая сочленение на месте.
- Управление скоростью : по сути, вы говорите роботу: «Двигайся с этой скоростью». Технически, вы задаёте целевую скорость и прикладываете силу, чтобы постепенно довести её от нуля до целевой, а затем поддерживаете её, уравновешивая приложенную силу и силы сопротивления (например, трение, гравитацию).
- Управление силой/крутящим моментом : по сути, вы «толкаете робота и смотрите, что происходит». Технически, вы непосредственно задаёте силу, прикладываемую к сочленению, а результирующее движение зависит исключительно от массы объекта, его инерции и окружающей среды. Кстати, в физике слово « сила » используется для обозначения линейного движения (толкание/тяга), а « крутящий момент » — для обозначения вращательного движения (поворот/скручивание).
Мы можем использовать функцию p.setJointMotorControl2() для перемещения одного сочленения и функцию p.setJointMotorControlArray() для приложения силы к нескольким сочленениям одновременно. Например, я буду управлять положением, задавая случайную цель для каждого сочленения руки.
## настройка p.connect(p.GUI) p.resetSimulation() p.setGravity(gravX=0, gravY=0, gravZ=-9.8) p.setAdditionalSearchPath(path=pybullet_data.getDataPath()) ## загрузка URDF plane = p.loadURDF(«plane.urdf») cube = p.loadURDF(«cube.urdf», basePosition=[0,0,0.1], globalScaling=0.5, useFixedBase=True) robo = p.loadURDF(«franka_panda/panda.urdf», basePosition=[1,0,0.1], useFixedBase=True) obj = p.loadURDF(«cube_small.urdf», basePosition=[1,1,0.1], globalScaling=1.5) p.changeVisualShape(objectUniqueId=obj, linkIndex=-1, rgbaColor=[1,0,0,1]) #красный ## перемещение рук joints = [0,1,2,3,4,5,6] target_positions = [1,1,1,1,1,1,1] #<--случайные числа p.setJointMotorControlArray(bodyUniqueId=robo, jointIndices=joints, controlMode=p.POSITION_CONTROL, targetPositions=target_positions, forces=[50]*len(joints)) for _ in range(240*3): p.stepSimulation() time.sleep(1/240)
Настоящий вопрос заключается в следующем: « Каково правильное целевое положение для каждого сочленения? ». Ответ кроется в обратной кинематике — математическом процессе расчёта параметров, необходимых для размещения робота в заданном положении и ориентации относительно начальной точки. Каждое сочленение определяет угол, и вам не хочется вручную угадывать углы нескольких сочленений. Поэтому мы попросим PyBullet определить целевые положения в декартовом пространстве с помощью функции p.calculateInverseKinematics().
obj_position, _ = p.getBasePositionAndOrientation(obj) obj_position = list(obj_position) target_positions = p.calculateInverseKinematics( bodyUniqueId=robo, endEffectorLinkIndex=11, #связь захвата цели targetPosition=[obj_position[0], obj_position[1], obj_position[2]+0.25], #25 см выше объекта targetOrientation=p.getQuaternionFromEuler([0,-3.14,0]) #[крен, тангаж, рыскание]=[0,-π,0] -> рука направлена вниз ) arm_joints = [0,1,2,3,4,5,6] p.setJointMotorControlArray(bodyUniqueId=robo, controlMode=p.POSITION_CONTROL, jointIndices=arm_joints, targetPositions=target_positions[:len(arm_joints)], forces=[50]*len(arm_joints))

Обратите внимание, что я использовал функцию p.getQuaternionFromEuler() для преобразования трёхмерных углов (которые легко понять человеку) в четырёхмерные, поскольку кватернионы проще вычислять физическим движкам. Если говорить технически, то в кватернионе (x, y, z, w) первые три компонента описывают ось вращения, а четвёртое измерение кодирует величину поворота (cos/sin).
Приведённый выше код даёт роботу команду переместить руку в определённое положение над объектом, используя инверсную кинематику. Мы получаем координаты маленького красного кубика в мире с помощью p.getBasePositionAndOrientation() и используем эту информацию для расчёта целевого положения сочленений.
Взаимодействие с объектами
Роботизированная рука оснащена кистью («захватом»), поэтому её можно сжимать и разжимать для захвата объектов. Из предыдущего анализа мы знаем, что «пальцы» могут двигаться в диапазоне от 0 до 0,04 секунды. Таким образом, можно задать целевое положение как нижний предел ( кисть сжата ) или верхний предел ( кисть открыта ).

Более того, я хочу убедиться, что рука удерживает маленький красный кубик во время движения. Вы можете использовать функцию p.createConstraint(), чтобы создать временную физическую связь между захватом робота и объектом, чтобы он двигался вместе с рукой робота. В реальном мире захват прикладывал бы силу трения и контакта, сжимая объект и не давая ему упасть. Но, поскольку PyBullet — очень простой симулятор, я просто воспользуюсь этим сокращением.
## закрыть руку p.setJointMotorControl2(bodyUniqueId=robo, controlMode=p.POSITION_CONTROL, jointIndex=9, #finger_joint1 targetPosition=0, #нижний предел для finger_joint1 force=force) p.setJointMotorControl2(bodyUniqueId=robo, controlMode=p.POSITION_CONTROL, jointIndex=10, #finger_joint2 targetPosition=0, #нижний предел для finger_joint2 force=force) ## удерживать объект restriction = p.createConstraint( parentBodyUniqueId=robo, parentLinkIndex=11, childBodyUniqueId=obj, childLinkIndex=-1, jointType=p.JOINT_FIXED, jointAxis=[0,0,0], parentFramePosition=[0,0,0], childFramePosition=[0,0,0,1] )
После этого мы можем переместить руку к столу, используя ту же стратегию, что и раньше: Обратная кинематика -> целевое положение -> управление положением.

Наконец, когда робот достигает целевой позиции в декартовом пространстве, мы можем раскрыть кисть и снять ограничение между объектом и рукой.
## закрыть руку p.setJointMotorControl2(bodyUniqueId=robo, controlMode=p.POSITION_CONTROL, jointIndex=9, #finger_joint1 targetPosition=0.04, #верхний предел для finger_joint1 force=force) p.setJointMotorControl2(bodyUniqueId=robo, controlMode=p.POSITION_CONTROL, jointIndex=10, #finger_joint2 targetPosition=0.04, #верхний предел для finger_joint2 force=force) ## отпустить объект p.removeConstraint(constraint)

Полное ручное управление
В PyBullet необходимо визуализировать симуляцию каждого действия робота. Поэтому удобно использовать функцию utils, которая может ускорить (например, sec=0.1) или замедлить (например, sec=2) реальный промежуток времени (sec=1).
импорт pybullet как p импорт времени def render(sec=1): for _ in range(int(240*sec)): p.stepSimulation() time.sleep(1/240)
Вот полный код для моделирования, которое мы выполнили в этой статье.
импорт pybullet_data ## настройка p.connect(p.GUI) p.resetSimulation() p.setGravity(gravX=0, gravY=0, gravZ=-9.8) p.setAdditionalSearchPath(path=pybullet_data.getDataPath()) plane = p.loadURDF(«plane.urdf») cube = p.loadURDF(«cube.urdf», basePosition=[0,0,0.1], globalScaling=0.5, useFixedBase=True) robo = p.loadURDF(«franka_panda/panda.urdf», basePosition=[1,0,0.1], globalScaling=1.3, useFixedBase=True) obj = p.loadURDF(«cube_small.urdf», basePosition=[1,1,0.1], globalScaling=1.5) p.changeVisualShape(objectUniqueId=obj, linkIndex=-1, rgbaColor=[1,0,0,1]) render(0.1) force = 100 ## открытая ладонь print(«### открытая ладонь ###») p.setJointMotorControl2(bodyUniqueId=robo, controlMode=p.POSITION_CONTROL, jointIndex=9, #finger_joint1 targetPosition=0.04, #верхний предел для finger_joint1 force=force) p.setJointMotorControl2(bodyUniqueId=robo, controlMode=p.POSITION_CONTROL, jointIndex=10, #finger_joint2 targetPosition=0.04, #верхний предел для finger_joint2 force=force) render(0.1) print(» «) ## переместить руку print(«### переместить руку ### «) obj_position, _ = p.getBasePositionAndOrientation(obj) obj_position = list(obj_position) target_positions = p.calculateInverseKinematics( bodyUniqueId=robo, endEffectorLinkIndex=11, #связь захвата цели targetPosition=[obj_position[0], obj_position[1], obj_position[2]+0.25], #25 см над объектом targetOrientation=p.getQuaternionFromEuler([0,-3.14,0]) #[крен, тангаж, рыскание]=[0,-π,0] -> рука направлена вниз ) print(«целевая позиция:», target_positions) arm_joints = [0,1,2,3,4,5,6] p.setJointMotorControlArray(bodyUniqueId=robo, controlMode=p.POSITION_CONTROL, jointIndices=arm_joints, targetPositions=target_positions[:len(arm_joints)], forces=[force/3]*len(arm_joints)) render(0.5) print(» «) ## закрыть руку print(«### закрыть руку ###») p.setJointMotorControl2(bodyUniqueId=robo, controlMode=p.POSITION_CONTROL, jointIndex=9, #finger_joint1 targetPosition=0, #нижний предел для finger_joint1 force=force) p.setJointMotorControl2(bodyUniqueId=robo, controlMode=p.POSITION_CONTROL, jointIndex=10, #finger_joint2 targetPosition=0, #нижний предел для finger_joint2 force=force) render(0.1) print(» «) ## удержание объекта print(«### удержание объекта ###») restriction = p.createConstraint( parentBodyUniqueId=robo, parentLinkIndex=11, childBodyUniqueId=obj, childLinkIndex=-1, jointType=p.JOINT_FIXED, jointAxis=[0,0,0], parentFramePosition=[0,0,0], childFramePosition=[0,0,0,1] ) render(0.1) print(» «) ## движение к столу print(«### движение к столу ###») cube_position, _ = p.getBasePositionAndOrientation(cube) cube_position = list(cube_position) target_positions = p.calculateInverseKinematics( bodyUniqueId=robo, endEffectorLinkIndex=11, #ссылка «взять-цель» targetPosition=[cube_position[0], cube_position[1], cube_position[2]+0.80], #80 см над столом targetOrientation=p.getQuaternionFromEuler([0,-3.14,0]) #[roll,pitch,yaw]=[0,-π,0] -> рука направлена вниз ) print(«target position:», target_positions) arm_joints = [0,1,2,3,4,5,6] p.setJointMotorControlArray(bodyUniqueId=robo, controlMode=p.POSITION_CONTROL, jointIndices=arm_joints, targetPositions=target_positions[:len(arm_joints)], forces=[force*3]*len(arm_joints)) render() print(» «) ## раскрытая ладонь и отпущенный объект print(«### раскрытая ладонь и отпущенный объект ###») p.setJointMotorControl2(bodyUniqueId=robo, controlMode=p.POSITION_CONTROL, jointIndex=9, #finger_joint1 targetPosition=0.04, #верхний предел для finger_joint1 force=force) p.setJointMotorControl2(bodyUniqueId=robo, controlMode=p.POSITION_CONTROL, jointIndex=10, #finger_joint2 targetPosition=0.04, #верхний предел для finger_joint2 force=force) p.removeConstraint(constraint) render()

Заключение
Эта статья представляет собой руководство по ручному управлению роботизированной рукой . Мы изучили управление движением, инверсную кинематику, захват и перемещение объектов. В будущем появятся новые руководства с более продвинутыми роботами.
Полный код для этой статьи: GitHub
Надеюсь, вам понравилось! Обращайтесь ко мне с вопросами и отзывами, а также просто делитесь своими интересными проектами.
👉 Давайте общаться 👈

(Все изображения принадлежат автору, если не указано иное)
Источник: towardsdatascience.com



























