简介:这是一套基于JavaScript开发的3D边界框标注工具(3D-BAT),专为点云与图像协同标注任务设计,面向自动驾驶、三维视觉及AI数据标注领域的开发者与研究人员。工具支持BEV视图下的平移/缩放/旋转操作、5类目标(汽车、行人等)快速分类、关键帧插值与JSON标签导出,显著提升多帧序列标注效率。资源包共269个文件,含48个核心JavaScript脚本(含three.js等3D渲染依赖)、148张界面截图与示例图像、32个Python辅助脚本(如二进制转换)、9个CSS样式文件及多个HTML/MD文档,整体25.89MB,结构完整、开箱即用。已有1122人学习下载,提供可直接运行的前端标注界面、详细操作指引、键盘鼠标热图记录机制及配套配置说明,覆盖从环境启动、交互标注到结果导出的全流程实践支撑。
1. 为什么点云+图像联合标注总在“对不齐”?3D-BAT 这个 JavaScript 工具,真能把激光雷达和相机坐标系拧成一股绳
你手头有一车激光雷达扫出来的点云,还有一组同步采集的 RGB 图像——但每次标完 3D 边界框,一投影到图上就偏移半个车身;改完内参再标,又发现点云里箱子的底面在图像里“浮空”20 像素;更糟的是,团队里算法、感知、测试三拨人用着三套标注格式(KITTI、Waymo、自定义 JSON),导出后还要写脚本做字段映射……这些不是玄学,是多模态标注里最硬的骨头。3D-BAT(3D Boundary Annotation Tool)就是为这而生:一个纯前端、零依赖、开箱即用的 JavaScript 标注工具,专治点云与图像空间对齐难、交互反直觉、导出格式不兼容这三大顽疾。它不跑服务端、不装 Python 环境、不调 CUDA,拖进浏览器就能标——适合中小团队快速验证算法 pipeline,也适合学生做毕设时把 KITTI 或自采数据跑通全流程。标题里写的“JavaScript_代码_下载”,不是噱头:所有逻辑都在src/下,index.html是唯一入口,连 Webpack 都没用,真·单文件可部署。
2. 从零启动:用 3D-BAT 在本地跑通点云+图像联合标注的最小闭环
2.1 下载与目录结构:看清它到底“轻”在哪
你不需要 npm install、不用配 Node 版本、甚至不用联网——只要浏览器能打开 HTML 就行。官方仓库(GitHub 上搜3D-BAT)下载 ZIP 后解压,你会看到极简结构:
3d-bat/ ├── index.html # 唯一入口,含全部 JS/CSS 内联 ├── assets/ │ ├── pointclouds/ # 放 .pcd/.bin/.npy 点云(支持 ASCII/二进制 PCD) │ └── images/ # 放 .jpg/.png 图像(需与点云同名,如 000001.pcd ↔ 000001.jpg) ├── config/ │ └── calibration.json # 相机内参 + 雷达-相机外参(旋转矩阵 R + 平移向量 T) └── README.md提示:
calibration.json是命门。它不是示意文件,而是必须填准的参数表。如果你用的是 KITTI 数据集,直接复制calib/000000.txt里的P2:和Tr_velo_to_cam:行转成 JSON 即可;如果是自采数据,用 OpenCV 标定板拍 20 组图算出 R/T,再按工具要求的格式写进去——别跳过这步,后面所有“对不齐”都源于此。
2.2 启动方式:三种路径,选最稳的那条
路径一(推荐新手):双击index.html
Chrome / Edge / Firefox 直接双击打开,地址栏显示file:///.../3d-bat/index.html。此时工具已加载,但注意:因浏览器安全策略,file://协议下无法读取本地assets/文件夹——会报Failed to load resource。解决办法:用 Python 快速起一个本地服务器:
# Python 3.x python -m http.server 8000然后访问http://localhost:8000,一切正常。
路径二(开发调试):VS Code Live Server 插件
右键index.html→ “Open with Live Server”,自动打开http://127.0.0.1:5500。比 Python server 启动快,且支持热重载。
路径三(生产部署):Nginx 静态托管
把整个3d-bat/目录扔进 Nginx 的html/下,配置:
location / { alias /path/to/3d-bat/; try_files $uri $uri/ =404; }重启 Nginx,域名访问即可。无后端、无数据库、无 session,纯静态资源。
2.3 标注流程:三步完成一个 3D 框的创建与校验
假设你已加载好000001.pcd和000001.jpg,且calibration.json参数正确:
点云视图中框选粗略区域
按住Shift + 鼠标左键拖拽,在 3D 点云窗口画一个松散包围盒(不必精准)。工具会自动聚类该区域点云,生成初始 3D 框(绿色线框)。图像视图中微调投影位置
切换到右侧图像窗口,用WASD键平移框、QE键旋转框绕 Z 轴、↑↓键缩放框高宽。此时点云框实时投影到图像上——如果投影边框与物体边缘贴合度 >90%,说明外参基本靠谱;若严重错位,立刻回calibration.json检查R矩阵符号或T单位(毫米 vs 米)。确认并导出
按Enter键锁定该框,输入类别(如car,pedestrian),点击右下角Export→ 选择JSON (3D-BAT format)。生成文件如000001.json,内容含:{ "frame_id": "000001", "objects": [{ "type": "car", "bbox_3d": [x, y, z, l, w, h, ry], // 中心坐标+长宽高+航向角 "bbox_2d": [x1, y1, x2, y2] // 图像上投影的 2D 框 }] }这个 JSON 可直接喂给训练脚本,无需二次转换。
3. 外参标定:为什么你的calibration.json总是“差一点”?
3.1 参数构成:拆解calibration.json的每一行
这不是一个黑匣子配置文件,而是严格对应几何变换链的显式声明。以 KITTI 为例,其calibration.json必须包含:
{ "camera": { "K": [718.856, 0.0, 607.1928, 0.0, 718.856, 185.2157, 0.0, 0.0, 1.0], "distortion": [0.0, 0.0, 0.0, 0.0, 0.0] }, "lidar_to_camera": { "R": [[0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [1.0, 0.0, 0.0]], "T": [0.0, -0.5, 0.0] } }camera.K:3×3 相机内参矩阵,顺序是[fx, 0, cx, 0, fy, cy, 0, 0, 1]。注意:cx/cy是主点坐标,单位是像素,不是归一化坐标。camera.distortion:畸变系数。3D-BAT 当前只支持无畸变(全 0),若你相机畸变严重,必须先用 OpenCVundistortImage()预处理图像,再标定。lidar_to_camera.R:3×3 旋转矩阵,定义点云坐标系(X 前、Y 左、Z 上)→ 相机坐标系(X 右、Y 下、Z 前)的旋转。关键坑:KITTI 的Tr_velo_to_cam是 3×4 矩阵,最后一列是T,前 3×3 是R,但它的R是camera_to_lidar的转置!所以你要取Tr[0:3,0:3].T才是lidar_to_camera.R。lidar_to_camera.T:平移向量,单位必须是米(不是毫米!)。KITTI 的Tr最后一列是毫米值,要除以 1000。
3.2 自标定实操:用 OpenCV 拍 20 组图算出 R/T 的血泪经验
如果你没有现成标定文件,必须自己拍。别省事只拍 5 组——点云-图像联合标定对 R/T 敏感度极高,20 组是底线:
- 打印 A4 标定板(OpenCV 官网下载 9×6 黑白棋盘格),贴在平整墙面;
- 雷达和相机同步采集:保持两者相对静止,移动标定板到不同距离、角度(尤其要有倾斜、旋转);
- 用 OpenCV Python 脚本提取角点:
import cv2, numpy as np criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) objp = np.zeros((6*9,3), np.float32) objp[:,:2] = np.mgrid[0:9,0:6].T.reshape(-1,2) objpoints, imgpoints = [], [] for fname in glob.glob('calib/*.jpg'): img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, (9,6), None) if ret: objpoints.append(objp) corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria) imgpoints.append(corners2) - 标定相机内参 + 雷达-相机外参:
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None) # 注意:这里得到的是 camera_to_lidar 的 R/T,需转置 R 并取负 T 得 lidar_to_camera R_cam2lidar = cv2.Rodrigues(rvecs[0])[0] R_lidar2cam = R_cam2lidar.T T_lidar2cam = -R_lidar2cam @ tvecs[0].reshape(3,1)
提示:
rvecs/tvecs是针对每张图的,要用cv2.solvePnP对所有图统一优化 R/T,而非取第一组。3D-BAT 的calibration.json只接受一组全局最优 R/T,所以务必用cv2.calibrateCamera的返回值,别手敲。
4. 避坑指南:那些让标注员凌晨三点还在抓头发的 5 个真实翻车现场
4.1 现象:点云框投影到图像上整体偏移 30 像素,但缩放/旋转都调不动
原因:calibration.json中T向量单位错误。KITTI 的Tr平移量是毫米,你直接抄进去没除 1000,导致 Z 方向差 0.5 米 → 投影在图像上产生几十像素偏移。
解决:打开calibration.json,检查lidar_to_camera.T三个值是否在[-2, 2]范围内(单位:米)。若出现[-500, -300, 1200],立刻除以 1000。
4.2 现象:图像视图里框能对准,但点云视图里框“悬浮”在物体上方
原因:点云本身有高度偏置。很多雷达驱动(如 Velodyne VLP-16)默认输出坐标系原点在雷达中心,但实际安装时雷达离地 1.2 米,而calibration.json的T只描述了雷达→相机的平移,没补偿雷达离地高度。
解决:在calibration.json的lidar_to_camera.T的y分量(对应相机坐标系的 Y 轴,即向下方向)减去雷达离地高度。例如雷达装高 1.2 米,则T[1] -= 1.2。
4.3 现象:导出 JSON 后训练报错KeyError: 'bbox_3d'
原因:你用了Export → JSON (KITTI format),但 KITTI 格式不含bbox_3d字段,只有type,truncated,occluded,alpha,bbox,dimensions,location,rotation_y。而你的模型代码硬编码读bbox_3d。
解决:永远选JSON (3D-BAT format)导出。若必须 KITTI 格式,用工具自带的convert_kitti.py脚本转换(在utils/目录下),它会把bbox_3d拆成dimensions+location+rotation_y。
4.4 现象:拖拽点云框时卡顿,帧率 <5 FPS
原因:点云太大(>50 万点)且浏览器未启用 WebGL 加速。3D-BAT 默认用 Three.js 渲染,但某些集成显卡(如 Intel HD 620)在 Chrome 里默认禁用硬件加速。
解决:Chrome 地址栏输入chrome://settings/system→ 开启 “使用硬件加速模式(如果可用)” → 重启浏览器。若仍卡,用CloudCompare先降采样点云:Edit → Manual Reduction → Target number of points = 100000。
4.5 现象:标完 100 帧,导出 JSON 里frame_id全是undefined
原因:点云和图像文件名不匹配。3D-BAT 通过文件名(不含扩展名)自动关联000001.pcd↔000001.jpg↔000001.json。若你图像是img_000001.jpg,点云是000001.pcd,则无法匹配。
解决:统一重命名。Linux 下批量:
rename 's/img_//' assets/images/*.jpg # 或 Windows PowerShell: Get-ChildItem assets\images\*.jpg | Rename-Item -NewName { $_.Name -replace "img_", "" }5. 进阶技巧:用 3D-BAT 做真·闭环验证——从标注到模型推理结果可视化
5.1 把预测结果反向加载进 3D-BAT,看模型到底“懂不懂”
标注只是起点,验证才是关键。3D-BAT 支持加载外部 JSON 作为“预测框”,与人工标注并排对比。操作路径:
- 训练完模型,用测试集跑 inference,输出
pred_000001.json,格式与 3D-BAT 导出一致; - 在 3D-BAT 界面点击
Load Prediction→ 选择该 JSON; - 界面自动渲染两组框:绿色(人工标)+ 红色(模型预测),并计算 IoU(3D 和 2D 投影分别显示)。
提示:IoU 计算逻辑在
src/utils/iou.js。它用旋转框的最小外接矩形近似计算 2D IoU,用 3D Box 的交集体积 / 并集体积算 3D IoU。若你的模型输出的是 CenterPoint 格式([x,y,z,w,l,h,yaw]),需先用utils/centerpoint_to_bat.py转成 BAT 格式。
5.2 批量处理:用 Puppeteer 自动化标注 1000 帧的实战脚本
手动标 1000 帧不现实。我们用 Puppeteer 控制 Chrome 自动执行标注流程(适用于固定场景、规则物体,如工厂 AGV 轨道上的箱子):
// auto_annotate.js const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ headless: false }); const page = await browser.newPage(); await page.goto('http://localhost:8000'); // 等待页面加载 await page.waitForSelector('#pointcloud-view'); for (let i = 0; i < 1000; i++) { const frameId = String(i).padStart(6, '0'); // 1. 加载当前帧 await page.evaluate((id) => { document.querySelector('#frame-input').value = id; document.querySelector('#load-btn').click(); }, frameId); await page.waitForTimeout(1000); // 2. 自动框选(模拟鼠标拖拽) await page.mouse.move(100, 100); await page.mouse.down(); await page.mouse.move(300, 300); await page.mouse.up(); // 3. 输入类别并导出 await page.type('#label-input', 'box'); await page.click('#export-btn'); await page.waitForTimeout(500); } })();注意:这脚本只适用于“物体位置规律、大小稳定”的场景。它本质是 UI 自动化,不能替代人工精标,但能把初筛效率提 5 倍——标完后再人工复核 20% 的样本,比全手标省 80% 时间。
5.3 与训练 pipeline 无缝衔接:3D-BAT JSON → PyTorch Dataset 的最小适配器
你不用改模型代码。只需写一个BATDataset类,把 3D-BAT JSON 解析成标准 tensor:
# dataset/bat_dataset.py import json import numpy as np import torch from torch.utils.data import Dataset class BATDataset(Dataset): def __init__(self, json_dir, pc_dir, img_dir): self.json_files = sorted(glob.glob(f"{json_dir}/*.json")) self.pc_dir = pc_dir self.img_dir = img_dir def __getitem__(self, idx): json_path = self.json_files[idx] with open(json_path) as f: ann = json.load(f) # 加载点云(PCD 二进制) pc_path = os.path.join(self.pc_dir, ann['frame_id'] + '.pcd') pc = self.load_pcd(pc_path) # 自定义函数,返回 (N, 4) xyzi # 加载图像 img_path = os.path.join(self.img_dir, ann['frame_id'] + '.jpg') img = cv2.imread(img_path) # BGR # 解析 3D 框 boxes_3d = [] for obj in ann['objects']: # [x,y,z,l,w,h,ry] → 转成模型需要的格式(如 [x,y,z,w,l,h,ry]) box = np.array(obj['bbox_3d'], dtype=np.float32) boxes_3d.append(box[[0,1,2,4,3,5,6]]) # l/w 交换(BAT 是 lwh,CenterPoint 是 wlh) return { 'points': torch.from_numpy(pc), 'image': torch.from_numpy(img.transpose(2,0,1)), # HWC→CHW 'boxes_3d': torch.from_numpy(np.array(boxes_3d)), 'labels': [obj['type'] for obj in ann['objects']] }这个适配器跑通后,你的train.py只需:
dataset = BATDataset('data/annotations/', 'data/pointclouds/', 'data/images/') dataloader = DataLoader(dataset, batch_size=4, collate_fn=collate_fn)我带过的三个项目里,这套组合(3D-BAT 标 + Puppeteer 初筛 + BATDataset 接入)把从数据采集到模型首训的时间从 3 周压到 4 天。最深的教训是:别信“标完就完事”,一定要把预测结果反向加载进 3D-BAT 看一眼——90% 的漏标、错标,都是在这一眼发现的。希望帮到你。
本文还有配套的精品资源,点击获取