#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# File : pick_place_sampler.py
# Author : Jiayuan Mao
# Email : maojiayuan@gmail.com
# Date : 08/31/2024
#
# This file is part of Project Concepts.
# Distributed under terms of the MIT license.
import itertools
from typing import Optional, Iterator, Tuple, List, NamedTuple
from jacinle.utils.cache import cached_property
import numpy as np
import open3d as o3d
from concepts.math.rotationlib_xyzw import find_orthogonal_vector, quat_mul, rotate_vector, enumerate_quaternion_from_vectors, quaternion_from_axes
from concepts.math.cad.mesh_utils import mesh_line_intersect
from concepts.dm.crowhat.world.planning_world_interface import PlanningWorldInterface
from concepts.dm.crowhat.world.manipulator_interface import SingleArmMotionPlanningInterface
from concepts.dm.crowhat.manipulation_utils.contact_point_sampler import ContactPointProposal, gen_ee_pose_from_contact_point, pairwise_sample, _sample_points_uniformly
from concepts.dm.crowhat.manipulation_utils.path_generation_utils import is_collision_free_qpos
__all__ = [
'GraspContactPairProposal', 'gen_grasp_contact_pair',
'GraspParameter', 'gen_grasp_parameter',
'calc_grasp_approach_ee_pose_trajectory',
'PlacementParameter', 'gen_placement_parameter'
]
[docs]
class GraspParameter(NamedTuple):
contact_pair: GraspContactPairProposal
robot_ee_pose: Tuple[np.ndarray, np.ndarray]
robot_qpos: np.ndarray
[docs]
def gen_grasp_parameter(
planning_world: PlanningWorldInterface, robot: SingleArmMotionPlanningInterface,
object_id: int, gripper_distance: float, *,
gripper_min_distance: float = 0.0001,
surface_pointing_tol: float = 0.9,
mesh_filename: Optional[str] = None, mesh_scale: float = 1.0,
max_test_points_before_first: int = 250, max_test_points: int = 100000000, batch_size: int = 100,
max_intersection_distance: float = 10,
np_random: Optional[np.random.RandomState] = None,
verbose: bool = False
) -> Iterator[GraspParameter]:
for grasp_contact_pair in gen_grasp_contact_pair(
planning_world, object_id, gripper_distance,
max_intersection_distance=max_intersection_distance,
mesh_filename=mesh_filename, mesh_scale=mesh_scale,
max_test_points_before_first=max_test_points_before_first, max_test_points=max_test_points, batch_size=batch_size,
surface_pointing_tol=surface_pointing_tol, gripper_min_distance=gripper_min_distance,
np_random=np_random, verbose=verbose
):
grasp_center = grasp_contact_pair.center
ee_d = grasp_contact_pair.normal1
# ee_u and ee_v are two vectors that are perpendicular to ee_d
ee_u = find_orthogonal_vector(ee_d)
ee_v = np.cross(ee_u, ee_d)
# enumerate four possible grasp orientations
for ee_norm1 in [ee_u, ee_v, -ee_u, -ee_v]:
ee_norm2 = np.cross(ee_d, ee_norm1)
ee_quat = quaternion_from_axes(ee_norm2, ee_d, ee_norm1)
qpos = robot.ik(grasp_center, ee_quat)
if qpos is not None:
rv = is_collision_free_qpos(robot, qpos, verbose=verbose)
# robot.set_qpos(qpos)
# print('is_collision_free_qpos', rv)
# import ipdb; ipdb.set_trace()
if rv:
yield GraspParameter(
contact_pair=grasp_contact_pair,
robot_ee_pose=(grasp_center, ee_quat), robot_qpos=qpos
)
elif verbose:
print(' gripper pos', grasp_center)
print(' gripper quat', ee_quat)
print(' skip: collision')
[docs]
def calc_grasp_approach_ee_pose_trajectory(grasp_parameter: GraspParameter, pregrasp_distance: float = 0.1) -> List[Tuple[np.ndarray, np.ndarray]]:
"""Given the name of the object, the grasp pose, generate a trajectory of 6D poses that the robot should follow to grasp the object."""
grasp_pos, grasp_quat = grasp_parameter.robot_ee_pose
trajectory = [(grasp_pos + rotate_vector([0, 0, -pregrasp_distance], grasp_quat), grasp_quat)]
for i in reversed(range(10)):
trajectory.append((
grasp_pos + rotate_vector([0, 0, -pregrasp_distance / 10 * i], grasp_quat),
grasp_quat
))
return trajectory
[docs]
class BimanualGraspParameter(NamedTuple):
contact_pair: GraspContactPairProposal
pre_contact_distance: float
post_contact_distance: float
robot1_ee_pose: Tuple[np.ndarray, np.ndarray]
robot1_qpos: np.ndarray
robot2_ee_pose: Tuple[np.ndarray, np.ndarray]
robot2_qpos: np.ndarray
[docs]
def gen_bimanual_grasp_parameter(
planning_world: PlanningWorldInterface, robot1: SingleArmMotionPlanningInterface, robot2: SingleArmMotionPlanningInterface,
object_id: int, support_id: int, gripper_distance: float, *,
ee_contact_side: str = 'front',
pre_contact_distance: float = 0.05, post_contact_distance: float = 0.01,
gripper_min_distance: float = 0.0001,
surface_pointing_tol: float = 0.9,
mesh_filename: Optional[str] = None, mesh_scale: float = 1.0,
max_test_points_before_first: int = 250, max_test_points: int = 100000000, batch_size: int = 100,
max_intersection_distance: float = 10,
max_ik_attempts: int = 5,
np_random: Optional[np.random.RandomState] = None,
verbose: bool = False
) -> Iterator[BimanualGraspParameter]:
def gen_ee_poses(c1, c2, s1 = ee_contact_side, s2 = ee_contact_side): # contact point 1, contact point 2, side 1, side 2
for x in pairwise_sample(
gen_ee_pose_from_contact_point(planning_world, robot1, object_id, c1, distance=pre_contact_distance, z_delta=0.00, side=s1),
gen_ee_pose_from_contact_point(planning_world, robot2, object_id, c2, distance=pre_contact_distance, z_delta=0.00, side=s2),
product=True
):
yield c1, c2, *x
for contact_pair in gen_grasp_contact_pair_with_support_constraint(
planning_world, object_id, support_id, gripper_distance,
gripper_min_distance=gripper_min_distance,
surface_pointing_tol=surface_pointing_tol,
mesh_filename=mesh_filename, mesh_scale=mesh_scale,
max_test_points_before_first=max_test_points_before_first, max_test_points=max_test_points, batch_size=batch_size,
max_intersection_distance=max_intersection_distance,
np_random=np_random, verbose=verbose
):
for c1, c2, ee_pose1, ee_pose2 in itertools.chain(gen_ee_poses(contact_pair.contact_point1, contact_pair.contact_point2), gen_ee_poses(contact_pair.contact_point2, contact_pair.contact_point1)):
for qpos1, qpos2 in zip(robot1.gen_ik(*ee_pose1, max_returns=max_ik_attempts), robot2.gen_ik(*ee_pose2, max_returns=max_ik_attempts)):
with planning_world.checkpoint_world():
if robot1.check_collision(qpos1, checkpoint_world_state=False) or robot2.check_collision(qpos2, checkpoint_world_state=False):
continue
yield BimanualGraspParameter(
contact_pair=GraspContactPairProposal.from_contact_points(c1, c2),
pre_contact_distance=pre_contact_distance,
post_contact_distance=post_contact_distance,
robot1_ee_pose=ee_pose1, robot1_qpos=qpos1,
robot2_ee_pose=ee_pose2, robot2_qpos=qpos2
)
break # If we have found a valid pair of qpos, we can break the loop for zip(robot1.gen_ik, robot2.gen_ik)
[docs]
def pybullet_visualize_binamual_grasp_parameter(
planning_world: PlanningWorldInterface, robot1: SingleArmMotionPlanningInterface, robot2: SingleArmMotionPlanningInterface,
object_id: int, support_id: int, parameter: BimanualGraspParameter
):
from concepts.dm.crowhat.impl.pybullet.pybullet_planning_world_interface import PyBulletPlanningWorldInterface
assert isinstance(planning_world, PyBulletPlanningWorldInterface)
client = planning_world.client
# Visualize the contact points as a line
start_pos = parameter.contact_pair.point1 + parameter.contact_pair.normal1 * 0.1
end_pos = parameter.contact_pair.point2 + parameter.contact_pair.normal2 * 0.1
client.add_debug_line(start_pos, end_pos, [1, 0, 0], name='grasp_contact_line')
with client.world.save_world_builtin():
# Visualize the robot poses
robot1.set_qpos(parameter.robot1_qpos)
robot2.set_qpos(parameter.robot2_qpos)
client.update_viewer_twice()
client.wait_for_user('Press any key to continue.')
[docs]
class PlacementParameter(NamedTuple):
object_id: int
support_id: int
target_pos: np.ndarray
target_quat: np.ndarray
support_normal: np.ndarray
[docs]
def gen_placement_parameter(
planning_world: PlanningWorldInterface,
target_id: int, support_id: int,
*,
retain_target_orientation: bool = False,
batch_size: int = 100, max_attempts: int = 10000,
placement_tol: float = 0.03, support_dir_tol: float = 30,
np_random: Optional[np.random.RandomState] = None,
verbose: bool = False
) -> Iterator[PlacementParameter]:
"""Sample a placement of the target object on the support object.
Args:
planning_world: the planning world.
robot: the robot interface.
target_id: the id of the target object.
support_id: the id of the support object.
retain_target_orientation: if True, the target object will be placed with the same orientation as the current one.
batch_size: the number of samples in batch processing.
max_attempts: the maximum number of attempts for sampling.
placement_tol: the tolerance for the placement. To check collision, we will place the object at a position that is `placement_tol` away from the support surface and check for collision.
support_dir_tol: the tolerance for the support direction. We will sample the placement direction such that the support direction is within `support_dir_tol` degree from the gravity direction.
np_random: the random state.
verbose: whether to print the sampling information.
Yields:
the placement parameters.
"""
if np_random is None:
np_random = np.random
target_mesh = planning_world.get_object_mesh(target_id)
support_mesh = planning_world.get_object_mesh(support_id)
nr_batches = int(max_attempts / batch_size)
for _ in range(nr_batches):
support_points = _sample_points_uniformly(support_mesh, batch_size, use_triangle_normal=True, seed=np_random.randint(0, 1000000))
normals = np.asarray(support_points.normals)
support_indices = np.where(normals.dot(np.array([0, 0, 1])) > np.cos(np.deg2rad(support_dir_tol)))[0]
target_points = _sample_points_uniformly(target_mesh, batch_size, use_triangle_normal=True, seed=np_random.randint(0, 1000000))
if retain_target_orientation:
normals = np.asarray(target_points.normals)
target_indices = np.where(normals.dot(np.array([0, 0, -1])) > np.cos(np.deg2rad(support_dir_tol)))[0]
else:
target_indices = list(range(batch_size))
all_pair_indices = list(itertools.product(target_indices, support_indices))
np_random.shuffle(all_pair_indices)
for target_index, support_index in all_pair_indices:
target_point_pos = np.asarray(target_points.points[target_index])
target_point_normal = -np.asarray(target_points.normals[target_index])
support_point_pos = np.asarray(support_points.points[support_index])
support_point_normal = np.asarray(support_points.normals[support_index])
current_target_pos, current_target_quat = planning_world.get_object_pose(target_id)
# Solve for a quaternion that aligns the tool normal with the target normal
for rotation_quat in enumerate_quaternion_from_vectors(target_point_normal, support_point_normal, 4):
# This is the world coordinate for the tool point after rotation.
new_target_point_pos = current_target_pos + rotate_vector(target_point_pos - current_target_pos, rotation_quat)
# Now compute the displacement for the tool object
final_target_pos = support_point_pos - new_target_point_pos + current_target_pos
final_target_pos += support_point_normal * placement_tol
final_target_quat = quat_mul(rotation_quat, current_target_quat)
success = True
with planning_world.checkpoint_world():
planning_world.set_object_pose(target_id, (final_target_pos, final_target_quat))
contacts = planning_world.get_contact_points(target_id)
for contact in contacts:
if contact.body_b != target_id:
success = False
break
if success:
yield PlacementParameter(
object_id=target_id,
support_id=support_id,
target_pos=final_target_pos,
target_quat=final_target_quat,
support_normal=support_point_normal
)