This repository converts BOP-format 6D pose estimation datasets into the BOP-ASK visual question answering format for VLM fine-tuning. The pipeline generates 10+ question types covering camera parameters, object poses, grasps, spatial relationships, trajectories, depth, and object rearrangement.
Supported datasets: hope, handal, ycbv, hot3d
bopask-github/
├── config/
│ └── dataset_config.json # Per-dataset depth scales, resolutions, methods
│
├── generate_surface_masks.py # Step 1: Molmo + SAM2 surface segmentation
├── render_objects_and_descriptions.py # Step 2: PyVista renders + GPT descriptions
├── export_bounding_boxes.py # Step 3: 3D bbox export per scene
├── run_all_extrinsic_estimations.py # Step 4: Camera extrinsic estimation (wrapper)
├── bop2m2t2.py # Step 5: BOP -> M2T2 format conversion
├── generate_all_trajectories_parallel.py # Step 6: RRT pick-and-place trajectories (parallel)
├── run_all_grasp_generation.py # Step 7: M2T2 6-DoF grasp generation
├── filter_grasp_collisions.py # Step 8: 3D grasp collision filtering
├── generate_question_templates.py # Step 9: Pre-generate question variations
├── generate_batch_qa_dataset.py # Step 10: Final JSONL + image generation (emits debug visualizations)
│
├── utils/ # Shared helpers imported by the pipeline
│ ├── bop_utils.py # OpenAI client, text drawing
│ ├── estimate_cam2world.py # Core extrinsic computation (invoked by step 4)
│ ├── generate_all_trajectories.py # Sequential trajectory generation + helpers (imported by step 6)
│ ├── plan_motion_rrt.py # Core RRT planner (imported by step 6)
│ ├── grasp_vis_utils.py # 2D grasp point extraction (used by steps 8 and 10)
│ └── visualize_3d_boxes.py # BOP bbox visualizer class (used by steps 3 and 10)
│
├── M2T2/ # Vendored: M2T2 grasp prediction model
├── question_templates/ # Cached question templates (per dataset/split, git-ignored)
├── requirements.txt # pip dependencies (see Environment Setup)
└── download_bop_datasets.sh # Convenience script to fetch BOP datasets
The pipeline depends on PyTorch 2.6 built for CUDA 12.4 plus a matching
CUDA 12.4 toolkit (needed to compile the M2T2/pointnet2_ops C++/CUDA
extension). The steps below install both inside a dedicated conda env and are
known to work end-to-end from a clean machine.
Prerequisites: a recent
conda/mamba, an NVIDIA GPU with a driver supporting CUDA ≥ 12.4, andgcc11–13.
conda create -n bopask python=3.10 -y
conda activate bopaskconda install -c nvidia/label/cuda-12.4.1 -y \
cuda-nvcc=12.4 cuda-cudart=12.4 cuda-cudart-dev=12.4 \
cuda-libraries=12.4 cuda-libraries-dev=12.4 \
cuda-nvrtc=12.4 cuda-nvrtc-dev=12.4 \
cuda-nvtx=12.4 cuda-command-line-tools=12.4 \
cuda-version=12.4Make these persistent for the env (optional but highly recommended):
mkdir -p $CONDA_PREFIX/etc/conda/activate.d
cat > $CONDA_PREFIX/etc/conda/activate.d/cuda.sh <<'EOF'
export CUDA_HOME=$CONDA_PREFIX
export CUDA_PATH=$CONDA_PREFIX
export PATH=$CONDA_PREFIX/bin:$PATH
export LD_LIBRARY_PATH=$CONDA_PREFIX/lib:${LD_LIBRARY_PATH:-}
EOFRest of the installation:
pip install torch==2.6.0+cu124 torchvision==0.21.0+cu124 \
--index-url https://download.pytorch.org/whl/cu124
pip install -r requirements.txt
# Molmo's remote-code modeling file breaks on transformers >= 4.50, so pin 4.46.
pip install 'transformers==4.46.3' 'tokenizers<0.21' einops accelerate
pip install --no-build-isolation M2T2/pointnet2_ops/
pip install --no-build-isolation ./M2T2/cd M2T2/
wget https://huggingface.co/wentao-yuan/m2t2/resolve/main/m2t2.pth
cd ..Step 1 (surface-mask generation) uses Meta's official SAM 2, not the
similarly-named PyPI package. Clone it as a sibling directory, install it,
and fetch the hiera_large checkpoint:
# Clone and install SAM 2 from Meta's repo
git clone https://github.com/facebookresearch/sam2.git
cd sam2
pip install -e .
# Download the hiera_large checkpoint (~900 MB)
cd checkpoints && bash download_ckpts.sh && cd ..
cd ..Needed only for step 2 (object descriptions) and step 9 (question templates). Everything else runs offline.
export OPENAI_API_KEY="sk-..."# All four supported datasets
bash download_bop_datasets.sh
# Or just a subset
bash download_bop_datasets.sh --datasets hope --splits val --output_dir ./bop| Symptom | Cause | Fix |
|---|---|---|
ModuleNotFoundError: No module named 'torch' during pip install M2T2/pointnet2_ops/ |
pip built in an isolated env without torch | Add --no-build-isolation (step 6) |
RuntimeError: The detected CUDA version (X.Y) mismatches the version that was used to compile PyTorch (12.4) |
System nvcc is on PATH ahead of the conda one |
Re-run step 3; verify with which nvcc |
error: could not create 'build/...': Permission denied |
Stale root-owned build/ or *.egg-info/ from an earlier sudo install |
sudo rm -rf M2T2/pointnet2_ops/build M2T2/pointnet2_ops/pointnet2_ops.egg-info M2T2/build M2T2/m2t2.egg-info then retry step 6 |
ls $CONDA_PREFIX/bin/nvcc says "No such file or directory" |
cuda-toolkit metapackage didn't pull nvcc on your platform |
Install the explicit component list in step 2 |
torch.cuda.is_available() returns False |
GPU driver too old, or torch install mismatched CUDA | Check nvidia-smi shows a driver supporting ≥ 12.4, then reinstall the exact +cu124 wheel in step 4 |
Your data should follow standard BOP layout:
bop/
└── hope/
├── camera.json # Global camera intrinsics
├── models/
│ ├── obj_XXXXXX.ply # 3D meshes
│ ├── obj_XXXXXX.png # Textures (optional)
│ └── models_info.json # Per-model bounding box metadata
├── models_eval/ # Simplified models for evaluation
└── val/ # Split directory
└── 000001/ # Scene directory
├── rgb/ # RGB images: 000000.png, 000001.png, ...
├── depth/ # 16-bit PNG depth maps (values in mm)
├── mask/ # Per-object binary masks: {frame}_{idx}.png
├── mask_visib/ # Visible-part masks
├── scene_camera.json # Per-frame camera intrinsics
├── scene_gt.json # Ground-truth 6D poses
└── scene_gt_info.json # Additional GT metadata
Generates binary masks of the flat surface (table/shelf) on which objects rest. Uses Molmo (a pointing VLM) to identify the surface, then SAM 2 to segment it.
huggingface-cli download allenai/Molmo-72B-D-0924 \
--local-dir models/molmo-72b-d-0924
python generate_surface_masks.py \
--dataset_path bop/hope --split val \
--molmo_model models/molmo-72b-d-0924 \
--sam2_checkpoint sam2/checkpoints/sam2.1_hiera_large.pt \
--resumeOutput:
bop/hope/val/surface_masks/
├── 1_0.png # scene 1, frame 0 — binary mask (0/255), same resolution as RGB
├── 1_1.png
└── ...
Renders 4-view composites of each 3D model and optionally generates natural-language descriptions via OpenAI.
python render_objects_and_descriptions.py \
--dataset_path bop/hope \
--output_dir bop/hope/renders_pyvista_hope
cp bop/hope/renders_pyvista_hope/object_descriptions.json bop/hope/Pass --skip_description to skip the OpenAI call. On headless servers: xvfb-run -a python ...
Output:
renders_pyvista_hope/
├── renders/
│ └── obj_000001.png # 2x2 grid of 4 views per object
└── object_descriptions.json # {obj_key: {render_path, description, model_info}}
bop/hope/object_descriptions.json # Copy for downstream scripts
You typically run this once per dataset (not per split) since object models are shared.
Projects 3D model bounding boxes into each frame. Also assigns unique IDs when multiple instances of the same object appear (e.g., 2a, 2b).
python export_bounding_boxes.py bop/hope --split valOutput (per scene directory):
bop/hope/val/000001/
├── bboxes_000001.json # Per-frame bounding box data
├── scene_gt.json # Modified with unique object IDs
└── scene_gt_original.json # Backup of original scene_gt
Data format (bboxes_{scene}.json):
{
"0": [
{
"obj_id": "2a",
"corners_2d": [[x,y], ...], // 8 projected 2D corners
"rotation_matrix": [r00, r01, ...], // 3x3 row-major, 9 floats
"position": [tx, ty, tz], // mm, in camera frame
"scale": [sx, sy, sz], // mm, model bbox dimensions
"euler_xyz": [ex, ey, ez], // radians
"quaternion_xyzw": [qx, qy, qz, qw],
"original_obj_id": 2
}
]
}Estimates a camera-to-world transform for each frame using object pose up-vectors and RANSAC plane fitting on (surface-masked) depth data.
python run_all_extrinsic_estimations.py --dataset_path bop/hope --split valOutput (per scene directory):
bop/hope/val/000001/
├── T_wc_0.txt # 4x4 float matrix (or "FAILED" on first line)
├── T_wc_1.txt
└── extrinsic_visualizations/ # Debug overlays
├── object_up_vectors_0.png
└── fitted_ransac_plane_0.png
Data format (T_wc_{frame_id}.txt): 4x4 matrix, space-separated floats. First line is FAILED if estimation failed (these frames are skipped in step 5).
The depth scale and normal estimation method are auto-selected per dataset/split (see config/dataset_config.json).
Converts each valid frame (with successful extrinsics) into the per-folder format expected by M2T2 grasp generation.
python bop2m2t2.py --bop_root bop/hope/valOutput:
bop/hope/val/converted-bop/
└── scene_000001_frame_000000/
├── rgb.png # Resized to 1280x720
├── depth.npy # float32, metres, shape (720, 1280)
├── seg.png # uint16 semantic mask (0=bg, obj_label=fg)
└── meta_data.pkl # dict: camera_pose (4x4), intrinsics (3x3),
# label_map ({"obj_2a": 297, ...}),
# scene_bounds (None), ee_pose (None)
Important: Frames where T_wc_{frame_id}.txt contains FAILED are skipped entirely.
Generates collision-free pick-and-place trajectories for all object pairs using an RRT planner with cartesian smoothing. Runs in parallel.
python generate_all_trajectories_parallel.py \
--dataset_path bop/hope --split val --max_workers 8Output:
bop/hope/trajectories_val.json
Data format: JSON with keys image_paths and per-trajectory entries containing 3D waypoints, 2D pixel projections, pick/place object IDs, and motion descriptions.
Runs M2T2 inference on each converted frame to produce 6-DoF grasp candidates for every object.
python run_all_grasp_generation.py \
--converted_bop_dir bop/hope/val/converted-bop \
--checkpoint_path M2T2/m2t2.pthOutput (per converted frame):
bop/hope/val/converted-bop/scene_000001_frame_000000/
└── scene_state_complete.json # Per-object: grasp poses, point cloud, confidence
Removes grasps that collide with neighboring objects in 3D (within 1.5cm). Objects where all grasps collide are flagged for rearrangement questions.
python filter_grasp_collisions.py \
--converted_bop_dir bop/hope/val/converted-bopOutput: Updates scene_state_complete.json in-place. Adds filtered_grasps, needs_rearrangement, and blocking_objects fields.
Pre-generates question text variations offline using OpenAI, keyed by (question_type, object_description, spatial_phrase). Reduces API costs during QA generation.
python generate_question_templates.py \
--dataset_path bop/hope --split val \
--desc_json bop/hope/object_descriptions.json \
--incremental \
--output_file question_templates/question_templates_hope_val.jsonUse --incremental to accumulate templates across runs (e.g., run for val then train).
Output:
question_templates/question_templates_hope_val.json
Produces the final VQA dataset in LLaMA-Factory format.
python generate_batch_qa_dataset.py \
--dataset_path bop/hope --split val \
--desc_json bop/hope/object_descriptions.json \
--use_templates \
--template_file question_templates/question_templates_hope_val.json \
--use_depth_questionsOutput:
hope-val-hf/
├── val.jsonl # ShareGPT-format conversations
├── images/ # One RGB image per scene-frame
│ └── scene_000001_frame_000000.png
└── debug-images/ # QA overlays for verification
JSONL format (one line per conversation):
{
"conversations": [
{"from": "human", "value": "<image>\nQuestion text..."},
{"from": "gpt", "value": "Answer text..."}
],
"images": ["images/scene_000001_frame_000000.png"]
}bop/
└── hope/
├── camera.json
├── object_descriptions.json # From step 2
├── trajectories_val.json # From step 6
├── models/
├── models_eval/
└── val/
├── 000001/ # Scene directory
│ ├── rgb/
│ ├── depth/
│ ├── mask/
│ ├── mask_visib/
│ ├── scene_camera.json
│ ├── scene_gt.json # Modified with unique IDs (step 3)
│ ├── scene_gt_original.json # Backup (step 3)
│ ├── bboxes_000001.json # 3D bbox data (step 3)
│ ├── T_wc_0.txt # Extrinsics (step 4)
│ ├── T_wc_1.txt
│ └── extrinsic_visualizations/ # Debug images (step 4)
├── surface_masks/ # Surface masks (step 1)
│ ├── 1_0.png
│ └── ...
└── converted-bop/ # M2T2 format (step 5)
└── scene_000001_frame_000000/
├── rgb.png
├── depth.npy
├── seg.png
├── meta_data.pkl
└── scene_state_complete.json # Grasps (steps 7-8)
hope-val-hf/ # Final output (step 10)
├── val.jsonl
├── images/
└── debug-images/
renders_pyvista_hope/ # Object renders (step 2)
├── renders/
└── object_descriptions.json
question_templates/ # Cached templates (step 9)
└── question_templates_hope_val.json
Debug visualizations (one per question type) are emitted automatically under
${DATASET}-${SPLIT}-hf/debug_samples/ by step 10 — use those to eyeball
correctness before running the utilities below.
| Type | Subtype | Description |
|---|---|---|
| camera | extrinsics | 4x4 camera-to-world matrix |
| pose | 2dbbox | 8 projected 2D bounding box corners |
| pose | 3dbbox | 8 corners with depth values |
| pose | matrix | Position + scale + 3x3 rotation |
| pose | euler | Position + scale + Euler angles |
| grasp | 6dof | 6-DoF grasp pose (position + rotation) |
| grasp | 2dplane | 5 points defining the grasp in 2D |
| grasp | 3dplane | 5 grasp points with depth |
| trajectory | 2d | 2D pixel path from pick to place |
| trajectory | 3d | 3D path with depth |
| spatial | relative_position | Spatial relationship between objects |
| depth | absolute | Depth/distance measurements |
| object_rearrangement | point_wise | Which objects to move before grasping |
If you use this code or dataset, please cite the BOP-ASK paper (arXiv:2511.16857):
@article{bhat2025bop,
title={BOP-ASK: Object-Interaction Reasoning for Vision-Language Models},
author={Bhat, Vineet and Kim, Sungsu and Blukis, Valts and Heinrich, Greg and Krishnamurthy, Prashanth and Karri, Ramesh and Birchfield, Stan and Khorrami, Farshad and Tremblay, Jonathan},
journal={arXiv preprint arXiv:2511.16857},
year={2025}
}- Release data generation framework (this repository)
- Release evaluation scripts
- Release model checkpoints