This commit is contained in:
hofee 2024-10-09 16:13:22 +00:00
commit 0ea3f048dc
437 changed files with 44406 additions and 0 deletions

14
.gitignore vendored Executable file
View File

@ -0,0 +1,14 @@
__pycache__/
.DS_Store
.idea
experiments/
pytorch3d/
test/
*.xyz
*.zip
*.txt
*.pkl
*.log
/data_generation/data/*
/data_generation/output/*
test/

146
README.md Executable file
View File

@ -0,0 +1,146 @@
# ActivePerception
## 1 Installation
### 1.1 Requirements
- **Operating System**: Linux Only. We test this project on Ubuntu 22.04.
- **Cuda Toolkit Version**: cuda 11.8 or higher.
- **Python Version**: python3.9 or higher
- **Pytorch Version**: Pytorch 2.0.0+cu118
### 1.2 Install ActivePerception Environment
#### 1.2.1 Install Python and `requirements.txt`
Clone this repo with
```
git clone https://github.com/Jiyao06/ActivePerception.git
```
Go to repo root directory
```
cd ActivePerception
```
Create python Environment
```
conda create --name nbv python==3.9
```
Install the basic environment with `requirements.txt`
```
pip install -r requirements.txt
```
> if you encounter this issue while running the project: `ImportError: cannot import name 'NDArray' from 'numpy.typing'`, please upgrade numpy to version 1.23.5 with `pip install numpy==1.23.5`
#### 1.2.2 Install pointnet++ for ActivePerception
Go to pointnet2 repo root directory
```
cd modules/module_lib/pointnet2_utils/pointnet2
```
Install pointnet2
```
pip install -e .
```
### 1.3 Install GSNet Environment for Grasping Evaluation **(Optional)**
#### 1.3.1 Install MinkowskiEngine (Linux Only)
Install dependencies of MinkowskiEngine
```
sudo apt-get update
sudo apt-get install -y git ninja-build cmake build-essential libopenblas-dev xterm xauth openssh-server tmux wget mate-desktop-environment-core
```
Clone MinkowskiEngine git repo
```
cd <any_path>
git clone --recursive "https://github.com/NVIDIA/MinkowskiEngine"
```
Install MinkowskiEngine
```
cd MinkowskiEngine
python setup.py install --force_cuda --blas=openblas
```
### 1.3.2 Install pointnet++ for GSNet
Go to GSNet repo root
```
cd <your_ActivePerception_repo_root>/baselines/grasping/GSNet
```
Go to pointnet++ repo root and install it
```
cd pointnet2
python setup.py install
```
### 1.3.3 Install KNN operator
```
cd ../knn
python setup.py install
```
### 1.3.4 Install GraspnetAPI for Evaluation
```
cd ../graspnetAPI
pip install .
```
### 1.4 Install FoundationPose Environment for Object Pose Evaluation **(Optional)**
...
## 2 Training
### 2.1 Prapare Datasets
Please download the dataset from the links shown as below:
- [NBV Simulation (Train Dataset)](https://link-url-here.org)
- [NBV Simulation (Test Dataset)](https://link-url-here.org)
- [NBV Simulation (Object Models)](https://link-url-here.org)
or directly download a pre-organized dataset structure to skip "2.2 Organize Dataset Structure"
- [NBV Simulation (Pre-organized Dataset)](https://link-url-here.org)
### 2.2 Organize Dataset Structure
Please organize the dataset into the following structure:
```
$ROOT_OF_DATA
- objects
- obj_name_0
- obj_name_1
- ...
- train
- scene_name_0
- scene_name_1
- ...
- test
- scene_name_0
- scene_name_1
- ...
```
### 2.3 Training for Grasping
#### 2.3.1 Prepare Pretrained GSNet weights
Please download the pretrained GSNet weights from: [pretrained_gsnet_weights](https://link-url-here.org)
#### 2.3.2 Preprocess View Score
...
#### 2.3.3 Training Configuration
Open training config file in `ActivePerception/configs/server_train_config.yaml`.
To run the training task, you need to customize at least the following experiment configuration:
- **experiment name** at `experiment -> name`
- **grasp pretrained model path** at `experiment -> grasp_model_path`
- **dataset root path** at `datasets -> general -> data_dir`
#### 2.3.4 Run View Generator Web Server
In order to test the model's predictions under the new view after each epoch's training, a view generator is needed. Therefore, you need to run the following command to start it.
Otherwise, please comment out all instances of the `grasp_improvement` method for the test set in `server_train_config.yaml` under `settings -> test -> dataset_list -> eval_list`. And then you won't get the result of grasp score's improvement during training.
```
python runners/view_generator.py
```
#### 2.3.5 Start Training
Run following command to start the training task.
```
python runners/trainer.py
```
## 3 Evaluation
...
## 4 Data and Results Visualization
Vue.js framework is required to ...
## 5 Custom Sim Data Generation
...

7
annotations/external_module.py Executable file
View File

@ -0,0 +1,7 @@
EXTERNAL_FREEZE_MODULES = set()
def external_freeze(cls):
if not hasattr(cls, 'load') or not callable(getattr(cls, 'load')):
raise TypeError(f"external module <{cls.__name__}> must implement a 'load' method")
EXTERNAL_FREEZE_MODULES.add(cls)
return cls

8
annotations/singleton.py Executable file
View File

@ -0,0 +1,8 @@
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance

34
annotations/stereotype.py Executable file
View File

@ -0,0 +1,34 @@
# --- Classes --- #
def dataset():
pass
def module():
pass
def pipeline():
pass
def runner():
pass
def factory():
pass
# --- Functions --- #
evaluation_methods = {}
def evaluation_method(eval_type):
def decorator(func):
evaluation_methods[eval_type] = func
return func
return decorator
def loss_function():
pass
# --- Main --- #

0
baselines/__init__.py Executable file
View File

160
baselines/grasping/GSNet/LICENSE Executable file
View File

@ -0,0 +1,160 @@
GRASPNET-BASELINE
SOFTWARE LICENSE AGREEMENT
ACADEMIC OR NON-PROFIT ORGANIZATION NONCOMMERCIAL RESEARCH USE ONLY
BY USING OR DOWNLOADING THE SOFTWARE, YOU ARE AGREEING TO THE TERMS OF THIS LICENSE AGREEMENT. IF YOU DO NOT AGREE WITH THESE TERMS, YOU MAY NOT USE OR DOWNLOAD THE SOFTWARE.
This is a license agreement ("Agreement") between your academic institution or non-profit organization or self (called "Licensee" or "You" in this Agreement) and Shanghai Jiao Tong University (called "Licensor" in this Agreement). All rights not specifically granted to you in this Agreement are reserved for Licensor.
RESERVATION OF OWNERSHIP AND GRANT OF LICENSE:
Licensor retains exclusive ownership of any copy of the Software (as defined below) licensed under this Agreement and hereby grants to Licensee a personal, non-exclusive,
non-transferable license to use the Software for noncommercial research purposes, without the right to sublicense, pursuant to the terms and conditions of this Agreement. As used in this Agreement, the term "Software" means (i) the actual copy of all or any portion of code for program routines made accessible to Licensee by Licensor pursuant to this Agreement, inclusive of backups, updates, and/or merged copies permitted hereunder or subsequently supplied by Licensor, including all or any file structures, programming instructions, user interfaces and screen formats and sequences as well as any and all documentation and instructions related to it, and (ii) all or any derivatives and/or modifications created or made by You to any of the items specified in (i).
CONFIDENTIALITY: Licensee acknowledges that the Software is proprietary to Licensor, and as such, Licensee agrees to receive all such materials in confidence and use the Software only in accordance with the terms of this Agreement. Licensee agrees to use reasonable effort to protect the Software from unauthorized use, reproduction, distribution, or publication.
PERMITTED USES: The Software may be used for your own noncommercial internal research purposes. You understand and agree that Licensor is not obligated to implement any suggestions and/or feedback you might provide regarding the Software, but to the extent Licensor does so, you are not entitled to any compensation related thereto.
DERIVATIVES: You may create derivatives of or make modifications to the Software, however, You agree that all and any such derivatives and modifications will be owned by Licensor and become a part of the Software licensed to You under this Agreement. You may only use such derivatives and modifications for your own noncommercial internal research purposes, and you may not otherwise use, distribute or copy such derivatives and modifications in violation of this Agreement.
BACKUPS: If Licensee is an organization, it may make that number of copies of the Software necessary for internal noncommercial use at a single site within its organization provided that all information appearing in or on the original labels, including the copyright and trademark notices are copied onto the labels of the copies.
USES NOT PERMITTED: You may not distribute, copy or use the Software except as explicitly permitted herein. Licensee has not been granted any trademark license as part of this Agreement and may not use the name or mark “AlphaPose", "Shanghai Jiao Tong" or any renditions thereof without the prior written permission of Licensor.
You may not sell, rent, lease, sublicense, lend, time-share or transfer, in whole or in part, or provide third parties access to prior or present versions (or any parts thereof) of the Software.
ASSIGNMENT: You may not assign this Agreement or your rights hereunder without the prior written consent of Licensor. Any attempted assignment without such consent shall be null and void.
TERM: The term of the license granted by this Agreement is from Licensee's acceptance of this Agreement by downloading the Software or by using the Software until terminated as provided below.
The Agreement automatically terminates without notice if you fail to comply with any provision of this Agreement. Licensee may terminate this Agreement by ceasing using the Software. Upon any termination of this Agreement, Licensee will delete any and all copies of the Software. You agree that all provisions which operate to protect the proprietary rights of Licensor shall remain in force should breach occur and that the obligation of confidentiality described in this Agreement is binding in perpetuity and, as such, survives the term of the Agreement.
FEE: Provided Licensee abides completely by the terms and conditions of this Agreement, there is no fee due to Licensor for Licensee's use of the Software in accordance with this Agreement.
DISCLAIMER OF WARRANTIES: THE SOFTWARE IS PROVIDED "AS-IS" WITHOUT WARRANTY OF ANY KIND INCLUDING ANY WARRANTIES OF PERFORMANCE OR MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE OR PURPOSE OR OF NON-INFRINGEMENT. LICENSEE BEARS ALL RISK RELATING TO QUALITY AND PERFORMANCE OF THE SOFTWARE AND RELATED MATERIALS.
SUPPORT AND MAINTENANCE: No Software support or training by the Licensor is provided as part of this Agreement.
EXCLUSIVE REMEDY AND LIMITATION OF LIABILITY: To the maximum extent permitted under applicable law, Licensor shall not be liable for direct, indirect, special, incidental, or consequential damages or lost profits related to Licensee's use of and/or inability to use the Software, even if Licensor is advised of the possibility of such damage.
EXPORT REGULATION: Licensee agrees to comply with any and all applicable
U.S. export control laws, regulations, and/or other laws related to embargoes and sanction programs administered by the Office of Foreign Assets Control.
SEVERABILITY: If any provision(s) of this Agreement shall be held to be invalid, illegal, or unenforceable by a court or other tribunal of competent jurisdiction, the validity, legality and enforceability of the remaining provisions shall not in any way be affected or impaired thereby.
NO IMPLIED WAIVERS: No failure or delay by Licensor in enforcing any right or remedy under this Agreement shall be construed as a waiver of any future or other exercise of such right or remedy by Licensor.
ENTIRE AGREEMENT AND AMENDMENTS: This Agreement constitutes the sole and entire agreement between Licensee and Licensor as to the matter set forth herein and supersedes any previous agreements, understandings, and arrangements between the parties relating hereto.
************************************************************************
THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
This project incorporates material from the project(s) listed below (collectively, "Third Party Code"). This Third Party Code is licensed to you under their original license terms set forth below. We reserves all other rights not expressly granted, whether by implication, estoppel or otherwise.
1. PyTorch (https://github.com/pytorch/pytorch)
THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
This project incorporates material from the project(s) listed below (collectively, "Third Party Code"). This Third Party Code is licensed to you under their original license terms set forth below. We reserves all other rights not expressly granted, whether by implication, estoppel or otherwise.
From PyTorch:
Copyright (c) 2016- Facebook, Inc (Adam Paszke)
Copyright (c) 2014- Facebook, Inc (Soumith Chintala)
Copyright (c) 2011-2014 Idiap Research Institute (Ronan Collobert)
Copyright (c) 2012-2014 Deepmind Technologies (Koray Kavukcuoglu)
Copyright (c) 2011-2012 NEC Laboratories America (Koray Kavukcuoglu)
Copyright (c) 2011-2013 NYU (Clement Farabet)
Copyright (c) 2006-2010 NEC Laboratories America (Ronan Collobert, Leon Bottou, Iain Melvin, Jason Weston)
Copyright (c) 2006 Idiap Research Institute (Samy Bengio)
Copyright (c) 2001-2004 Idiap Research Institute (Ronan Collobert, Samy Bengio, Johnny Mariethoz)
From Caffe2:
Copyright (c) 2016-present, Facebook Inc. All rights reserved.
All contributions by Facebook:
Copyright (c) 2016 Facebook Inc.
All contributions by Google:
Copyright (c) 2015 Google Inc.
All rights reserved.
All contributions by Yangqing Jia:
Copyright (c) 2015 Yangqing Jia
All rights reserved.
All contributions by Kakao Brain:
Copyright 2019-2020 Kakao Brain
All contributions from Caffe:
Copyright(c) 2013, 2014, 2015, the respective contributors
All rights reserved.
All other contributions:
Copyright(c) 2015, 2016 the respective contributors
All rights reserved.
Caffe2 uses a copyright model similar to Caffe: each contributor holds
copyright over their contributions to Caffe2. The project versioning records
all such contribution and copyright details. If a contributor wants to further
mark their specific copyright on a particular contribution, they should
indicate their copyright solely in the commit message of the change when it is
committed.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the names of Facebook, Deepmind Technologies, NYU, NEC Laboratories America
and IDIAP Research Institute nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
2. VoteNet (https://github.com/facebookresearch/votenet)
MIT License
Copyright (c) Facebook, Inc. and its affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
************END OF THIRD-PARTY SOFTWARE NOTICES AND INFORMATION**********

View File

@ -0,0 +1,86 @@
# GraspNet graspness
This project aims to address issues encountered during the migration of the repository [GS-Net](https://github.com/graspnet/graspness_unofficial) to an RTX 4090 GPU.
The original repo is a fork of paper "Graspness Discovery in Clutters for Fast and Accurate Grasp Detection" (ICCV 2021) by [Zibo Chen](https://github.com/rhett-chen).
[[paper](https://openaccess.thecvf.com/content/ICCV2021/papers/Wang_Graspness_Discovery_in_Clutters_for_Fast_and_Accurate_Grasp_Detection_ICCV_2021_paper.pdf)]
[[dataset](https://graspnet.net/)]
[[API](https://github.com/graspnet/graspnetAPI)]
## Requirements
- Python 3
- PyTorch 1.8
- Open3d 0.8
- TensorBoard 2.3
- NumPy
- SciPy
- Pillow
- tqdm
- MinkowskiEngine
## Installation
Get the code.
```bash
git clone https://github.com/graspnet/graspness_unofficial.git
cd graspness_unofficial
```
Install packages via Pip.
```bash
pip install -r requirements.txt
```
Compile and install pointnet2 operators (code adapted from [votenet](https://github.com/facebookresearch/votenet)).
```bash
cd pointnet2
python setup.py install
```
Compile and install knn operator (code adapted from [pytorch_knn_cuda](https://github.com/chrischoy/pytorch_knn_cuda)).
```bash
cd knn
python setup.py install
```
Install graspnetAPI for evaluation.
```bash
git clone https://github.com/graspnet/graspnetAPI.git
cd graspnetAPI
pip install .
```
For MinkowskiEngine, please refer https://github.com/NVIDIA/MinkowskiEngine
## Point level Graspness Generation
Point level graspness label are not included in the original dataset, and need additional generation. Make sure you have downloaded the orginal dataset from [GraspNet](https://graspnet.net/). The generation code is in [dataset/generate_graspness.py](dataset/generate_graspness.py).
```bash
cd dataset
python generate_graspness.py --dataset_root /data3/graspnet --camera_type kinect
```
## Simplify dataset
original dataset grasp_label files have redundant data, We can significantly save the memory cost. The code is in [dataset/simplify_dataset.py](dataset/simplify_dataset.py)
```bash
cd dataset
python simplify_dataset.py --dataset_root /data3/graspnet
```
## Training and Testing
Training examples are shown in [command_train.sh](command_train.sh). `--dataset_root`, `--camera` and `--log_dir` should be specified according to your settings. You can use TensorBoard to visualize training process.
Testing examples are shown in [command_test.sh](command_test.sh), which contains inference and result evaluation. `--dataset_root`, `--camera`, `--checkpoint_path` and `--dump_dir` should be specified according to your settings. Set `--collision_thresh` to -1 for fast inference.
## Model Weights
We provide trained model weights. The model trained with RealSense data is available at [Google drive](https://drive.google.com/file/d/1RfdpEM2y0x98rV28d7B2Dg8LLFKnBkfL/view?usp=sharing) (this model is recommended for real-world application). The model trained with Kinect data is available at [Google drive](https://drive.google.com/file/d/10o5fc8LQsbI8H0pIC2RTJMNapW9eczqF/view?usp=sharing).
## Results
Results "In repo" report the model performance of my results without collision detection.
Evaluation results on Kinect camera:
| | | Seen | | | Similar | | | Novel | |
|:--------:|:------:|:----------------:|:----------------:|:------:|:----------------:|:----------------:|:------:|:----------------:|:----------------:|
| | __AP__ | AP<sub>0.8</sub> | AP<sub>0.4</sub> | __AP__ | AP<sub>0.8</sub> | AP<sub>0.4</sub> | __AP__ | AP<sub>0.8</sub> | AP<sub>0.4</sub> |
| In paper | 61.19 | 71.46 | 56.04 | 47.39 | 56.78 | 40.43 | 19.01 | 23.73 | 10.60 |
| In repo | 61.83 | 73.28 | 54.14 | 51.13 | 62.53 | 41.57 | 19.94 | 24.90 | 11.02 |
## Troubleshooting
If you meet the torch.floor error in MinkowskiEngine, you can simply solve it by changing the source code of MinkowskiEngine:
MinkowskiEngine/utils/quantization.py 262from discrete_coordinates =_auto_floor(coordinates) to discrete_coordinates = coordinates
## Acknowledgement
My code is mainly based on Graspnet-baseline https://github.com/graspnet/graspnet-baseline.

View File

@ -0,0 +1 @@
tensorboard --logdir=logs/log_kn/train --port=8000

View File

@ -0,0 +1 @@
CUDA_VISIBLE_DEVICES=0 python test.py --camera kinect --dump_dir logs/log_kn/dump_kinect --checkpoint_path logs/log_kn/epoch10.tar --batch_size 1 --dataset_root /mnt/h/AI/Datasets/graspnet-1billion/test_seen --infer --eval --collision_thresh -1

View File

@ -0,0 +1 @@
CUDA_VISIBLE_DEVICES=0 python train.py --camera kinect --log_dir logs/log_kn --batch_size 8 --learning_rate 0.001 --model_name minkuresunet --dataset_root data/GraspNet-1Billion

View File

@ -0,0 +1,119 @@
import numpy as np
import os
from PIL import Image
import scipy.io as scio
import sys
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(ROOT_DIR)
from utils.data_utils import get_workspace_mask, CameraInfo, create_point_cloud_from_depth_image
from knn.knn_modules import knn
import torch
from graspnetAPI.utils.xmlhandler import xmlReader
from graspnetAPI.utils.utils import get_obj_pose_list, transform_points
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--dataset_root', default=None, required=True)
parser.add_argument('--camera_type', default='kinect', help='Camera split [realsense/kinect]')
if __name__ == '__main__':
cfgs = parser.parse_args()
dataset_root = cfgs.dataset_root # set dataset root
camera_type = cfgs.camera_type # kinect / realsense
save_path_root = os.path.join(dataset_root, 'graspness')
num_views, num_angles, num_depths = 300, 12, 4
fric_coef_thresh = 0.8
point_grasp_num = num_views * num_angles * num_depths
for scene_id in range(100):
save_path = os.path.join(save_path_root, 'scene_' + str(scene_id).zfill(4), camera_type)
if not os.path.exists(save_path):
os.makedirs(save_path)
labels = np.load(
os.path.join(dataset_root, 'collision_label', 'scene_' + str(scene_id).zfill(4), 'collision_labels.npz'))
collision_dump = []
for j in range(len(labels)):
collision_dump.append(labels['arr_{}'.format(j)])
for ann_id in range(256):
# get scene point cloud
print('generating scene: {} ann: {}'.format(scene_id, ann_id))
depth = np.array(Image.open(os.path.join(dataset_root, 'scenes', 'scene_' + str(scene_id).zfill(4),
camera_type, 'depth', str(ann_id).zfill(4) + '.png')))
seg = np.array(Image.open(os.path.join(dataset_root, 'scenes', 'scene_' + str(scene_id).zfill(4),
camera_type, 'label', str(ann_id).zfill(4) + '.png')))
meta = scio.loadmat(os.path.join(dataset_root, 'scenes', 'scene_' + str(scene_id).zfill(4),
camera_type, 'meta', str(ann_id).zfill(4) + '.mat'))
intrinsic = meta['intrinsic_matrix']
factor_depth = meta['factor_depth']
camera = CameraInfo(1280.0, 720.0, intrinsic[0][0], intrinsic[1][1], intrinsic[0][2], intrinsic[1][2],
factor_depth)
cloud = create_point_cloud_from_depth_image(depth, camera, organized=True)
# remove outlier and get objectness label
depth_mask = (depth > 0)
camera_poses = np.load(os.path.join(dataset_root, 'scenes', 'scene_' + str(scene_id).zfill(4),
camera_type, 'camera_poses.npy'))
camera_pose = camera_poses[ann_id]
align_mat = np.load(os.path.join(dataset_root, 'scenes', 'scene_' + str(scene_id).zfill(4),
camera_type, 'cam0_wrt_table.npy'))
trans = np.dot(align_mat, camera_pose)
workspace_mask = get_workspace_mask(cloud, seg, trans=trans, organized=True, outlier=0.02)
mask = (depth_mask & workspace_mask)
cloud_masked = cloud[mask]
objectness_label = seg[mask]
# get scene object and grasp info
scene_reader = xmlReader(os.path.join(dataset_root, 'scenes', 'scene_' + str(scene_id).zfill(4),
camera_type, 'annotations', '%04d.xml' % ann_id))
pose_vectors = scene_reader.getposevectorlist()
obj_list, pose_list = get_obj_pose_list(camera_pose, pose_vectors)
grasp_labels = {}
for i in obj_list:
file = np.load(os.path.join(dataset_root, 'grasp_label', '{}_labels.npz'.format(str(i).zfill(3))))
grasp_labels[i] = (file['points'].astype(np.float32), file['offsets'].astype(np.float32),
file['scores'].astype(np.float32))
grasp_points = []
grasp_points_graspness = []
for i, (obj_idx, trans_) in enumerate(zip(obj_list, pose_list)):
sampled_points, offsets, fric_coefs = grasp_labels[obj_idx]
collision = collision_dump[i] # Npoints * num_views * num_angles * num_depths
num_points = sampled_points.shape[0]
valid_grasp_mask = ((fric_coefs <= fric_coef_thresh) & (fric_coefs > 0) & ~collision)
valid_grasp_mask = valid_grasp_mask.reshape(num_points, -1)
graspness = np.sum(valid_grasp_mask, axis=1) / point_grasp_num
target_points = transform_points(sampled_points, trans_)
target_points = transform_points(target_points, np.linalg.inv(camera_pose)) # fix bug
grasp_points.append(target_points)
grasp_points_graspness.append(graspness.reshape(num_points, 1))
grasp_points = np.vstack(grasp_points)
grasp_points_graspness = np.vstack(grasp_points_graspness)
grasp_points = torch.from_numpy(grasp_points).cuda()
grasp_points_graspness = torch.from_numpy(grasp_points_graspness).cuda()
grasp_points = grasp_points.transpose(0, 1).contiguous().unsqueeze(0)
masked_points_num = cloud_masked.shape[0]
cloud_masked_graspness = np.zeros((masked_points_num, 1))
part_num = int(masked_points_num / 10000)
for i in range(1, part_num + 2): # lack of cuda memory
if i == part_num + 1:
cloud_masked_partial = cloud_masked[10000 * part_num:]
if len(cloud_masked_partial) == 0:
break
else:
cloud_masked_partial = cloud_masked[10000 * (i - 1):(i * 10000)]
cloud_masked_partial = torch.from_numpy(cloud_masked_partial).cuda()
cloud_masked_partial = cloud_masked_partial.transpose(0, 1).contiguous().unsqueeze(0)
nn_inds = knn(grasp_points, cloud_masked_partial, k=1).squeeze() - 1
cloud_masked_graspness[10000 * (i - 1):(i * 10000)] = torch.index_select(
grasp_points_graspness, 0, nn_inds).cpu().numpy()
max_graspness = np.max(cloud_masked_graspness)
min_graspness = np.min(cloud_masked_graspness)
cloud_masked_graspness = (cloud_masked_graspness - min_graspness) / (max_graspness - min_graspness)
np.save(os.path.join(save_path, str(ann_id).zfill(4) + '.npy'), cloud_masked_graspness)

View File

@ -0,0 +1,268 @@
""" GraspNet dataset processing.
Author: chenxi-wang
"""
import os
import numpy as np
import scipy.io as scio
from PIL import Image
import torch
import collections.abc as container_abcs
from torch.utils.data import Dataset
from tqdm import tqdm
import MinkowskiEngine as ME
from data_utils import CameraInfo, transform_point_cloud, create_point_cloud_from_depth_image, get_workspace_mask
class GraspNetDataset(Dataset):
def __init__(self, root, grasp_labels=None, camera='kinect', split='train', num_points=20000,
voxel_size=0.005, remove_outlier=True, augment=False, load_label=True):
assert (num_points <= 50000)
self.root = root
self.split = split
self.voxel_size = voxel_size
self.num_points = num_points
self.remove_outlier = remove_outlier
self.grasp_labels = grasp_labels
self.camera = camera
self.augment = augment
self.load_label = load_label
self.collision_labels = {}
if split == 'train':
self.sceneIds = list(range(100))
elif split == 'test':
self.sceneIds = list(range(100, 190))
elif split == 'test_seen':
self.sceneIds = list(range(100, 130))
elif split == 'test_similar':
self.sceneIds = list(range(130, 160))
elif split == 'test_novel':
self.sceneIds = list(range(160, 190))
self.sceneIds = ['scene_{}'.format(str(x).zfill(4)) for x in self.sceneIds]
self.depthpath = []
self.labelpath = []
self.metapath = []
self.scenename = []
self.frameid = []
self.graspnesspath = []
for x in tqdm(self.sceneIds, desc='Loading data path and collision labels...'):
for img_num in range(256):
self.depthpath.append(os.path.join(root, 'scenes', x, camera, 'depth', str(img_num).zfill(4) + '.png'))
self.labelpath.append(os.path.join(root, 'scenes', x, camera, 'label', str(img_num).zfill(4) + '.png'))
self.metapath.append(os.path.join(root, 'scenes', x, camera, 'meta', str(img_num).zfill(4) + '.mat'))
self.graspnesspath.append(os.path.join(root, 'graspness', x, camera, str(img_num).zfill(4) + '.npy'))
self.scenename.append(x.strip())
self.frameid.append(img_num)
if self.load_label:
collision_labels = np.load(os.path.join(root, 'collision_label', x.strip(), 'collision_labels.npz'))
self.collision_labels[x.strip()] = {}
for i in range(len(collision_labels)):
self.collision_labels[x.strip()][i] = collision_labels['arr_{}'.format(i)]
def scene_list(self):
return self.scenename
def __len__(self):
return len(self.depthpath)
def augment_data(self, point_clouds, object_poses_list):
# Flipping along the YZ plane
if np.random.random() > 0.5:
flip_mat = np.array([[-1, 0, 0],
[0, 1, 0],
[0, 0, 1]])
point_clouds = transform_point_cloud(point_clouds, flip_mat, '3x3')
for i in range(len(object_poses_list)):
object_poses_list[i] = np.dot(flip_mat, object_poses_list[i]).astype(np.float32)
# Rotation along up-axis/Z-axis
rot_angle = (np.random.random() * np.pi / 3) - np.pi / 6 # -30 ~ +30 degree
c, s = np.cos(rot_angle), np.sin(rot_angle)
rot_mat = np.array([[1, 0, 0],
[0, c, -s],
[0, s, c]])
point_clouds = transform_point_cloud(point_clouds, rot_mat, '3x3')
for i in range(len(object_poses_list)):
object_poses_list[i] = np.dot(rot_mat, object_poses_list[i]).astype(np.float32)
return point_clouds, object_poses_list
def __getitem__(self, index):
if self.load_label:
return self.get_data_label(index)
else:
return self.get_data(index)
def get_data(self, index, return_raw_cloud=False):
depth = np.array(Image.open(self.depthpath[index]))
seg = np.array(Image.open(self.labelpath[index]))
meta = scio.loadmat(self.metapath[index])
scene = self.scenename[index]
try:
intrinsic = meta['intrinsic_matrix']
factor_depth = meta['factor_depth']
except Exception as e:
print(repr(e))
print(scene)
camera = CameraInfo(1280.0, 720.0, intrinsic[0][0], intrinsic[1][1], intrinsic[0][2], intrinsic[1][2],
factor_depth)
# generate cloud
cloud = create_point_cloud_from_depth_image(depth, camera, organized=True)
# get valid points
depth_mask = (depth > 0)
if self.remove_outlier:
camera_poses = np.load(os.path.join(self.root, 'scenes', scene, self.camera, 'camera_poses.npy'))
align_mat = np.load(os.path.join(self.root, 'scenes', scene, self.camera, 'cam0_wrt_table.npy'))
trans = np.dot(align_mat, camera_poses[self.frameid[index]])
workspace_mask = get_workspace_mask(cloud, seg, trans=trans, organized=True, outlier=0.02)
mask = (depth_mask & workspace_mask)
else:
mask = depth_mask
cloud_masked = cloud[mask]
if return_raw_cloud:
return cloud_masked
# sample points random
if len(cloud_masked) >= self.num_points:
idxs = np.random.choice(len(cloud_masked), self.num_points, replace=False)
else:
idxs1 = np.arange(len(cloud_masked))
idxs2 = np.random.choice(len(cloud_masked), self.num_points - len(cloud_masked), replace=True)
idxs = np.concatenate([idxs1, idxs2], axis=0)
cloud_sampled = cloud_masked[idxs]
ret_dict = {'point_clouds': cloud_sampled.astype(np.float32),
'coors': cloud_sampled.astype(np.float32) / self.voxel_size,
'feats': np.ones_like(cloud_sampled).astype(np.float32),
}
return ret_dict
def get_data_label(self, index):
depth = np.array(Image.open(self.depthpath[index]))
seg = np.array(Image.open(self.labelpath[index]))
meta = scio.loadmat(self.metapath[index])
graspness = np.load(self.graspnesspath[index]) # for each point in workspace masked point cloud
scene = self.scenename[index]
try:
obj_idxs = meta['cls_indexes'].flatten().astype(np.int32)
poses = meta['poses']
intrinsic = meta['intrinsic_matrix']
factor_depth = meta['factor_depth']
except Exception as e:
print(repr(e))
print(scene)
camera = CameraInfo(1280.0, 720.0, intrinsic[0][0], intrinsic[1][1], intrinsic[0][2], intrinsic[1][2],
factor_depth)
# generate cloud
cloud = create_point_cloud_from_depth_image(depth, camera, organized=True)
# get valid points
depth_mask = (depth > 0)
if self.remove_outlier:
camera_poses = np.load(os.path.join(self.root, 'scenes', scene, self.camera, 'camera_poses.npy'))
align_mat = np.load(os.path.join(self.root, 'scenes', scene, self.camera, 'cam0_wrt_table.npy'))
trans = np.dot(align_mat, camera_poses[self.frameid[index]])
workspace_mask = get_workspace_mask(cloud, seg, trans=trans, organized=True, outlier=0.02)
mask = (depth_mask & workspace_mask)
else:
mask = depth_mask
cloud_masked = cloud[mask]
seg_masked = seg[mask]
# sample points
if len(cloud_masked) >= self.num_points:
idxs = np.random.choice(len(cloud_masked), self.num_points, replace=False)
else:
idxs1 = np.arange(len(cloud_masked))
idxs2 = np.random.choice(len(cloud_masked), self.num_points - len(cloud_masked), replace=True)
idxs = np.concatenate([idxs1, idxs2], axis=0)
cloud_sampled = cloud_masked[idxs]
seg_sampled = seg_masked[idxs]
graspness_sampled = graspness[idxs]
objectness_label = seg_sampled.copy()
objectness_label[objectness_label > 1] = 1
object_poses_list = []
grasp_points_list = []
grasp_widths_list = []
grasp_scores_list = []
for i, obj_idx in enumerate(obj_idxs):
if (seg_sampled == obj_idx).sum() < 50:
continue
object_poses_list.append(poses[:, :, i])
points, widths, scores = self.grasp_labels[obj_idx]
collision = self.collision_labels[scene][i] # (Np, V, A, D)
idxs = np.random.choice(len(points), min(max(int(len(points) / 4), 300), len(points)), replace=False)
grasp_points_list.append(points[idxs])
grasp_widths_list.append(widths[idxs])
collision = collision[idxs].copy()
scores = scores[idxs].copy()
scores[collision] = 0
grasp_scores_list.append(scores)
if self.augment:
cloud_sampled, object_poses_list = self.augment_data(cloud_sampled, object_poses_list)
from ipdb import set_trace; set_trace()
ret_dict = {'point_clouds': cloud_sampled.astype(np.float32),
'coors': cloud_sampled.astype(np.float32) / self.voxel_size,
'feats': np.ones_like(cloud_sampled).astype(np.float32),
'graspness_label': graspness_sampled.astype(np.float32),
'objectness_label': objectness_label.astype(np.int64),
'object_poses_list': object_poses_list,
'grasp_points_list': grasp_points_list,
'grasp_widths_list': grasp_widths_list,
'grasp_scores_list': grasp_scores_list}
set_trace()
return ret_dict
def load_grasp_labels(root):
obj_names = list(range(1, 89))
grasp_labels = {}
for obj_name in tqdm(obj_names, desc='Loading grasping labels...'):
label = np.load(os.path.join(root, 'grasp_label_simplified', '{}_labels.npz'.format(str(obj_name - 1).zfill(3))))
grasp_labels[obj_name] = (label['points'].astype(np.float32), label['width'].astype(np.float32),
label['scores'].astype(np.float32))
return grasp_labels
def minkowski_collate_fn(list_data):
coordinates_batch, features_batch = ME.utils.sparse_collate([d["coors"] for d in list_data],
[d["feats"] for d in list_data])
frame_path_batch = [d["frame_path"] for d in list_data]
object_name_batch = [d["object_name"] for d in list_data]
obj_pcl_dict = [d["obj_pcl_dict"] for d in list_data]
coordinates_batch = np.ascontiguousarray(coordinates_batch, dtype=np.int32)
coordinates_batch, features_batch, _, quantize2original = ME.utils.sparse_quantize(
coordinates_batch, features_batch, return_index=True, return_inverse=True)
res = {
"coors": coordinates_batch,
"feats": features_batch,
"quantize2original": quantize2original,
"obj_pcl_dict": obj_pcl_dict,
"frame_path":frame_path_batch,
"object_name": object_name_batch
}
def collate_fn_(batch):
if type(batch[0]).__module__ == 'numpy':
return torch.stack([torch.from_numpy(b) for b in batch], 0)
elif isinstance(batch[0], container_abcs.Sequence):
return [[torch.from_numpy(sample) for sample in b] for b in batch]
elif isinstance(batch[0], container_abcs.Mapping):
for key in batch[0]:
if key == 'coors' or key == 'feats' or key == "frame_path" or key == "object_name" or key == "obj_pcl_dict":
continue
res[key] = collate_fn_([d[key] for d in batch])
return res
res = collate_fn_(list_data)
return res

View File

@ -0,0 +1,43 @@
import numpy as np
import os
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--dataset_root', default=None, required=True)
def simplify_grasp_labels(root, save_path):
"""
original dataset grasp_label files have redundant data, We can significantly save the memory cost
"""
obj_names = list(range(88))
if not os.path.exists(save_path):
os.makedirs(save_path)
for i in obj_names:
print('\nsimplifying object {}:'.format(i))
label = np.load(os.path.join(root, 'grasp_label', '{}_labels.npz'.format(str(i).zfill(3))))
# point_num = len(label['points'])
print('original shape: ', label['points'].shape, label['offsets'].shape, label['scores'].shape)
# if point_num > 4820:
# idxs = np.random.choice(point_num, 4820, False)
# points = label['points'][idxs]
# offsets = label['offsets'][idxs]
# scores = label['scores'][idxs]
# print('Warning!!! down sample object {}'.format(i))
# else:
points = label['points']
scores = label['scores']
offsets = label['offsets']
width = offsets[:, :, :, :, 2]
print('after simplify, offset shape: ', points.shape, scores.shape, width.shape)
np.savez(os.path.join(save_path, '{}_labels.npz'.format(str(i).zfill(3))),
points=points, scores=scores, width=width)
if __name__ == '__main__':
cfgs = parser.parse_args()
root = cfgs.dataset_root # set root and save path
save_path = os.path.join(root, 'grasp_label_simplified')
simplify_grasp_labels(root, save_path)

View File

@ -0,0 +1,42 @@
import open3d as o3d
import scipy.io as scio
from PIL import Image
import os
import numpy as np
import sys
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(ROOT_DIR)
from utils.data_utils import get_workspace_mask, CameraInfo, create_point_cloud_from_depth_image
data_path = '/media/bot/980A6F5E0A6F38801/datasets/graspnet/'
scene_id = 'scene_0060'
ann_id = '0000'
camera_type = 'realsense'
color = np.array(Image.open(os.path.join(data_path, 'scenes', scene_id, camera_type, 'rgb', ann_id + '.png')), dtype=np.float32) / 255.0
depth = np.array(Image.open(os.path.join(data_path, 'scenes', scene_id, camera_type, 'depth', ann_id + '.png')))
seg = np.array(Image.open(os.path.join(data_path, 'scenes', scene_id, camera_type, 'label', ann_id + '.png')))
meta = scio.loadmat(os.path.join(data_path, 'scenes', scene_id, camera_type, 'meta', ann_id + '.mat'))
intrinsic = meta['intrinsic_matrix']
factor_depth = meta['factor_depth']
camera = CameraInfo(1280.0, 720.0, intrinsic[0][0], intrinsic[1][1], intrinsic[0][2], intrinsic[1][2], factor_depth)
point_cloud = create_point_cloud_from_depth_image(depth, camera, organized=True)
depth_mask = (depth > 0)
camera_poses = np.load(os.path.join(data_path, 'scenes', scene_id, camera_type, 'camera_poses.npy'))
align_mat = np.load(os.path.join(data_path, 'scenes', scene_id, camera_type, 'cam0_wrt_table.npy'))
trans = np.dot(align_mat, camera_poses[int(ann_id)])
workspace_mask = get_workspace_mask(point_cloud, seg, trans=trans, organized=True, outlier=0.02)
mask = (depth_mask & workspace_mask)
point_cloud = point_cloud[mask]
color = color[mask]
seg = seg[mask]
graspness_full = np.load(os.path.join(data_path, 'graspness', scene_id, camera_type, ann_id + '.npy')).squeeze()
graspness_full[seg == 0] = 0.
print('graspness full scene: ', graspness_full.shape, (graspness_full > 0.1).sum())
color[graspness_full > 0.1] = [0., 1., 0.]
cloud = o3d.geometry.PointCloud()
cloud.points = o3d.utility.Vector3dVector(point_cloud.astype(np.float32))
cloud.colors = o3d.utility.Vector3dVector(color.astype(np.float32))
o3d.visualization.draw_geometries([cloud])

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

View File

@ -0,0 +1,13 @@
*.pyc
*.so
*.o
*.egg-info/
/graspnetAPI/dump_full/
/graspnetAPI/eval/acc_novel
/dump_full/
/dist/
/build/
/.vscode/
/graspnms/build/
*.npy
/graspnms/grasp_nms.cpp

View File

@ -0,0 +1,30 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/source/conf.py
# Build documentation with MkDocs
#mkdocs:
# configuration: mkdocs.yml
build:
image: stable
# Optionally build your docs in additional formats such as PDF
formats:
- pdf
- epub
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.6
install:
- requirements: docs/requirements.txt
- method: pip
path: .
system_packages: true

View File

@ -0,0 +1,95 @@
# graspnetAPI
[![PyPI version](https://badge.fury.io/py/graspnetAPI.svg)](https://badge.fury.io/py/graspnetAPI)
## Dataset
Visit the [GraspNet Website](http://graspnet.net) to get the dataset.
## Install
You can install using pip.
```bash
pip install graspnetAPI
```
You can also install from source.
```bash
git clone https://github.com/graspnet/graspnetAPI.git
cd graspnetAPI
pip install .
```
## Document
Refer to [online document](https://graspnetapi.readthedocs.io/en/latest/index.html) for more details.
[PDF Document](https://graspnetapi.readthedocs.io/_/downloads/en/latest/pdf/) is available, too.
You can also build the doc manually.
```bash
cd docs
pip install -r requirements.txt
bash build_doc.sh
```
LaTeX is required to build the pdf, but html can be built anyway.
## Grasp Definition
The frame of our gripper is defined as
<div align="center">
<img src="grasp_definition.png", width="400">
</div>
## Examples
```bash
cd examples
# change the path of graspnet root
# How to load labels from graspnet.
python3 exam_loadGrasp.py
# How to convert between 6d and rectangle grasps.
python3 exam_convert.py
# Check the completeness of the data.
python3 exam_check_data.py
# you can also run other examples
```
Please refer to our document for more examples.
## Citation
Please cite these papers in your publications if it helps your research:
```
@article{fang2023robust,
title={Robust grasping across diverse sensor qualities: The GraspNet-1Billion dataset},
author={Fang, Hao-Shu and Gou, Minghao and Wang, Chenxi and Lu, Cewu},
journal={The International Journal of Robotics Research},
year={2023},
publisher={SAGE Publications Sage UK: London, England}
}
@inproceedings{fang2020graspnet,
title={GraspNet-1Billion: A Large-Scale Benchmark for General Object Grasping},
author={Fang, Hao-Shu and Wang, Chenxi and Gou, Minghao and Lu, Cewu},
booktitle={Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition(CVPR)},
pages={11444--11453},
year={2020}
}
```
## Change Log
#### 1.2.6
- Add transformation for Grasp and GraspGroup.
#### 1.2.7
- Add inpainting for depth image.
#### 1.2.8
- Minor fix bug on loadScenePointCloud.

View File

@ -0,0 +1,19 @@
import os
from tqdm import tqdm
### change the root to you path ####
graspnet_root = '/home/gmh/graspnet'
### change the root to the folder contains rectangle grasp labels ###
rect_labels_root = 'rect_labels'
for sceneId in tqdm(range(190), 'Copying Rectangle Grasp Labels'):
for camera in ['kinect', 'realsense']:
dest_dir = os.path.join(graspnet_root, 'scenes', 'scene_%04d' % sceneId, camera, 'rect')
src_dir = os.path.join(rect_labels_root, 'scene_%04d' % sceneId, camera)
if not os.path.exists(dest_dir):
os.mkdir(dest_dir)
for annId in range(256):
src_path = os.path.join(src_dir,'%04d.npy' % annId)
assert os.path.exists(src_path)
os.system('cp {} {}'.format(src_path, dest_dir))

View File

@ -0,0 +1 @@
/build/

View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@ -0,0 +1,8 @@
rm source/graspnetAPI.*
rm source/modules.rst
sphinx-apidoc -o ./source ../graspnetAPI
make clean
make html
make latex
cd build/latex
make

View File

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

@ -0,0 +1,28 @@
About graspnetAPI
=================
.. image:: _static/graspnetlogo1-blue.png
GraspNet is an open project for general object grasping that is continuously enriched. Currently we release GraspNet-1Billion, a large-scale benchmark for general object grasping, as well as other related areas (e.g. 6D pose estimation, unseen object segmentation, etc.). graspnetAPI is a Python API that assists in loading, parsing and visualizing the annotations in GraspNet. Please visit graspnet website_ for more information on GraspNet, including for the data, paper, and tutorials. The exact format of the annotations is also described on the GraspNet website. In addition to this API, please download both the GraspNet images and annotations in order to run the demo.
.. _website: https://graspnet.net/
Resources
---------
- Documentations_
- PDF_Documentations_
- Website_
- Code_
.. _Code: https://github.com/graspnet/graspnetapi
.. _Documentations: https://graspnetapi.readthedocs.io/en/latest/
.. _PDF_Documentations: https://graspnetapi.readthedocs.io/_/downloads/en/latest/pdf/
.. _Website: https://graspnet.net/
License
-------
graspnetAPI is licensed under the none commercial CC4.0 license [see https://graspnet.net/about]

View File

@ -0,0 +1,58 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('../../graspnetAPI'))
# -- Project information -----------------------------------------------------
project = 'graspnetAPI'
copyright = '2021, MVIG, Shanghai Jiao Tong University'
author = 'graspnet'
# The full version, including alpha/beta/rc tags
release = '1.2.11'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.todo',
'sphinx.ext.viewcode'
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
master_doc = 'index'

View File

@ -0,0 +1,8 @@
.. _example_check_data:
Check Dataset Files
===================
You can check if there is any missing file in the dataset by the following code.
.. literalinclude:: ../../examples/exam_check_data.py

View File

@ -0,0 +1,62 @@
.. _example_vis:
Convert Labels between rectangle format and 6d format
=====================================================
Get a GraspNet instance.
.. literalinclude:: ../../examples/exam_convert.py
:lines: 4-22
Convert rectangle format to 6d format
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
First, load rectangle labels from dataset.
.. literalinclude:: ../../examples/exam_convert.py
:lines: 24-25
**Convert a single RectGrasp to Grasp.**
.. note:: This conversion may fail due to invalid depth information.
.. literalinclude:: ../../examples/exam_convert.py
:lines: 27-42
Before Conversion:
.. image:: _static/convert_single_before.png
After Conversion:
.. image:: _static/convert_single_after.png
**Convert RectGraspGroup to GraspGroup.**
.. literalinclude:: ../../examples/exam_convert.py
:lines: 44-56
Before Conversion:
.. image:: _static/convert_rect_before.png
After Conversion:
.. image:: _static/convert_rect_after.png
Convert 6d format to rectangle format
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. note:: Grasp to RectGrasp conversion is not applicable as only very few 6d grasp can be converted to rectangle grasp.
.. literalinclude:: ../../examples/exam_convert.py
:lines: 58-
Before Conversion:
.. image:: _static/convert_6d_before.png
After Conversion:
.. image:: _static/convert_6d_after.png

View File

@ -0,0 +1,73 @@
.. _example_eval:
Evaluation
==========
Data Preparation
^^^^^^^^^^^^^^^^
The first step of evaluation is to prepare your own results.
You need to run your code and generate a `GraspGroup` for each image in each scene.
Then call the `save_npy` function of `GraspGroup` to dump the results.
To generate a `GraspGroup` and save it, you can directly input a 2D numpy array for the `GraspGroup` class:
::
gg=GraspGroup(np.array([[score_1, width_1, height_1, depth_1, rotation_matrix_1(9), translation_1(3), object_id_1],
[score_2, width_2, height_2, depth_2, rotation_matrix_2(9), translation_2(3), object_id_2],
...,
[score_N, width_N, height_N, depth_N, rotation_matrix_N(9), translation_N(3), object_id_N]]
))
gg.save_npy(save_path)
where your algorithm predicts N grasp poses for an image. For the `object_id`, you can simply input `0`. For the meaning of other entries, you should refer to the doc for Grasp Label Format-API Loaded Labels
The file structure of dump folder should be as follows:
::
|-- dump_folder
|-- scene_0100
| |-- kinect
| | |
| | --- 0000.npy to 0255.npy
| |
| --- realsense
| |
| --- 0000.npy to 0255.npy
|
|-- scene_0101
|
...
|
--- scene_0189
You can choose to generate dump files for only one camera, there will be no error for doing that.
Evaluation API
^^^^^^^^^^^^^^
Get GraspNetEval instances.
.. literalinclude:: ../../examples/exam_eval.py
:lines: 4-17
Evaluate A Single Scene
-----------------------
.. literalinclude:: ../../examples/exam_eval.py
:lines: 19-23
Evaluate All Scenes
-------------------
.. literalinclude:: ../../examples/exam_eval.py
:lines: 25-27
Evaluate 'Seen' Split
---------------------
.. literalinclude:: ../../examples/exam_eval.py
:lines: 29-31

View File

@ -0,0 +1,26 @@
.. _example_generate_rectangle_labels:
Generating Rectangle Grasp Labels
=================================
You can generate the rectangle grasp labels by yourself.
Import necessary libs:
.. literalinclude:: ../../examples/exam_generate_rectangle_grasp.py
:lines: 4-11
Setup how many processes to use in generating the labels.
.. literalinclude:: ../../examples/exam_generate_rectangle_grasp.py
:lines: 13-15
The function to generate labels.
.. literalinclude:: ../../examples/exam_generate_rectangle_grasp.py
:lines: 17-31
Run the function for each scene and camera.
.. literalinclude:: ../../examples/exam_generate_rectangle_grasp.py
:lines: 33-52

View File

@ -0,0 +1,30 @@
.. _example_loadGrasp:
Loading Grasp Labels
====================
Both `6d` and `rect` format labels can be loaded.
First, import relative libs.
.. literalinclude:: ../../examples/exam_loadGrasp.py
:lines: 4-7
Then, get a GraspNet instance and setup parameters.
.. literalinclude:: ../../examples/exam_loadGrasp.py
:lines: 11-19
Load GraspLabel in `6d` format and visulize the result.
.. literalinclude:: ../../examples/exam_loadGrasp.py
:lines: 21-29
.. image:: _static/6d_example.png
Load GraspLabel in `rect` format and visulize the result.
.. literalinclude:: ../../examples/exam_loadGrasp.py
:lines: 31-40
.. image:: _static/rect_example.png

View File

@ -0,0 +1,112 @@
.. _example_nms:
Apply NMS on Grasps
===================
Get a GraspNet instance.
.. literalinclude:: ../../examples/exam_nms.py
:lines: 4-19
Loading and visualizing grasp lables before NMS.
.. literalinclude:: ../../examples/exam_nms.py
:lines: 21-29
::
6d grasp:
----------
Grasp Group, Number=90332:
Grasp: score:0.9000000357627869, width:0.11247877031564713, height:0.019999999552965164, depth:0.029999999329447746, translation:[-0.09166837 -0.16910084 0.39480919]
rotation:
[[-0.81045675 -0.57493848 0.11227506]
[ 0.49874267 -0.77775514 -0.38256073]
[ 0.30727136 -0.25405255 0.91708326]]
object id:66
Grasp: score:0.9000000357627869, width:0.10030215978622437, height:0.019999999552965164, depth:0.019999999552965164, translation:[-0.09166837 -0.16910084 0.39480919]
rotation:
[[-0.73440629 -0.67870212 0.0033038 ]
[ 0.64608938 -0.70059127 -0.3028869 ]
[ 0.20788456 -0.22030747 0.95302087]]
object id:66
Grasp: score:0.9000000357627869, width:0.08487851172685623, height:0.019999999552965164, depth:0.019999999552965164, translation:[-0.10412319 -0.13797761 0.38312319]
rotation:
[[ 0.03316294 0.78667939 -0.61647028]
[-0.47164679 0.55612743 0.68430364]
[ 0.88116372 0.26806271 0.38947764]]
object id:66
......
Grasp: score:0.9000000357627869, width:0.11909123510122299, height:0.019999999552965164, depth:0.019999999552965164, translation:[-0.05140382 0.11790846 0.48782501]
rotation:
[[-0.71453273 0.63476181 -0.2941435 ]
[-0.07400083 0.3495101 0.93400562]
[ 0.69567728 0.68914449 -0.20276351]]
object id:14
Grasp: score:0.9000000357627869, width:0.10943549126386642, height:0.019999999552965164, depth:0.019999999552965164, translation:[-0.05140382 0.11790846 0.48782501]
rotation:
[[ 0.08162415 0.4604325 -0.88393396]
[-0.52200603 0.77526748 0.3556262 ]
[ 0.84902728 0.4323912 0.30362913]]
object id:14
Grasp: score:0.9000000357627869, width:0.11654743552207947, height:0.019999999552965164, depth:0.009999999776482582, translation:[-0.05140382 0.11790846 0.48782501]
rotation:
[[-0.18380146 0.39686993 -0.89928377]
[-0.61254776 0.66926688 0.42055583]
[ 0.76876676 0.62815309 0.12008961]]
object id:14
------------
.. image:: _static/before_nms.png
Apply nms to GraspGroup and visualizing the result.
.. literalinclude:: ../../examples/exam_nms.py
:lines: 31-38
::
grasp after nms:
----------
Grasp Group, Number=358:
Grasp: score:1.0, width:0.11948642134666443, height:0.019999999552965164, depth:0.03999999910593033, translation:[-0.00363996 0.03692623 0.3311775 ]
rotation:
[[ 0.32641056 -0.8457799 0.42203382]
[-0.68102902 -0.52005678 -0.51550031]
[ 0.65548128 -0.11915252 -0.74575269]]
object id:0
Grasp: score:1.0, width:0.12185929715633392, height:0.019999999552965164, depth:0.009999999776482582, translation:[-0.03486454 0.08384828 0.35117128]
rotation:
[[-0.00487804 -0.8475557 0.53068405]
[-0.27290785 -0.50941664 -0.81609803]
[ 0.96202785 -0.14880882 -0.22881967]]
object id:0
Grasp: score:1.0, width:0.04842342436313629, height:0.019999999552965164, depth:0.019999999552965164, translation:[0.10816982 0.10254505 0.50272578]
rotation:
[[-0.98109186 -0.01696888 -0.19279723]
[-0.1817532 0.42313483 0.88765001]
[ 0.06651681 0.90590769 -0.41821831]]
object id:20
......
Grasp: score:0.9000000357627869, width:0.006192661356180906, height:0.019999999552965164, depth:0.009999999776482582, translation:[0.0122985 0.29616502 0.53319722]
rotation:
[[-0.26423979 0.39734706 0.87880182]
[-0.95826042 -0.00504095 -0.28585231]
[-0.10915259 -0.91765451 0.38209397]]
object id:46
Grasp: score:0.9000000357627869, width:0.024634981527924538, height:0.019999999552965164, depth:0.009999999776482582, translation:[0.11430283 0.18761221 0.51991153]
rotation:
[[-0.17379239 -0.96953499 0.17262182]
[-0.9434278 0.11365268 -0.31149188]
[ 0.28238329 -0.2169912 -0.93443805]]
object id:70
Grasp: score:0.9000000357627869, width:0.03459500893950462, height:0.019999999552965164, depth:0.009999999776482582, translation:[0.02079188 0.11184558 0.50796509]
rotation:
[[ 0.38108557 -0.27480939 0.88275337]
[-0.92043257 -0.20266907 0.33425891]
[ 0.08704928 -0.93989623 -0.33017775]]
object id:20
----------
.. image:: _static/after_nms.png

View File

@ -0,0 +1,39 @@
.. _example_vis:
Visualization of Dataset
========================
Get a GraspNet instance.
.. literalinclude:: ../../examples/exam_vis.py
:lines: 7-14
Show grasp labels on a object.
.. literalinclude:: ../../examples/exam_vis.py
:lines: 16-17
.. image:: _static/vis_single.png
Show 6D poses of objects in a scene.
.. literalinclude:: ../../examples/exam_vis.py
:lines: 19-20
.. image:: _static/vis_6d.png
Show Rectangle grasp labels in a scene.
.. literalinclude:: ../../examples/exam_vis.py
:lines: 22-23
.. image:: _static/vis_rect.png
Show 6D grasp labels in a scene.
.. literalinclude:: ../../examples/exam_vis.py
:lines: 25-26
.. image:: _static/vis_grasp.png

View File

@ -0,0 +1,178 @@
.. grasp_format:
Grasp Label Format
==================
Raw Label Format
----------------
The raw label is composed of two parts, i.e. labels for all grasp candidates on each object and collision masks for each scene.
Labels on Objects
^^^^^^^^^^^^^^^^^
The raw label on each object is a list of numpy arrays.
::
>>> import numpy as np
>>> l = np.load('000_labels.npz') # GRASPNET_ROOT/grasp_label/000_labels.npz
>>> l.files
['points', 'offsets', 'collision', 'scores']
>>> l['points'].shape
(3459, 3)
>>> l['offsets'].shape
(3459, 300, 12, 4, 3)
>>> l['collision'].shape
(3459, 300, 12, 4)
>>> l['collision'].dtype
dtype('bool')
>>> l['scores'].shape
(3459, 300, 12, 4)
>>> l['scores'][0][0][0][0]
-1.0
- 'points' records the grasp center point coordinates in model frame.
- 'offsets' records the in-plane rotation, depth and width of the gripper respectively in the last dimension.
- 'collision' records the bool mask for if the grasp pose collides with the model.
- 'scores' records the minimum coefficient of friction between the gripper and object to achieve a stable grasp.
.. note::
In the raw label, the **lower** score the grasp has, the **better** it is. However, -1.0 score means the grasp pose is totally not acceptable.
300, 12, 4 denote view id, in-plane rotation id and depth id respectively. The views are defined here in graspnetAPI/utils/utils.py.
.. literalinclude:: ../../graspnetAPI/utils/utils.py
:lines: 51-58
:linenos:
Collision Masks on Each Scene
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Collision mask on each scene is a list of numpy arrays.
::
>>> import numpy as np
>>> c = np.load('collision_labels.npz') # GRASPNET_ROOT/collision_label/scene_0000/collision_labels.npz
>>> c.files
['arr_0', 'arr_4', 'arr_5', 'arr_2', 'arr_3', 'arr_7', 'arr_1', 'arr_8', 'arr_6']
>>> c['arr_0'].shape
(487, 300, 12, 4)
>>> c['arr_0'].dtype
dtype('bool')
>>> c['arr_0'][10][20][3]
array([ True, True, True, True])
'arr_i' is the collision mask for the `i` th object in the `object_id_list.txt` for each scene whose shape is (num_points, 300, 12, 4).
num_points, 300, 12, 4 denote the number of points in the object, view id, in-plane rotation id and depth id respectively.
Users can refer to :py:func:`graspnetAPI.GraspNet.loadGrasp` for more details of how to use the labels.
API Loaded Labels
-----------------
Dealing with the raw labels are time-consuming and need high familiarity with graspnet.
So the API also provides an easy access to the labels.
By calling :py:func:`graspnetAPI.GraspNet.loadGrasp`, users can get all the positive grasp labels in a scene with their parameters and scores.
There are totally four kinds of data structures for loaded grasp labels: **Grasp**, **GraspGroup**, **RectGrasp** and **RectGraspGroup**.
The internal data format of each class is a numpy array which is more efficient than the Python list.
Their definitions are given in grasp.py
Example Labels
^^^^^^^^^^^^^^
Before looking into the details, an example is given below.
Loading a GraspGroup instance.
.. literalinclude:: ../../examples/exam_grasp_format.py
:lines: 1-27
Users can access elements by index or slice.
.. literalinclude:: ../../examples/exam_grasp_format.py
:lines: 29-35
Each element of GraspGroup is a Grasp instance.
The properties of Grasp can be accessed via provided methods.
.. literalinclude:: ../../examples/exam_grasp_format.py
:lines: 37-46
RectGrasp is the class for rectangle grasps. The format is different from Grasp.
But the provided APIs are similar.
.. literalinclude:: ../../examples/exam_grasp_format.py
:lines: 49-65
6D Grasp
^^^^^^^^
Actually, 17 float numbers are used to define a general 6d grasp.
The width, height, depth, score and attached object id are also part of the definition.
.. note::
In the loaded label, the **higher** score the grasp has, the **better** it is which is different from raw labels. Actually, score = 1.1 - raw_score (which is the coefficient of friction)
.. literalinclude:: ../../graspnetAPI/graspnet.py
:lines: 635-637
:emphasize-lines: 2
The detailed defition of each parameter is shown in the figure.
.. image:: _static/grasp_definition.png
.. literalinclude:: ../../graspnetAPI/grasp.py
:lines: 14-36
6D Grasp Group
^^^^^^^^^^^^^^
Usually, there are a lot of grasps in a scene, :py:class:`GraspGroup` is a class for these grasps.
Compared with :py:class:`Grasp`, :py:class:`GraspGroup` contains a 2D numpy array, the additional dimension is the index for each grasp.
.. literalinclude:: ../../graspnetAPI/grasp.py
:lines: 201-218
Common operations on a list such as indexing, slicing and sorting are implemented.
Besides, one important function is that users can **dump** a GraspGroup into a numpy file and **load** it in another program by calling :py:func:`GraspGroup.save_npy` and :py:func:`GraspGroup.from_npy`.
Rectangle Grasp
^^^^^^^^^^^^^^^
7 float numbers are used to define a general rectangle grasp, i.e. the center point, the open point, height, score and the attached object id.
The detailed definition of each parameter is shown in the figure above and below and the coordinates for center point and open point are in the pixel frame.
.. image:: _static/rect_grasp_definition.png
.. literalinclude:: ../../graspnetAPI/grasp.py
:lines: 553-572
Rectangle Grasp Group
^^^^^^^^^^^^^^^^^^^^^
The format for :py:class:`RectGraspGroup` is similar to that of :py:class:`RectGrasp` and :py:class:`GraspGroup`.
.. literalinclude:: ../../graspnetAPI/grasp.py
:lines: 752-769
.. note::
We recommend users to access and modify the labels by provided functions although users can also manipulate the data directly but it is **Not Recommended**.
Please refer to the Python API for more details.
Grasp and GraspGroup Transformation
-----------------------------------
Users can transform a Grasp or GraspGroup giving a 4x4 matrix.
.. literalinclude:: ../../examples/exam_grasp_format.py
:lines: 67-95
.. image:: _static/transformation.png

View File

@ -0,0 +1,46 @@
graspnetAPI package
===================
Subpackages
-----------
.. toctree::
:maxdepth: 4
graspnetAPI.utils
Submodules
----------
graspnetAPI.grasp module
------------------------
.. automodule:: graspnetAPI.grasp
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.graspnet module
---------------------------
.. automodule:: graspnetAPI.graspnet
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.graspnet\_eval module
---------------------------------
.. automodule:: graspnetAPI.graspnet_eval
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: graspnetAPI
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,54 @@
graspnetAPI.utils.dexnet.grasping.meshpy package
================================================
Submodules
----------
graspnetAPI.utils.dexnet.grasping.meshpy.mesh module
----------------------------------------------------
.. automodule:: graspnetAPI.utils.dexnet.grasping.meshpy.mesh
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.dexnet.grasping.meshpy.obj\_file module
---------------------------------------------------------
.. automodule:: graspnetAPI.utils.dexnet.grasping.meshpy.obj_file
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.dexnet.grasping.meshpy.sdf module
---------------------------------------------------
.. automodule:: graspnetAPI.utils.dexnet.grasping.meshpy.sdf
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.dexnet.grasping.meshpy.sdf\_file module
---------------------------------------------------------
.. automodule:: graspnetAPI.utils.dexnet.grasping.meshpy.sdf_file
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.dexnet.grasping.meshpy.stable\_pose module
------------------------------------------------------------
.. automodule:: graspnetAPI.utils.dexnet.grasping.meshpy.stable_pose
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: graspnetAPI.utils.dexnet.grasping.meshpy
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,70 @@
graspnetAPI.utils.dexnet.grasping package
=========================================
Subpackages
-----------
.. toctree::
:maxdepth: 4
graspnetAPI.utils.dexnet.grasping.meshpy
Submodules
----------
graspnetAPI.utils.dexnet.grasping.contacts module
-------------------------------------------------
.. automodule:: graspnetAPI.utils.dexnet.grasping.contacts
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.dexnet.grasping.grasp module
----------------------------------------------
.. automodule:: graspnetAPI.utils.dexnet.grasping.grasp
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.dexnet.grasping.grasp\_quality\_config module
---------------------------------------------------------------
.. automodule:: graspnetAPI.utils.dexnet.grasping.grasp_quality_config
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.dexnet.grasping.grasp\_quality\_function module
-----------------------------------------------------------------
.. automodule:: graspnetAPI.utils.dexnet.grasping.grasp_quality_function
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.dexnet.grasping.graspable\_object module
----------------------------------------------------------
.. automodule:: graspnetAPI.utils.dexnet.grasping.graspable_object
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.dexnet.grasping.quality module
------------------------------------------------
.. automodule:: graspnetAPI.utils.dexnet.grasping.quality
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: graspnetAPI.utils.dexnet.grasping
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,38 @@
graspnetAPI.utils.dexnet package
================================
Subpackages
-----------
.. toctree::
:maxdepth: 4
graspnetAPI.utils.dexnet.grasping
Submodules
----------
graspnetAPI.utils.dexnet.abstractstatic module
----------------------------------------------
.. automodule:: graspnetAPI.utils.dexnet.abstractstatic
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.dexnet.constants module
-----------------------------------------
.. automodule:: graspnetAPI.utils.dexnet.constants
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: graspnetAPI.utils.dexnet
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,86 @@
graspnetAPI.utils package
=========================
Subpackages
-----------
.. toctree::
:maxdepth: 4
graspnetAPI.utils.dexnet
Submodules
----------
graspnetAPI.utils.config module
-------------------------------
.. automodule:: graspnetAPI.utils.config
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.eval\_utils module
------------------------------------
.. automodule:: graspnetAPI.utils.eval_utils
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.pose module
-----------------------------
.. automodule:: graspnetAPI.utils.pose
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.rotation module
---------------------------------
.. automodule:: graspnetAPI.utils.rotation
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.trans3d module
--------------------------------
.. automodule:: graspnetAPI.utils.trans3d
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.utils module
------------------------------
.. automodule:: graspnetAPI.utils.utils
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.vis module
----------------------------
.. automodule:: graspnetAPI.utils.vis
:members:
:undoc-members:
:show-inheritance:
graspnetAPI.utils.xmlhandler module
-----------------------------------
.. automodule:: graspnetAPI.utils.xmlhandler
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: graspnetAPI.utils
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,48 @@
.. graspnetAPI documentation master file, created by
sphinx-quickstart on Tue Nov 3 13:04:51 2020.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to graspnetAPI's documentation!
=======================================
.. toctree::
:maxdepth: 2
:caption: Contents:
about
install
grasp_format
Examples
=========
.. toctree::
:maxdepth: 1
:caption: Examples
example_check_data
example_generate_rectangle_labels
example_loadGrasp
example_vis
example_nms
example_convert
example_eval
Python API
==========
.. toctree::
:maxdepth: 1
:caption: Modules
graspnetAPI
graspnetAPI.utils
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -0,0 +1,61 @@
Installation
============
.. note::
Only Python 3 on Linux is supported.
Prerequisites
^^^^^^^^^^^^^
Python version under 3.6 is not tested.
Dataset
^^^^^^^
Download
--------
Download the dataset at https://graspnet.net/datasets.html
Unzip
-----
Unzip the files as shown in https://graspnet.net/datasets.html.
Rectangle Grasp Labels
----------------------
Rectangle grasp labels are optional if you need labels in this format.
You can both generate the labels or download the file_.
If you want to generate the labels by yourself, you may refer to :ref:`example_generate_rectangle_labels`.
.. note::
Generating rectangle grasp labels may take a long time.
After generating the labels or unzipping the labels, you need to run copy_rect_labels.py_ to copy rectangle grasp labels to corresponding folders.
.. _copy_rect_labels.py: https://github.com/graspnet/graspnetAPI/blob/master/copy_rect_labels.py
.. _file: https://graspnet.net/datasets.html
Dexnet Model Cache
------------------
Dexnet model cache is optional without which the evaluation will be much slower(about 10x time slower).
You can both download the file or generate it by yourself by running gen_pickle_dexmodel.py_(recommended).
.. _gen_pickle_dexmodel.py: https://github.com/graspnet/graspnetAPI/blob/master/gen_pickle_dexmodel.py
Install API
^^^^^^^^^^^
You may install using pip::
pip install graspnetAPI
You can also install from source::
git clone https://github.com/graspnet/graspnetAPI.git
cd graspnetAPI/
pip install .

View File

@ -0,0 +1,7 @@
graspnetAPI
===========
.. toctree::
:maxdepth: 4
graspnetAPI

View File

@ -0,0 +1,22 @@
__author__ = 'mhgou'
__version__ = '1.0'
from graspnetAPI import GraspNet
# GraspNetAPI example for checking the data completeness.
# change the graspnet_root path
if __name__ == '__main__':
####################################################################
graspnet_root = '/home/gmh/graspnet' ### ROOT PATH FOR GRASPNET ###
####################################################################
g = GraspNet(graspnet_root, 'kinect', 'all')
if g.checkDataCompleteness():
print('Check for kinect passed')
g = GraspNet(graspnet_root, 'realsense', 'all')
if g.checkDataCompleteness():
print('Check for realsense passed')

View File

@ -0,0 +1,76 @@
__author__ = 'mhgou'
__version__ = '1.0'
from graspnetAPI import GraspNet
import cv2
import open3d as o3d
# GraspNetAPI example for checking the data completeness.
# change the graspnet_root path
camera = 'kinect'
sceneId = 5
annId = 3
####################################################################
graspnet_root = '/home/gmh/graspnet' # ROOT PATH FOR GRASPNET
####################################################################
g = GraspNet(graspnet_root, camera = camera, split = 'all')
bgr = g.loadBGR(sceneId = sceneId, camera = camera, annId = annId)
depth = g.loadDepth(sceneId = sceneId, camera = camera, annId = annId)
# Rect to 6d
rect_grasp_group = g.loadGrasp(sceneId = sceneId, camera = camera, annId = annId, fric_coef_thresh = 0.2, format = 'rect')
# RectGrasp to Grasp
rect_grasp = rect_grasp_group.random_sample(1)[0]
img = rect_grasp.to_opencv_image(bgr)
cv2.imshow('rect grasp', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
grasp = rect_grasp.to_grasp(camera, depth)
if grasp is not None:
geometry = []
geometry.append(g.loadScenePointCloud(sceneId, camera, annId))
geometry.append(grasp.to_open3d_geometry())
o3d.visualization.draw_geometries(geometry)
else:
print('No result because the depth is invalid, please try again!')
# RectGraspGroup to GraspGroup
sample_rect_grasp_group = rect_grasp_group.random_sample(20)
img = sample_rect_grasp_group.to_opencv_image(bgr)
cv2.imshow('rect grasp', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
grasp_group = sample_rect_grasp_group.to_grasp_group(camera, depth)
if grasp_group is not None:
geometry = []
geometry.append(g.loadScenePointCloud(sceneId, camera, annId))
geometry += grasp_group.to_open3d_geometry_list()
o3d.visualization.draw_geometries(geometry)
# 6d to Rect
_6d_grasp_group = g.loadGrasp(sceneId = sceneId, camera = camera, annId = annId, fric_coef_thresh = 0.2, format = '6d')
# Grasp to RectGrasp conversion is not applicable as only very few 6d grasp can be converted to rectangle grasp.
# GraspGroup to RectGraspGroup
sample_6d_grasp_group = _6d_grasp_group.random_sample(20)
geometry = []
geometry.append(g.loadScenePointCloud(sceneId, camera, annId))
geometry += sample_6d_grasp_group.to_open3d_geometry_list()
o3d.visualization.draw_geometries(geometry)
rect_grasp_group = _6d_grasp_group.to_rect_grasp_group(camera)
img = rect_grasp_group.to_opencv_image(bgr)
cv2.imshow('rect grasps', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

View File

@ -0,0 +1,31 @@
__author__ = 'mhgou'
__version__ = '1.0'
# GraspNetAPI example for evaluate grasps for a scene.
# change the graspnet_root path
import numpy as np
from graspnetAPI import GraspNetEval
####################################################################
graspnet_root = '/home/gmh/graspnet' # ROOT PATH FOR GRASPNET
dump_folder = '/home/gmh/git/rgbd_graspnet/dump_affordance_iounan/' # ROOT PATH FOR DUMP
####################################################################
sceneId = 121
camera = 'kinect'
ge_k = GraspNetEval(root = graspnet_root, camera = 'kinect', split = 'test')
ge_r = GraspNetEval(root = graspnet_root, camera = 'realsense', split = 'test')
# eval a single scene
print('Evaluating scene:{}, camera:{}'.format(sceneId, camera))
acc = ge_k.eval_scene(scene_id = sceneId, dump_folder = dump_folder)
np_acc = np.array(acc)
print('mean accuracy:{}'.format(np.mean(np_acc)))
# # eval all data for kinect
# print('Evaluating kinect')
# res, ap = ge_k.eval_all(dump_folder, proc = 24)
# # eval 'seen' split for realsense
# print('Evaluating realsense')
# res, ap = ge_r.eval_seen(dump_folder, proc = 24)

View File

@ -0,0 +1,52 @@
__author__ = 'mhgou'
__version__ = '1.0'
# GraspNetAPI example for generating rectangle grasp from 6d grasp.
# change the graspnet_root path and NUM_PROCESS
from graspnetAPI import GraspNet
from graspnetAPI.graspnet import TOTAL_SCENE_NUM
import os
import numpy as np
from tqdm import tqdm
######################################################################
NUM_PROCESS = 24 # change NUM_PROCESS to the number of cores to use. #
######################################################################
def generate_scene_rectangle_grasp(sceneId, dump_folder, camera):
g = GraspNet(graspnet_root, camera=camera, split='all')
objIds = g.getObjIds(sceneIds = sceneId)
grasp_labels = g.loadGraspLabels(objIds)
collision_labels = g.loadCollisionLabels(sceneIds = sceneId)
scene_dir = os.path.join(dump_folder,'scene_%04d' % sceneId)
if not os.path.exists(scene_dir):
os.mkdir(scene_dir)
camera_dir = os.path.join(scene_dir, camera)
if not os.path.exists(camera_dir):
os.mkdir(camera_dir)
for annId in tqdm(range(256), 'Scene:{}, Camera:{}'.format(sceneId, camera)):
_6d_grasp = g.loadGrasp(sceneId = sceneId, annId = annId, format = '6d', camera = camera, grasp_labels = grasp_labels, collision_labels = collision_labels, fric_coef_thresh = 1.0)
rect_grasp_group = _6d_grasp.to_rect_grasp_group(camera)
rect_grasp_group.save_npy(os.path.join(camera_dir, '%04d.npy' % annId))
if __name__ == '__main__':
####################################################################
graspnet_root = '/home/minghao/graspnet' # ROOT PATH FOR GRASPNET ##
####################################################################
dump_folder = 'rect_labels'
if not os.path.exists(dump_folder):
os.mkdir(dump_folder)
if NUM_PROCESS > 1:
from multiprocessing import Pool
pool = Pool(24)
for camera in ['realsense', 'kinect']:
for sceneId in range(120):
pool.apply_async(func = generate_scene_rectangle_grasp, args = (sceneId, dump_folder, camera))
pool.close()
pool.join()
else:
generate_scene_rectangle_grasp(sceneId, dump_folder, camera)

View File

@ -0,0 +1,95 @@
__author__ = 'mhgou'
__version__ = '1.0'
from graspnetAPI import GraspNet, Grasp, GraspGroup
import open3d as o3d
import cv2
import numpy as np
# GraspNetAPI example for loading grasp for a scene.
# change the graspnet_root path
####################################################################
graspnet_root = '/disk1/graspnet' # ROOT PATH FOR GRASPNET
####################################################################
sceneId = 1
annId = 3
# initialize a GraspNet instance
g = GraspNet(graspnet_root, camera='kinect', split='train')
# load grasps of scene 1 with annotation id = 3, camera = kinect and fric_coef_thresh = 0.2
_6d_grasp = g.loadGrasp(sceneId = sceneId, annId = annId, format = '6d', camera = 'kinect', fric_coef_thresh = 0.2)
print('6d grasp:\n{}'.format(_6d_grasp))
# _6d_grasp is an GraspGroup instance defined in grasp.py
print('_6d_grasp:\n{}'.format(_6d_grasp))
# index
grasp = _6d_grasp[0]
print('_6d_grasp[0](grasp):\n{}'.format(grasp))
# slice
print('_6d_grasp[0:2]:\n{}'.format(_6d_grasp[0:2]))
print('_6d_grasp[[0,1]]:\n{}'.format(_6d_grasp[[0,1]]))
# grasp is a Grasp instance defined in grasp.py
# access and set properties
print('grasp.translation={}'.format(grasp.translation))
grasp.translation = np.array([1.0, 2.0, 3.0])
print('After modification, grasp.translation={}'.format(grasp.translation))
print('grasp.rotation_matrix={}'.format(grasp.rotation_matrix))
grasp.rotation_matrix = np.eye(3).reshape((9))
print('After modification, grasp.rotation_matrix={}'.format(grasp.rotation_matrix))
print('grasp.width={}, height:{}, depth:{}, score:{}'.format(grasp.width, grasp.height, grasp.depth, grasp.score))
print('More operation on Grasp and GraspGroup can be seen in the API document')
# load rectangle grasps of scene 1 with annotation id = 3, camera = realsense and fric_coef_thresh = 0.2
rect_grasp_group = g.loadGrasp(sceneId = sceneId, annId = annId, format = 'rect', camera = 'realsense', fric_coef_thresh = 0.2)
print('rectangle grasp group:\n{}'.format(rect_grasp_group))
# rect_grasp is an RectGraspGroup instance defined in grasp.py
print('rect_grasp_group:\n{}'.format(rect_grasp_group))
# index
rect_grasp = rect_grasp_group[0]
print('rect_grasp_group[0](rect_grasp):\n{}'.format(rect_grasp))
# slice
print('rect_grasp_group[0:2]:\n{}'.format(rect_grasp_group[0:2]))
print('rect_grasp_group[[0,1]]:\n{}'.format(rect_grasp_group[[0,1]]))
# properties of rect_grasp
print('rect_grasp.center_point:{}, open_point:{}, height:{}, score:{}'.format(rect_grasp.center_point, rect_grasp.open_point, rect_grasp.height, rect_grasp.score))
# transform grasp
g = Grasp() # simple Grasp
frame = o3d.geometry.TriangleMesh.create_coordinate_frame(0.1)
# Grasp before transformation
o3d.visualization.draw_geometries([g.to_open3d_geometry(), frame])
g.translation = np.array((0,0,0.01))
# setup a transformation matrix
T = np.eye(4)
T[:3,3] = np.array((0.01, 0.02, 0.03))
T[:3,:3] = np.array([[0,0,1.0],[1,0,0],[0,1,0]])
g.transform(T)
# Grasp after transformation
o3d.visualization.draw_geometries([g.to_open3d_geometry(), frame])
g1 = Grasp()
gg = GraspGroup()
gg.add(g)
gg.add(g1)
# GraspGroup before transformation
o3d.visualization.draw_geometries([*gg.to_open3d_geometry_list(), frame])
gg.transform(T)
# GraspGroup after transformation
o3d.visualization.draw_geometries([*gg.to_open3d_geometry_list(), frame])

View File

@ -0,0 +1,40 @@
__author__ = 'mhgou'
__version__ = '1.0'
from graspnetAPI import GraspNet
import open3d as o3d
import cv2
# GraspNetAPI example for loading grasp for a scene.
# change the graspnet_root path
####################################################################
graspnet_root = '/mnt/h/AI/Datasets/graspnet-1billion/test_seen' # ROOT PATH FOR GRASPNET
####################################################################
sceneId = 100
annId = 3
# initialize a GraspNet instance
g = GraspNet(graspnet_root, camera='kinect', split='test_seen')
# load grasps of scene 1 with annotation id = 3, camera = kinect and fric_coef_thresh = 0.2
_6d_grasp = g.loadGrasp(sceneId = sceneId, annId = annId, format = '6d', camera = 'kinect', fric_coef_thresh = 0.2)
print('6d grasp:\n{}'.format(_6d_grasp))
# visualize the grasps using open3d
geometries = []
geometries.append(g.loadScenePointCloud(sceneId = sceneId, annId = annId, camera = 'kinect'))
geometries += _6d_grasp.random_sample(numGrasp = 20).to_open3d_geometry_list()
o3d.visualization.draw_geometries(geometries)
# load rectangle grasps of scene 1 with annotation id = 3, camera = realsense and fric_coef_thresh = 0.2
rect_grasp = g.loadGrasp(sceneId = sceneId, annId = annId, format = 'rect', camera = 'realsense', fric_coef_thresh = 0.2)
print('rectangle grasp:\n{}'.format(rect_grasp))
# visualize the rectanglegrasps using opencv
bgr = g.loadBGR(sceneId = sceneId, annId = annId, camera = 'realsense')
img = rect_grasp.to_opencv_image(bgr, numGrasp = 20)
cv2.imshow('rectangle grasps', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

View File

@ -0,0 +1,38 @@
__author__ = 'mhgou'
__version__ = '1.0'
# GraspNetAPI example for grasp nms.
# change the graspnet_root path
####################################################################
graspnet_root = '/home/gmh/graspnet' # ROOT PATH FOR GRASPNET
####################################################################
sceneId = 1
annId = 3
from graspnetAPI import GraspNet
import open3d as o3d
import cv2
# initialize a GraspNet instance
g = GraspNet(graspnet_root, camera='kinect', split='train')
# load grasps of scene 1 with annotation id = 3, camera = kinect and fric_coef_thresh = 0.2
_6d_grasp = g.loadGrasp(sceneId = sceneId, annId = annId, format = '6d', camera = 'kinect', fric_coef_thresh = 0.2)
print('6d grasp:\n{}'.format(_6d_grasp))
# visualize the grasps using open3d
geometries = []
geometries.append(g.loadScenePointCloud(sceneId = sceneId, annId = annId, camera = 'kinect'))
geometries += _6d_grasp.random_sample(numGrasp = 1000).to_open3d_geometry_list()
o3d.visualization.draw_geometries(geometries)
nms_grasp = _6d_grasp.nms(translation_thresh = 0.1, rotation_thresh = 30 / 180.0 * 3.1416)
print('grasp after nms:\n{}'.format(nms_grasp))
# visualize the grasps using open3d
geometries = []
geometries.append(g.loadScenePointCloud(sceneId = sceneId, annId = annId, camera = 'kinect'))
geometries += nms_grasp.to_open3d_geometry_list()
o3d.visualization.draw_geometries(geometries)

View File

@ -0,0 +1,26 @@
__author__ = 'mhgou'
__version__ = '1.0'
# GraspNetAPI example for visualization.
# change the graspnet_root path
####################################################################
graspnet_root = '/mnt/h/AI/Datasets/graspnet-1billion/test_seen' # ROOT PATH FOR GRASPNET
####################################################################
from graspnetAPI import GraspNet
# initialize a GraspNet instance
g = GraspNet(graspnet_root, camera='kinect', split='test_seen')
# show object grasps
g.showObjGrasp(objIds = 0, show=True)
# show 6d poses
g.show6DPose(sceneIds = 0, show = True)
# show scene rectangle grasps
g.showSceneGrasp(sceneId = 0, camera = 'realsense', annId = 0, format = 'rect', numGrasp = 20)
# show scene 6d grasps(You may need to wait several minutes)
g.showSceneGrasp(sceneId = 4, camera = 'kinect', annId = 2, format = '6d')

View File

@ -0,0 +1,21 @@
__author__ = 'mhgou'
from graspnetAPI.utils.eval_utils import load_dexnet_model
from tqdm import tqdm
import pickle
import os
##### Change the root to your path #####
graspnet_root = '/home/gmh/graspnet'
##### Do NOT change this folder name #####
dex_folder = 'dex_models'
if not os.path.exists(dex_folder):
os.makedirs(dex_folder)
model_dir = os.path.join(graspnet_root, 'models')
for obj_id in tqdm(range(88), 'dump models'):
dex_model = load_dexnet_model(os.path.join(model_dir, '%03d' % obj_id, 'textured'))
with open(os.path.join(dex_folder, '%03d.pkl' % obj_id), 'wb') as f:
pickle.dump(dex_model, f)

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

View File

@ -0,0 +1,6 @@
__author__ = 'mhgou'
__version__ = '1.2.11'
from .graspnet import GraspNet
from .graspnet_eval import GraspNetEval
from .grasp import Grasp, GraspGroup, RectGrasp, RectGraspGroup

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,801 @@
__author__ = 'hsfang, mhgou, cxwang'
# Interface for accessing the GraspNet-1Billion dataset.
# Description and part of the codes modified from MSCOCO api
# GraspNet is an open project for general object grasping that is continuously enriched.
# Currently we release GraspNet-1Billion, a large-scale benchmark for general object grasping,
# as well as other related areas (e.g. 6D pose estimation, unseen object segmentation, etc.).
# graspnetapi is a Python API that # assists in loading, parsing and visualizing the
# annotations in GraspNet. Please visit https://graspnet.net/ for more information on GraspNet,
# including for the data, paper, and tutorials. The exact format of the annotations
# is also described on the GraspNet website. For example usage of the graspnetapi
# please see graspnetapi_demo.ipynb. In addition to this API, please download both
# the GraspNet images and annotations in order to run the demo.
# An alternative to using the API is to load the annotations directly
# into Python dictionary
# Using the API provides additional utility functions. Note that this API
# supports both *grasping* and *6d pose* annotations. In the case of
# 6d poses not all functions are defined (e.g. collisions are undefined).
# The following API functions are defined:
# GraspNet - GraspNet api class that loads GraspNet annotation file and prepare data structures.
# checkDataCompleteness- Check the file completeness of the dataset.
# getSceneIds - Get scene ids that satisfy given filter conditions.
# getObjIds - Get obj ids that satisfy given filter conditions.
# getDataIds - Get data ids that satisfy given filter conditions.
# loadBGR - Load image in BGR format.
# loadRGB - Load image in RGB format.
# loadDepth - Load depth image.
# loadMask - Load the segmentation masks.
# loadSceneModels - Load object models in a scene.
# loadScenePointCloud - Load point cloud constructed by the depth and color image.
# loadWorkSpace - Load the workspace bounding box.
# loadGraspLabels - Load grasp labels with the specified object ids.
# loadObjModels - Load object 3d mesh model with the specified object ids.
# loadObjTrimesh - Load object 3d mesh in Trimesh format.
# loadCollisionLabels - Load collision labels with the specified scene ids.
# loadGrasp - Load grasp labels with the specified scene and annotation id.
# loadData - Load data path with the specified data ids.
# showObjGrasp - Save visualization of the grasp pose of specified object ids.
# showSceneGrasp - Save visualization of the grasp pose of specified scene ids.
# show6DPose - Save visualization of the 6d pose of specified scene ids, project obj models onto pointcloud
# Throughout the API "ann"=annotation, "obj"=object, and "img"=image.
# GraspNet Toolbox. version 1.0
# Data, paper, and tutorials available at: https://graspnet.net/
# Code written by Hao-Shu Fang, Minghao Gou and Chenxi Wang, 2020.
# Licensed under the none commercial CC4.0 license [see https://graspnet.net/about]
import os
import numpy as np
from tqdm import tqdm
import open3d as o3d
import cv2
import trimesh
from .grasp import Grasp, GraspGroup, RectGrasp, RectGraspGroup, RECT_GRASP_ARRAY_LEN
from .utils.utils import transform_points, parse_posevector
from .utils.xmlhandler import xmlReader
TOTAL_SCENE_NUM = 190
GRASP_HEIGHT = 0.02
def _isArrayLike(obj):
return hasattr(obj, '__iter__') and hasattr(obj, '__len__')
class GraspNet():
def __init__(self, root, camera='kinect', split='train', sceneIds=[]):
'''
graspnetAPI main class.
**input**:
- camera: string of type of camera: "kinect" or "realsense"
- split: string of type of split of dataset: "all", "train", "test", "test_seen", "test_similar", "test_novel" or "custom"
- sceneIds: list of custom scene ids.
'''
assert camera in ['kinect', 'realsense'], 'camera should be kinect or realsense'
assert split in ['all', 'train', 'test', 'test_seen', 'test_similar', 'test_novel', "custom"], 'split should be all/train/test/test_seen/test_similar/test_novel'
self.root = root
self.camera = camera
self.split = split
self.collisionLabels = {}
if split == 'all':
self.sceneIds = list(range(TOTAL_SCENE_NUM))
elif split == 'train':
self.sceneIds = list(range(100))
elif split == 'test':
self.sceneIds = list(range(100, 190))
elif split == 'test_seen':
self.sceneIds = list(range(100, 130))
elif split == 'test_similar':
self.sceneIds = list(range(130, 160))
elif split == 'test_novel':
self.sceneIds = list(range(160, 190))
elif split == "custom":
self.sceneIds = sceneIds
self.rgbPath = []
self.depthPath = []
self.segLabelPath = []
self.metaPath = []
self.rectLabelPath = []
self.sceneName = []
self.annId = []
for i in tqdm(self.sceneIds, desc='Loading data path...'):
for img_num in range(256):
self.rgbPath.append(os.path.join(
root, 'scenes', 'scene_'+str(i).zfill(4), camera, 'rgb', str(img_num).zfill(4)+'.png'))
self.depthPath.append(os.path.join(
root, 'scenes', 'scene_'+str(i).zfill(4), camera, 'depth', str(img_num).zfill(4)+'.png'))
self.segLabelPath.append(os.path.join(
root, 'scenes', 'scene_'+str(i).zfill(4), camera, 'label', str(img_num).zfill(4)+'.png'))
self.metaPath.append(os.path.join(
root, 'scenes', 'scene_'+str(i).zfill(4), camera, 'meta', str(img_num).zfill(4)+'.mat'))
self.rectLabelPath.append(os.path.join(
root, 'scenes', 'scene_'+str(i).zfill(4), camera, 'rect', str(img_num).zfill(4)+'.npy'))
self.sceneName.append('scene_'+str(i).zfill(4))
self.annId.append(img_num)
self.objIds = self.getObjIds(self.sceneIds)
def __len__(self):
return len(self.depthPath)
def checkDataCompleteness(self):
'''
Check whether the dataset files are complete.
**Output:**
- bool, True for complete, False for not complete.
'''
error_flag = False
for obj_id in tqdm(range(88), 'Checking Models'):
if not os.path.exists(os.path.join(self.root, 'models','%03d' % obj_id, 'nontextured.ply')):
error_flag = True
print('No nontextured.ply For Object {}'.format(obj_id))
if not os.path.exists(os.path.join(self.root, 'models','%03d' % obj_id, 'textured.sdf')):
error_flag = True
print('No textured.sdf For Object {}'.format(obj_id))
if not os.path.exists(os.path.join(self.root, 'models','%03d' % obj_id, 'textured.obj')):
error_flag = True
print('No textured.obj For Object {}'.format(obj_id))
for obj_id in tqdm(range(88), 'Checking Grasp Labels'):
if not os.path.exists(os.path.join(self.root, 'grasp_label', '%03d_labels.npz' % obj_id)):
error_flag = True
print('No Grasp Label For Object {}'.format(obj_id))
for sceneId in tqdm(self.sceneIds, 'Checking Collosion Labels'):
if not os.path.exists(os.path.join(self.root, 'collision_label', 'scene_%04d' % sceneId, 'collision_labels.npz')):
error_flag = True
print('No Collision Labels For Scene {}'.format(sceneId))
for sceneId in tqdm(self.sceneIds, 'Checking Scene Datas'):
scene_dir = os.path.join(self.root, 'scenes', 'scene_%04d' % sceneId)
if not os.path.exists(os.path.join(scene_dir,'object_id_list.txt')):
error_flag = True
print('No Object Id List For Scene {}'.format(sceneId))
if not os.path.exists(os.path.join(scene_dir,'rs_wrt_kn.npy')):
error_flag = True
print('No rs_wrt_kn.npy For Scene {}'.format(sceneId))
for camera in [self.camera]:
camera_dir = os.path.join(scene_dir, camera)
if not os.path.exists(os.path.join(camera_dir,'cam0_wrt_table.npy')):
error_flag = True
print('No cam0_wrt_table.npy For Scene {}, Camera:{}'.format(sceneId, camera))
if not os.path.exists(os.path.join(camera_dir,'camera_poses.npy')):
error_flag = True
print('No camera_poses.npy For Scene {}, Camera:{}'.format(sceneId, camera))
if not os.path.exists(os.path.join(camera_dir,'camK.npy')):
error_flag = True
print('No camK.npy For Scene {}, Camera:{}'.format(sceneId, camera))
for annId in range(256):
if not os.path.exists(os.path.join(camera_dir,'rgb','%04d.png' % annId)):
error_flag = True
print('No RGB Image For Scene {}, Camera:{}, annotion:{}'.format(sceneId, camera, annId))
if not os.path.exists(os.path.join(camera_dir,'depth','%04d.png' % annId)):
error_flag = True
print('No Depth Image For Scene {}, Camera:{}, annotion:{}'.format(sceneId, camera, annId))
if not os.path.exists(os.path.join(camera_dir,'label','%04d.png' % annId)):
error_flag = True
print('No Mask Label image For Scene {}, Camera:{}, annotion:{}'.format(sceneId, camera, annId))
if not os.path.exists(os.path.join(camera_dir,'meta','%04d.mat' % annId)):
error_flag = True
print('No Meta Data For Scene {}, Camera:{}, annotion:{}'.format(sceneId, camera, annId))
if not os.path.exists(os.path.join(camera_dir,'annotations','%04d.xml' % annId)):
error_flag = True
print('No Annotations For Scene {}, Camera:{}, annotion:{}'.format(sceneId, camera, annId))
if not os.path.exists(os.path.join(camera_dir,'rect','%04d.npy' % annId)):
error_flag = True
print('No Rectangle Labels For Scene {}, Camera:{}, annotion:{}'.format(sceneId, camera, annId))
return not error_flag
def getSceneIds(self, objIds=None):
'''
**Input:**
- objIds: int or list of int of the object ids.
**Output:**
- a list of int of the scene ids that contains **all** the objects.
'''
if objIds is None:
return self.sceneIds
assert _isArrayLike(objIds) or isinstance(objIds, int), 'objIds must be integer or a list/numpy array of integers'
objIds = objIds if _isArrayLike(objIds) else [objIds]
sceneIds = []
for i in self.sceneIds:
f = open(os.path.join(self.root, 'scenes', 'scene_' + str(i).zfill(4), 'object_id_list.txt'))
idxs = [int(line.strip()) for line in f.readlines()]
check = all(item in idxs for item in objIds)
if check:
sceneIds.append(i)
return sceneIds
def getObjIds(self, sceneIds=None):
'''
**Input:**
- sceneIds: int or list of int of the scene ids.
**Output:**
- a list of int of the object ids in the given scenes.
'''
# get object ids in the given scenes
if sceneIds is None:
return self.objIds
assert _isArrayLike(sceneIds) or isinstance(sceneIds, int), 'sceneIds must be an integer or a list/numpy array of integers'
sceneIds = sceneIds if _isArrayLike(sceneIds) else [sceneIds]
objIds = []
for i in sceneIds:
f = open(os.path.join(self.root, 'scenes', 'scene_' + str(i).zfill(4), 'object_id_list.txt'))
idxs = [int(line.strip()) for line in f.readlines()]
objIds = list(set(objIds+idxs))
return objIds
def getDataIds(self, sceneIds=None):
'''
**Input:**
- sceneIds:int or list of int of the scenes ids.
**Output:**
- a list of int of the data ids. Data could be accessed by calling self.loadData(ids).
'''
# get index for datapath that contains the given scenes
if sceneIds is None:
return list(range(len(self.sceneName)))
ids = []
indexPosList = []
for i in sceneIds:
indexPosList += [ j for j in range(0,len(self.sceneName),256) if self.sceneName[j] == 'scene_'+str(i).zfill(4) ]
for idx in indexPosList:
ids += list(range(idx, idx+256))
return ids
def loadGraspLabels(self, objIds=None):
'''
**Input:**
- objIds: int or list of int of the object ids.
**Output:**
- a dict of grasplabels of each object.
'''
# load object-level grasp labels of the given obj ids
objIds = self.objIds if objIds is None else objIds
assert _isArrayLike(objIds) or isinstance(objIds, int), 'objIds must be an integer or a list/numpy array of integers'
objIds = objIds if _isArrayLike(objIds) else [objIds]
graspLabels = {}
for i in tqdm(objIds, desc='Loading grasping labels...'):
file = np.load(os.path.join(self.root, 'grasp_label', '{}_labels.npz'.format(str(i).zfill(3))))
graspLabels[i] = (file['points'].astype(np.float32), file['offsets'].astype(np.float32), file['scores'].astype(np.float32))
return graspLabels
def loadObjModels(self, objIds=None):
'''
**Function:**
- load object 3D models of the given obj ids
**Input:**
- objIDs: int or list of int of the object ids
**Output:**
- a list of open3d.geometry.PointCloud of the models
'''
objIds = self.objIds if objIds is None else objIds
assert _isArrayLike(objIds) or isinstance(objIds, int), 'objIds must be an integer or a list/numpy array of integers'
objIds = objIds if _isArrayLike(objIds) else [objIds]
models = []
for i in tqdm(objIds, desc='Loading objects...'):
plyfile = os.path.join(self.root, 'models','%03d' % i, 'nontextured.ply')
models.append(o3d.io.read_point_cloud(plyfile))
return models
def loadObjTrimesh(self, objIds=None):
'''
**Function:**
- load object 3D trimesh of the given obj ids
**Input:**
- objIDs: int or list of int of the object ids
**Output:**
- a list of trimesh.Trimesh of the models
'''
objIds = self.objIds if objIds is None else objIds
assert _isArrayLike(objIds) or isinstance(objIds, int), 'objIds must be an integer or a list/numpy array of integers'
objIds = objIds if _isArrayLike(objIds) else [objIds]
models = []
for i in tqdm(objIds, desc='Loading objects...'):
plyfile = os.path.join(self.root, 'models','%03d' % i, 'nontextured.ply')
models.append(trimesh.load(plyfile))
return models
def loadCollisionLabels(self, sceneIds=None):
'''
**Input:**
- sceneIds: int or list of int of the scene ids.
**Output:**
- dict of the collision labels.
'''
sceneIds = self.sceneIds if sceneIds is None else sceneIds
assert _isArrayLike(sceneIds) or isinstance(sceneIds, int), 'sceneIds must be an integer or a list/numpy array of integers'
sceneIds = sceneIds if _isArrayLike(sceneIds) else [sceneIds]
collisionLabels = {}
for sid in tqdm(sceneIds, desc='Loading collision labels...'):
labels = np.load(os.path.join(self.root, 'collision_label','scene_'+str(sid).zfill(4), 'collision_labels.npz'))
collisionLabel = []
for j in range(len(labels)):
collisionLabel.append(labels['arr_{}'.format(j)])
collisionLabels['scene_'+str(sid).zfill(4)] = collisionLabel
return collisionLabels
def loadRGB(self, sceneId, camera, annId):
'''
**Input:**
- sceneId: int of the scene index.
- camera: string of type of camera, 'realsense' or 'kinect'
- annId: int of the annotation index.
**Output:**
- numpy array of the rgb in RGB order.
'''
return cv2.cvtColor(cv2.imread(os.path.join(self.root, 'scenes', 'scene_%04d' % sceneId, camera, 'rgb', '%04d.png' % annId)), cv2.COLOR_BGR2RGB)
def loadBGR(self, sceneId, camera, annId):
'''
**Input:**
- sceneId: int of the scene index.
- camera: string of type of camera, 'realsense' or 'kinect'
- annId: int of the annotation index.
**Output:**
- numpy array of the rgb in BGR order.
'''
return cv2.imread(os.path.join(self.root, 'scenes', 'scene_%04d' % sceneId, camera, 'rgb', '%04d.png' % annId))
def loadDepth(self, sceneId, camera, annId):
'''
**Input:**
- sceneId: int of the scene index.
- camera: string of type of camera, 'realsense' or 'kinect'
- annId: int of the annotation index.
**Output:**
- numpy array of the depth with dtype = np.uint16
'''
return cv2.imread(os.path.join(self.root, 'scenes', 'scene_%04d' % sceneId, camera, 'depth', '%04d.png' % annId), cv2.IMREAD_UNCHANGED)
def loadMask(self, sceneId, camera, annId):
'''
**Input:**
- sceneId: int of the scene index.
- camera: string of type of camera, 'realsense' or 'kinect'
- annId: int of the annotation index.
**Output:**
- numpy array of the mask with dtype = np.uint16
'''
return cv2.imread(os.path.join(self.root, 'scenes', 'scene_%04d' % sceneId, camera, 'label', '%04d.png' % annId), cv2.IMREAD_UNCHANGED)
def loadWorkSpace(self, sceneId, camera, annId):
'''
**Input:**
- sceneId: int of the scene index.
- camera: string of type of camera, 'realsense' or 'kinect'
- annId: int of the annotation index.
**Output:**
- tuple of the bounding box coordinates (x1, y1, x2, y2).
'''
mask = self.loadMask(sceneId, camera, annId)
maskx = np.any(mask, axis=0)
masky = np.any(mask, axis=1)
x1 = np.argmax(maskx)
y1 = np.argmax(masky)
x2 = len(maskx) - np.argmax(maskx[::-1])
y2 = len(masky) - np.argmax(masky[::-1])
return (x1, y1, x2, y2)
def loadScenePointCloud(self, sceneId, camera, annId, align=False, format = 'open3d', use_workspace = False, use_mask = True, use_inpainting = False):
'''
**Input:**
- sceneId: int of the scene index.
- camera: string of type of camera, 'realsense' or 'kinect'
- annId: int of the annotation index.
- aligh: bool of whether align to the table frame.
- format: string of the returned type. 'open3d' or 'numpy'
- use_workspace: bool of whether crop the point cloud in the work space.
- use_mask: bool of whether crop the point cloud use mask(z>0), only open3d 0.9.0 is supported for False option.
Only turn to False if you know what you are doing.
- use_inpainting: bool of whether inpaint the depth image for the missing information.
**Output:**
- open3d.geometry.PointCloud instance of the scene point cloud.
- or tuple of numpy array of point locations and colors.
'''
colors = self.loadRGB(sceneId = sceneId, camera = camera, annId = annId).astype(np.float32) / 255.0
depths = self.loadDepth(sceneId = sceneId, camera = camera, annId = annId)
if use_inpainting:
fault_mask = depths < 200
depths[fault_mask] = 0
inpainting_mask = (np.abs(depths) < 10).astype(np.uint8)
depths = cv2.inpaint(depths, inpainting_mask, 5, cv2.INPAINT_NS)
intrinsics = np.load(os.path.join(self.root, 'scenes', 'scene_%04d' % sceneId, camera, 'camK.npy'))
fx, fy = intrinsics[0,0], intrinsics[1,1]
cx, cy = intrinsics[0,2], intrinsics[1,2]
s = 1000.0
if align:
camera_poses = np.load(os.path.join(self.root, 'scenes', 'scene_%04d' % sceneId, camera, 'camera_poses.npy'))
camera_pose = camera_poses[annId]
align_mat = np.load(os.path.join(self.root, 'scenes', 'scene_%04d' % sceneId, camera, 'cam0_wrt_table.npy'))
camera_pose = align_mat.dot(camera_pose)
xmap, ymap = np.arange(colors.shape[1]), np.arange(colors.shape[0])
xmap, ymap = np.meshgrid(xmap, ymap)
points_z = depths / s
points_x = (xmap - cx) / fx * points_z
points_y = (ymap - cy) / fy * points_z
# print(f'points_x.shape:{points_x.shape}')
# print(f'points_y.shape:{points_y.shape}')
# print(f'points_z.shape:{points_z.shape}')
if use_workspace:
(x1, y1, x2, y2) = self.loadWorkSpace(sceneId, camera, annId)
points_z = points_z[y1:y2,x1:x2]
points_x = points_x[y1:y2,x1:x2]
points_y = points_y[y1:y2,x1:x2]
colors = colors[y1:y2,x1:x2]
mask = (points_z > 0)
points = np.stack([points_x, points_y, points_z], axis=-1)
# print(f'points.shape:{points.shape}')
if use_mask:
points = points[mask]
colors = colors[mask]
else:
points = points.reshape((-1, 3))
colors = colors.reshape((-1, 3))
if align:
points = transform_points(points, camera_pose)
if format == 'open3d':
cloud = o3d.geometry.PointCloud()
cloud.points = o3d.utility.Vector3dVector(points)
cloud.colors = o3d.utility.Vector3dVector(colors)
return cloud
elif format == 'numpy':
return points, colors
else:
raise ValueError('Format must be either "open3d" or "numpy".')
def loadSceneModel(self, sceneId, camera = 'kinect', annId = 0, align = False):
'''
**Input:**
- sceneId: int of the scene index.
- camera: string of type of camera, 'realsense' or 'kinect'
- annId: int of the annotation index.
- align: bool of whether align to the table frame.
**Output:**
- open3d.geometry.PointCloud list of the scene models.
'''
if align:
camera_poses = np.load(os.path.join(self.root, 'scenes', 'scene_%04d' % sceneId, camera, 'camera_poses.npy'))
camera_pose = camera_poses[annId]
align_mat = np.load(os.path.join(self.root, 'scenes', 'scene_%04d' % sceneId, camera, 'cam0_wrt_table.npy'))
camera_pose = np.matmul(align_mat,camera_pose)
scene_reader = xmlReader(os.path.join(self.root, 'scenes', 'scene_%04d' % sceneId, camera, 'annotations', '%04d.xml'% annId))
posevectors = scene_reader.getposevectorlist()
obj_list = []
mat_list = []
model_list = []
pose_list = []
for posevector in posevectors:
obj_idx, pose = parse_posevector(posevector)
obj_list.append(obj_idx)
mat_list.append(pose)
for obj_idx, pose in zip(obj_list, mat_list):
plyfile = os.path.join(self.root, 'models', '%03d'%obj_idx, 'nontextured.ply')
model = o3d.io.read_point_cloud(plyfile)
points = np.array(model.points)
if align:
pose = np.dot(camera_pose, pose)
points = transform_points(points, pose)
model.points = o3d.utility.Vector3dVector(points)
model_list.append(model)
pose_list.append(pose)
return model_list
def loadGrasp(self, sceneId, annId=0, format = '6d', camera='kinect', grasp_labels = None, collision_labels = None, fric_coef_thresh=0.4):
'''
**Input:**
- sceneId: int of scene id.
- annId: int of annotation id.
- format: string of grasp format, '6d' or 'rect'.
- camera: string of camera type, 'kinect' or 'realsense'.
- grasp_labels: dict of grasp labels. Call self.loadGraspLabels if not given.
- collision_labels: dict of collision labels. Call self.loadCollisionLabels if not given.
- fric_coef_thresh: float of the frcition coefficient threshold of the grasp.
**ATTENTION**
the LOWER the friction coefficient is, the better the grasp is.
**Output:**
- If format == '6d', return a GraspGroup instance.
- If format == 'rect', return a RectGraspGroup instance.
'''
import numpy as np
assert format == '6d' or format == 'rect', 'format must be "6d" or "rect"'
if format == '6d':
from .utils.xmlhandler import xmlReader
from .utils.utils import get_obj_pose_list, generate_views, get_model_grasps, transform_points
from .utils.rotation import batch_viewpoint_params_to_matrix
camera_poses = np.load(os.path.join(self.root,'scenes','scene_%04d' %(sceneId,),camera, 'camera_poses.npy'))
camera_pose = camera_poses[annId]
scene_reader = xmlReader(os.path.join(self.root,'scenes','scene_%04d' %(sceneId,),camera,'annotations','%04d.xml' %(annId,)))
pose_vectors = scene_reader.getposevectorlist()
obj_list,pose_list = get_obj_pose_list(camera_pose,pose_vectors)
if grasp_labels is None:
print('warning: grasp_labels are not given, calling self.loadGraspLabels to retrieve them')
grasp_labels = self.loadGraspLabels(objIds = obj_list)
if collision_labels is None:
print('warning: collision_labels are not given, calling self.loadCollisionLabels to retrieve them')
collision_labels = self.loadCollisionLabels(sceneId)
num_views, num_angles, num_depths = 300, 12, 4
template_views = generate_views(num_views)
template_views = template_views[np.newaxis, :, np.newaxis, np.newaxis, :]
template_views = np.tile(template_views, [1, 1, num_angles, num_depths, 1])
collision_dump = collision_labels['scene_'+str(sceneId).zfill(4)]
# grasp = dict()
grasp_group = GraspGroup()
for i, (obj_idx, trans) in enumerate(zip(obj_list, pose_list)):
sampled_points, offsets, fric_coefs = grasp_labels[obj_idx]
collision = collision_dump[i]
point_inds = np.arange(sampled_points.shape[0])
num_points = len(point_inds)
target_points = sampled_points[:, np.newaxis, np.newaxis, np.newaxis, :]
target_points = np.tile(target_points, [1, num_views, num_angles, num_depths, 1])
views = np.tile(template_views, [num_points, 1, 1, 1, 1])
angles = offsets[:, :, :, :, 0]
depths = offsets[:, :, :, :, 1]
widths = offsets[:, :, :, :, 2]
mask1 = ((fric_coefs <= fric_coef_thresh) & (fric_coefs > 0) & ~collision)
target_points = target_points[mask1]
target_points = transform_points(target_points, trans)
target_points = transform_points(target_points, np.linalg.inv(camera_pose))
views = views[mask1]
angles = angles[mask1]
depths = depths[mask1]
widths = widths[mask1]
fric_coefs = fric_coefs[mask1]
Rs = batch_viewpoint_params_to_matrix(-views, angles)
Rs = np.matmul(trans[np.newaxis, :3, :3], Rs)
Rs = np.matmul(np.linalg.inv(camera_pose)[np.newaxis,:3,:3], Rs)
num_grasp = widths.shape[0]
scores = (1.1 - fric_coefs).reshape(-1,1)
widths = widths.reshape(-1,1)
heights = GRASP_HEIGHT * np.ones((num_grasp,1))
depths = depths.reshape(-1,1)
rotations = Rs.reshape((-1,9))
object_ids = obj_idx * np.ones((num_grasp,1), dtype=np.int32)
obj_grasp_array = np.hstack([scores, widths, heights, depths, rotations, target_points, object_ids]).astype(np.float32)
grasp_group.grasp_group_array = np.concatenate((grasp_group.grasp_group_array, obj_grasp_array))
return grasp_group
else:
# 'rect'
rect_grasps = RectGraspGroup(os.path.join(self.root,'scenes','scene_%04d' % sceneId,camera,'rect','%04d.npy' % annId))
return rect_grasps
def loadData(self, ids=None, *extargs):
'''
**Input:**
- ids: int or list of int of the the data ids.
- extargs: extra arguments. This function can also be called with loadData(sceneId, camera, annId)
**Output:**
- if ids is int, returns a tuple of data path
- if ids is not specified or is a list, returns a tuple of data path lists
'''
if ids is None:
return (self.rgbPath, self.depthPath, self.segLabelPath, self.metaPath, self.rectLabelPath, self.sceneName, self.annId)
if len(extargs) == 0:
if isinstance(ids, int):
return (self.rgbPath[ids], self.depthPath[ids], self.segLabelPath[ids], self.metaPath[ids], self.rectLabelPath[ids], self.sceneName[ids], self.annId[ids])
else:
return ([self.rgbPath[id] for id in ids],
[self.depthPath[id] for id in ids],
[self.segLabelPath[id] for id in ids],
[self.metaPath[id] for id in ids],
[self.rectLabelPath[id] for id in ids],
[self.sceneName[id] for id in ids],
[self.annId[id] for id in ids])
if len(extargs) == 2:
sceneId = ids
camera, annId = extargs
rgbPath = os.path.join(self.root, 'scenes', 'scene_'+str(sceneId).zfill(4), camera, 'rgb', str(annId).zfill(4)+'.png')
depthPath = os.path.join(self.root, 'scenes', 'scene_'+str(sceneId).zfill(4), camera, 'depth', str(annId).zfill(4)+'.png')
segLabelPath = os.path.join(self.root, 'scenes', 'scene_'+str(sceneId).zfill(4), camera, 'label', str(annId).zfill(4)+'.png')
metaPath = os.path.join(self.root, 'scenes', 'scene_'+str(sceneId).zfill(4), camera, 'meta', str(annId).zfill(4)+'.mat')
rectLabelPath = os.path.join(self.root, 'scenes', 'scene_'+str(sceneId).zfill(4), camera, 'rect', str(annId).zfill(4)+'.npy')
scene_name = 'scene_'+str(sceneId).zfill(4)
return (rgbPath, depthPath, segLabelPath, metaPath, rectLabelPath, scene_name,annId)
def showObjGrasp(self, objIds=[], numGrasp=10, th=0.5, maxWidth=0.08, saveFolder='save_fig', show=False):
'''
**Input:**
- objIds: int of list of objects ids.
- numGrasp: how many grasps to show in the image.
- th: threshold of the coefficient of friction.
- maxWidth: float, only visualize grasps with width<=maxWidth
- saveFolder: string of the path to save the rendered image.
- show: bool of whether to show the image.
**Output:**
- No output but save the rendered image and maybe show it.
'''
from .utils.vis import visObjGrasp
objIds = objIds if _isArrayLike(objIds) else [objIds]
if len(objIds) == 0:
print('You need to specify object ids.')
return 0
if not os.path.exists(saveFolder):
os.mkdir(saveFolder)
for obj_id in objIds:
visObjGrasp(self.root, obj_id, num_grasp=numGrasp, th=th, max_width=maxWidth, save_folder=saveFolder, show=show)
def showSceneGrasp(self, sceneId, camera = 'kinect', annId = 0, format = '6d', numGrasp = 20, show_object = True, coef_fric_thresh = 0.1):
'''
**Input:**
- sceneId: int of the scene index.
- camera: string of the camera type, 'realsense' or 'kinect'.
- annId: int of the annotation index.
- format: int of the annotation type, 'rect' or '6d'.
- numGrasp: int of the displayed grasp number, grasps will be randomly sampled.
- coef_fric_thresh: float of the friction coefficient of grasps.
'''
if format == '6d':
geometries = []
sceneGrasp = self.loadGrasp(sceneId = sceneId, annId = annId, camera = camera, format = '6d', fric_coef_thresh = coef_fric_thresh)
sceneGrasp = sceneGrasp.random_sample(numGrasp = numGrasp)
scenePCD = self.loadScenePointCloud(sceneId = sceneId, camera = camera, annId = annId, align = False)
geometries.append(scenePCD)
geometries += sceneGrasp.to_open3d_geometry_list()
if show_object:
objectPCD = self.loadSceneModel(sceneId = sceneId, camera = camera, annId = annId, align = False)
geometries += objectPCD
o3d.visualization.draw_geometries(geometries)
elif format == 'rect':
bgr = self.loadBGR(sceneId = sceneId, camera = camera, annId = annId)
sceneGrasp = self.loadGrasp(sceneId = sceneId, camera = camera, annId = annId, format = 'rect', fric_coef_thresh = coef_fric_thresh)
sceneGrasp = sceneGrasp.random_sample(numGrasp = numGrasp)
img = sceneGrasp.to_opencv_image(bgr, numGrasp = numGrasp)
cv2.imshow('Rectangle Grasps',img)
cv2.waitKey(0)
cv2.destroyAllWindows()
def show6DPose(self, sceneIds, saveFolder='save_fig', show=False, perObj=False):
'''
**Input:**
- sceneIds: int or list of scene ids.
- saveFolder: string of the folder to store the image.
- show: bool of whether to show the image.
- perObj: bool, show grasps on each object
**Output:**
- No output but to save the rendered image and maybe show the result.
'''
from .utils.vis import vis6D
sceneIds = sceneIds if _isArrayLike(sceneIds) else [sceneIds]
if len(sceneIds) == 0:
print('You need specify scene ids.')
return 0
if not os.path.exists(saveFolder):
os.mkdir(saveFolder)
for scene_id in sceneIds:
scene_name = 'scene_'+str(scene_id).zfill(4)
vis6D(self.root, scene_name, 0, self.camera,
align_to_table=True, save_folder=saveFolder, show=show, per_obj=perObj)

View File

@ -0,0 +1,306 @@
__author__ = 'mhgou, cxwang and hsfang'
import numpy as np
import os
import time
import pickle
import open3d as o3d
from .graspnet import GraspNet
from .grasp import GraspGroup
from .utils.config import get_config
from .utils.eval_utils import get_scene_name, create_table_points, parse_posevector, load_dexnet_model, transform_points, compute_point_distance, compute_closest_points, voxel_sample_points, topk_grasps, get_grasp_score, collision_detection, eval_grasp
from .utils.xmlhandler import xmlReader
from .utils.utils import generate_scene_model
class GraspNetEval(GraspNet):
'''
Class for evaluation on GraspNet dataset.
**Input:**
- root: string of root path for the dataset.
- camera: string of type of the camera.
- split: string of the date split.
'''
def __init__(self, root, camera, split = 'test'):
super(GraspNetEval, self).__init__(root, camera, split)
def get_scene_models(self, scene_id, ann_id):
'''
return models in model coordinate
'''
model_dir = os.path.join(self.root, 'models')
# print('Scene {}, {}'.format(scene_id, camera))
scene_reader = xmlReader(os.path.join(self.root, 'scenes', get_scene_name(scene_id), self.camera, 'annotations', '%04d.xml' % (ann_id,)))
posevectors = scene_reader.getposevectorlist()
obj_list = []
model_list = []
dexmodel_list = []
for posevector in posevectors:
obj_idx, _ = parse_posevector(posevector)
obj_list.append(obj_idx)
for obj_idx in obj_list:
model = o3d.io.read_point_cloud(os.path.join(model_dir, '%03d' % obj_idx, 'nontextured.ply'))
dex_cache_path = os.path.join(self.root, 'dex_models', '%03d.pkl' % obj_idx)
if os.path.exists(dex_cache_path):
with open(dex_cache_path, 'rb') as f:
dexmodel = pickle.load(f)
else:
dexmodel = load_dexnet_model(os.path.join(model_dir, '%03d' % obj_idx, 'textured'))
points = np.array(model.points)
model_list.append(points)
dexmodel_list.append(dexmodel)
return model_list, dexmodel_list, obj_list
def get_model_poses(self, scene_id, ann_id):
'''
**Input:**
- scene_id: int of the scen index.
- ann_id: int of the annotation index.
**Output:**
- obj_list: list of int of object index.
- pose_list: list of 4x4 matrices of object poses.
- camera_pose: 4x4 matrix of the camera pose relative to the first frame.
- align mat: 4x4 matrix of camera relative to the table.
'''
scene_dir = os.path.join(self.root, 'scenes')
camera_poses_path = os.path.join(self.root, 'scenes', get_scene_name(scene_id), self.camera, 'camera_poses.npy')
camera_poses = np.load(camera_poses_path)
camera_pose = camera_poses[ann_id]
align_mat_path = os.path.join(self.root, 'scenes', get_scene_name(scene_id), self.camera, 'cam0_wrt_table.npy')
align_mat = np.load(align_mat_path)
# print('Scene {}, {}'.format(scene_id, camera))
scene_reader = xmlReader(os.path.join(scene_dir, get_scene_name(scene_id), self.camera, 'annotations', '%04d.xml'% (ann_id,)))
posevectors = scene_reader.getposevectorlist()
obj_list = []
pose_list = []
for posevector in posevectors:
obj_idx, mat = parse_posevector(posevector)
obj_list.append(obj_idx)
pose_list.append(mat)
return obj_list, pose_list, camera_pose, align_mat
def eval_scene(self, scene_id, dump_folder, TOP_K = 50, return_list = False,vis = False, max_width = 0.1):
'''
**Input:**
- scene_id: int of the scene index.
- dump_folder: string of the folder that saves the dumped npy files.
- TOP_K: int of the top number of grasp to evaluate
- return_list: bool of whether to return the result list.
- vis: bool of whether to show the result
- max_width: float of the maximum gripper width in evaluation
**Output:**
- scene_accuracy: np.array of shape (256, 50, 6) of the accuracy tensor.
'''
config = get_config()
table = create_table_points(1.0, 1.0, 0.05, dx=-0.5, dy=-0.5, dz=-0.05, grid_size=0.008)
list_coe_of_friction = [0.2,0.4,0.6,0.8,1.0,1.2]
model_list, dexmodel_list, _ = self.get_scene_models(scene_id, ann_id=0)
model_sampled_list = list()
for model in model_list:
model_sampled = voxel_sample_points(model, 0.008)
model_sampled_list.append(model_sampled)
scene_accuracy = []
grasp_list_list = []
score_list_list = []
collision_list_list = []
for ann_id in range(256):
grasp_group = GraspGroup().from_npy(os.path.join(dump_folder,get_scene_name(scene_id), self.camera, '%04d.npy' % (ann_id,)))
_, pose_list, camera_pose, align_mat = self.get_model_poses(scene_id, ann_id)
table_trans = transform_points(table, np.linalg.inv(np.matmul(align_mat, camera_pose)))
# clip width to [0,max_width]
gg_array = grasp_group.grasp_group_array
min_width_mask = (gg_array[:,1] < 0)
max_width_mask = (gg_array[:,1] > max_width)
gg_array[min_width_mask,1] = 0
gg_array[max_width_mask,1] = max_width
grasp_group.grasp_group_array = gg_array
grasp_list, score_list, collision_mask_list = eval_grasp(grasp_group, model_sampled_list, dexmodel_list, pose_list, config, table=table_trans, voxel_size=0.008, TOP_K = TOP_K)
# remove empty
grasp_list = [x for x in grasp_list if len(x) != 0]
score_list = [x for x in score_list if len(x) != 0]
collision_mask_list = [x for x in collision_mask_list if len(x)!=0]
if len(grasp_list) == 0:
grasp_accuracy = np.zeros((TOP_K,len(list_coe_of_friction)))
scene_accuracy.append(grasp_accuracy)
grasp_list_list.append([])
score_list_list.append([])
collision_list_list.append([])
print('\rMean Accuracy for scene:{} ann:{}='.format(scene_id, ann_id),np.mean(grasp_accuracy[:,:]), end='')
continue
# concat into scene level
grasp_list, score_list, collision_mask_list = np.concatenate(grasp_list), np.concatenate(score_list), np.concatenate(collision_mask_list)
if vis:
t = o3d.geometry.PointCloud()
t.points = o3d.utility.Vector3dVector(table_trans)
model_list = generate_scene_model(self.root, 'scene_%04d' % scene_id , ann_id, return_poses=False, align=False, camera=self.camera)
import copy
gg = GraspGroup(copy.deepcopy(grasp_list))
scores = np.array(score_list)
scores = scores / 2 + 0.5 # -1 -> 0, 0 -> 0.5, 1 -> 1
scores[collision_mask_list] = 0.3
gg.scores = scores
gg.widths = 0.1 * np.ones((len(gg)), dtype = np.float32)
grasps_geometry = gg.to_open3d_geometry_list()
pcd = self.loadScenePointCloud(scene_id, self.camera, ann_id)
o3d.visualization.draw_geometries([pcd, *grasps_geometry])
o3d.visualization.draw_geometries([pcd, *grasps_geometry, *model_list])
o3d.visualization.draw_geometries([*grasps_geometry, *model_list, t])
# sort in scene level
grasp_confidence = grasp_list[:,0]
indices = np.argsort(-grasp_confidence)
grasp_list, score_list, collision_mask_list = grasp_list[indices], score_list[indices], collision_mask_list[indices]
grasp_list_list.append(grasp_list)
score_list_list.append(score_list)
collision_list_list.append(collision_mask_list)
#calculate AP
grasp_accuracy = np.zeros((TOP_K,len(list_coe_of_friction)))
for fric_idx, fric in enumerate(list_coe_of_friction):
for k in range(0,TOP_K):
if k+1 > len(score_list):
grasp_accuracy[k,fric_idx] = np.sum(((score_list<=fric) & (score_list>0)).astype(int))/(k+1)
else:
grasp_accuracy[k,fric_idx] = np.sum(((score_list[0:k+1]<=fric) & (score_list[0:k+1]>0)).astype(int))/(k+1)
print('\rMean Accuracy for scene:%04d ann:%04d = %.3f' % (scene_id, ann_id, 100.0 * np.mean(grasp_accuracy[:,:])), end='', flush=True)
scene_accuracy.append(grasp_accuracy)
if not return_list:
return scene_accuracy
else:
return scene_accuracy, grasp_list_list, score_list_list, collision_list_list
def parallel_eval_scenes(self, scene_ids, dump_folder, proc = 2):
'''
**Input:**
- scene_ids: list of int of scene index.
- dump_folder: string of the folder that saves the npy files.
- proc: int of the number of processes to use to evaluate.
**Output:**
- scene_acc_list: list of the scene accuracy.
'''
from multiprocessing import Pool
p = Pool(processes = proc)
res_list = []
for scene_id in scene_ids:
res_list.append(p.apply_async(self.eval_scene, (scene_id, dump_folder)))
p.close()
p.join()
scene_acc_list = []
for res in res_list:
scene_acc_list.append(res.get())
return scene_acc_list
def eval_seen(self, dump_folder, proc = 2):
'''
**Input:**
- dump_folder: string of the folder that saves the npy files.
- proc: int of the number of processes to use to evaluate.
**Output:**
- res: numpy array of the detailed accuracy.
- ap: float of the AP for seen split.
'''
res = np.array(self.parallel_eval_scenes(scene_ids = list(range(100, 130)), dump_folder = dump_folder, proc = proc))
ap = np.mean(res)
print('\nEvaluation Result:\n----------\n{}, AP Seen={}'.format(self.camera, ap))
return res, ap
def eval_similar(self, dump_folder, proc = 2):
'''
**Input:**
- dump_folder: string of the folder that saves the npy files.
- proc: int of the number of processes to use to evaluate.
**Output:**
- res: numpy array of the detailed accuracy.
- ap: float of the AP for similar split.
'''
res = np.array(self.parallel_eval_scenes(scene_ids = list(range(130, 160)), dump_folder = dump_folder, proc = proc))
ap = np.mean(res)
print('\nEvaluation Result:\n----------\n{}, AP={}, AP Similar={}'.format(self.camera, ap, ap))
return res, ap
def eval_novel(self, dump_folder, proc = 2):
'''
**Input:**
- dump_folder: string of the folder that saves the npy files.
- proc: int of the number of processes to use to evaluate.
**Output:**
- res: numpy array of the detailed accuracy.
- ap: float of the AP for novel split.
'''
res = np.array(self.parallel_eval_scenes(scene_ids = list(range(160, 190)), dump_folder = dump_folder, proc = proc))
ap = np.mean(res)
print('\nEvaluation Result:\n----------\n{}, AP={}, AP Novel={}'.format(self.camera, ap, ap))
return res, ap
def eval_all(self, dump_folder, proc = 2):
'''
**Input:**
- dump_folder: string of the folder that saves the npy files.
- proc: int of the number of processes to use to evaluate.
**Output:**
- res: numpy array of the detailed accuracy.
- ap: float of the AP for all split.
'''
res = np.array(self.parallel_eval_scenes(scene_ids = list(range(100, 190)), dump_folder = dump_folder, proc = proc))
ap = [np.mean(res), np.mean(res[0:30]), np.mean(res[30:60]), np.mean(res[60:90])]
print('\nEvaluation Result:\n----------\n{}, AP={}, AP Seen={}, AP Similar={}, AP Novel={}'.format(self.camera, ap[0], ap[1], ap[2], ap[3]))
return res, ap

View File

@ -0,0 +1,18 @@
def get_config():
'''
- return the config dict
'''
config = dict()
force_closure = dict()
force_closure['quality_method'] = 'force_closure'
force_closure['num_cone_faces'] = 8
force_closure['soft_fingers'] = 1
force_closure['quality_type'] = 'quasi_static'
force_closure['all_contacts_required']= 1
force_closure['check_approach'] = False
force_closure['torque_scaling'] = 0.01
force_closure['wrench_norm_thresh'] = 0.001
force_closure['wrench_regularizer'] = 0.0000000001
config['metrics'] = dict()
config['metrics']['force_closure'] = force_closure
return config

View File

@ -0,0 +1,18 @@
Copyright ©2017. The Regents of the University of California (Regents). All Rights Reserved.
Permission to use, copy, modify, and distribute this software and its documentation for educational,
research, and not-for-profit purposes, without fee and without a signed licensing agreement, is
hereby granted, provided that the above copyright notice, this paragraph and the following two
paragraphs appear in all copies, modifications, and distributions. Contact The Office of Technology
Licensing, UC Berkeley, 2150 Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-
7201, otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial licensing opportunities.
IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL,
INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF
THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF REGENTS HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED
HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE
MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.

View File

@ -0,0 +1,24 @@
# # -*- coding: utf-8 -*-
# """
# Copyright ©2017. The Regents of the University of California (Regents). All Rights Reserved.
# Permission to use, copy, modify, and distribute this software and its documentation for educational,
# research, and not-for-profit purposes, without fee and without a signed licensing agreement, is
# hereby granted, provided that the above copyright notice, this paragraph and the following two
# paragraphs appear in all copies, modifications, and distributions. Contact The Office of Technology
# Licensing, UC Berkeley, 2150 Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-
# 7201, otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial licensing opportunities.
# IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL,
# INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF
# THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF REGENTS HAS BEEN
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED
# HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE
# MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
# """
# # from .constants import *
# # from .abstractstatic import abstractstatic
# # from .api import DexNet

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
"""
Copyright ©2017. The Regents of the University of California (Regents). All Rights Reserved.
Permission to use, copy, modify, and distribute this software and its documentation for educational,
research, and not-for-profit purposes, without fee and without a signed licensing agreement, is
hereby granted, provided that the above copyright notice, this paragraph and the following two
paragraphs appear in all copies, modifications, and distributions. Contact The Office of Technology
Licensing, UC Berkeley, 2150 Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-
7201, otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial licensing opportunities.
IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL,
INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF
THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF REGENTS HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED
HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE
MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
"""
# Abstact static methods
# Source: https://stackoverflow.com/questions/4474395/staticmethod-and-abc-abstractmethod-will-it-blend
class abstractstatic(staticmethod):
__slots__ = ()
def __init__(self, function):
super(abstractstatic, self).__init__(function)
function.__isabstractmethod__ = True
__isabstractmethod__ = True

View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""
Copyright ©2017. The Regents of the University of California (Regents). All Rights Reserved.
Permission to use, copy, modify, and distribute this software and its documentation for educational,
research, and not-for-profit purposes, without fee and without a signed licensing agreement, is
hereby granted, provided that the above copyright notice, this paragraph and the following two
paragraphs appear in all copies, modifications, and distributions. Contact The Office of Technology
Licensing, UC Berkeley, 2150 Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-
7201, otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial licensing opportunities.
IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL,
INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF
THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF REGENTS HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED
HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE
MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
"""
# Grasp contact params
NO_CONTACT_DIST = 0.2 # distance to points that are not in contact for window extraction
WIN_DIST_LIM = 0.02 # limits for window plotting
# File extensions
HDF5_EXT = '.hdf5'
OBJ_EXT = '.obj'
OFF_EXT = '.off'
STL_EXT = '.stl'
SDF_EXT = '.sdf'
URDF_EXT = '.urdf'
# Tags for intermediate files
DEC_TAG = '_dec'
PROC_TAG = '_proc'
# Solver default max iterations
DEF_MAX_ITER = 100
# Access levels for db
READ_ONLY_ACCESS = 'READ_ONLY'
READ_WRITE_ACCESS = 'READ_WRITE'
WRITE_ACCESS = 'WRITE'

View File

@ -0,0 +1,20 @@
"""
Copyright ©2017. The Regents of the University of California (Regents). All Rights Reserved.
Permission to use, copy, modify, and distribute this software and its documentation for educational,
research, and not-for-profit purposes, without fee and without a signed licensing agreement, is
hereby granted, provided that the above copyright notice, this paragraph and the following two
paragraphs appear in all copies, modifications, and distributions. Contact The Office of Technology
Licensing, UC Berkeley, 2150 Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-
7201, otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial licensing opportunities.
IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL,
INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF
THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF REGENTS HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED
HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE
MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
"""

View File

@ -0,0 +1,703 @@
# -*- coding: utf-8 -*-
"""
Copyright ©2017. The Regents of the University of California (Regents). All Rights Reserved.
Permission to use, copy, modify, and distribute this software and its documentation for educational,
research, and not-for-profit purposes, without fee and without a signed licensing agreement, is
hereby granted, provided that the above copyright notice, this paragraph and the following two
paragraphs appear in all copies, modifications, and distributions. Contact The Office of Technology
Licensing, UC Berkeley, 2150 Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-
7201, otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial licensing opportunities.
IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL,
INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF
THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF REGENTS HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED
HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE
MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
"""
"""
Contact class that encapsulates friction cone and surface window computation.
Authors: Brian Hou and Jeff Mahler
"""
from abc import ABCMeta, abstractmethod
import itertools as it
import logging
import numpy as np
from skimage.restoration import denoise_bilateral
from autolab_core import RigidTransform
from ..constants import NO_CONTACT_DIST
from ..constants import WIN_DIST_LIM
import IPython
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
# class Contact(metaclass=ABCMeta): # for python3
class Contact:
""" Abstract class for contact models. """
__metaclass__ = ABCMeta
class Contact3D(Contact):
""" 3D contact points.
Attributes
----------
graspable : :obj:`GraspableObject3D`
object to use to get contact information
contact_point : 3x1 :obj:`numpy.ndarray`
point of contact on the object
in_direction : 3x1 :obj:`numpy.ndarray`
direction along which contact was made
normal : normalized 3x1 :obj:`numpy.ndarray`
surface normal at the contact point
"""
def __init__(self, graspable, contact_point, in_direction=None):
self.graspable_ = graspable
self.point_ = contact_point # in world coordinates
# cached attributes
self.in_direction_ = in_direction # inward facing grasp axis
self.friction_cone_ = None
self.normal_ = None # outward facing normal
self.surface_info_ = None
self._compute_normal()
@property
def graspable(self):
return self.graspable_
@property
def point(self):
return self.point_
@property
def normal(self):
return self.normal_
@normal.setter
def normal(self, normal):
self.normal_ = normal
@property
def in_direction(self):
return self.in_direction_
def _compute_normal(self):
"""Compute outward facing normal at contact, according to in_direction.
Indexes into the SDF grid coordinates to lookup the normal info.
"""
# tf to grid
as_grid = self.graspable.sdf.transform_pt_obj_to_grid(self.point)
on_surface, _ = self.graspable.sdf.on_surface(as_grid)
if not on_surface:
logging.debug('Contact point not on surface')
return None
# compute outward facing normal from SDF
normal = self.graspable.sdf.surface_normal(as_grid)
# flip normal to point outward if in_direction is defined
if self.in_direction_ is not None and np.dot(self.in_direction_, normal) > 0:
normal = -normal
# transform to world frame
normal = self.graspable.sdf.transform_pt_grid_to_obj(normal, direction=True)
self.normal_ = normal
def tangents(self, direction=None, align_axes=True, max_samples=1000):
"""Returns the direction vector and tangent vectors at a contact point.
The direction vector defaults to the *inward-facing* normal vector at
this contact.
The direction and tangent vectors for a right handed coordinate frame.
Parameters
----------
direction : 3x1 :obj:`numpy.ndarray`
direction to find orthogonal plane for
align_axes : bool
whether or not to align the tangent plane to the object reference frame
max_samples : int
number of samples to use in discrete optimization for alignment of reference frame
Returns
-------
direction : normalized 3x1 :obj:`numpy.ndarray`
direction to find orthogonal plane for
t1 : normalized 3x1 :obj:`numpy.ndarray`
first tangent vector, x axis
t2 : normalized 3x1 :obj:`numpy.ndarray`
second tangent vector, y axis
"""
# illegal contact, cannot return tangents
if self.normal_ is None:
return None, None, None
# default to inward pointing normal
if direction is None:
direction = -self.normal_
# force direction to face inward
if np.dot(self.normal_, direction) > 0:
direction = -direction
# transform to
direction = direction.reshape((3, 1)) # make 2D for SVD
# get orthogonal plane
U, _, _ = np.linalg.svd(direction)
# U[:, 1:] spans the tanget plane at the contact
x, y = U[:, 1], U[:, 2]
# make sure t1 and t2 obey right hand rule
z_hat = np.cross(x, y)
if z_hat.dot(direction) < 0:
y = -y
v = x
w = y
# redefine tangent x axis to automatically align with the object x axis
if align_axes:
max_ip = 0
max_theta = 0
target = np.array([1, 0, 0])
theta = 0
d_theta = 2 * np.pi / float(max_samples)
for i in range(max_samples):
v = np.cos(theta) * x + np.sin(theta) * y
if v.dot(target) > max_ip:
max_ip = v.dot(target)
max_theta = theta
theta = theta + d_theta
v = np.cos(max_theta) * x + np.sin(max_theta) * y
w = np.cross(direction.ravel(), v)
return np.squeeze(direction), v, w
def reference_frame(self, align_axes=True):
"""Returns the local reference frame of the contact.
Z axis in the in direction (or surface normal if not specified)
X and Y axes in the tangent plane to the direction
Parameters
----------
align_axes : bool
whether or not to align to the object axes
Returns
-------
:obj:`RigidTransform`
rigid transformation from contact frame to object frame
"""
t_obj_contact = self.point
rz, rx, ry = self.tangents(self.in_direction_, align_axes=align_axes)
R_obj_contact = np.array([rx, ry, rz]).T
T_contact_obj = RigidTransform(rotation=R_obj_contact,
translation=t_obj_contact,
from_frame='contact', to_frame='obj')
return T_contact_obj
def normal_force_magnitude(self):
""" Returns the component of the force that the contact would apply along the normal direction.
Returns
-------
float
magnitude of force along object surface normal
"""
normal_force_mag = 1.0
if self.in_direction_ is not None and self.normal_ is not None:
in_normal = -self.normal_
in_direction_norm = self.in_direction_ / np.linalg.norm(self.in_direction_)
normal_force_mag = np.dot(in_direction_norm, in_normal)
return max(normal_force_mag, 0.0)
def friction_cone(self, num_cone_faces=8, friction_coef=0.5):
""" Computes the friction cone and normal for a contact point.
Parameters
----------
num_cone_faces : int
number of cone faces to use in discretization
friction_coef : float
coefficient of friction at contact point
Returns
-------
success : bool
False when cone can't be computed
cone_support : :obj:`numpy.ndarray`
array where each column is a vector on the boundary of the cone
normal : normalized 3x1 :obj:`numpy.ndarray`
outward facing surface normal
"""
if self.friction_cone_ is not None and self.normal_ is not None:
return True, self.friction_cone_, self.normal_
# get normal and tangents
in_normal, t1, t2 = self.tangents()
if in_normal is None:
return False, self.friction_cone_, self.normal_
friction_cone_valid = True
# check whether contact would slip, which is whether or not the tangent force is always
# greater than the frictional force
if self.in_direction_ is not None:
in_direction_norm = self.in_direction_ / np.linalg.norm(self.in_direction_)
normal_force_mag = self.normal_force_magnitude()
tan_force_x = np.dot(in_direction_norm, t1)
tan_force_y = np.dot(in_direction_norm, t2)
tan_force_mag = np.sqrt(tan_force_x ** 2 + tan_force_y ** 2)
friction_force_mag = friction_coef * normal_force_mag
if friction_force_mag < tan_force_mag:
logging.debug('Contact would slip')
return False, self.friction_cone_, self.normal_
# set up friction cone
tan_len = friction_coef
force = in_normal
cone_support = np.zeros((3, num_cone_faces))
# find convex combinations of tangent vectors
for j in range(num_cone_faces):
tan_vec = t1 * np.cos(2 * np.pi * (float(j) / num_cone_faces)) + t2 * np.sin(
2 * np.pi * (float(j) / num_cone_faces))
cone_support[:, j] = force + friction_coef * tan_vec
self.friction_cone_ = cone_support
return True, self.friction_cone_, self.normal_
def torques(self, forces):
"""
Get the torques that can be applied by a set of force vectors at the contact point.
Parameters
----------
forces : 3xN :obj:`numpy.ndarray`
the forces applied at the contact
Returns
-------
success : bool
whether or not computation was successful
torques : 3xN :obj:`numpy.ndarray`
the torques that can be applied by given forces at the contact
"""
as_grid = self.graspable.sdf.transform_pt_obj_to_grid(self.point)
on_surface, _ = self.graspable.sdf.on_surface(as_grid)
if not on_surface:
logging.debug('Contact point not on surface')
return False, None
num_forces = forces.shape[1]
torques = np.zeros([3, num_forces])
moment_arm = self.graspable.moment_arm(self.point)
for i in range(num_forces):
torques[:, i] = np.cross(moment_arm, forces[:, i])
return True, torques
def surface_window_sdf(self, width=1e-2, num_steps=21):
"""Returns a window of SDF values on the tangent plane at a contact point.
Used for patch computation.
Parameters
----------
width : float
width of the window in obj frame
num_steps : int
number of steps to use along the contact in direction
Returns
-------
window : NUM_STEPSxNUM_STEPS :obj:`numpy.ndarray`
array of distances from tangent plane to obj along in direction, False if surface window can't be computed
"""
in_normal, t1, t2 = self.tangents()
if in_normal is None: # normal and tangents not found
return False
scales = np.linspace(-width / 2.0, width / 2.0, num_steps)
window = np.zeros(num_steps ** 2)
for i, (c1, c2) in enumerate(it.product(scales, repeat=2)):
curr_loc = self.point + c1 * t1 + c2 * t2
curr_loc_grid = self.graspable.sdf.transform_pt_obj_to_grid(curr_loc)
if self.graspable.sdf.is_out_of_bounds(curr_loc_grid):
window[i] = -1e-2
continue
window[i] = self.graspable.sdf[curr_loc_grid]
return window.reshape((num_steps, num_steps))
def _compute_surface_window_projection(self, u1=None, u2=None, width=1e-2,
num_steps=21, max_projection=0.1, back_up=0, samples_per_grid=2.0,
sigma_range=0.1, sigma_spatial=1, direction=None, vis=False,
compute_weighted_covariance=False,
disc=False, num_radial_steps=5, debug_objs=None):
"""Compute the projection window onto the basis defined by u1 and u2.
Params:
u1, u2 - orthogonal numpy 3 arrays
width - float width of the window in obj frame
num_steps - int number of steps
max_projection - float maximum amount to search forward for a
contact (meters)
back_up - amount in meters to back up before projecting
samples_per_grid - float number of samples per grid when finding contacts
sigma - bandwidth of gaussian filter on window
direction - dir to do the projection along
compute_weighted_covariance - whether to return the weighted
covariance matrix, along with the window
Returns:
window - numpy NUM_STEPSxNUM_STEPS array of distances from tangent
plane to obj, False if surface window can't be computed
"""
direction, t1, t2 = self.tangents(direction)
if direction is None: # normal and tangents not found
raise ValueError('Direction could not be computed')
if u1 is not None and u2 is not None: # use given basis
t1, t2 = u1, u2
# number of samples used when looking for contacts
no_contact = NO_CONTACT_DIST
num_samples = int(samples_per_grid * (max_projection + back_up) / self.graspable.sdf.resolution)
window = np.zeros(num_steps ** 2)
res = width / num_steps
scales = np.linspace(-width / 2.0 + res / 2.0, width / 2.0 - res / 2.0, num_steps)
scales_it = it.product(scales, repeat=2)
if disc:
scales_it = []
for i in range(num_steps):
theta = 2.0 * np.pi / i
for j in range(num_radial_steps):
r = (j + 1) * width / num_radial_steps
p = (r * np.cos(theta), r * np.sin(theta))
scales_it.append(p)
# start computing weighted covariance matrix
if compute_weighted_covariance:
cov = np.zeros((3, 3))
cov_weight = 0
if vis:
ax = plt.gca(projection='3d')
self.graspable_.sdf.scatter()
for i, (c1, c2) in enumerate(scales_it):
curr_loc = self.point + c1 * t1 + c2 * t2
curr_loc_grid = self.graspable.sdf.transform_pt_obj_to_grid(curr_loc)
if self.graspable.sdf.is_out_of_bounds(curr_loc_grid):
window[i] = no_contact
continue
if vis:
ax.scatter(curr_loc_grid[0], curr_loc_grid[1], curr_loc_grid[2], s=130, c='y')
found, projection_contact = self.graspable._find_projection(
curr_loc, direction, max_projection, back_up, num_samples, vis=vis)
if found:
# logging.debug('%d found.' %(i))
sign = direction.dot(projection_contact.point - curr_loc)
projection = (sign / abs(sign)) * np.linalg.norm(projection_contact.point - curr_loc)
projection = min(projection, max_projection)
if compute_weighted_covariance:
# weight according to SHOT: R - d_i
weight = width / np.sqrt(2) - np.sqrt(c1 ** 2 + c2 ** 2)
diff = (projection_contact.point - self.point).reshape((3, 1))
cov += weight * np.dot(diff, diff.T)
cov_weight += weight
else:
logging.debug('%d not found.' % (i))
projection = no_contact
window[i] = projection
if vis:
plt.show()
if not disc:
window = window.reshape((num_steps, num_steps)).T # transpose to make x-axis along columns
if debug_objs is not None:
debug_objs.append(window)
# apply bilateral filter
if sigma_range > 0.0 and sigma_spatial > 0.0:
window_min_val = np.min(window)
window_pos = window - window_min_val
window_pos_blur = denoise_bilateral(window_pos, sigma_range=sigma_range, sigma_spatial=sigma_spatial,
mode='nearest')
window = window_pos_blur + window_min_val
if compute_weighted_covariance:
if cov_weight > 0:
return window, cov / cov_weight
return window, cov
return window
def surface_window_projection_unaligned(self, width=1e-2, num_steps=21,
max_projection=0.1, back_up=0.0, samples_per_grid=2.0,
sigma=1.5, direction=None, vis=False):
"""Projects the local surface onto the tangent plane at a contact point. Deprecated.
"""
return self._compute_surface_window_projection(width=width,
num_steps=num_steps, max_projection=max_projection,
back_up=back_up, samples_per_grid=samples_per_grid,
sigma=sigma, direction=direction, vis=vis)
def surface_window_projection(self, width=1e-2, num_steps=21,
max_projection=0.1, back_up=0.0, samples_per_grid=2.0,
sigma_range=0.1, sigma_spatial=1, direction=None, compute_pca=False, vis=False,
debug_objs=None):
"""Projects the local surface onto the tangent plane at a contact point.
Parameters
----------
width : float
width of the window in obj frame
num_steps : int
number of steps to use along the in direction
max_projection : float
maximum amount to search forward for a contact (meters)
back_up : float
amount to back up before finding a contact in meters
samples_per_grid : float
number of samples per grid when finding contacts
sigma_range : float
bandwidth of bilateral range filter on window
sigma_spatial : float
bandwidth of gaussian spatial filter of bilateral filter
direction : 3x1 :obj:`numpy.ndarray`
dir to do the projection along
Returns
-------
window : NUM_STEPSxNUM_STEPS :obj:`numpy.ndarray`
array of distances from tangent plane to obj, False if surface window can't be computed
"""
# get initial projection
direction, t1, t2 = self.tangents(direction)
window, cov = self._compute_surface_window_projection(t1, t2,
width=width, num_steps=num_steps,
max_projection=max_projection,
back_up=back_up, samples_per_grid=samples_per_grid,
sigma_range=sigma_range, sigma_spatial=sigma_spatial,
direction=direction,
vis=False, compute_weighted_covariance=True,
debug_objs=debug_objs)
if not compute_pca:
return window
# compute principal axis
pca = PCA()
pca.fit(cov)
R = pca.components_
principal_axis = R[0, :]
if np.isclose(abs(np.dot(principal_axis, direction)), 1):
# principal axis is aligned with direction of projection, use secondary axis
principal_axis = R[1, :]
if vis:
# reshape window
window = window.reshape((num_steps, num_steps))
# project principal axis onto tangent plane (t1, t2) to get u1
u1t = np.array([np.dot(principal_axis, t1), np.dot(principal_axis, t2)])
u2t = np.array([-u1t[1], u1t[0]])
if sigma > 0:
window = spfilt.gaussian_filter(window, sigma)
plt.figure()
plt.title('Principal Axis')
plt.imshow(window, extent=[0, num_steps - 1, num_steps - 1, 0],
interpolation='none', cmap=plt.cm.binary)
plt.colorbar()
plt.clim(-WIN_DIST_LIM, WIN_DIST_LIM) # fixing color range for visual comparisons
center = num_steps // 2
plt.scatter([center, center * u1t[0] + center], [center, -center * u1t[1] + center], color='blue')
plt.scatter([center, center * u2t[0] + center], [center, -center * u2t[1] + center], color='green')
u1 = np.dot(principal_axis, t1) * t1 + np.dot(principal_axis, t2) * t2
u2 = np.cross(direction, u1) # u2 must be orthogonal to u1 on plane
u1 = u1 / np.linalg.norm(u1)
u2 = u2 / np.linalg.norm(u2)
window = self._compute_surface_window_projection(u1, u2,
width=width, num_steps=num_steps,
max_projection=max_projection,
back_up=back_up, samples_per_grid=samples_per_grid,
sigma=sigma, direction=direction, vis=False)
# arbitrarily require that right_avg > left_avg (inspired by SHOT)
left_avg = np.average(window[:, :num_steps // 2])
right_avg = np.average(window[:, num_steps // 2:])
if left_avg > right_avg:
# need to flip both u1 and u2, i.e. rotate 180 degrees
window = np.rot90(window, k=2)
if vis:
if sigma > 0:
window = spfilt.gaussian_filter(window, sigma)
plt.figure()
plt.title('Tfd')
plt.imshow(window, extent=[0, num_steps - 1, num_steps - 1, 0],
interpolation='none', cmap=plt.cm.binary)
plt.colorbar()
plt.clim(-WIN_DIST_LIM, WIN_DIST_LIM) # fixing color range for visual comparisons
plt.show()
return window
def surface_information(self, width, num_steps, sigma_range=0.1, sigma_spatial=1,
back_up=0.0, max_projection=0.1, direction=None, debug_objs=None, samples_per_grid=2):
"""
Returns the local surface window, gradient, and curvature for a single contact.
Parameters
----------
width : float
width of surface window in object frame
num_steps : int
number of steps to use along the in direction
sigma_range : float
bandwidth of bilateral range filter on window
sigma_spatial : float
bandwidth of gaussian spatial filter of bilateral filter
back_up : float
amount to back up before finding a contact in meters
max_projection : float
maximum amount to search forward for a contact (meters)
direction : 3x1 :obj:`numpy.ndarray`
direction along width to render the window
debug_objs : :obj:`list`
list to put debugging info into
samples_per_grid : float
number of samples per grid when finding contacts
Returns
-------
surface_window : :obj:`SurfaceWindow`
window information for local surface patch of contact on the given object
"""
if self.surface_info_ is not None:
return self.surface_info_
if direction is None:
direction = self.in_direction_
proj_window = self.surface_window_projection(width, num_steps,
sigma_range=sigma_range, sigma_spatial=sigma_spatial,
back_up=back_up, max_projection=max_projection,
samples_per_grid=samples_per_grid,
direction=direction, vis=False, debug_objs=debug_objs)
if proj_window is None:
raise ValueError('Surface window could not be computed')
grad_win = np.gradient(proj_window)
hess_x = np.gradient(grad_win[0])
hess_y = np.gradient(grad_win[1])
gauss_curvature = np.zeros(proj_window.shape)
for i in range(num_steps):
for j in range(num_steps):
local_hess = np.array([[hess_x[0][i, j], hess_x[1][i, j]],
[hess_y[0][i, j], hess_y[1][i, j]]])
# symmetrize
local_hess = (local_hess + local_hess.T) / 2.0
# curvature
gauss_curvature[i, j] = np.linalg.det(local_hess)
return SurfaceWindow(proj_window, grad_win, hess_x, hess_y, gauss_curvature)
def plot_friction_cone(self, color='y', scale=1.0):
success, cone, in_normal = self.friction_cone()
ax = plt.gca(projection='3d')
self.graspable.sdf.scatter() # object
x, y, z = self.graspable.sdf.transform_pt_obj_to_grid(self.point)
nx, ny, nz = self.graspable.sdf.transform_pt_obj_to_grid(in_normal, direction=True)
ax.scatter([x], [y], [z], c=color, s=60) # contact
ax.scatter([x - nx], [y - ny], [z - nz], c=color, s=60) # normal
if success:
ax.scatter(x + scale * cone[0], y + scale * cone[1], z + scale * cone[2], c=color, s=40) # cone
ax.set_xlim3d(0, self.graspable.sdf.dims_[0])
ax.set_ylim3d(0, self.graspable.sdf.dims_[1])
ax.set_zlim3d(0, self.graspable.sdf.dims_[2])
return plt.Rectangle((0, 0), 1, 1, fc=color) # return a proxy for legend
class SurfaceWindow:
"""Struct for encapsulating local surface window features.
Attributes
----------
proj_win : NxN :obj:`numpy.ndarray`
the window of distances to a surface (depth image created by orthographic projection)
grad : NxN :obj:`numpy.ndarray`
X and Y gradients of the projection window
hess_x : NxN :obj:`numpy.ndarray`
hessian, partial derivatives of the X gradient window
hess_y : NxN :obj:`numpy.ndarray`
hessian, partial derivatives of the Y gradient window
gauss_curvature : NxN :obj:`numpy.ndarray`
gauss curvature at each point (function of hessian determinant)
"""
def __init__(self, proj_win, grad, hess_x, hess_y, gauss_curvature):
self.proj_win_ = proj_win
self.grad_ = grad
self.hess_x_ = hess_x
self.hess_y_ = hess_y
self.gauss_curvature_ = gauss_curvature
@property
def proj_win_2d(self):
return self.proj_win_
@property
def proj_win(self):
return self.proj_win_.flatten()
@property
def grad_x(self):
return self.grad_[0].flatten()
@property
def grad_y(self):
return self.grad_[1].flatten()
@property
def grad_x_2d(self):
return self.grad_[0]
@property
def grad_y_2d(self):
return self.grad_[1]
@property
def curvature(self):
return self.gauss_curvature_.flatten()
def asarray(self, proj_win_weight=0.0, grad_x_weight=0.0,
grad_y_weight=0.0, curvature_weight=0.0):
proj_win = proj_win_weight * self.proj_win
grad_x = grad_x_weight * self.grad_x
grad_y = grad_y_weight * self.grad_y
curvature = curvature_weight * self.gauss_curvature
return np.append([], [proj_win, grad_x, grad_y, curvature])

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
"""
Copyright ©2017. The Regents of the University of California (Regents). All Rights Reserved.
Permission to use, copy, modify, and distribute this software and its documentation for educational,
research, and not-for-profit purposes, without fee and without a signed licensing agreement, is
hereby granted, provided that the above copyright notice, this paragraph and the following two
paragraphs appear in all copies, modifications, and distributions. Contact The Office of Technology
Licensing, UC Berkeley, 2150 Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-
7201, otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial licensing opportunities.
IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL,
INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF
THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF REGENTS HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED
HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE
MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
"""
"""
Configurations for grasp quality computation.
Author: Jeff Mahler
"""
from abc import ABCMeta, abstractmethod
import copy
import itertools as it
import logging
import matplotlib.pyplot as plt
try:
import mayavi.mlab as mlab
except:
# logging.warning('Failed to import mayavi')
pass
import numpy as np
import os
import sys
import time
import IPython
# class GraspQualityConfig(object, metaclass=ABCMeta):
class GraspQualityConfig(object):
"""
Base wrapper class for parameters used in grasp quality computation.
Used to elegantly enforce existence and type of required parameters.
Attributes
----------
config : :obj:`dict`
dictionary mapping parameter names to parameter values
"""
__metaclass__ = ABCMeta
def __init__(self, config):
# check valid config
self.check_valid(config)
# parse config
for key, value in list(config.items()):
setattr(self, key, value)
def contains(self, key):
""" Checks whether or not the key is supported """
if key in list(self.__dict__.keys()):
return True
return False
def __getattr__(self, key):
if self.contains(key):
return object.__getattribute__(self, key)
return None
def __getitem__(self, key):
if self.contains(key):
return object.__getattribute__(self, key)
raise KeyError('Key %s not found' %(key))
def keys(self):
return list(self.__dict__.keys())
@abstractmethod
def check_valid(self, config):
""" Raise an exception if the config is missing required keys """
pass
class QuasiStaticGraspQualityConfig(GraspQualityConfig):
"""
Parameters for quasi-static grasp quality computation.
Attributes
----------
config : :obj:`dict`
dictionary mapping parameter names to parameter values
Notes
-----
Required configuration key-value pairs in Other Parameters.
Other Parameters
----------------
quality_method : :obj:`str`
string name of quasi-static quality metric
friction_coef : float
coefficient of friction at contact point
num_cone_faces : int
number of faces to use in friction cone approximation
soft_fingers : bool
whether to use a soft finger model
quality_type : :obj:`str`
string name of grasp quality type (e.g. quasi-static, robust quasi-static)
check_approach : bool
whether or not to check the approach direction
"""
REQUIRED_KEYS = ['quality_method',
'friction_coef',
'num_cone_faces',
'soft_fingers',
'quality_type',
'check_approach',
'all_contacts_required']
def __init__(self, config):
GraspQualityConfig.__init__(self, config)
def __copy__(self):
""" Makes a copy of the config """
obj_copy = QuasiStaticGraspQualityConfig(self.__dict__)
return obj_copy
def check_valid(self, config):
for key in QuasiStaticGraspQualityConfig.REQUIRED_KEYS:
if key not in list(config.keys()):
raise ValueError('Invalid configuration. Key %s must be specified' %(key))
class RobustQuasiStaticGraspQualityConfig(GraspQualityConfig):
"""
Parameters for quasi-static grasp quality computation.
Attributes
----------
config : :obj:`dict`
dictionary mapping parameter names to parameter values
Notes
-----
Required configuration key-value pairs in Other Parameters.
Other Parameters
----------------
quality_method : :obj:`str`
string name of quasi-static quality metric
friction_coef : float
coefficient of friction at contact point
num_cone_faces : int
number of faces to use in friction cone approximation
soft_fingers : bool
whether to use a soft finger model
quality_type : :obj:`str`
string name of grasp quality type (e.g. quasi-static, robust quasi-static)
check_approach : bool
whether or not to check the approach direction
num_quality_samples : int
number of samples to use
"""
ROBUST_REQUIRED_KEYS = ['num_quality_samples']
def __init__(self, config):
GraspQualityConfig.__init__(self, config)
def __copy__(self):
""" Makes a copy of the config """
obj_copy = RobustQuasiStaticGraspQualityConfig(self.__dict__)
return obj_copy
def check_valid(self, config):
required_keys = QuasiStaticGraspQualityConfig.REQUIRED_KEYS + \
RobustQuasiStaticGraspQualityConfig.ROBUST_REQUIRED_KEYS
for key in required_keys:
if key not in list(config.keys()):
raise ValueError('Invalid configuration. Key %s must be specified' %(key))
class GraspQualityConfigFactory:
""" Helper class to automatically create grasp quality configurations of different types. """
@staticmethod
def create_config(config):
""" Automatically create a quality config from a dictionary.
Parameters
----------
config : :obj:`dict`
dictionary mapping parameter names to parameter values
"""
if config['quality_type'] == 'quasi_static':
return QuasiStaticGraspQualityConfig(config)
elif config['quality_type'] == 'robust_quasi_static':
return RobustQuasiStaticGraspQualityConfig(config)
else:
raise ValueError('Quality config type %s not supported' %(config['type']))

View File

@ -0,0 +1,226 @@
# -*- coding: utf-8 -*-
"""
Copyright ©2017. The Regents of the University of California (Regents). All Rights Reserved.
Permission to use, copy, modify, and distribute this software and its documentation for educational,
research, and not-for-profit purposes, without fee and without a signed licensing agreement, is
hereby granted, provided that the above copyright notice, this paragraph and the following two
paragraphs appear in all copies, modifications, and distributions. Contact The Office of Technology
Licensing, UC Berkeley, 2150 Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-
7201, otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial licensing opportunities.
IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL,
INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF
THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF REGENTS HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED
HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE
MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
"""
"""
User-friendly functions for computing grasp quality metrics.
Author: Jeff Mahler
"""
from abc import ABCMeta, abstractmethod
import copy
import itertools as it
import logging
import matplotlib.pyplot as plt
import numpy as np
import os
import scipy.stats
import sys
import time
from .grasp import Grasp
from .graspable_object import GraspableObject
from .graspable_object import GraspQualityConfig
from .robust_grasp_quality import RobustPointGraspMetrics3D
from .random_variables import GraspableObjectPoseGaussianRV, ParallelJawGraspPoseGaussianRV, ParamsGaussianRV
from .quality import PointGraspMetrics3D
from autolab_core import RigidTransform
import IPython
class GraspQualityResult:
""" Stores the results of grasp quality computation.
Attributes
----------
quality : float
value of quality
uncertainty : float
uncertainty estimate of the quality value
quality_config : :obj:`GraspQualityConfig`
"""
def __init__(self, quality, uncertainty=0.0, quality_config=None):
self.quality = quality
self.uncertainty = uncertainty
self.quality_config = quality_config
# class GraspQualityFunction(object, metaclass=ABCMeta):
class GraspQualityFunction(object):
"""
Abstraction for grasp quality functions to make scripts for labeling with quality functions simple and readable.
Attributes
----------
graspable : :obj:`GraspableObject3D`
object to evaluate grasp quality on
quality_config : :obj:`GraspQualityConfig`
set of parameters to evaluate grasp quality
"""
__metaclass__ = ABCMeta
def __init__(self, graspable, quality_config):
# check valid types
if not isinstance(graspable, GraspableObject):
raise ValueError('Must provide GraspableObject')
if not isinstance(quality_config, GraspQualityConfig):
raise ValueError('Must provide GraspQualityConfig')
# set member variables
self.graspable_ = graspable
self.quality_config_ = quality_config
self._setup()
def __call__(self, grasp):
return self.quality(grasp)
@abstractmethod
def _setup(self):
""" Sets up common variables for grasp quality evaluations """
pass
@abstractmethod
def quality(self, grasp):
""" Compute grasp quality.
Parameters
----------
grasp : :obj:`GraspableObject3D`
grasp to quality quality on
Returns
-------
:obj:`GraspQualityResult`
result of quality computation
"""
pass
class QuasiStaticQualityFunction(GraspQualityFunction):
""" Grasp quality metric using a quasi-static model.
"""
def __init__(self, graspable, quality_config):
GraspQualityFunction.__init__(self, graspable, quality_config)
@property
def graspable(self):
return self.graspable_
@graspable.setter
def graspable(self, obj):
self.graspable_ = obj
def _setup(self):
if self.quality_config_.quality_type != 'quasi_static':
raise ValueError('Quality configuration must be quasi static')
def quality(self, grasp):
""" Compute grasp quality using a quasistatic method.
Parameters
----------
grasp : :obj:`GraspableObject3D`
grasp to quality quality on
Returns
-------
:obj:`GraspQualityResult`
result of quality computation
"""
if not isinstance(grasp, Grasp):
raise ValueError('Must provide Grasp object to compute quality')
quality = PointGraspMetrics3D.grasp_quality(grasp, self.graspable_,
self.quality_config_)
return GraspQualityResult(quality, quality_config=self.quality_config_)
class RobustQuasiStaticQualityFunction(GraspQualityFunction):
""" Grasp quality metric using a robust quasi-static model (average over random perturbations)
"""
def __init__(self, graspable, quality_config, T_obj_world=RigidTransform(from_frame='obj', to_frame='world')):
self.T_obj_world_ = T_obj_world
GraspQualityFunction.__init__(self, graspable, quality_config)
@property
def graspable(self):
return self.graspable_
@graspable.setter
def graspable(self, obj):
self.graspable_ = obj
self._setup()
def _setup(self):
if self.quality_config_.quality_type != 'robust_quasi_static':
raise ValueError('Quality configuration must be robust quasi static')
self.graspable_rv_ = GraspableObjectPoseGaussianRV(self.graspable_,
self.T_obj_world_,
self.quality_config_.obj_uncertainty)
self.params_rv_ = ParamsGaussianRV(self.quality_config_,
self.quality_config_.params_uncertainty)
def quality(self, grasp):
""" Compute grasp quality using a robust quasistatic method.
Parameters
----------
grasp : :obj:`GraspableObject3D`
grasp to quality quality on
Returns
-------
:obj:`GraspQualityResult`
result of quality computation
"""
if not isinstance(grasp, Grasp):
raise ValueError('Must provide Grasp object to compute quality')
grasp_rv = ParallelJawGraspPoseGaussianRV(grasp,
self.quality_config_.grasp_uncertainty)
mean_q, std_q = RobustPointGraspMetrics3D.expected_quality(grasp_rv,
self.graspable_rv_,
self.params_rv_,
self.quality_config_)
return GraspQualityResult(mean_q, std_q, quality_config=self.quality_config_)
class GraspQualityFunctionFactory:
@staticmethod
def create_quality_function(graspable, quality_config):
""" Creates a quality function for a particular object based on a configuration, which can be passed directly from a configuration file.
Parameters
----------
graspable : :obj:`GraspableObject3D`
object to create quality function for
quality_config : :obj:`GraspQualityConfig`
parameters for quality function
"""
# check valid types
if not isinstance(graspable, GraspableObject):
raise ValueError('Must provide GraspableObject')
if not isinstance(quality_config, GraspQualityConfig):
raise ValueError('Must provide GraspQualityConfig')
if quality_config.quality_type == 'quasi_static':
return QuasiStaticQualityFunction(graspable, quality_config)
elif quality_config.quality_type == 'robust_quasi_static':
return RobustQuasiStaticQualityFunction(graspable, quality_config)
else:
raise ValueError('Grasp quality type %s not supported' %(quality_config.quality_type))

View File

@ -0,0 +1,232 @@
# -*- coding: utf-8 -*-
"""
Copyright ©2017. The Regents of the University of California (Regents). All Rights Reserved.
Permission to use, copy, modify, and distribute this software and its documentation for educational,
research, and not-for-profit purposes, without fee and without a signed licensing agreement, is
hereby granted, provided that the above copyright notice, this paragraph and the following two
paragraphs appear in all copies, modifications, and distributions. Contact The Office of Technology
Licensing, UC Berkeley, 2150 Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-
7201, otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial licensing opportunities.
IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL,
INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF
THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF REGENTS HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED
HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE
MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
"""
"""
Encapsulates data and operations on a 2D or 3D object that can be grasped
Author: Jeff Mahler
"""
from abc import ABCMeta, abstractmethod
import copy
import logging
import numpy as np
from .meshpy import mesh as m
from .meshpy import sdf as s
import IPython
import matplotlib.pyplot as plt
from autolab_core import RigidTransform, SimilarityTransform
# class GraspableObject(metaclass=ABCMeta):
class GraspableObject:
""" Encapsulates geometric structures for computing contact in grasping.
Attributes
----------
sdf : :obj:`Sdf3D`
signed distance field, for quickly computing contact points
mesh : :obj:`Mesh3D`
3D triangular mesh to specify object geometry, should match SDF
key : :obj:`str`
object identifier, usually given from the database
model_name : :obj:`str`
name of the object mesh as a .obj file, for use in collision checking
mass : float
mass of the object
convex_pieces : :obj:`list` of :obj:`Mesh3D`
convex decomposition of the object geom for collision checking
"""
__metaclass__ = ABCMeta
def __init__(self, sdf, mesh, key='', model_name='', mass=1.0, convex_pieces=None):
self.sdf_ = sdf
self.mesh_ = mesh
self.key_ = key
self.model_name_ = model_name # for OpenRave usage, gross!
self.mass_ = mass
self.convex_pieces_ = convex_pieces
@property
def sdf(self):
return self.sdf_
@property
def mesh(self):
return self.mesh_
@property
def mass(self):
return self.mass_
@property
def key(self):
return self.key_
@property
def model_name(self):
return self.model_name_
@property
def convex_pieces(self):
return self.convex_pieces_
class GraspableObject3D(GraspableObject):
""" 3D Graspable object for computing contact in grasping.
Attributes
----------
sdf : :obj:`Sdf3D`
signed distance field, for quickly computing contact points
mesh : :obj:`Mesh3D`
3D triangular mesh to specify object geometry, should match SDF
key : :obj:`str`
object identifier, usually given from the database
model_name : :obj:`str`
name of the object mesh as a .obj file, for use in collision checking
mass : float
mass of the object
convex_pieces : :obj:`list` of :obj:`Mesh3D`
convex decomposition of the object geom for collision checking
"""
def __init__(self, sdf, mesh, key='',
model_name='', mass=1.0,
convex_pieces=None):
if not isinstance(sdf, s.Sdf3D):
raise ValueError('Must initialize 3D graspable object with 3D sdf')
if not isinstance(mesh, m.Mesh3D):
raise ValueError('Must initialize 3D graspable object with 3D mesh')
GraspableObject.__init__(self, sdf, mesh, key=key,
model_name=model_name, mass=mass,
convex_pieces=convex_pieces)
def moment_arm(self, x):
""" Computes the moment arm to a point x.
Parameters
----------
x : 3x1 :obj:`numpy.ndarray`
point to get moment arm for
Returns
-------
3x1 :obj:`numpy.ndarray`
"""
return x - self.mesh.center_of_mass
def rescale(self, scale):
""" Rescales uniformly by a given factor.
Parameters
----------
scale : float
the amount to scale the object
Returns
-------
:obj:`GraspableObject3D`
the graspable object rescaled by the given factor
"""
stf = SimilarityTransform(scale=scale)
sdf_rescaled = self.sdf_.rescale(scale)
mesh_rescaled = self.mesh_.transform(stf)
convex_pieces_rescaled = None
if self.convex_pieces_ is not None:
convex_pieces_rescaled = []
for convex_piece in self.convex_pieces_:
convex_piece_rescaled = convex_piece.transform(stf)
convex_pieces_rescaled.append(convex_piece_rescaled)
return GraspableObject3D(sdf_rescaled, mesh_rescaled, key=self.key,
model_name=self.model_name, mass=self.mass,
convex_pieces=convex_pieces_rescaled)
def transform(self, delta_T):
""" Transform by a delta transform.
Parameters
----------
delta_T : :obj:`RigidTransform`
the transformation from the current reference frame to the alternate reference frame
Returns
-------
:obj:`GraspableObject3D`
graspable object trasnformed by the delta
"""
sdf_tf = self.sdf_.transform(delta_T)
mesh_tf = self.mesh_.transform(delta_T)
convex_pieces_tf = None
if self.convex_pieces_ is not None:
convex_pieces_tf = []
for convex_piece in self.convex_pieces_:
convex_piece_tf = convex_piece.transform(delta_T)
convex_pieces_tf.append(convex_piece_tf)
return GraspableObject3D(sdf_tf, mesh_tf, key=self.key,
model_name=self.model_name, mass=self.mass,
convex_pieces=convex_pieces_tf)
def surface_information(self, grasp, width, num_steps, plot=False, direction1=None, direction2=None):
""" Returns the patches on this object for a given grasp.
Parameters
----------
grasp : :obj:`ParallelJawPtGrasp3D`
grasp to get the patch information for
width : float
width of jaw opening
num_steps : int
number of steps
plot : bool
whether to plot the intermediate computation, for debugging
direction1 : normalized 3x1 :obj:`numpy.ndarray`
direction along which to compute the surface information for the first jaw, if None then defaults to grasp axis
direction2 : normalized 3x1 :obj:`numpy.ndarray`
direction along which to compute the surface information for the second jaw, if None then defaults to grasp axis
Returns
-------
:obj:`list` of :obj:`SurfaceWindow`
surface patches, one for each contact
"""
contacts_found, contacts = grasp.close_fingers(self)#, vis=True)
if not contacts_found:
raise ValueError('Failed to find contacts')
contact1, contact2 = contacts
if plot:
plt.figure()
contact1.plot_friction_cone()
contact2.plot_friction_cone()
ax = plt.gca(projection = '3d')
ax.set_xlim3d(0, self.sdf.dims_[0])
ax.set_ylim3d(0, self.sdf.dims_[1])
ax.set_zlim3d(0, self.sdf.dims_[2])
window1 = contact1.surface_information(width, num_steps, direction=direction1)
window2 = contact2.surface_information(width, num_steps, direction=direction2)
return window1, window2, contact1, contact2

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 Berkeley AUTOLAB & University of California, Berkeley
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,26 @@
# try:
# # from meshpy import meshrender
# import meshrender
# except:
# print('Unable to import meshrender shared library! Rendering will not work. Likely due to missing Boost.Numpy')
# print('Boost.Numpy can be installed following the instructions in https://github.com/ndarray/Boost.NumPy')
from .mesh import Mesh3D
# from .image_converter import ImageToMeshConverter
from .obj_file import ObjFile
# from .off_file import OffFile
# from .render_modes import RenderMode
from .sdf import Sdf, Sdf3D
from .sdf_file import SdfFile
from .stable_pose import StablePose
from . import mesh
from . import obj_file
from . import sdf_file
# from .stp_file import StablePoseFile
# from .urdf_writer import UrdfWriter, convex_decomposition
# from .lighting import MaterialProperties, LightingProperties
# from .mesh_renderer import ViewsphereDiscretizer, PlanarWorksurfaceDiscretizer, VirtualCamera, SceneObject
# from .random_variables import CameraSample, RenderSample, UniformViewsphereRandomVariable, \
# UniformPlanarWorksurfaceRandomVariable, UniformPlanarWorksurfaceImageRandomVariable
__all__ = ['Mesh3D','ObjFile','Sdf','Sdf3D','SdfFile','StablePose','mesh','obj_file','sdf_file']

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,150 @@
"""
File for loading and saving meshes from .OBJ files
Author: Jeff Mahler
"""
import os
try:
from . import mesh
except ImportError:
import mesh
class ObjFile(object):
"""
A Wavefront .obj file reader and writer.
Attributes
----------
filepath : :obj:`str`
The full path to the .obj file associated with this reader/writer.
"""
def __init__(self, filepath):
"""Construct and initialize a .obj file reader and writer.
Parameters
----------
filepath : :obj:`str`
The full path to the desired .obj file
Raises
------
ValueError
If the file extension is not .obj.
"""
self.filepath_ = filepath
file_root, file_ext = os.path.splitext(self.filepath_)
if file_ext != '.obj':
raise ValueError('Extension %s invalid for OBJs' %(file_ext))
@property
def filepath(self):
"""Returns the full path to the .obj file associated with this reader/writer.
Returns
-------
:obj:`str`
The full path to the .obj file associated with this reader/writer.
"""
return self.filepath_
def read(self):
"""Reads in the .obj file and returns a Mesh3D representation of that mesh.
Returns
-------
:obj:`Mesh3D`
A Mesh3D created from the data in the .obj file.
"""
numVerts = 0
verts = []
norms = None
faces = []
tex_coords = []
face_norms = []
f = open(self.filepath_, 'r')
for line in f:
# Break up the line by whitespace
vals = line.split()
if len(vals) > 0:
# Look for obj tags (see http://en.wikipedia.org/wiki/Wavefront_.obj_file)
if vals[0] == 'v':
# Add vertex
v = list(map(float, vals[1:4]))
verts.append(v)
if vals[0] == 'vn':
# Add normal
if norms is None:
norms = []
n = list(map(float, vals[1:4]))
norms.append(n)
if vals[0] == 'f':
# Add faces (includes vertex indices, texture coordinates, and normals)
vi = []
vti = []
nti = []
if vals[1].find('/') == -1:
vi = list(map(int, vals[1:]))
vi = [i - 1 for i in vi]
else:
for j in range(1, len(vals)):
# Break up like by / to read vert inds, tex coords, and normal inds
val = vals[j]
tokens = val.split('/')
for i in range(len(tokens)):
if i == 0:
vi.append(int(tokens[i]) - 1) # adjust for python 0 - indexing
elif i == 1:
if tokens[i] != '':
vti.append(int(tokens[i]))
elif i == 2:
nti.append(int(tokens[i]))
faces.append(vi)
# Below two lists are currently not in use
tex_coords.append(vti)
face_norms.append(nti)
f.close()
return mesh.Mesh3D(verts, faces, norms)
def write(self, mesh):
"""Writes a Mesh3D object out to a .obj file format
Parameters
----------
mesh : :obj:`Mesh3D`
The Mesh3D object to write to the .obj file.
Note
----
Does not support material files or texture coordinates.
"""
f = open(self.filepath_, 'w')
vertices = mesh.vertices
faces = mesh.triangles
normals = mesh.normals
# write human-readable header
f.write('###########################################################\n')
f.write('# OBJ file generated by UC Berkeley Automation Sciences Lab\n')
f.write('#\n')
f.write('# Num Vertices: %d\n' %(vertices.shape[0]))
f.write('# Num Triangles: %d\n' %(faces.shape[0]))
f.write('#\n')
f.write('###########################################################\n')
f.write('\n')
for v in vertices:
f.write('v %f %f %f\n' %(v[0], v[1], v[2]))
# write the normals list
if normals is not None and normals.shape[0] > 0:
for n in normals:
f.write('vn %f %f %f\n' %(n[0], n[1], n[2]))
# write the normals list
for t in faces:
f.write('f %d %d %d\n' %(t[0]+1, t[1]+1, t[2]+1)) # convert back to 1-indexing
f.close()

View File

@ -0,0 +1,773 @@
"""
Definition of SDF Class
Author: Sahaana Suri, Jeff Mahler, and Matt Matl
**Currently assumes clean input**
"""
from abc import ABCMeta, abstractmethod
import logging
import numpy as np
from numbers import Number
import time
from autolab_core import RigidTransform, SimilarityTransform, PointCloud, Point, NormalCloud
from sys import version_info
if version_info[0] != 3:
range = xrange
# class Sdf(metaclass=ABCMeta): # work for python3
class Sdf():
""" Abstract class for signed distance fields.
"""
__metaclass__ = ABCMeta
##################################################################
# General SDF Properties
##################################################################
@property
def dimensions(self):
"""SDF dimension information.
Returns
-------
:obj:`numpy.ndarray` of int
The ndarray that contains the dimensions of the sdf.
"""
return self.dims_
@property
def origin(self):
"""The location of the origin in the SDF grid.
Returns
-------
:obj:`numpy.ndarray` of float
The 2- or 3-ndarray that contains the location of
the origin of the mesh grid in real space.
"""
return self.origin_
@property
def resolution(self):
"""The grid resolution (how wide each grid cell is).
Returns
-------
float
The width of each grid cell.
"""
return self.resolution_
@property
def center(self):
"""Center of grid.
This basically transforms the world frame to grid center.
Returns
-------
:obj:`numpy.ndarray`
"""
return self.center_
@property
def gradients(self):
"""Gradients of the SDF.
Returns
-------
:obj:`list` of :obj:`numpy.ndarray` of float
A list of ndarrays of the same dimension as the SDF. The arrays
are in axis order and specify the gradients for that axis
at each point.
"""
return self.gradients_
@property
def data(self):
"""The SDF data.
Returns
-------
:obj:`numpy.ndarray` of float
The 2- or 3-dimensional ndarray that holds the grid of signed
distances.
"""
return self.data_
##################################################################
# General SDF Abstract Methods
##################################################################
@abstractmethod
def transform(self, tf):
"""Returns a new SDF transformed by similarity tf.
"""
pass
@abstractmethod
def transform_pt_obj_to_grid(self, x_world, direction=False):
"""Transforms points from world frame to grid frame
"""
pass
@abstractmethod
def transform_pt_grid_to_obj(self, x_grid, direction=False):
"""Transforms points from grid frame to world frame
"""
pass
@abstractmethod
def surface_points(self):
"""Returns the points on the surface.
Returns
-------
:obj:`tuple` of :obj:`numpy.ndarray` of int, :obj:`numpy.ndarray` of float
The points on the surface and the signed distances at those points.
"""
pass
@abstractmethod
def __getitem__(self, coords):
"""Returns the signed distance at the given coordinates.
Parameters
----------
coords : :obj:`numpy.ndarray` of int
A 2- or 3-dimensional ndarray that indicates the desired
coordinates in the grid.
Returns
-------
float
The signed distance at the given coords (interpolated).
"""
pass
##################################################################
# Universal SDF Methods
##################################################################
def transform_to_world(self):
"""Returns an sdf object with center in the world frame of reference.
"""
return self.transform(self.pose_, scale=self.scale_)
def center_world(self):
"""Center of grid (basically transforms world frame to grid center)
"""
return self.transform_pt_grid_to_obj(self.center_)
def on_surface(self, coords):
"""Determines whether or not a point is on the object surface.
Parameters
----------
coords : :obj:`numpy.ndarray` of int
A 2- or 3-dimensional ndarray that indicates the desired
coordinates in the grid.
Returns
-------
:obj:`tuple` of bool, float
Is the point on the object's surface, and what
is the signed distance at that point?
"""
sdf_val = self[coords]
if np.abs(sdf_val) < self.surface_thresh_:
return True, sdf_val
return False, sdf_val
def is_out_of_bounds(self, coords):
"""Returns True if coords is an out of bounds access.
Parameters
----------
coords : :obj:`numpy.ndarray` of int
A 2- or 3-dimensional ndarray that indicates the desired
coordinates in the grid.
Returns
-------
bool
Are the coordinates in coords out of bounds?
"""
return np.array(coords < 0).any() or np.array(coords >= self.dims_).any()
def _compute_gradients(self):
"""Computes the gradients of the SDF.
Returns
-------
:obj:`list` of :obj:`numpy.ndarray` of float
A list of ndarrays of the same dimension as the SDF. The arrays
are in axis order and specify the gradients for that axis
at each point.
"""
self.gradients_ = np.gradient(self.data_)
class Sdf3D(Sdf):
# static indexing vars
num_interpolants = 8
min_coords_x = [0, 2, 3, 5]
max_coords_x = [1, 4, 6, 7]
min_coords_y = [0, 1, 3, 6]
max_coords_y = [2, 4, 5, 7]
min_coords_z = [0, 1, 2, 4]
max_coords_z = [3, 5, 6, 7]
def __init__(self, sdf_data, origin, resolution, use_abs=False,
T_sdf_world=RigidTransform(from_frame='sdf', to_frame='world')):
self.data_ = sdf_data
self.origin_ = origin
self.resolution_ = resolution
self.dims_ = self.data_.shape
# set up surface params
self.surface_thresh_ = self.resolution_ * np.sqrt(2) / 2
self.surface_points_ = None
self.surface_points_w_ = None
self.surface_vals_ = None
self._compute_surface_points()
# resolution is max dist from surface when surf is orthogonal to diagonal grid cells
spts, _ = self.surface_points()
self.center_ = 0.5 * (np.min(spts, axis=0) + np.max(spts, axis=0))
self.points_buf_ = np.zeros([Sdf3D.num_interpolants, 3], dtype=np.int)
self.coords_buf_ = np.zeros([3, ])
self.pts_ = None
# tranform sdf basis to grid (X and Z axes are flipped!)
t_world_grid = self.resolution_ * self.center_
s_world_grid = 1.0 / self.resolution_
# FIXME: Since in autolab_core==0.0.4, it applies (un)scale transformation before translation in SimilarityTransform
# here we shoule use unscaled origin to get the correct world coordinates
# PS: in world coordinate, the origin here is the left-bottom-down corner of the padded bounding squre box
t_grid_sdf = self.origin
self.T_grid_sdf_ = SimilarityTransform(translation=t_grid_sdf,
scale=self.resolution,
from_frame='grid',
to_frame='sdf')
self.T_sdf_world_ = T_sdf_world
self.T_grid_world_ = self.T_sdf_world_ * self.T_grid_sdf_
self.T_sdf_grid_ = self.T_grid_sdf_.inverse()
self.T_world_grid_ = self.T_grid_world_.inverse()
self.T_world_sdf_ = self.T_sdf_world_.inverse()
# optionally use only the absolute values (useful for non-closed meshes in 3D)
self.use_abs_ = use_abs
if use_abs:
self.data_ = np.abs(self.data_)
self._compute_gradients()
self.surface_points_w_ = self.transform_pt_grid_to_obj(self.surface_points_.T).T
surface, _ = self.surface_points(grid_basis=True)
self.surface_for_signed_val = surface[np.random.choice(len(surface), 1000)] # FIXME: for speed
def transform(self, delta_T):
""" Creates a new SDF with a given pose with respect to world coordinates.
Parameters
----------
delta_T : :obj:`autolab_core.RigidTransform`
transform from cur sdf to transformed sdf coords
"""
new_T_sdf_world = self.T_sdf_world_ * delta_T.inverse().as_frames('sdf', 'sdf')
return Sdf3D(self.data_, self.origin_, self.resolution_, use_abs=self.use_abs_,
T_sdf_world=new_T_sdf_world)
def _signed_distance(self, coords):
"""Returns the signed distance at the given coordinates, interpolating
if necessary.
Parameters
----------
coords : :obj:`numpy.ndarray` of int
A 3-dimensional ndarray that indicates the desired
coordinates in the grid.
Returns
-------
float
The signed distance at the given coords (interpolated).
Raises
------
IndexError
If the coords vector does not have three entries.
"""
if len(coords) != 3:
raise IndexError('Indexing must be 3 dimensional')
if self.is_out_of_bounds(coords):
logging.debug('Out of bounds access. Snapping to SDF dims')
# find cloest surface point
surface = self.surface_for_signed_val
closest_surface_coord = surface[np.argmin(np.linalg.norm(surface - coords, axis=-1))]
sd = np.linalg.norm(self.transform_pt_grid_to_obj(closest_surface_coord) -
self.transform_pt_grid_to_obj(coords)) + \
self.data_[closest_surface_coord[0], closest_surface_coord[1], closest_surface_coord[2]]
else:
# snap to grid dims
self.coords_buf_[0] = max(0, min(coords[0], self.dims_[0] - 1))
self.coords_buf_[1] = max(0, min(coords[1], self.dims_[1] - 1))
self.coords_buf_[2] = max(0, min(coords[2], self.dims_[2] - 1))
# regular indexing if integers
if np.issubdtype(type(coords[0]), np.integer) and \
np.issubdtype(type(coords[1]), np.integer) and \
np.issubdtype(type(coords[2]), np.integer):
return self.data_[int(self.coords_buf_[0]), int(self.coords_buf_[1]), int(self.coords_buf_[2])]
# otherwise interpolate
min_coords = np.floor(self.coords_buf_)
max_coords = min_coords + 1 # assumed to be on grid
self.points_buf_[Sdf3D.min_coords_x, 0] = min_coords[0]
self.points_buf_[Sdf3D.max_coords_x, 0] = max_coords[0]
self.points_buf_[Sdf3D.min_coords_y, 1] = min_coords[1]
self.points_buf_[Sdf3D.max_coords_y, 1] = max_coords[1]
self.points_buf_[Sdf3D.min_coords_z, 2] = min_coords[2]
self.points_buf_[Sdf3D.max_coords_z, 2] = max_coords[2]
# bilinearly interpolate points
sd = 0.0
for i in range(Sdf3D.num_interpolants):
p = self.points_buf_[i, :]
if self.is_out_of_bounds(p):
v = 0.0
else:
v = self.data_[p[0], p[1], p[2]]
w = np.prod(-np.abs(p - self.coords_buf_) + 1)
sd = sd + w * v
return sd
def __getitem__(self, coords):
"""Returns the signed distance at the given coordinates.
Parameters
----------
coords : :obj:`numpy.ndarray` of int
A or 3-dimensional ndarray that indicates the desired
coordinates in the grid.
Returns
-------
float
The signed distance at the given coords (interpolated).
Raises
------
IndexError
If the coords vector does not have three entries.
"""
return self._signed_distance(coords)
def gradient(self, coords):
"""Returns the SDF gradient at the given coordinates, interpolating if necessary
Parameters
----------
coords : :obj:`numpy.ndarray` of int
A 3-dimensional ndarray that indicates the desired
coordinates in the grid.
Returns
-------
float
The gradient at the given coords (interpolated).
Raises
------
IndexError
If the coords vector does not have three entries.
"""
if len(coords) != 3:
raise IndexError('Indexing must be 3 dimensional')
# log warning if out of bounds access
if self.is_out_of_bounds(coords):
logging.debug('Out of bounds access. Snapping to SDF dims')
# snap to grid dims
self.coords_buf_[0] = max(0, min(coords[0], self.dims_[0] - 1))
self.coords_buf_[1] = max(0, min(coords[1], self.dims_[1] - 1))
self.coords_buf_[2] = max(0, min(coords[2], self.dims_[2] - 1))
# regular indexing if integers
if type(coords[0]) is int and type(coords[1]) is int and type(coords[2]) is int:
self.coords_buf_ = self.coords_buf_.astype(np.int)
return self.data_[self.coords_buf_[0], self.coords_buf_[1], self.coords_buf_[2]]
# otherwise interpolate
min_coords = np.floor(self.coords_buf_)
max_coords = min_coords + 1
self.points_buf_[Sdf3D.min_coords_x, 0] = min_coords[0]
self.points_buf_[Sdf3D.max_coords_x, 0] = min_coords[0]
self.points_buf_[Sdf3D.min_coords_y, 1] = min_coords[1]
self.points_buf_[Sdf3D.max_coords_y, 1] = max_coords[1]
self.points_buf_[Sdf3D.min_coords_z, 2] = min_coords[2]
self.points_buf_[Sdf3D.max_coords_z, 2] = max_coords[2]
# bilinear interpolation
g = np.zeros(3)
gp = np.zeros(3)
w_sum = 0.0
for i in range(Sdf3D.num_interpolants):
p = self.points_buf_[i, :]
if self.is_out_of_bounds(p):
gp[0] = 0.0
gp[1] = 0.0
gp[2] = 0.0
else:
gp[0] = self.gradients_[0][p[0], p[1], p[2]]
gp[1] = self.gradients_[1][p[0], p[1], p[2]]
gp[2] = self.gradients_[2][p[0], p[1], p[2]]
w = np.prod(-np.abs(p - self.coords_buf_) + 1)
g = g + w * gp
return g
def curvature(self, coords, delta=0.001):
"""
Returns an approximation to the local SDF curvature (Hessian) at the
given coordinate in grid basis.
Parameters
---------
coords : numpy 3-vector
the grid coordinates at which to get the curvature
delta :
Returns
-------
curvature : 3x3 ndarray of the curvature at the surface points
"""
# perturb local coords
coords_x_up = coords + np.array([delta, 0, 0])
coords_x_down = coords + np.array([-delta, 0, 0])
coords_y_up = coords + np.array([0, delta, 0])
coords_y_down = coords + np.array([0, -delta, 0])
coords_z_up = coords + np.array([0, 0, delta])
coords_z_down = coords + np.array([0, 0, -delta])
# get gradient
grad_x_up = self.gradient(coords_x_up)
grad_x_down = self.gradient(coords_x_down)
grad_y_up = self.gradient(coords_y_up)
grad_y_down = self.gradient(coords_y_down)
grad_z_up = self.gradient(coords_z_up)
grad_z_down = self.gradient(coords_z_down)
# finite differences
curvature_x = (grad_x_up - grad_x_down) / (4 * delta)
curvature_y = (grad_y_up - grad_y_down) / (4 * delta)
curvature_z = (grad_z_up - grad_z_down) / (4 * delta)
curvature = np.c_[curvature_x, np.c_[curvature_y, curvature_z]]
curvature = curvature + curvature.T
return curvature
def surface_normal(self, coords, delta=1.5):
"""Returns the sdf surface normal at the given coordinates by
computing the tangent plane using SDF interpolation.
Parameters
----------
coords : :obj:`numpy.ndarray` of int
A 3-dimensional ndarray that indicates the desired
coordinates in the grid.
delta : float
A radius for collecting surface points near the target coords
for calculating the surface normal.
Returns
-------
:obj:`numpy.ndarray` of float
The 3-dimensional ndarray that represents the surface normal.
Raises
------
IndexError
If the coords vector does not have three entries.
"""
if len(coords) != 3:
raise IndexError('Indexing must be 3 dimensional')
# log warning if out of bounds access
if self.is_out_of_bounds(coords):
logging.debug('Out of bounds access. Snapping to SDF dims')
# snap to grid dims
# coords[0] = max(0, min(coords[0], self.dims_[0] - 1))
# coords[1] = max(0, min(coords[1], self.dims_[1] - 1))
# coords[2] = max(0, min(coords[2], self.dims_[2] - 1))
index_coords = np.zeros(3)
# check points on surface
sdf_val = self[coords]
if np.abs(sdf_val) >= self.surface_thresh_:
logging.debug('Cannot compute normal. Point must be on surface')
return None
# collect all surface points within the delta sphere
X = []
d = np.zeros(3)
dx = -delta
while dx <= delta:
dy = -delta
while dy <= delta:
dz = -delta
while dz <= delta:
d = np.array([dx, dy, dz])
if dx != 0 or dy != 0 or dz != 0:
d = delta * d / np.linalg.norm(d)
index_coords[0] = coords[0] + d[0]
index_coords[1] = coords[1] + d[1]
index_coords[2] = coords[2] + d[2]
sdf_val = self[index_coords]
if np.abs(sdf_val) < self.surface_thresh_:
X.append([index_coords[0], index_coords[1], index_coords[2], sdf_val])
dz += delta
dy += delta
dx += delta
# fit a plane to the surface points
X.sort(key=lambda x: x[3])
X = np.array(X)[:, :3]
A = X - np.mean(X, axis=0)
try:
U, S, V = np.linalg.svd(A.T)
n = U[:, 2]
except:
logging.warning('Tangent plane does not exist. Returning None.')
return None
# make sure surface normal is outward
# referenced from Zhou Xian's github, but if the model is not watertight, this method may fail
# https://github.com/zhouxian/meshpy_berkeley/commit/96428f3b7af618a0828a7eb88f22541cdafacfc7
if self[coords + n * 0.01] < self[coords]:
n = -n
return n
def _compute_surface_points(self):
surface_points = np.where(np.abs(self.data_) < self.surface_thresh_)
x = surface_points[0]
y = surface_points[1]
z = surface_points[2]
self.surface_points_ = np.c_[x, np.c_[y, z]]
self.surface_vals_ = self.data_[self.surface_points_[:, 0], self.surface_points_[:, 1],
self.surface_points_[:, 2]]
def surface_points(self, grid_basis=True):
"""Returns the points on the surface.
Parameters
----------
grid_basis : bool
If False, the surface points are transformed to the world frame.
If True (default), the surface points are left in grid coordinates.
Returns
-------
:obj:`tuple` of :obj:`numpy.ndarray` of int, :obj:`numpy.ndarray` of float
The points on the surface and the signed distances at those points.
"""
if not grid_basis:
return self.surface_points_w_, self.surface_vals_
return self.surface_points_, self.surface_vals_
def rescale(self, scale):
""" Rescale an SDF by a given scale factor.
Parameters
----------
scale : float
the amount to scale the SDF
Returns
-------
:obj:`Sdf3D`
new sdf with given scale
"""
resolution_tf = scale * self.resolution_
return Sdf3D(self.data_, self.origin_, resolution_tf, use_abs=self.use_abs_,
T_sdf_world=self.T_sdf_world_)
def transform_dense(self, delta_T, detailed=False):
""" Transform the grid by pose T and scale with canonical reference
frame at the SDF center with axis alignment.
Parameters
----------
delta_T : SimilarityTransform
the transformation from the current frame of reference to the new frame of reference
detailed : bool
whether or not to use interpolation
Returns
-------
:obj:`Sdf3D`
new sdf with grid warped by T
"""
# map all surface points to their new location
start_t = time.clock()
# form points array
if self.pts_ is None:
[x_ind, y_ind, z_ind] = np.indices(self.dims_)
self.pts_ = np.c_[x_ind.flatten().T, np.c_[y_ind.flatten().T, z_ind.flatten().T]].astype(np.float32)
# transform points
num_pts = self.pts_.shape[0]
pts_sdf = self.T_grid_sdf_ * PointCloud(self.pts_.T, frame='grid')
pts_sdf_tf = delta_T.as_frames('sdf', 'sdf') * pts_sdf
pts_grid_tf = self.T_sdf_grid_ * pts_sdf_tf
pts_tf = pts_grid_tf.data.T
all_points_t = time.clock()
# transform the center
origin_sdf = self.T_grid_sdf_ * Point(self.origin_, frame='grid')
origin_sdf_tf = delta_T.as_frames('sdf', 'sdf') * origin_sdf
origin_tf = self.T_sdf_grid_ * origin_sdf_tf
origin_tf = origin_tf.data
# use same resolution (since indices will be rescaled)
resolution_tf = self.resolution_
origin_res_t = time.clock()
# add each point to the new pose
if detailed:
sdf_data_tf = np.zeros([num_pts, 1])
for i in range(num_pts):
sdf_data_tf[i] = self[pts_tf[i, 0], pts_tf[i, 1], pts_tf[i, 2]]
else:
pts_tf_round = np.round(pts_tf).astype(np.int64)
# snap to closest boundary
pts_tf_round[:, 0] = np.max(np.c_[np.zeros([num_pts, 1]), pts_tf_round[:, 0]], axis=1)
pts_tf_round[:, 0] = np.min(np.c_[(self.dims_[0] - 1) * np.ones([num_pts, 1]), pts_tf_round[:, 0]], axis=1)
pts_tf_round[:, 1] = np.max(np.c_[np.zeros([num_pts, 1]), pts_tf_round[:, 1]], axis=1)
pts_tf_round[:, 1] = np.min(np.c_[(self.dims_[1] - 1) * np.ones([num_pts, 1]), pts_tf_round[:, 1]], axis=1)
pts_tf_round[:, 2] = np.max(np.c_[np.zeros([num_pts, 1]), pts_tf_round[:, 2]], axis=1)
pts_tf_round[:, 2] = np.min(np.c_[(self.dims_[2] - 1) * np.ones([num_pts, 1]), pts_tf_round[:, 2]], axis=1)
sdf_data_tf = self.data_[pts_tf_round[:, 0], pts_tf_round[:, 1], pts_tf_round[:, 2]]
sdf_data_tf_grid = sdf_data_tf.reshape(self.dims_)
tf_t = time.clock()
logging.debug('Sdf3D: Time to transform coords: %f' % (all_points_t - start_t))
logging.debug('Sdf3D: Time to transform origin: %f' % (origin_res_t - all_points_t))
logging.debug('Sdf3D: Time to transfer sd: %f' % (tf_t - origin_res_t))
return Sdf3D(sdf_data_tf_grid, origin_tf, resolution_tf, use_abs=self._use_abs_, T_sdf_world=self.T_sdf_world_)
def transform_pt_obj_to_grid(self, x_sdf, direction=False):
""" Converts a point in sdf coords to the grid basis. If direction then don't translate.
Parameters
----------
x_sdf : numpy 3xN ndarray or numeric scalar
points to transform from sdf basis in meters to grid basis
direction : bool
Returns
-------
x_grid : numpy 3xN ndarray or scalar
points in grid basis
"""
if isinstance(x_sdf, Number):
return self.T_world_grid_.scale * x_sdf
if direction:
points_sdf = NormalCloud(x_sdf.astype(np.float32), frame='world')
else:
points_sdf = PointCloud(x_sdf.astype(np.float32), frame='world')
x_grid = self.T_world_grid_ * points_sdf
return x_grid.data
def transform_pt_grid_to_obj(self, x_grid, direction=False):
""" Converts a point in grid coords to the world basis. If direction then don't translate.
Parameters
----------
x_grid : numpy 3xN ndarray or numeric scalar
points to transform from grid basis to sdf basis in meters
direction : bool
Returns
-------
x_sdf : numpy 3xN ndarray
points in sdf basis (meters)
"""
if isinstance(x_grid, Number):
return self.T_grid_world_.scale * x_grid
if direction:
points_grid = NormalCloud(x_grid.astype(np.float32), frame='grid')
else:
points_grid = PointCloud(x_grid.astype(np.float32), frame='grid')
x_sdf = self.T_grid_world_ * points_grid
return x_sdf.data
@staticmethod
def find_zero_crossing_linear(x1, y1, x2, y2):
""" Find zero crossing using linear approximation"""
# NOTE: use sparingly, approximations can be shoddy
d = x2 - x1
t1 = 0
t2 = np.linalg.norm(d)
v = d / t2
m = (y2 - y1) / (t2 - t1)
b = y1
t_zc = -b / m
x_zc = x1 + t_zc * v
return x_zc
@staticmethod
def find_zero_crossing_quadratic(x1, y1, x2, y2, x3, y3, eps=1.0):
""" Find zero crossing using quadratic approximation along 1d line"""
# compute coords along 1d line
v = x2 - x1
v = v / np.linalg.norm(v)
if v[v != 0].shape[0] == 0:
logging.error('Difference is 0. Probably a bug')
t1 = 0
t2 = (x2 - x1)[v != 0] / v[v != 0]
t2 = t2[0]
t3 = (x3 - x1)[v != 0] / v[v != 0]
t3 = t3[0]
# solve for quad approx
x1_row = np.array([t1 ** 2, t1, 1])
x2_row = np.array([t2 ** 2, t2, 1])
x3_row = np.array([t3 ** 2, t3, 1])
X = np.array([x1_row, x2_row, x3_row])
y_vec = np.array([y1, y2, y3])
try:
w = np.linalg.solve(X, y_vec)
except np.linalg.LinAlgError:
logging.error('Singular matrix. Probably a bug')
return None
# get positive roots
possible_t = np.roots(w)
t_zc = None
for i in range(possible_t.shape[0]):
if 0 <= possible_t[i] <= 10 and not np.iscomplex(possible_t[i]):
t_zc = possible_t[i]
# if no positive roots find min
if np.abs(w[0]) < 1e-10:
return None
if t_zc is None:
t_zc = -w[1] / (2 * w[0])
if t_zc < -eps or t_zc > eps:
return None
x_zc = x1 + t_zc * v
return x_zc

View File

@ -0,0 +1,127 @@
'''
Reads and writes sdfs to file
Author: Jeff Mahler
'''
import numpy as np
import os
from . import sdf
class SdfFile:
"""
A Signed Distance Field .sdf file reader and writer.
Attributes
----------
filepath : :obj:`str`
The full path to the .sdf or .csv file associated with this reader/writer.
"""
def __init__(self, filepath):
"""Construct and initialize a .sdf file reader and writer.
Parameters
----------
filepath : :obj:`str`
The full path to the desired .sdf or .csv file
Raises
------
ValueError
If the file extension is not .sdf of .csv.
"""
self.filepath_ = filepath
file_root, file_ext = os.path.splitext(self.filepath_)
if file_ext == '.sdf':
self.use_3d_ = True
elif file_ext == '.csv':
self.use_3d_ = False
else:
raise ValueError('Extension %s invalid for SDFs' %(file_ext))
@property
def filepath(self):
"""Returns the full path to the file associated with this reader/writer.
Returns
-------
:obj:`str`
The full path to the file associated with this reader/writer.
"""
return self.filepath_
def read(self):
"""Reads in the SDF file and returns a Sdf object.
Returns
-------
:obj:`Sdf`
A Sdf created from the data in the file.
"""
if self.use_3d_:
return self._read_3d()
else:
return self._read_2d()
def _read_3d(self):
"""Reads in a 3D SDF file and returns a Sdf object.
Returns
-------
:obj:`Sdf3D`
A 3DSdf created from the data in the file.
"""
if not os.path.exists(self.filepath_):
return None
my_file = open(self.filepath_, 'r')
nx, ny, nz = [int(i) for i in my_file.readline().split()] #dimension of each axis should all be equal for LSH
ox, oy, oz = [float(i) for i in my_file.readline().split()] #shape origin
dims = np.array([nx, ny, nz])
origin = np.array([ox, oy, oz])
resolution = float(my_file.readline()) # resolution of the grid cells in original mesh coords
sdf_data = np.zeros(dims)
# loop through file, getting each value
count = 0
for k in range(nz):
for j in range(ny):
for i in range(nx):
sdf_data[i][j][k] = float(my_file.readline())
count += 1
my_file.close()
return sdf.Sdf3D(sdf_data, origin, resolution)
def _read_2d(self):
"""Reads in a 2D SDF file and returns a Sdf object.
Returns
-------
:obj:`Sdf2D`
A 2DSdf created from the data in the file.
"""
if not os.path.exists(self.filepath_):
return None
sdf_data = np.loadtxt(self.filepath_, delimiter=',')
return sdf.Sdf2D(sdf_data)
def write(self, sdf):
"""Writes an SDF to a file.
Parameters
----------
sdf : :obj:`Sdf`
An Sdf object to write out.
Note
----
This is not currently implemented or supported.
"""
pass
if __name__ == '__main__':
pass

View File

@ -0,0 +1,86 @@
"""
A basic struct-like Stable Pose class to make accessing pose probability and rotation matrix easier
Author: Matt Matl and Nikhil Sharma
"""
import numpy as np
from autolab_core import RigidTransform
d_theta = np.deg2rad(1)
class StablePose(object):
"""A representation of a mesh's stable pose.
Attributes
----------
p : float
Probability associated with this stable pose.
r : :obj:`numpy.ndarray` of :obj`numpy.ndarray` of float
3x3 rotation matrix that rotates the mesh into the stable pose from
standardized coordinates.
x0 : :obj:`numpy.ndarray` of float
3D point in the mesh that is resting on the table.
face : :obj:`numpy.ndarray`
3D vector of indices corresponding to vertices forming the resting face
stp_id : :obj:`str`
A string identifier for the stable pose
T_obj_table : :obj:`RigidTransform`
A RigidTransform representation of the pose's rotation matrix.
"""
def __init__(self, p, r, x0, face=None, stp_id=-1):
"""Create a new stable pose object.
Parameters
----------
p : float
Probability associated with this stable pose.
r : :obj:`numpy.ndarray` of :obj`numpy.ndarray` of float
3x3 rotation matrix that rotates the mesh into the stable pose from
standardized coordinates.
x0 : :obj:`numpy.ndarray` of float
3D point in the mesh that is resting on the table.
face : :obj:`numpy.ndarray`
3D vector of indices corresponding to vertices forming the resting face
stp_id : :obj:`str`
A string identifier for the stable pose
"""
self.p = p
self.r = r
self.x0 = x0
self.face = face
self.id = stp_id
# fix stable pose bug
if np.abs(np.linalg.det(self.r) + 1) < 0.01:
self.r[1,:] = -self.r[1,:]
def __eq__(self, other):
""" Check equivalence by rotation about the z axis """
if not isinstance(other, StablePose):
raise ValueError('Can only compare stable pose objects')
R0 = self.r
R1 = other.r
dR = R1.T.dot(R0)
theta = 0
while theta < 2 * np.pi:
Rz = RigidTransform.z_axis_rotation(theta)
dR = R1.T.dot(Rz).dot(R0)
if np.linalg.norm(dR - np.eye(3)) < 1e-5:
return True
theta += d_theta
return False
@property
def T_obj_table(self):
return RigidTransform(rotation=self.r, from_frame='obj', to_frame='table')
@property
def T_obj_world(self):
T_world_obj = RigidTransform(rotation=self.r.T,
translation=self.x0,
from_frame='world',
to_frame='obj')
return T_world_obj.inverse()

View File

@ -0,0 +1,819 @@
# -*- coding: utf-8 -*-
# """
# Copyright ©2017. The Regents of the University of California (Regents). All Rights Reserved.
# Permission to use, copy, modify, and distribute this software and its documentation for educational,
# research, and not-for-profit purposes, without fee and without a signed licensing agreement, is
# hereby granted, provided that the above copyright notice, this paragraph and the following two
# paragraphs appear in all copies, modifications, and distributions. Contact The Office of Technology
# Licensing, UC Berkeley, 2150 Shattuck Avenue, Suite 510, Berkeley, CA 94720-1620, (510) 643-
# 7201, otl@berkeley.edu, http://ipira.berkeley.edu/industry-info for commercial licensing opportunities.
#
# IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL,
# INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF
# THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF REGENTS HAS BEEN
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIgit clone https://github.com/jeffmahler/Boost.NumPy.git
# ED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED
# HEREUNDER IS PROVIDED "AS IS". REGENTS HAS NO OBLIGATION TO PROVIDE
# MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
# """
# """
# Quasi-static point-based grasp quality metrics.
# Author: Jeff Mahler and Brian Hou
# """
import logging
# logging.root.setLevel(level=logging.DEBUG)
import numpy as np
try:
import pyhull.convex_hull as cvh
except:
# logging.warning('Failed to import pyhull')
pass
try:
import cvxopt as cvx
except:
# logging.warning('Failed to import cvx')
pass
import os
import scipy.spatial as ss
import sys
import time
from .grasp import PointGrasp
from. graspable_object import GraspableObject3D
from .grasp_quality_config import GraspQualityConfig
from .meshpy import mesh as m
from .meshpy import sdf as s
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import IPython
# turn off output logging
cvx.solvers.options['show_progress'] = False
class PointGraspMetrics3D:
""" Class to wrap functions for quasistatic point grasp quality metrics.
"""
@staticmethod
def grasp_quality(grasp, obj, params, contacts=None, vis=False):
"""
Computes the quality of a two-finger point grasps on a given object using a quasi-static model.
Parameters
----------
grasp : :obj:`ParallelJawPtGrasp3D`
grasp to evaluate
obj : :obj:`GraspableObject3D`
object to evaluate quality on
params : :obj:`GraspQualityConfig`
parameters of grasp quality function
"""
start = time.time()
if not isinstance(grasp, PointGrasp):
raise ValueError('Must provide a point grasp object')
if not isinstance(obj, GraspableObject3D):
raise ValueError('Must provide a 3D graspable object')
if not isinstance(params, GraspQualityConfig):
raise ValueError('Must provide GraspQualityConfig')
# read in params
method = params.quality_method
friction_coef = params.friction_coef
num_cone_faces = params.num_cone_faces
soft_fingers = params.soft_fingers
check_approach = params.check_approach
if not hasattr(PointGraspMetrics3D, method):
raise ValueError('Illegal point grasp metric %s specified' % (method))
# get point grasp contacts
contacts_start = time.time()
if contacts is None:
contacts_found, contacts = grasp.close_fingers(obj, check_approach=check_approach, vis=vis)
if not contacts_found:
logging.debug('Contacts not found')
return 0
if method == 'force_closure':
# Use fast force closure test (Nguyen 1988) if possible.
if len(contacts) == 2:
c1, c2 = contacts
return PointGraspMetrics3D.force_closure(c1, c2, friction_coef)
# Default to QP force closure test.
method = 'force_closure_qp'
# add the forces, torques, etc at each contact point
forces_start = time.time()
num_contacts = len(contacts)
forces = np.zeros([3, 0])
torques = np.zeros([3, 0])
normals = np.zeros([3, 0])
for i in range(num_contacts):
contact = contacts[i]
if vis:
if i == 0:
contact.plot_friction_cone(color='y')
else:
contact.plot_friction_cone(color='c')
# get contact forces
force_success, contact_forces, contact_outward_normal = contact.friction_cone(num_cone_faces, friction_coef)
if not force_success:
print('Force computation failed')
logging.debug('Force computation failed')
if params.all_contacts_required:
return 0
# get contact torques
torque_success, contact_torques = contact.torques(contact_forces)
if not torque_success:
print('Torque computation failed')
logging.debug('Torque computation failed')
if params.all_contacts_required:
return 0
# get the magnitude of the normal force that the contacts could apply
n = contact.normal_force_magnitude()
forces = np.c_[forces, n * contact_forces]
torques = np.c_[torques, n * contact_torques]
normals = np.c_[normals, n * -contact_outward_normal] # store inward pointing normals
if normals.shape[1] == 0:
logging.debug('No normals')
print('No normals')
return 0
# normalize torques
if 'torque_scaling' not in list(params.keys()):
torque_scaling = 1.0
if method == 'ferrari_canny_L1':
mn, mx = obj.mesh.bounding_box()
torque_scaling = 1.0 / np.median(mx)
print("torque scaling", torque_scaling)
params.torque_scaling = torque_scaling
if vis:
ax = plt.gca()
ax.set_xlim3d(0, obj.sdf.dims_[0])
ax.set_ylim3d(0, obj.sdf.dims_[1])
ax.set_zlim3d(0, obj.sdf.dims_[2])
plt.show()
# evaluate the desired quality metric
quality_start = time.time()
Q_func = getattr(PointGraspMetrics3D, method)
quality = Q_func(forces, torques, normals,
soft_fingers=soft_fingers,
params=params)
end = time.time()
logging.debug('Contacts took %.3f sec' % (forces_start - contacts_start))
logging.debug('Forces took %.3f sec' % (quality_start - forces_start))
logging.debug('Quality eval took %.3f sec' % (end - quality_start))
logging.debug('Everything took %.3f sec' % (end - start))
return quality
@staticmethod
def grasp_matrix(forces, torques, normals, soft_fingers=False,
finger_radius=0.005, params=None):
""" Computes the grasp map between contact forces and wrenchs on the object in its reference frame.
Parameters
----------
forces : 3xN :obj:`numpy.ndarray`
set of forces on object in object basis
torques : 3xN :obj:`numpy.ndarray`
set of torques on object in object basis
normals : 3xN :obj:`numpy.ndarray`
surface normals at the contact points
soft_fingers : bool
whether or not to use the soft finger contact model
finger_radius : float
the radius of the fingers to use
params : :obj:`GraspQualityConfig`
set of parameters for grasp matrix and contact model
Returns
-------
G : 6xM :obj:`numpy.ndarray`
grasp map
"""
if params is not None and 'finger_radius' in list(params.keys()):
finger_radius = params.finger_radius
num_forces = forces.shape[1]
num_torques = torques.shape[1]
if num_forces != num_torques:
raise ValueError('Need same number of forces and torques')
num_cols = num_forces
if soft_fingers:
num_normals = 2
if normals.ndim > 1:
num_normals = 2 * normals.shape[1]
num_cols = num_cols + num_normals
G = np.zeros([6, num_cols])
for i in range(num_forces):
G[:3, i] = forces[:, i]
# print("liang", params.torque_scaling)
G[3:, i] = params.torque_scaling * torques[:, i]
if soft_fingers:
torsion = np.pi * finger_radius ** 2 * params.friction_coef * normals * params.torque_scaling
pos_normal_i = int(-num_normals)
neg_normal_i = int(-num_normals + num_normals / 2)
G[3:, pos_normal_i:neg_normal_i] = torsion
G[3:, neg_normal_i:] = -torsion
return G
@staticmethod
def force_closure(c1, c2, friction_coef, use_abs_value=True):
"""" Checks force closure using the antipodality trick.
Parameters
----------
c1 : :obj:`Contact3D`
first contact point
c2 : :obj:`Contact3D`
second contact point
friction_coef : float
coefficient of friction at the contact point
use_abs_value : bool
whether or not to use directoinality of the surface normal (useful when mesh is not oriented)
Returns
-------
int : 1 if in force closure, 0 otherwise
"""
if c1.point is None or c2.point is None or c1.normal is None or c2.normal is None:
return 0
p1, p2 = c1.point, c2.point
n1, n2 = -c1.normal, -c2.normal # inward facing normals
if (p1 == p2).all(): # same point
return 0
for normal, contact, other_contact in [(n1, p1, p2), (n2, p2, p1)]:
diff = other_contact - contact
normal_proj = normal.dot(diff) / np.linalg.norm(normal)
if use_abs_value:
normal_proj = abs(normal.dot(diff)) / np.linalg.norm(normal)
if normal_proj < 0:
return 0 # wrong side
alpha = np.arccos(normal_proj / np.linalg.norm(diff))
if alpha > np.arctan(friction_coef):
return 0 # outside of friction cone
return 1
@staticmethod
def force_closure_qp(forces, torques, normals, soft_fingers=False,
wrench_norm_thresh=1e-3, wrench_regularizer=1e-10,
params=None):
""" Checks force closure by solving a quadratic program (whether or not zero is in the convex hull)
Parameters
----------
forces : 3xN :obj:`numpy.ndarray`
set of forces on object in object basis
torques : 3xN :obj:`numpy.ndarray`
set of torques on object in object basis
normals : 3xN :obj:`numpy.ndarray`
surface normals at the contact points
soft_fingers : bool
whether or not to use the soft finger contact model
wrench_norm_thresh : float
threshold to use to determine equivalence of target wrenches
wrench_regularizer : float
small float to make quadratic program positive semidefinite
params : :obj:`GraspQualityConfig`
set of parameters for grasp matrix and contact model
Returns
-------
int : 1 if in force closure, 0 otherwise
"""
if params is not None:
if 'wrench_norm_thresh' in list(params.keys()):
wrench_norm_thresh = params.wrench_norm_thresh
if 'wrench_regularizer' in list(params.keys()):
wrench_regularizer = params.wrench_regularizer
G = PointGraspMetrics3D.grasp_matrix(forces, torques, normals, soft_fingers, params=params)
min_norm, _ = PointGraspMetrics3D.min_norm_vector_in_facet(G, wrench_regularizer=wrench_regularizer)
return 1 * (min_norm < wrench_norm_thresh) # if greater than wrench_norm_thresh, 0 is outside of hull
@staticmethod
def partial_closure(forces, torques, normals, soft_fingers=False,
wrench_norm_thresh=1e-3, wrench_regularizer=1e-10,
params=None):
""" Evalutes partial closure: whether or not the forces and torques can resist a specific wrench.
Estimates resistance by sollving a quadratic program (whether or not the target wrench is in the convex hull).
Parameters
----------
forces : 3xN :obj:`numpy.ndarray`
set of forces on object in object basis
torques : 3xN :obj:`numpy.ndarray`
set of torques on object in object basis
normals : 3xN :obj:`numpy.ndarray`
surface normals at the contact points
soft_fingers : bool
whether or not to use the soft finger contact model
wrench_norm_thresh : float
threshold to use to determine equivalence of target wrenches
wrench_regularizer : float
small float to make quadratic program positive semidefinite
params : :obj:`GraspQualityConfig`
set of parameters for grasp matrix and contact model
Returns
-------
int : 1 if in partial closure, 0 otherwise
"""
force_limit = None
if params is None:
return 0
force_limit = params.force_limits
target_wrench = params.target_wrench
if 'wrench_norm_thresh' in list(params.keys()):
wrench_norm_thresh = params.wrench_norm_thresh
if 'wrench_regularizer' in list(params.keys()):
wrench_regularizer = params.wrench_regularizer
# reorganize the grasp matrix for easier constraint enforcement in optimization
num_fingers = normals.shape[1]
num_wrenches_per_finger = forces.shape[1] / num_fingers
G = np.zeros([6, 0])
for i in range(num_fingers):
start_i = num_wrenches_per_finger * i
end_i = num_wrenches_per_finger * (i + 1)
G_i = PointGraspMetrics3D.grasp_matrix(forces[:, start_i:end_i], torques[:, start_i:end_i],
normals[:, i:i + 1],
soft_fingers, params=params)
G = np.c_[G, G_i]
wrench_resisted, _ = PointGraspMetrics3D.wrench_in_positive_span(G, target_wrench, force_limit, num_fingers,
wrench_norm_thresh=wrench_norm_thresh,
wrench_regularizer=wrench_regularizer)
return 1 * wrench_resisted
@staticmethod
def wrench_resistance(forces, torques, normals, soft_fingers=False,
wrench_norm_thresh=1e-3, wrench_regularizer=1e-10,
finger_force_eps=1e-9, params=None):
""" Evalutes wrench resistance: the inverse norm of the contact forces required to resist a target wrench
Estimates resistance by sollving a quadratic program (min normal contact forces to produce a wrench).
Parameters
----------
forces : 3xN :obj:`numpy.ndarray`
set of forces on object in object basis
torques : 3xN :obj:`numpy.ndarray`
set of torques on object in object basis
normals : 3xN :obj:`numpy.ndarray`
surface normals at the contact points
soft_fingers : bool
whether or not to use the soft finger contact model
wrench_norm_thresh : float
threshold to use to determine equivalence of target wrenches
wrench_regularizer : float
small float to make quadratic program positive semidefinite
finger_force_eps : float
small float to prevent numeric issues in wrench resistance metric
params : :obj:`GraspQualityConfig`
set of parameters for grasp matrix and contact model
Returns
-------
float : value of wrench resistance metric
"""
force_limit = None
if params is None:
return 0
force_limit = params.force_limits
target_wrench = params.target_wrench
if 'wrench_norm_thresh' in list(params.keys()):
wrench_norm_thresh = params.wrench_norm_thresh
if 'wrench_regularizer' in list(params.keys()):
wrench_regularizer = params.wrench_regularizer
if 'finger_force_eps' in list(params.keys()):
finger_force_eps = params.finger_force_eps
# reorganize the grasp matrix for easier constraint enforcement in optimization
num_fingers = normals.shape[1]
num_wrenches_per_finger = forces.shape[1] / num_fingers
G = np.zeros([6, 0])
for i in range(num_fingers):
start_i = num_wrenches_per_finger * i
end_i = num_wrenches_per_finger * (i + 1)
G_i = PointGraspMetrics3D.grasp_matrix(forces[:, start_i:end_i], torques[:, start_i:end_i],
normals[:, i:i + 1],
soft_fingers, params=params)
G = np.c_[G, G_i]
# compute metric from finger force norm
Q = 0
wrench_resisted, finger_force_norm = PointGraspMetrics3D.wrench_in_positive_span(G, target_wrench, force_limit,
num_fingers,
wrench_norm_thresh=wrench_norm_thresh,
wrench_regularizer=wrench_regularizer)
if wrench_resisted:
Q = 1.0 / (finger_force_norm + finger_force_eps) - 1.0 / (2 * force_limit)
return Q
@staticmethod
def min_singular(forces, torques, normals, soft_fingers=False, params=None):
""" Min singular value of grasp matrix - measure of wrench that grasp is "weakest" at resisting.
Parameters
----------
forces : 3xN :obj:`numpy.ndarray`
set of forces on object in object basis
torques : 3xN :obj:`numpy.ndarray`
set of torques on object in object basis
normals : 3xN :obj:`numpy.ndarray`
surface normals at the contact points
soft_fingers : bool
whether or not to use the soft finger contact model
params : :obj:`GraspQualityConfig`
set of parameters for grasp matrix and contact model
Returns
-------
float : value of smallest singular value
"""
G = PointGraspMetrics3D.grasp_matrix(forces, torques, normals, soft_fingers)
_, S, _ = np.linalg.svd(G)
min_sig = S[5]
return min_sig
@staticmethod
def wrench_volume(forces, torques, normals, soft_fingers=False, params=None):
""" Volume of grasp matrix singular values - score of all wrenches that the grasp can resist.
Parameters
----------
forces : 3xN :obj:`numpy.ndarray`
set of forces on object in object basis
torques : 3xN :obj:`numpy.ndarray`
set of torques on object in object basis
normals : 3xN :obj:`numpy.ndarray`
surface normals at the contact points
soft_fingers : bool
whether or not to use the soft finger contact model
params : :obj:`GraspQualityConfig`
set of parameters for grasp matrix and contact model
Returns
-------
float : value of wrench volume
"""
k = 1
if params is not None and 'k' in list(params.keys()):
k = params.k
G = PointGraspMetrics3D.grasp_matrix(forces, torques, normals, soft_fingers)
_, S, _ = np.linalg.svd(G)
sig = S
return k * np.sqrt(np.prod(sig))
@staticmethod
def grasp_isotropy(forces, torques, normals, soft_fingers=False, params=None):
""" Condition number of grasp matrix - ratio of "weakest" wrench that the grasp can exert to the "strongest" one.
Parameters
----------
forces : 3xN :obj:`numpy.ndarray`
set of forces on object in object basis
torques : 3xN :obj:`numpy.ndarray`
set of torques on object in object basis
normals : 3xN :obj:`numpy.ndarray`
surface normals at the contact points
soft_fingers : bool
whether or not to use the soft finger contact model
params : :obj:`GraspQualityConfig`
set of parameters for grasp matrix and contact model
Returns
-------
float : value of grasp isotropy metric
"""
G = PointGraspMetrics3D.grasp_matrix(forces, torques, normals, soft_fingers)
_, S, _ = np.linalg.svd(G)
max_sig = S[0]
min_sig = S[5]
isotropy = min_sig / max_sig
if np.isnan(isotropy) or np.isinf(isotropy):
return 0
return isotropy
@staticmethod
def ferrari_canny_L1(forces, torques, normals, soft_fingers=False, params=None,
wrench_norm_thresh=1e-3,
wrench_regularizer=1e-10):
""" Ferrari & Canny's L1 metric. Also known as the epsilon metric.
Parameters
----------
forces : 3xN :obj:`numpy.ndarray`
set of forces on object in object basis
torques : 3xN :obj:`numpy.ndarray`
set of torques on object in object basis
normals : 3xN :obj:`numpy.ndarray`
surface normals at the contact points
soft_fingers : bool
whether or not to use the soft finger contact model
params : :obj:`GraspQualityConfig`
set of parameters for grasp matrix and contact model
wrench_norm_thresh : float
threshold to use to determine equivalence of target wrenches
wrench_regularizer : float
small float to make quadratic program positive semidefinite
Returns
-------
float : value of metric
"""
if params is not None and 'wrench_norm_thresh' in list(params.keys()):
wrench_norm_thresh = params.wrench_norm_thresh
if params is not None and 'wrench_regularizer' in list(params.keys()):
wrench_regularizer = params.wrench_regularizer
# create grasp matrix
G = PointGraspMetrics3D.grasp_matrix(forces, torques, normals,
soft_fingers, params=params)
s = time.time()
# center grasp matrix for better convex hull comp
hull = cvh.ConvexHull(G.T)
# TODO: suppress ridiculous amount of output for perfectly valid input to qhull
e = time.time()
logging.debug('CVH took %.3f sec' % (e - s))
debug = False
if debug:
fig = plt.figure()
torques = G[3:, :].T
ax = Axes3D(fig)
ax.scatter(torques[:, 0], torques[:, 1], torques[:, 2], c='b', s=50)
ax.scatter(0, 0, 0, c='k', s=80)
ax.set_xlim3d(-1.5, 1.5)
ax.set_ylim3d(-1.5, 1.5)
ax.set_zlim3d(-1.5, 1.5)
ax.set_xlabel('tx')
ax.set_ylabel('ty')
ax.set_zlabel('tz')
plt.show()
if len(hull.vertices) == 0:
logging.warning('Convex hull could not be computed')
return 0.0
# determine whether or not zero is in the convex hull
s = time.time()
min_norm_in_hull, v = PointGraspMetrics3D.min_norm_vector_in_facet(G, wrench_regularizer=wrench_regularizer)
e = time.time()
logging.debug('Min norm took %.3f sec' % (e - s))
# print("shunang",min_norm_in_hull)
# if norm is greater than 0 then forces are outside of hull
if min_norm_in_hull > wrench_norm_thresh:
logging.debug('Zero not in convex hull')
return 0.0
# if there are fewer nonzeros than D-1 (dim of space minus one)
# then zero is on the boundary and therefore we do not have
# force closure
if np.sum(v > 1e-4) <= G.shape[0] - 1:
logging.warning('Zero not in interior of convex hull')
return 0.0
# find minimum norm vector across all facets of convex hull
s = time.time()
min_dist = sys.float_info.max
closest_facet = None
# print("shunang",G)
for v in hull.vertices:
if np.max(np.array(v)) < G.shape[1]: # because of some occasional odd behavior from pyhull
facet = G[:, v]
# print("shunang1",facet)
dist, _ = PointGraspMetrics3D.min_norm_vector_in_facet(facet, wrench_regularizer=wrench_regularizer)
if dist < min_dist:
min_dist = dist
closest_facet = v
e = time.time()
logging.debug('Min dist took %.3f sec for %d vertices' % (e - s, len(hull.vertices)))
return min_dist
@staticmethod
def ferrari_canny_L1_force_only(forces, torques, normals, soft_fingers=False, params=None,
wrench_norm_thresh=1e-3,
wrench_regularizer=1e-10):
""" Ferrari & Canny's L1 metric with force only. Also known as the epsilon metric.
Parameters
----------
forces : 3xN :obj:`numpy.ndarray`
set of forces on object in object basis
torques : 3xN :obj:`numpy.ndarray`
set of torques on object in object basis
normals : 3xN :obj:`numpy.ndarray`
surface normals at the contact points
soft_fingers : bool
whether or not to use the soft finger contact model
params : :obj:`GraspQualityConfig`
set of parameters for grasp matrix and contact model
wrench_norm_thresh : float
threshold to use to determine equivalence of target wrenches
wrench_regularizer : float
small float to make quadratic program positive semidefinite
Returns
-------
float : value of metric
"""
if params is not None and 'wrench_norm_thresh' in list(params.keys()):
wrench_norm_thresh = params.wrench_norm_thresh
if params is not None and 'wrench_regularizer' in list(params.keys()):
wrench_regularizer = params.wrench_regularizer
# create grasp matrix
G = PointGraspMetrics3D.grasp_matrix(forces, torques, normals,
soft_fingers, params=params)
G = G[:3, :]
s = time.time()
# center grasp matrix for better convex hull comp
hull = cvh.ConvexHull(G.T)
# TODO: suppress ridiculous amount of output for perfectly valid input to qhull
e = time.time()
logging.debug('CVH took %.3f sec' % (e - s))
debug = False
if debug:
fig = plt.figure()
torques = G[3:, :].T
ax = Axes3D(fig)
ax.scatter(torques[:, 0], torques[:, 1], torques[:, 2], c='b', s=50)
ax.scatter(0, 0, 0, c='k', s=80)
ax.set_xlim3d(-1.5, 1.5)
ax.set_ylim3d(-1.5, 1.5)
ax.set_zlim3d(-1.5, 1.5)
ax.set_xlabel('tx')
ax.set_ylabel('ty')
ax.set_zlabel('tz')
plt.show()
if len(hull.vertices) == 0:
logging.warning('Convex hull could not be computed')
return 0.0
# determine whether or not zero is in the convex hull
s = time.time()
min_norm_in_hull, v = PointGraspMetrics3D.min_norm_vector_in_facet(G, wrench_regularizer=wrench_regularizer)
e = time.time()
logging.debug('Min norm took %.3f sec' % (e - s))
# print("shunang",min_norm_in_hull)
# if norm is greater than 0 then forces are outside of hull
if min_norm_in_hull > wrench_norm_thresh:
logging.debug('Zero not in convex hull')
return 0.0
# if there are fewer nonzeros than D-1 (dim of space minus one)
# then zero is on the boundary and therefore we do not have
# force closure
if np.sum(v > 1e-4) <= G.shape[0] - 1:
logging.warning('Zero not in interior of convex hull')
return 0.0
# find minimum norm vector across all facets of convex hull
s = time.time()
min_dist = sys.float_info.max
closest_facet = None
# print("shunang",G)
for v in hull.vertices:
if np.max(np.array(v)) < G.shape[1]: # because of some occasional odd behavior from pyhull
facet = G[:, v]
# print("shunang1",facet)
dist, _ = PointGraspMetrics3D.min_norm_vector_in_facet(facet, wrench_regularizer=wrench_regularizer)
if dist < min_dist:
min_dist = dist
closest_facet = v
e = time.time()
logging.debug('Min dist took %.3f sec for %d vertices' % (e - s, len(hull.vertices)))
return min_dist
@staticmethod
def wrench_in_positive_span(wrench_basis, target_wrench, force_limit, num_fingers=1,
wrench_norm_thresh=1e-4, wrench_regularizer=1e-10):
""" Check whether a target can be exerted by positive combinations of wrenches in a given basis with L1 norm fonger force limit limit.
Parameters
----------
wrench_basis : 6xN :obj:`numpy.ndarray`
basis for the wrench space
target_wrench : 6x1 :obj:`numpy.ndarray`
target wrench to resist
force_limit : float
L1 upper bound on the forces per finger (aka contact point)
num_fingers : int
number of contacts, used to enforce L1 finger constraint
wrench_norm_thresh : float
threshold to use to determine equivalence of target wrenches
wrench_regularizer : float
small float to make quadratic program positive semidefinite
Returns
-------
int
whether or not wrench can be resisted
float
minimum norm of the finger forces required to resist the wrench
"""
num_wrenches = wrench_basis.shape[1]
# quadratic and linear costs
P = wrench_basis.T.dot(wrench_basis) + wrench_regularizer * np.eye(num_wrenches)
q = -wrench_basis.T.dot(target_wrench)
# inequalities
lam_geq_zero = -1 * np.eye(num_wrenches)
num_wrenches_per_finger = num_wrenches / num_fingers
force_constraint = np.zeros([num_fingers, num_wrenches])
for i in range(num_fingers):
start_i = num_wrenches_per_finger * i
end_i = num_wrenches_per_finger * (i + 1)
force_constraint[i, start_i:end_i] = np.ones(num_wrenches_per_finger)
G = np.r_[lam_geq_zero, force_constraint]
h = np.zeros(num_wrenches + num_fingers)
for i in range(num_fingers):
h[num_wrenches + i] = force_limit
# convert to cvx and solve
P = cvx.matrix(P)
q = cvx.matrix(q)
G = cvx.matrix(G)
h = cvx.matrix(h)
sol = cvx.solvers.qp(P, q, G, h)
v = np.array(sol['x'])
min_dist = np.linalg.norm(wrench_basis.dot(v).ravel() - target_wrench) ** 2
# add back in the target wrench
return min_dist < wrench_norm_thresh, np.linalg.norm(v)
@staticmethod
def min_norm_vector_in_facet(facet, wrench_regularizer=1e-10):
""" Finds the minimum norm point in the convex hull of a given facet (aka simplex) by solving a QP.
Parameters
----------
facet : 6xN :obj:`numpy.ndarray`
vectors forming the facet
wrench_regularizer : float
small float to make quadratic program positive semidefinite
Returns
-------
float
minimum norm of any point in the convex hull of the facet
Nx1 :obj:`numpy.ndarray`
vector of coefficients that achieves the minimum
"""
dim = facet.shape[1] # num vertices in facet
# create alpha weights for vertices of facet
G = facet.T.dot(facet)
grasp_matrix = G + wrench_regularizer * np.eye(G.shape[0])
# Solve QP to minimize .5 x'Px + q'x subject to Gx <= h, Ax = b
P = cvx.matrix(2 * grasp_matrix) # quadratic cost for Euclidean dist
q = cvx.matrix(np.zeros((dim, 1)))
G = cvx.matrix(-np.eye(dim)) # greater than zero constraint
h = cvx.matrix(np.zeros((dim, 1)))
A = cvx.matrix(np.ones((1, dim))) # sum constraint to enforce convex
b = cvx.matrix(np.ones(1)) # combinations of vertices
sol = cvx.solvers.qp(P, q, G, h, A, b)
v = np.array(sol['x'])
min_norm = np.sqrt(sol['primal objective'])
return abs(min_norm), v

Some files were not shown because too many files have changed in this diff Show More