记录日期:2026-07-09(Label Studio 流程已按实测踩坑更新)
适用环境:AutoDL Linux(无桌面 GUI)、RTX 3090、已有 SAM3 + SAM3_LoRA
项目根目录:
/root/autodl-tmp/label
本文档面向第一次从头搭建的同学,按顺序操作即可复现当前状态:半自动标注 → COCO 导出 → SAM3 LoRA 微调冒烟通过。
目录
- 你要做什么
- 前置条件检查
- [第一步:创建目录与 prompt](#第一步:创建目录与 prompt)
- [第二步:安装 Label Studio(独立 venv)](#第二步:安装 Label Studio(独立 venv))
- 第三步:放入原始数据
- 第四步:视频抽帧(如有视频)
- [第五步:SAM3 半自动预标注](#第五步:SAM3 半自动预标注)
- [第六步:Label Studio 人工修正(可选但推荐)](#第六步:Label Studio 人工修正(可选但推荐))
- [第七步:导出 COCO 格式数据集](#第七步:导出 COCO 格式数据集)
- [第八步:SAM3 LoRA 微调](#第八步:SAM3 LoRA 微调)
- 一键冒烟测试
- 脚本与配置速查索引
- 常见问题与踩坑记录
- 当前状态摘要(2026-07-09)
- 附录:脚本、配置与目录详解
- [A. 流水线总览](#A. 流水线总览)
- [B. 目录与文件](#B. 目录与文件)
- [C. 脚本完整源码](#C. 脚本完整源码)
- [D. 配置与生成文件](#D. 配置与生成文件)
- [E. 环境变量](#E. 环境变量)
- [F. 外部关联路径](#F. 外部关联路径)
1. 你要做什么
SAM3 微调需要 COCO 格式实例分割标注。在 AutoDL 无桌面环境下,推荐流程:
原始图片/视频
→ SAM3 自动预标注(mask PNG)
→ Label Studio 浏览器人工修正(可选)
→ 转换为 COCO JSON(RLE)
→ SAM3_LoRA 微调
关键概念 :SAM3 是开放词汇模型,微调时 COCO categories[].name 会直接作为文本 prompt 。因此 prompt.txt 里的英文描述必须与推理时完全一致。
2. 前置条件检查
在 AutoDL 实例上确认以下内容已存在:
| 项目 | 路径 | 说明 |
|---|---|---|
| SAM3 代码 | /root/autodl-tmp/sam3 |
已 pip install -e |
| SAM3 权重 | /root/autodl-tmp/weights/sam3_ms/sam3.pt |
本地 checkpoint |
| SAM3_LoRA | /root/autodl-tmp/SAM3_LoRA |
LoRA 微调框架 |
| PyTorch + CUDA | torch 2.7.x |
GPU 可用 |
| ffmpeg | 系统命令 | 视频抽帧 |
快速检查:
bash
nvidia-smi
python3 -c "import sam3, torch; print('sam3 ok, cuda:', torch.cuda.is_available())"
test -f /root/autodl-tmp/weights/sam3_ms/sam3.pt && echo "checkpoint ok"
ffmpeg -version | head -1
安装 ffmpeg:
bash
apt update
apt install -y ffmpeg
激活环境(若使用 conda):
bash
conda activate sam3 # 若你有 sam3 环境
3. 第一步:创建目录与 prompt
项目根目录为 /root/autodl-tmp/label,目录结构如下:
label/
├── raw/
│ ├── images/ # 静态原图(.jpg/.png)
│ └── videos/ # 原始视频(.mp4/.mov)
├── frames/
│ └── sam3_001/ # 抽帧后:0.jpg, 1.jpg, ...
├── prelabels/
│ ├── static/ # 静态图 SAM3 预标注
│ └── video/ # 视频跟踪预标注
├── reviewed/ # Label Studio 导入/导出
├── scripts/ # 全部流水线脚本
├── docs/ # 文档
├── prompt.txt # 当前项目的文本 prompt
└── README.md
若目录尚未创建,执行:
bash
mkdir -p /root/autodl-tmp/label/{raw/images,raw/videos,frames,prelabels,reviewed,scripts,docs}
编辑 prompt.txt(示例):
text
black rectangular block with a hole
命名规范详见 <prompt_guide.md>。
4. 第二步:安装 Label Studio(独立 venv)
为什么用独立 venv?
- SAM3 要求
numpy<2 - Label Studio 1.23 要求
numpy>=2.2 - 两者不能装在同一 Python 环境,否则会互相冲突
正确做法 :Label Studio 单独装在 .venv-labelstudio,SAM3 继续用主环境。
bash
cd /root/autodl-tmp/label
python3 -m venv .venv-labelstudio
.venv-labelstudio/bin/pip install label-studio
验证(见下方「踩坑记录」------不要用 --version 判断):
bash
test -x .venv-labelstudio/bin/label-studio && echo "Label Studio venv: OK"
python3 -c "import sam3, numpy; print('SAM3 numpy:', numpy.__version__)"
期望:Label Studio 在 venv 里可用,SAM3 侧 numpy 为 1.26.x。
安装成功之后,如果想要再次进入 venv 环境,则运行下面的命令:
bash
source /root/autodl-tmp/label/.venv-labelstudio/bin/activate
退出的话用:deactivate
5. 第三步:放入原始数据
静态图片
bash
cp /path/to/your/*.jpg /root/autodl-tmp/label/raw/images/
建议至少 50--100 张高质量图用于正式微调;冒烟可先用 10 张。
视频(可选)
bash
cp /path/to/your/video.mov /root/autodl-tmp/label/raw/videos/
6. 第四步:视频抽帧(如有视频)
bash
cd /root/autodl-tmp/label
bash scripts/extract_frames.sh 5 50
参数说明:
| 参数 | 含义 | 示例 |
|---|---|---|
| 第 1 个 | 抽帧帧率 fps | 5 = 每秒 5 帧 |
| 第 2 个 | 每段视频最多帧数 | 50 = 最多 50 帧 |
输出:frames/sam3_001/、frames/sam3_002/ ... 帧命名为 0.jpg, 1.jpg, ...
7. 第五步:SAM3 半自动预标注
一条命令同时处理静态图 + 视频:
bash
cd /root/autodl-tmp/label
bash scripts/run_prelabel.sh all
也可分开跑:
bash
bash scripts/run_prelabel.sh static # 仅 raw/images/
bash scripts/run_prelabel.sh video # 仅 frames/sam3_*/
静态图预标注做了什么?
脚本:scripts/prelabel_static.py
- 读取
prompt.txt - 对
raw/images/每张图跑 SAM3 文本分割 - 输出到
prelabels/static/<图片名>/:masks/<name>_mask_0.png... 实例 mask<name>_seg.jpg可视化
- 汇总:
prelabels/static/seg_summary.json
视频预标注做了什么?
脚本:scripts/prelabel_video.py
- 对
frames/sam3_<id>/首帧做 SAM3 分割,生成参考 mask - 调用
use_sam3/track_episode_sop.py跨帧跟踪传播 - 输出到
prelabels/video/<id>/:001_mask_ref.png参考 maskmasks/<id>_frame_<n>_mask.png逐帧 masktracking_summary.json跟踪统计
可调参数(环境变量)
bash
CONFIDENCE=0.4 bash scripts/run_prelabel.sh all
PROMPT_FILE=/path/to/prompt.txt bash scripts/run_prelabel.sh static
预标注阶段会占用 GPU,3090 上静态 10 张 + 视频 30 帧约 2--5 分钟。
进入第 8 步前自检
预标注完成后、启动 Label Studio 之前,在服务器确认数据已就绪:
bash
cd /root/autodl-tmp/label
# 1. 预标注汇总存在
ls prelabels/static/seg_summary.json
ls prelabels/video/*/tracking_summary.json 2>/dev/null # 无视频可忽略
# 2. import JSON 已有任务(start_labelstudio.sh 也会自动生成,此处提前确认)
python3 -c "import json; print('import tasks:', len(json.load(open('reviewed/labelstudio_import.json'))))"
| 结果 | 说明 |
|---|---|
import tasks: 0 |
回到 §7:检查 prompt.txt、confidence,或是否跑了 run_prelabel.sh |
import tasks: ≥ 1 |
预标注正常(冒烟数据约 40),可进入 §8 |
注意 :即使此处 tasks ≥ 1,浏览器里仍看不到任务 ,直到 §8.1 阶段 4 执行
labelstudio_reload.sh完成 API 导入。
8. 第六步:Label Studio 人工修正(可选但推荐)
SAM3 预标注会有漏检、误检、跟踪漂移。正式训练前建议人工过一遍。
AutoDL 要点 :数据在服务器
/root/autodl-tmp/label/reviewed/,浏览器 不能 用 Upload Files 上传本地盘路径。必须用 API 导入 + Local Files 云存储。
8.1 四阶段流程(推荐)
常见误区 :只执行「阶段 1 启动」就打开浏览器,期待看到标注数据------此时看不到任务是正常的 。必须完成 阶段 1 → 2 → 3 → 4 全部步骤。
端口说明 :AutoDL 实例默认在 6006/6007 跑 TensorBoard。Label Studio 请用 6006 (或任意空闲端口),不要用 6007,否则浏览器可能打开 TensorBoard 而非 Label Studio。
阶段 1:启动服务(完成后仍无任务,属正常)
新终端常驻运行(不要关):
bash
bash /root/autodl-tmp/label/scripts/start_labelstudio.sh 6006
服务器验证(必须看到 OK,再进入阶段 2):
bash
curl -sf http://127.0.0.1:6006/health && echo "Label Studio OK"
若失败:检查终端是否有报错;换端口重试(如 6080),三处端口(start / AutoDL 映射 / reload)须一致。
阶段 2:保存 Token
Label Studio 启动后,浏览器登录 → Account & Settings → Access Token → 复制 JWT refresh token:
bash
echo '<你的 refresh token>' > /root/autodl-tmp/label/reviewed/.ls_token # echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6ODA5MDc5NzY0NiwiaWF0IjoxNzgzNTk3NjQ2LCJqdGkiOiIzYjY5OTM1YWM1ODY0NzdjODAwNzEyZjdmNDA1NzFjZiIsInVzZXJfaWQiOiIxIn0.zXS_H0dWnTkoVyc2GwC_VJ5mtm-CrXljxwz3qi9Vdqk' > /root/autodl-tmp/label/reviewed/.ls_token
chmod 600 /root/autodl-tmp/label/reviewed/.ls_token
阶段 3:浏览器建项目(完成后仍无任务,属正常)
- AutoDL 控制台 → 自定义服务 → 映射端口 6006 (与阶段 1 一致)
- 若用 SSH 隧道:
ssh -CNg -L 6006:127.0.0.1:6006 root@<实例地址> -p <端口>
- 若用 SSH 隧道:
- 浏览器打开映射 URL
- 确认界面是 Label Studio (Projects 列表),不是 TensorBoard(橙色、顶部有 SCALARS/GRAPHS)
- 新建项目,记下
project_id(通常为 1)
阶段 4:API 导入(做完此步才能在浏览器看到任务)
bash
bash /root/autodl-tmp/label/scripts/labelstudio_reload.sh <project_id> 6006
# 示例:bash /root/autodl-tmp/label/scripts/labelstudio_reload.sh 1 6006
验证:
- 终端应打印
task_count(冒烟约 40) - 浏览器 Ctrl+F5 → 进入 Data Manager 或 Label(不是 Dashboard 统计页)→ 打开任务应见原图 + 红色 mask
浏览器现象对照(排错用)
| 浏览器现象 | 说明 | 处理 |
|---|---|---|
No dashboards are active for the current data set. + 橙色 TensorBoard |
打开了 6007 上的 TensorBoard ,不是 Label Studio;与预标注无关 | 用 6006 启动 LS;AutoDL 映射改 6006 |
| Label Studio 项目页但 0 tasks | 只做了阶段 1--3,未执行阶段 4 reload | 执行 labelstudio_reload.sh <project_id> 6006 |
| Label Studio Dashboard 空白 | 进了 Dashboard 而非任务列表 | 点 Label 或 Data Manager |
| 有 tasks 但图片不加载 | Cloud Storage 未配或路径错 | 见 Q9 |
labelstudio_reload.sh 会自动完成:
labelstudio_import.py--- 复制图片/mask,生成 import JSON- 同步
labelstudio_config.xml到项目 DB labelstudio_push_import.py --replace--- 删除旧任务,API 导入 annotations(红色 mask 打开即见)
8.2 生成导入包(单独执行时)
bash
python3 /root/autodl-tmp/label/scripts/labelstudio_import.py
生成:
| 文件 | 用途 |
|---|---|
reviewed/labelstudio_import.json |
API 导入任务(含 Brush RLE annotations) |
reviewed/labelstudio_media/ |
原图 + mask 副本(Local Files 读取) |
reviewed/labelstudio_config.xml |
标注界面配置 |
reviewed/labelstudio_tasks/labelstudio_import.json |
Sync 模式备用(不推荐) |
导入方式说明:
- SAM3 mask 以
annotations导入(不是predictions),打开任务直接显示红色区域 - 每个 region 带
id,且ground_truth: false(LS 默认 true 会导致难以编辑) - RLE 来自
image2rle()的第一个返回值 (三元组(rle, w, h),勿整段写入rle字段)
8.3 启动 Label Studio
bash
bash /root/autodl-tmp/label/scripts/start_labelstudio.sh 6006
启动日志应显示:
Document root: /root/autodl-tmp/label/reviewed
Storage path: /root/autodl-tmp/label/reviewed/labelstudio_media
8.4 AutoDL 端口映射
- AutoDL 控制台 → 自定义服务
- 添加端口 6006 (与
start_labelstudio.sh参数一致) - 勿映射 6007 (AutoDL 默认 TensorBoard,打开会看到
No dashboards are active...) - 浏览器打开映射 URL,确认是 Label Studio 登录/项目页,登录 Label Studio
8.5 项目配置(首次或 reload 后核对)
8.5.1 标注界面(Labeling Setup)
内容来自 reviewed/labelstudio_config.xml,reload 脚本会自动写入 DB。若界面异常,手动粘贴并 Save:
xml
<View>
<Header value="Step1: Edit | Step2: click target | Step3: Brush/Eraser | Step4: Submit"/>
<Text name="meta" value="Episode: $episode_id | Type: $data_source | SAM3 instances: $instances"/>
<Image name="image" value="$image" zoom="true" zoomControl="true"/>
<BrushLabels name="brush" toName="image">
<Label value="target" background="rgba(255, 0, 0, 0.6)"/>
</BrushLabels>
</View>
勿用
$source:这是 Label Studio 保留变量,会导致任务 meta 显示整段 JSON。本项目字段名为data_source。
8.5.2 图片 Source Storage
Settings → Cloud Storage → Add Source Storage (只需一条,Files 模式仅用于提供图片 URL):
| 字段 | 值 |
|---|---|
| Storage Type | Local files |
| Absolute local path | /root/autodl-tmp/label/reviewed/labelstudio_media |
| File filter | `.*.(jpg |
| Import method | Files |
Path 必须是 reviewed/ 的子目录 ,不能填 reviewed/ 本身(会与 Document root 冲突)。
labelstudio_push_import.py 会在 API 导入时自动创建上述 Storage(若不存在)。
8.5.3 导入任务(API,不要用 Upload Files / Files Sync)
不要:
- Import → Upload Files (浏览器无法访问
/root/...) - Cloud Storage Files Sync 批量建任务(会产生
$undefined$字段、与 API 任务重复)
推荐:
bash
# Token:环境变量 或 reviewed/.ls_token(一行 JWT refresh/access)
export LS_TOKEN="<refresh token>" # 可选,若已写 .ls_token 可省略
bash /root/autodl-tmp/label/scripts/labelstudio_reload.sh <project_id> 6006
等价分步:
bash
python3 scripts/labelstudio_import.py
python3 scripts/labelstudio_push_import.py <project_id> --port 6006 --replace
8.6 浏览器里如何修正 mask
打开任务后应看到 原图 + 红色 SAM3 mask。
| 步骤 | 操作 |
|---|---|
| 1 | 若界面只读,点右上角或标注栏的 Edit / 铅笔 进入编辑 |
| 2 | 左侧 点红色 target 标签(不点则 Brush 无效) |
| 3 | 底部工具栏 选 Brush (补画)或 Eraser(擦除) |
| 4 | 可滚轮缩放、拖拽平移 |
| 5 | 改完点 Submit 保存 |
预标注更新后,在服务器重新执行:
bash
bash scripts/labelstudio_reload.sh 1 6006
浏览器 Ctrl+F5 强制刷新。
8.7 修正后导出 COCO
不要在浏览器 Export 下载 zip。在云服务器终端:
bash
bash /root/autodl-tmp/label/scripts/labelstudio_server_export.sh <project_id>
输出:/root/autodl-tmp/label/reviewed/labelstudio_export/result.json
注意 :Label Studio CLI 导出的是 zip 包 (即使文件名叫
result.json),且 Brush/mask 项目必须用 BRUSH_TO_COCO 格式(脚本已默认)。export_coco.py会自动解压读取。
更细步骤见 <labelstudio_setup.md>。
9. 第七步:导出 COCO 格式数据集
SAM3_LoRA 只接受如下结构:
sam3_lora_data/<项目名>/
├── train/
│ ├── *.jpg
│ └── _annotations.coco.json
├── valid/
│ ├── *.jpg
│ └── _annotations.coco.json
└── dataset_meta.json
从预标注直接导出(跳过 Label Studio)
bash
python3 /root/autodl-tmp/label/scripts/export_coco.py \
--source prelabels \
--output-dir /root/autodl-tmp/sam3_lora_data/my_dataset
从 Label Studio 导出结果
bash
python3 /root/autodl-tmp/label/scripts/export_coco.py \
--source labelstudio \
--output-dir /root/autodl-tmp/sam3_lora_data/my_dataset
自动选择来源
bash
python3 /root/autodl-tmp/label/scripts/export_coco.py \
--output-dir /root/autodl-tmp/sam3_lora_data/my_dataset
若 reviewed/labelstudio_export/ 有 COCO JSON,优先用 Label Studio;否则用 prelabels。
划分策略
- 默认 train/valid = 85% / 15%,seed=42
- 按 episode 分组 :同一
sam3_001视频的所有帧不会同时出现在 train 和 valid,避免数据泄漏
10. 第八步:SAM3 LoRA 微调
10.1 准备配置文件
复制并修改 SAM3_LoRA/configs/smoke_hole_block.yaml,关键是 training.data_dir:
yaml
training:
data_dir: "/root/autodl-tmp/sam3_lora_data/my_dataset"
num_epochs: 20 # 正式训练建议 20--50
batch_size: 1
learning_rate: 1.0e-4
# ...
10.2 安装训练依赖
bash
pip install decord # SAM3_LoRA 训练需要,冒烟脚本会自动安装
10.3 开始训练
bash
cd /root/autodl-tmp/SAM3_LoRA
python3 train_sam3_lora_native.py --config configs/my_dataset.yaml
输出权重在 outputs/<项目名>/best_lora_weights.pt(约 4MB)。
10.4 验证(可选)
bash
python3 validate_sam3_lora.py \
--config configs/my_dataset.yaml \
--weights outputs/my_dataset/best_lora_weights.pt \
--val_data_dir /root/autodl-tmp/sam3_lora_data/my_dataset/valid
11. 一键冒烟测试
若只想验证「整条管线能跑通」,无需自己准备数据:
bash
bash /root/autodl-tmp/label/scripts/smoke_pipeline.sh
脚本会自动:
- 从
sam3/assets/test/复制 10 张静态图 + 30 帧视频 - 跑 SAM3 预标注
- 导出 COCO 到
sam3_lora_data/label_smoke - 生成
SAM3_LoRA/configs/label_smoke.yaml并训练 1 epoch
日志:/root/autodl-tmp/label/smoke_pipeline.log
12. 脚本与配置速查索引
正文各步只写「怎么跑」;每个脚本、配置文件、生成物的完整说明见 [附录](#每个脚本、配置文件、生成物的完整说明见 附录)。
| 类别 | 数量 | 详见 |
|---|---|---|
| 流水线脚本 | 13 个 shell/python + labelstudio_config.xml |
[附录 C](#类别 数量 详见 流水线脚本 13 个 shell/python + labelstudio_config.xml 附录 C 配置文件 4 个(prompt、LS XML、token、gitignore) 附录 D 运行时生成物 prelabels / reviewed / frames 等 附录 B 环境变量 LS_TOKEN、CONFIDENCE 等 附录 E) |
| 配置文件 | 4 个(prompt、LS XML、token、gitignore) | [附录 D](#类别 数量 详见 流水线脚本 13 个 shell/python + labelstudio_config.xml 附录 C 配置文件 4 个(prompt、LS XML、token、gitignore) 附录 D 运行时生成物 prelabels / reviewed / frames 等 附录 B 环境变量 LS_TOKEN、CONFIDENCE 等 附录 E) |
| 运行时生成物 | prelabels / reviewed / frames 等 | [附录 B](#类别 数量 详见 流水线脚本 13 个 shell/python + labelstudio_config.xml 附录 C 配置文件 4 个(prompt、LS XML、token、gitignore) 附录 D 运行时生成物 prelabels / reviewed / frames 等 附录 B 环境变量 LS_TOKEN、CONFIDENCE 等 附录 E) |
| 环境变量 | LS_TOKEN、CONFIDENCE 等 |
[附录 E](#类别 数量 详见 流水线脚本 13 个 shell/python + labelstudio_config.xml 附录 C 配置文件 4 个(prompt、LS XML、token、gitignore) 附录 D 运行时生成物 prelabels / reviewed / frames 等 附录 B 环境变量 LS_TOKEN、CONFIDENCE 等 附录 E) |
按阶段最常用命令:
| 阶段 | 命令 |
|---|---|
| 抽帧 | bash scripts/extract_frames.sh 5 50 |
| 预标注 | bash scripts/run_prelabel.sh all |
| 启动 LS | bash scripts/start_labelstudio.sh 6006 |
| 导入/重载 | bash scripts/labelstudio_reload.sh 1 6006 |
| 导出 COCO | bash scripts/labelstudio_server_export.sh 1 → export_coco.py |
| 冒烟 | bash scripts/smoke_pipeline.sh |
| 文档 | 内容 |
|---|---|
| <prompt_guide.md> | prompt 命名规范 |
| <labelstudio_setup.md> | Label Studio 专项步骤 |
| 本文档附录 | 脚本完整源码(C)/ 配置与目录说明(D--F) |
13. 常见问题与踩坑记录
Q1:label-studio --version 卡住不动
现象 :执行 label-studio --version 后长时间无输出,或后台任务显示 error。
原因 :Label Studio 的 --version 会触发数据库初始化,并非快速打印版本号就退出。
结论 :不能 用 --version 判断安装是否成功。
正确验证方式:
bash
test -x /root/autodl-tmp/label/.venv-labelstudio/bin/label-studio && echo "OK"
python3 -c "import sam3, numpy; print(numpy.__version__)" # 应为 1.26.x
启动标注 QA 请用:
bash
bash /root/autodl-tmp/label/scripts/start_labelstudio.sh 6006
bash /root/autodl-tmp/label/scripts/labelstudio_reload.sh 1 6006
Q2:pip install label-studio 后 SAM3 报错
原因:Label Studio 会把 numpy 升级到 2.x,破坏 SAM3(要求 numpy<2)。
解决 :Label Studio 只装在 .venv-labelstudio,不要 pip install label-studio 到 SAM3 主环境。若已误装:
bash
pip install 'numpy<2,>=1.26'
Q3:训练报 ModuleNotFoundError: No module named 'decord'
bash
pip install decord
smoke_pipeline.sh 训练前会自动尝试安装。
Q4:视频抽帧路径含空格导致 cp 失败
视频目录名如 2026-07-02 191302_frames 含空格。smoke_pipeline.sh 已用 find -print0 处理;自写脚本时注意引号。
Q5:预标注 0 instance
- 检查
prompt.txt是否匹配画面内容 - 降低 confidence:
CONFIDENCE=0.3 bash scripts/run_prelabel.sh static - 冒烟中
IMG_8098/8099对hole blockprompt 无匹配,属正常(prompt 与内容不符)
Q6:valid 集只有 1 张图
按 episode 划分时,episode 数量少则 valid 样本也少。可增加数据量,或修改 export_coco.py 的 --valid-ratio。
Q7:GPU 已被占用
预标注/训练需 GPU。用 nvidia-smi 查看占用;可等其它任务结束再跑,或调小 batch。
Q8:Label Studio 找不到 Import / 不能用 Upload Files
原因 :AutoDL 是远程 Linux,标注数据在 /root/autodl-tmp/label/reviewed/,浏览器 Upload Files 只能传你电脑上的文件。
正确做法:
bash
bash scripts/labelstudio_reload.sh <project_id> 6006
Token 写入 reviewed/.ls_token 或 export LS_TOKEN=...。详见第 8 节。
Q9:图片加载失败(There was an issue loading URL from $image)
- 确认 Cloud Storage 路径为
/root/autodl-tmp/label/reviewed/labelstudio_media - 确认 Label Studio 端口与 AutoDL 映射一致
- 执行 reload 后 Ctrl+F5 刷新
- 服务端自检:
curl -I "http://127.0.0.1:6006/data/local-files/?d=labelstudio_media/static/xxx.jpg"应返回 200
Q10:能看到 mask 但 Runtime error / 无法编辑
| 现象 | 原因 | 解决 |
|---|---|---|
argument should be integer or bytes-like object, not 'str' |
image2rle() 返回 (rle,w,h),误把整段三元组写入 rle |
已修复;重新 labelstudio_reload.sh |
| mask 可见但 Brush 无效 | 未点 target 标签,或未点 Edit | 先 Edit → 左侧 target → 底部 Brush/Eraser |
| 导入后只读 | LS 导入默认 ground_truth: true |
脚本已设 ground_truth: false,reload 后生效 |
| 用了 predictions | predictions 只读,需 Accept | 本项目改用 annotations 导入 |
Q11:Token 401 / refresh 与 access
Label Studio 1.23 的 JWT refresh token 不能直接当 access 用。labelstudio_push_import.py 会自动调用 /api/token/refresh/ 换取 access。reviewed/.ls_token 存 refresh 即可。
Q12:任务重复或字段变成 $undefined$
不要混用 Upload Files 、Files Sync 与 API 导入。出问题时:
bash
bash scripts/labelstudio_reload.sh 1 6006 # --replace 会删光旧任务再导入
Q13:Label Studio 配置里 $source 显示整段 JSON
$source 是 LS 保留变量。任务字段请用 data_source (见 labelstudio_config.xml)。
Q14:Web 端提示 No dashboards are active for the current data set.
现象 :浏览器显示上述英文,界面为橙色 TensorBoard(顶部有 SCALARS / GRAPHS 等标签)。
原因(按优先级排查):
- 8.1 未完成 :只执行了
start_labelstudio.sh就打开浏览器------阶段 1--3 完成后本来就没有任务 ,须执行阶段 4labelstudio_reload.sh - 打开了错误服务 :AutoDL 6006/6007 默认跑 TensorBoard(空 logdir),不是 Label Studio
- 不是预标注失败 :先查
reviewed/labelstudio_import.json的 tasks 数量;若为 0 才回到 §7
处理:
bash
# 1. 确认预标注数据存在(tasks 应 ≥ 1)
python3 -c "import json; print(len(json.load(open('reviewed/labelstudio_import.json'))))"
# 2. 用 6006 启动 Label Studio(勿用 6007)
bash scripts/start_labelstudio.sh 6006
curl -sf http://127.0.0.1:6006/health && echo OK
# 3. AutoDL 映射 6006,浏览器确认是 Label Studio(Projects 页)
# 4. 完成导入
bash scripts/labelstudio_reload.sh <project_id> 6006
正确界面 :Label Studio → Data Manager / Label 中有 tasks,打开任务可见红色 mask。
Q15:export_coco.py --source labelstudio 报 UnicodeDecodeError
现象:
UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 10-11: invalid continuation byte
原因:
label-studio export ... COCO写入的是 zip 包 (即使路径叫result.json),不是纯文本 JSON- 本项目用 Brush mask ,普通
COCO导出 annotations 为空 ;须用BRUSH_TO_COCO
处理:
bash
# 1. 重新导出(脚本已改为 BRUSH_TO_COCO)
bash scripts/labelstudio_server_export.sh <project_id>
# 2. 再转训练集
python3 scripts/export_coco.py --source labelstudio \
--output-dir /root/autodl-tmp/sam3_lora_data/my_dataset
若尚未做 Label Studio 人工修正,可直接跳过 LS 导出:
bash
python3 scripts/export_coco.py --source prelabels \
--output-dir /root/autodl-tmp/sam3_lora_data/my_dataset
14. 当前状态摘要(2026-07-09)
已验证的 Label Studio 流程
| 项目 | 状态 |
|---|---|
| 导入方式 | API + labelstudio_reload.sh(不用 Upload Files) |
| Mask 显示 | annotations + Brush RLE,打开即见红色区域 |
| 典型任务量 | 40 tasks / 38 annotations(冒烟数据) |
| Token | reviewed/.ls_token(JWT refresh,已 gitignore) |
| 推荐端口 | 6006(AutoDL 6006/6007 常为 TensorBoard,勿用于 LS) |
已搭建内容
| 组件 | 路径 |
|---|---|
| 流水线根目录 | /root/autodl-tmp/label |
| Label Studio venv | /root/autodl-tmp/label/.venv-labelstudio |
| 冒烟数据集 | /root/autodl-tmp/sam3_lora_data/label_smoke |
| LoRA 权重 | /root/autodl-tmp/SAM3_LoRA/outputs/label_smoke/best_lora_weights.pt |
| 训练配置 | /root/autodl-tmp/SAM3_LoRA/configs/label_smoke.yaml |
冒烟测试数据
| 项目 | 数值 |
|---|---|
| 静态图输入 | 10 张(其中 2 张 0 instance) |
| 视频帧 | 30 帧(episode 001) |
| 有效 COCO 样本 | 38 |
| train | 37 图 / 60 标注 |
| valid | 1 图 / 2 标注 |
| 训练 | 1 epoch,val_loss = 0.386 |
环境验证
- Label Studio 在独立 venv:
.venv-labelstudio/ - SAM3 环境未受影响(numpy 1.26.4)
- 不要 用
label-studio --version判断安装(会卡住初始化 DB)
日常使用最短路径
bash
cd /root/autodl-tmp/label
# 1. 数据 + prompt
# 2. 抽帧(视频)
bash scripts/extract_frames.sh 5 50
# 3. 预标注
bash scripts/run_prelabel.sh all
# 4. Label Studio QA
bash scripts/start_labelstudio.sh 6006
bash scripts/labelstudio_reload.sh 1 6006 # 浏览器修正 mask
# 5. 导出 COCO
python3 scripts/export_coco.py --output-dir /root/autodl-tmp/sam3_lora_data/my_dataset
# 6. 微调
cd /root/autodl-tmp/SAM3_LoRA
python3 train_sam3_lora_native.py --config configs/my_dataset.yaml
附录
本附录 C 提供 scripts/ 下全部脚本的完整源码 ;D / E / F 对配置文件、生成目录与环境变量做说明,便于查阅和维护。
附录 A:流水线总览与脚本调用关系
A.1 数据流(与正文一致)
raw/images/ ──────────► prelabel_static.py ──► prelabels/static/
│
raw/videos/ ──► extract_frames.sh ──► frames/sam3_* ──┤
│ │
└──► prelabel_video.py ──► prelabels/video/
│
▼
labelstudio_import.py ──► reviewed/(浏览器 QA)
│
labelstudio_reload.sh ──┘(import + push + config)
│
▼
export_coco.py ──► sam3_lora_data/<name>/
│
▼
train_sam3_lora_native.py ──► best_lora_weights.pt
A.2 脚本调用链
| 你执行的命令 | 内部实际调用 |
|---|---|
run_prelabel.sh all |
→ prelabel_static.py + prelabel_video.py |
start_labelstudio.sh 6006 |
→ labelstudio_import.py(刷新 JSON)→ label-studio start |
labelstudio_reload.sh 1 6006 |
→ labelstudio_import.py → 写 DB label_config → labelstudio_push_import.py --replace |
labelstudio_reset_import.sh |
→ 同 reload(reset 为别名脚本) |
labelstudio_server_import.sh |
→ curl POST import API(无 Cloud Storage 自动配置,建议用 reload) |
labelstudio_server_export.sh |
→ label-studio export ... BRUSH_TO_COCO |
smoke_pipeline.sh |
→ 复制测试数据 → run_prelabel.sh → export_coco.py → 写 SAM3_LoRA yaml → 训练 |
A.3 脚本分层
| 层级 | 脚本 | 说明 |
|---|---|---|
| 入口 shell | extract_frames.sh, run_prelabel.sh, start_labelstudio.sh, labelstudio_reload.sh, smoke_pipeline.sh |
人类日常只记这些 |
| 核心 Python | prelabel_*.py, labelstudio_import.py, export_coco.py |
业务逻辑 |
| LS API | labelstudio_push_import.py |
REST 导入、删任务、建 Storage |
| 遗留/备选 | labelstudio_server_import.sh, labelstudio_reset_import.sh |
可用但推荐 reload |
附录 B:目录与文件说明
项目根目录:/root/autodl-tmp/label
label/
├── prompt.txt # [配置] 文本 prompt,= COCO category name
├── .gitignore # [配置] 忽略 .ls_token、.venv-labelstudio
├── .venv-labelstudio/ # [运行时] Label Studio 独立 Python 环境
├── README.md # 英文快速入口
│
├── raw/
│ ├── images/ # 输入:静态原图
│ └── videos/ # 输入:原始视频
│
├── frames/
│ └── sam3_001/ # 中间:0.jpg, 1.jpg, ...(episode 编号三位)
│
├── prelabels/
│ ├── static/
│ │ ├── seg_summary.json # 汇总:每张图的 mask 路径、instance 数
│ │ └── IMG_8086/ # 每图一目录:masks/*.png, *_seg.jpg
│ └── video/
│ └── 001/
│ ├── tracking_summary.json
│ ├── 001_mask_ref.png
│ └── masks/
│
├── reviewed/ # Label Studio 专用
│ ├── .ls_token # [Secret] JWT refresh token,chmod 600
│ ├── labelstudio_import.json # API 导入任务列表
│ ├── labelstudio_config.xml # 标注界面(从 scripts/ 复制)
│ ├── labelstudio_media/ # 原图+mask 副本(Local Files 读取)
│ ├── labelstudio_tasks/ # Sync 模式备用 JSON(不推荐)
│ └── labelstudio_export/
│ └── result.json # LS 导出的 COCO
│
├── scripts/ # 全部流水线脚本(见附录 C)
└── docs/ # 文档
| 路径 | 谁写入 | 谁读取 | 可否删后重建 |
|---|---|---|---|
raw/ |
你手动放数据 | prelabel、extract | 否(源数据) |
frames/ |
extract_frames.sh |
prelabel_video.py |
可(重新抽帧) |
prelabels/ |
prelabel 脚本 | import、export_coco | 可(重新预标注) |
reviewed/labelstudio_media/ |
labelstudio_import.py |
LS Local Files | 可(reload 重建) |
reviewed/labelstudio_import.json |
labelstudio_import.py |
push_import | 可 |
reviewed/labelstudio_export/ |
labelstudio_server_export.sh |
export_coco.py |
人工修正后需保留 |
~/.label-studio/ |
Label Studio | LS 自身 | 删则丢项目/用户 |
附录 C:脚本完整源码
以下按流水线顺序 列出 scripts/ 下全部脚本及 labelstudio_config.xml 的完整源码。文档与仓库文件不一致时,以仓库为准。
C.1 scripts/extract_frames.sh
bash
#!/usr/bin/env bash
# =============================================================================
# extract_frames.sh --- 视频抽帧
# =============================================================================
#
# 功能:
# 将 raw/videos/ 下的视频文件用 ffmpeg 抽帧,输出到 frames/sam3_<episode_id>/
# 帧命名约定为 0.jpg, 1.jpg, ... 与 use_sam3 / prelabel_video.py 保持一致
#
# 用法:
# ./extract_frames.sh [fps] [max_frames]
#
# 参数:
# fps 抽帧帧率,默认 5(每秒 5 帧,避免相邻帧过于冗余)
# max_frames 每段视频最多抽取帧数,0 表示不限制(默认 0)
#
# 示例:
# ./extract_frames.sh 5 # 5 fps,抽完全部
# ./extract_frames.sh 5 50 # 5 fps,每段视频最多 50 帧
#
# 输入:label/raw/videos/*.{mp4,mov,avi,mkv}
# 输出:label/frames/sam3_001/, sam3_002/, ...
#
# 依赖:ffmpeg(apt-get install -y ffmpeg)
# 下一步:bash scripts/run_prelabel.sh video
# =============================================================================
set -euo pipefail # 遇错退出;未定义变量报错;管道中任一命令失败则失败
# 定位脚本目录与项目根目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LABEL_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# 路径与参数
VIDEO_DIR="${LABEL_ROOT}/raw/videos" # 原始视频存放处
FRAMES_DIR="${LABEL_ROOT}/frames" # 抽帧输出根目录
FPS="${1:-5}" # 第 1 个参数:帧率,默认 5
MAX_FRAMES="${2:-0}" # 第 2 个参数:最大帧数,0=不限
# 检查 ffmpeg 是否可用
if ! command -v ffmpeg >/dev/null 2>&1; then
echo "ffmpeg not found. Install with: apt-get install -y ffmpeg" >&2
exit 1
fi
# nullglob:无匹配文件时展开为空数组,而非字面量 "*.mp4"
shopt -s nullglob
videos=("${VIDEO_DIR}"/*.{mp4,mov,avi,mkv,MP4,MOV,AVI,MKV})
if [[ ${#videos[@]} -eq 0 ]]; then
echo "No videos found in ${VIDEO_DIR}"
exit 0 # 无视频不算错误,静默退出
fi
# 按视频文件顺序分配 episode_id:001, 002, ...
episode_idx=1
for video in "${videos[@]}"; do
episode_id="$(printf '%03d' "${episode_idx}")"
out_dir="${FRAMES_DIR}/sam3_${episode_id}"
mkdir -p "${out_dir}"
echo "[extract] ${video} -> ${out_dir}/ (${FPS} fps)"
# -hide_banner -loglevel error:减少 ffmpeg 日志
# -vf fps=N:按目标帧率采样
# -frames:v M:可选,限制输出帧数
# %d.jpg:输出 1.jpg, 2.jpg... 后续 prelabel_video 会重命名为 0.jpg 起(若需从 0 开始可改 ffmpeg 表达式)
# 注意:ffmpeg 默认从 1 开始编号;prelabel_video 的 list_frame_paths 按数字 stem 排序,0.jpg 优先
if [[ "${MAX_FRAMES}" -gt 0 ]]; then
ffmpeg -hide_banner -loglevel error -y -i "${video}" \
-vf "fps=${FPS}" -frames:v "${MAX_FRAMES}" \
"${out_dir}/%d.jpg"
else
ffmpeg -hide_banner -loglevel error -y -i "${video}" \
-vf "fps=${FPS}" \
"${out_dir}/%d.jpg"
fi
count="$(find "${out_dir}" -maxdepth 1 -name '*.jpg' | wc -l)"
echo " -> ${count} frame(s)"
episode_idx=$((episode_idx + 1))
done
echo "Done. Frame dirs under ${FRAMES_DIR}/sam3_*"
C.2 scripts/run_prelabel.sh
bash
#!/usr/bin/env bash
# =============================================================================
# run_prelabel.sh --- SAM3 预标注统一入口
# =============================================================================
#
# 功能:
# 调用 prelabel_static.py / prelabel_video.py,对静态图和视频帧序列做 SAM3 半自动预标注
#
# 用法:
# ./run_prelabel.sh [static|video|all]
#
# 模式:
# static 仅处理 raw/images/ 下的静态图
# video 仅处理 frames/sam3_*/ 下的视频抽帧(需先跑 extract_frames.sh)
# all 两者都跑(默认)
#
# 环境变量(可选覆盖):
# PROMPT_FILE 文本 prompt 文件,默认 label/prompt.txt
# CONFIDENCE SAM3 置信度阈值,默认 0.4(偏低以减少漏检,人工后续删误检)
# CHECKPOINT SAM3 权重路径
#
# 输出:
# prelabels/static/seg_summary.json
# prelabels/video/<episode>/tracking_summary.json
#
# 依赖:GPU、SAM3 已安装、权重文件存在
# 下一步:Label Studio QA(可选)或 export_coco.py
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LABEL_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
MODE="${1:-all}"
# 默认配置,可通过环境变量覆盖
PROMPT_FILE="${PROMPT_FILE:-${LABEL_ROOT}/prompt.txt}"
CONFIDENCE="${CONFIDENCE:-0.4}"
CHECKPOINT="${CHECKPOINT:-/root/autodl-tmp/weights/sam3_ms/sam3.pt}"
# ---------------------------------------------------------------------------
# run_static --- 静态图预标注
# ---------------------------------------------------------------------------
run_static() {
local n
# 统计 raw/images/ 下支持的图片格式数量
n="$(find "${LABEL_ROOT}/raw/images" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.png' -o -iname '*.jpeg' -o -iname '*.webp' \) | wc -l)"
if [[ "${n}" -eq 0 ]]; then
echo "[prelabel] skip static: no images in raw/images/"
return 0
fi
echo "[prelabel] static images (${n})"
python3 "${SCRIPT_DIR}/prelabel_static.py" \
--prompt-file "${PROMPT_FILE}" \
--confidence "${CONFIDENCE}" \
--checkpoint "${CHECKPOINT}"
}
# ---------------------------------------------------------------------------
# run_video --- 视频帧序列预标注(首帧分割 + 跨帧跟踪)
# ---------------------------------------------------------------------------
run_video() {
local n
# 匹配 frames/sam3_001 等目录,排除 sam3_seg* 可视化目录
n="$(find "${LABEL_ROOT}/frames" -maxdepth 1 -type d -name 'sam3_*' ! -name 'sam3_seg*' | wc -l)"
if [[ "${n}" -eq 0 ]]; then
echo "[prelabel] skip video: no frames/sam3_* dirs (run extract_frames.sh first)"
return 0
fi
echo "[prelabel] video episodes (${n})"
python3 "${SCRIPT_DIR}/prelabel_video.py" \
--prompt-file "${PROMPT_FILE}" \
--confidence "${CONFIDENCE}" \
--checkpoint "${CHECKPOINT}" \
--rematch-each-frame # 每帧与 ref mask 做 IoU 重匹配,跟踪漂移时更稳健
}
case "${MODE}" in
static) run_static ;;
video) run_video ;;
all)
run_static
run_video
;;
*)
echo "Usage: $0 [static|video|all]" >&2
exit 1
;;
esac
echo "[prelabel] done. Outputs under ${LABEL_ROOT}/prelabels/"
C.3 scripts/prelabel_static.py
python
#!/usr/bin/env python3
"""
prelabel_static.py --- 静态图 SAM3 预标注
功能:
对 raw/images/ 下每张图片运行 SAM3 文本提示实例分割,输出 mask PNG 与可视化。
输入:
- raw/images/*.jpg|png|...
- prompt.txt(或通过 --prompt 指定)
输出(prelabels/static/<图片stem>/):
- masks/<stem>_mask_0.png, _mask_1.png, ... 各实例二值 mask
- <stem>_seg.jpg 叠加可视化
- seg_summary.json(汇总所有图片的元信息)
下游:
- labelstudio_import.py(人工 QA)
- export_coco.py(转 COCO)
用法:
python3 prelabel_static.py
python3 prelabel_static.py --confidence 0.3 --dry-run
依赖:GPU、sam3、matplotlib、Pillow
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import matplotlib
matplotlib.use("Agg") # 无头环境,不弹 GUI 窗口
import matplotlib.pyplot as plt
import sam3
import sam3.perflib.fused as fused
from PIL import Image
from sam3.model_builder import build_sam3_image_model
from sam3.model.sam3_image_processor import Sam3Processor
from sam3.visualization_utils import COLORS, plot_bbox, plot_mask
# ---------------------------------------------------------------------------
# RTX 3090 等 GPU 上 bf16/fp16 融合算子可能不稳定,强制 addmm 走 fp32
# 与 use_sam3/seg_all.py 保持一致
# ---------------------------------------------------------------------------
_orig_addmm_act = fused.addmm_act
def _addmm_act_fp32(activation, linear, mat1):
return _orig_addmm_act(activation, linear, mat1).float()
fused.addmm_act = _addmm_act_fp32
import sam3.model.vitdet as vitdet
vitdet.addmm_act = _addmm_act_fp32
LABEL_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_CKPT = Path("/root/autodl-tmp/weights/sam3_ms/sam3.pt")
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
def read_prompt(prompt_file: Path) -> str:
"""从 prompt.txt 读取文本 query;文件为空则回退为 'object'。"""
if prompt_file.exists():
text = prompt_file.read_text(encoding="utf-8").strip()
if text:
return text
return "object"
def list_images(images_dir: Path) -> list[Path]:
"""列出目录下所有支持的图片文件,按文件名排序。"""
files = [
p
for p in sorted(images_dir.iterdir())
if p.is_file() and p.suffix.lower() in IMAGE_EXTS
]
return files
def build_processor(checkpoint: Path, confidence: float) -> Sam3Processor:
"""
加载 SAM3 图像模型并构建 Sam3Processor。
Args:
checkpoint: sam3.pt 本地路径
confidence: 检测置信度阈值,低于此值的实例会被过滤
"""
bpe_path = Path(sam3.__file__).parent / "assets/bpe_simple_vocab_16e6.txt.gz"
model = build_sam3_image_model(
bpe_path=str(bpe_path),
checkpoint_path=str(checkpoint),
load_from_HF=False,
)
return Sam3Processor(model, confidence_threshold=confidence)
def save_visualization(image: Image.Image, state: dict, out_path: Path, prompt: str) -> None:
"""保存 mask + bbox 叠加可视化图,供人工快速质检。"""
fig, ax = plt.subplots(figsize=(12, 8))
ax.imshow(image)
ax.axis("off")
ax.set_title(f'"{prompt}" -> {len(state["scores"])} instance(s)', fontsize=12)
img_w, img_h = image.size
for i in range(len(state["scores"])):
color = COLORS[i % len(COLORS)]
plot_mask(state["masks"][i].squeeze(0).cpu(), color=color, ax=ax)
score = state["scores"][i].item()
plot_bbox(
img_h,
img_w,
state["boxes"][i].cpu(),
text=f"id={i + 1}, score={score:.2f}",
box_format="XYXY",
color=color,
relative_coords=False,
ax=ax,
)
fig.tight_layout()
fig.savefig(out_path, dpi=150, bbox_inches="tight")
plt.close(fig)
def segment_image(
image_path: Path,
out_dir: Path,
processor: Sam3Processor,
prompt: str,
) -> dict:
"""
对单张图片做 SAM3 文本分割并保存结果。
Returns:
包含 episode_id、mask 路径、实例数等的 dict,写入 seg_summary.json
"""
out_dir.mkdir(parents=True, exist_ok=True)
masks_dir = out_dir / "masks"
masks_dir.mkdir(exist_ok=True)
image = Image.open(image_path).convert("RGB")
state = processor.set_image(image)
processor.reset_all_prompts(state)
state = processor.set_text_prompt(state=state, prompt=prompt)
stem = image_path.stem
mask_paths: list[str] = []
# 每个检测实例保存为独立 PNG(255=前景,0=背景)
for i in range(len(state["scores"])):
mask_name = f"{stem}_mask_{i}.png"
mask_np = state["masks"][i].squeeze(0).cpu().numpy().astype("uint8") * 255
Image.fromarray(mask_np).save(masks_dir / mask_name)
mask_paths.append(f"masks/{mask_name}")
save_visualization(image, state, out_dir / f"{stem}_seg.jpg", prompt)
scores = [round(s.item(), 4) for s in state["scores"]]
return {
"source": "static",
"episode_id": f"static_{stem}", # export_coco 按 episode 分组划分 train/valid
"image": image_path.name,
"image_path": str(image_path.resolve()),
"output_dir": str(out_dir.resolve()),
"instances": len(scores),
"scores": scores,
"mask_paths": [str((out_dir / p).resolve()) for p in mask_paths],
"visualization": f"{stem}_seg.jpg",
}
def main() -> None:
parser = argparse.ArgumentParser(
description="Pre-label static images with SAM3",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--images-dir",
type=Path,
default=LABEL_ROOT / "raw" / "images",
help="静态原图目录",
)
parser.add_argument(
"--output-dir",
type=Path,
default=LABEL_ROOT / "prelabels" / "static",
help="预标注输出目录",
)
parser.add_argument(
"--prompt-file",
type=Path,
default=LABEL_ROOT / "prompt.txt",
help="文本 prompt 文件(= COCO category name)",
)
parser.add_argument("--prompt", type=str, default=None, help="直接指定 prompt,覆盖 prompt-file")
parser.add_argument("--checkpoint", type=Path, default=DEFAULT_CKPT, help="SAM3 权重路径")
parser.add_argument(
"--confidence",
type=float,
default=0.4,
help="置信度阈值;预标注建议 0.3--0.5,宁可多检后人工删",
)
parser.add_argument("--dry-run", action="store_true", help="仅列出待处理图片,不加载模型")
args = parser.parse_args()
images = list_images(args.images_dir)
if not images:
print(f"No images in {args.images_dir}", file=sys.stderr)
sys.exit(1)
prompt = args.prompt or read_prompt(args.prompt_file)
print(f"Images: {len(images)} from {args.images_dir}")
print(f"Prompt: {prompt!r}")
if args.dry_run:
for p in images:
print(f" would segment: {p.name}")
return
processor = build_processor(args.checkpoint, args.confidence)
results: list[dict] = []
for image_path in images:
out_dir = args.output_dir / image_path.stem
print(f" [{image_path.name}] -> {out_dir.name}/")
item = segment_image(image_path, out_dir, processor, prompt)
print(f" -> {item['instances']} instance(s)")
results.append(item)
# 汇总 JSON 供 export_coco / labelstudio_import 读取
summary_path = args.output_dir / "seg_summary.json"
summary_path.write_text(json.dumps(results, indent=2), encoding="utf-8")
print(f"\nSummary -> {summary_path}")
if __name__ == "__main__":
main()
C.4 scripts/prelabel_video.py
python
#!/usr/bin/env python3
"""
prelabel_video.py --- 视频帧序列 SAM3 预标注
功能:
1. 对 frames/sam3_<id>/ 首帧(0.jpg)做 SAM3 文本分割,取最高分实例作为参考 mask
2. 调用 use_sam3/track_episode_sop.py 将参考 mask 传播到后续所有帧
输入:
- frames/sam3_001/, sam3_002/, ...(0.jpg, 1.jpg, ...)
- prompt.txt
输出(prelabels/video/<episode_id>/):
- <id>_mask_ref.png 首帧参考 mask
- masks/<id>_frame_<n>_mask.png 逐帧传播 mask
- tracking_summary.json 每帧 IoU、是否有效等统计
下游:
- labelstudio_import.py
- export_coco.py
用法:
python3 prelabel_video.py
python3 prelabel_video.py --episode 001 --rematch-each-frame
依赖:GPU、sam3、use_sam3/track_episode_sop.py
"""
from __future__ import annotations
import argparse
import json
import re
import shutil
import subprocess
import sys
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import sam3
import sam3.perflib.fused as fused
from PIL import Image
from sam3.model_builder import build_sam3_image_model
from sam3.model.sam3_image_processor import Sam3Processor
from sam3.visualization_utils import COLORS, plot_bbox, plot_mask
# fp32 补丁,与 prelabel_static.py 相同
_orig_addmm_act = fused.addmm_act
def _addmm_act_fp32(activation, linear, mat1):
return _orig_addmm_act(activation, linear, mat1).float()
fused.addmm_act = _addmm_act_fp32
import sam3.model.vitdet as vitdet
vitdet.addmm_act = _addmm_act_fp32
LABEL_ROOT = Path(__file__).resolve().parents[1]
USE_SAM3 = Path("/root/autodl-tmp/use_sam3/track_episode_sop.py") # 已有跟踪 SOP 脚本
DEFAULT_CKPT = Path("/root/autodl-tmp/weights/sam3_ms/sam3.pt")
SAM3_DIR_PATTERN = re.compile(r"^sam3_(.+)$") # 匹配 sam3_001 → episode_id=001
def read_prompt(prompt_file: Path) -> str:
"""从 prompt.txt 读取文本 query。"""
if prompt_file.exists():
text = prompt_file.read_text(encoding="utf-8").strip()
if text:
return text
return "object"
def list_frame_dirs(frames_root: Path) -> list[tuple[str, Path]]:
"""
扫描 frames/ 下所有 sam3_<episode_id> 目录。
排除 sam3_seg* 前缀目录(那是 seg_all 可视化输出,不是帧序列)。
"""
dirs: list[tuple[str, Path]] = []
for path in sorted(frames_root.iterdir()):
if not path.is_dir():
continue
m = SAM3_DIR_PATTERN.match(path.name)
if m and not path.name.startswith("sam3_seg"):
dirs.append((m.group(1), path))
return dirs
def build_processor(checkpoint: Path, confidence: float) -> Sam3Processor:
"""加载 SAM3 图像模型(首帧分割用)。"""
bpe_path = Path(sam3.__file__).parent / "assets/bpe_simple_vocab_16e6.txt.gz"
model = build_sam3_image_model(
bpe_path=str(bpe_path),
checkpoint_path=str(checkpoint),
load_from_HF=False,
)
return Sam3Processor(model, confidence_threshold=confidence)
def save_ref_mask_from_frame0(
frames_dir: Path,
ref_path: Path,
processor: Sam3Processor,
prompt: str,
) -> int:
"""
在首帧上跑 SAM3 分割,取置信度最高的实例作为跟踪参考 mask。
Args:
frames_dir: 帧目录,优先读 0.jpg
ref_path: 参考 mask 输出路径(PNG,255=前景)
processor: SAM3 处理器
prompt: 文本 query
Returns:
被选中的实例索引(0-based)
Raises:
RuntimeError: 首帧上未检测到任何实例(需调整 prompt 或 confidence)
"""
frame0 = frames_dir / "0.jpg"
if not frame0.exists():
# 若无 0.jpg,取数字 stem 最小的帧
frame_paths = sorted(frames_dir.glob("*.jpg"), key=lambda p: int(p.stem))
if not frame_paths:
raise FileNotFoundError(f"No frames in {frames_dir}")
frame0 = frame_paths[0]
image = Image.open(frame0).convert("RGB")
state = processor.set_image(image)
processor.reset_all_prompts(state)
state = processor.set_text_prompt(state=state, prompt=prompt)
if len(state["scores"]) == 0:
raise RuntimeError(f"No instances on {frame0} for prompt {prompt!r}")
# 多实例时选 score 最高的作为跟踪目标
best_idx = max(range(len(state["scores"])), key=lambda i: state["scores"][i].item())
mask_np = state["masks"][best_idx].squeeze(0).cpu().numpy().astype("uint8") * 255
ref_path.parent.mkdir(parents=True, exist_ok=True)
Image.fromarray(mask_np).save(ref_path)
# 保存参考帧预览图,便于人工确认跟踪目标是否正确
preview = frames_dir.parent / f"ref_preview_{frames_dir.name}.jpg"
fig, ax = plt.subplots(figsize=(10, 6))
ax.imshow(image)
ax.axis("off")
color = COLORS[best_idx % len(COLORS)]
plot_mask(state["masks"][best_idx].squeeze(0).cpu(), color=color, ax=ax)
plot_bbox(
image.size[1],
image.size[0],
state["boxes"][best_idx].cpu(),
text=f"ref score={state['scores'][best_idx].item():.2f}",
box_format="XYXY",
color=color,
relative_coords=False,
ax=ax,
)
fig.tight_layout()
fig.savefig(preview, dpi=120, bbox_inches="tight")
plt.close(fig)
return best_idx
def run_tracking(
episode_id: str,
frames_dir: Path,
ref_mask: Path,
output_dir: Path,
prompt: str,
checkpoint: Path,
iou_threshold: float,
rematch_each_frame: bool,
) -> None:
"""
调用 track_episode_sop.py 做 SAM3 视频传播 + ref mask IoU 映射。
track_episode_sop 会:
- 用文本 prompt 在视频上跑 SAM3 跟踪
- 将 tracker 输出的多个 obj_id 与 ref_mask 做 IoU 匹配
- IoU 低于阈值的帧输出空 mask
- 写出 tracking_summary.json
"""
output_dir.mkdir(parents=True, exist_ok=True)
cmd = [
sys.executable,
str(USE_SAM3),
"--episode",
episode_id,
"--frames-dir",
str(frames_dir),
"--ref-mask",
str(ref_mask),
"--output-dir",
str(output_dir / "masks"), # 逐帧 mask PNG 输出目录
"--prompt",
prompt,
"--checkpoint",
str(checkpoint),
"--iou-threshold",
str(iou_threshold),
"--no-video", # 冒烟/批处理不需要 overlay 视频
]
if rematch_each_frame:
# 每帧重新与 ref_mask 做 IoU 匹配,跟踪 ID 漂移时更稳健
cmd.append("--rematch-each-frame")
subprocess.run(cmd, check=True)
# track_episode_sop 把 summary 写在 masks/ 下,移到 episode 根目录方便读取
summary_src = output_dir / "masks" / "tracking_summary.json"
summary_dst = output_dir / "tracking_summary.json"
if summary_src.exists():
shutil.move(str(summary_src), str(summary_dst))
def main() -> None:
parser = argparse.ArgumentParser(
description="Pre-label video frame sequences",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--frames-dir",
type=Path,
default=LABEL_ROOT / "frames",
help="抽帧根目录,内含 sam3_* 子目录",
)
parser.add_argument(
"--output-dir",
type=Path,
default=LABEL_ROOT / "prelabels" / "video",
help="视频预标注输出目录",
)
parser.add_argument("--prompt-file", type=Path, default=LABEL_ROOT / "prompt.txt")
parser.add_argument("--prompt", type=str, default=None)
parser.add_argument("--checkpoint", type=Path, default=DEFAULT_CKPT)
parser.add_argument("--confidence", type=float, default=0.4, help="首帧分割置信度阈值")
parser.add_argument(
"--iou-threshold",
type=float,
default=0.5,
help="传播帧与 ref mask 的 IoU 低于此值则输出空 mask",
)
parser.add_argument(
"--rematch-each-frame",
action="store_true",
help="每帧 IoU 重匹配(run_prelabel.sh 默认开启)",
)
parser.add_argument("--episode", type=str, default=None, help="仅处理指定 episode,如 001")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
episodes = list_frame_dirs(args.frames_dir)
if args.episode:
episodes = [(eid, p) for eid, p in episodes if eid == args.episode]
if not episodes:
print(f"No sam3_* frame dirs under {args.frames_dir}", file=sys.stderr)
sys.exit(1)
prompt = args.prompt or read_prompt(args.prompt_file)
print(f"Episodes: {len(episodes)}")
print(f"Prompt: {prompt!r}")
if args.dry_run:
for eid, p in episodes:
print(f" would track: sam3_{eid} ({len(list(p.glob('*.jpg')))} frames)")
return
processor = build_processor(args.checkpoint, args.confidence)
results: list[dict] = []
for episode_id, frames_dir in episodes:
out_dir = args.output_dir / episode_id
ref_mask = out_dir / f"{episode_id}_mask_ref.png"
print(f"\n[sam3_{episode_id}] frames={len(list(frames_dir.glob('*.jpg')))}")
try:
ref_idx = save_ref_mask_from_frame0(frames_dir, ref_mask, processor, prompt)
print(f" ref mask from instance {ref_idx} -> {ref_mask.name}")
run_tracking(
episode_id,
frames_dir,
ref_mask,
out_dir,
prompt,
args.checkpoint,
args.iou_threshold,
args.rematch_each_frame,
)
summary_path = out_dir / "tracking_summary.json"
if summary_path.exists():
summary = json.loads(summary_path.read_text(encoding="utf-8"))
# 写入 frames_dir 绝对路径,export_coco 据此找原图
summary["frames_dir"] = str(frames_dir.resolve())
summary_path.write_text(json.dumps(summary, indent=2), encoding="utf-8")
results.append(summary)
print(
f" tracked: {summary.get('frames_with_mask', 0)}/"
f"{summary.get('num_frames', 0)} frames with mask"
)
except (FileNotFoundError, RuntimeError, subprocess.CalledProcessError) as exc:
print(f" ERROR: {exc}", file=sys.stderr)
if results:
summary_all = args.output_dir / "tracking_summary_all.json"
summary_all.write_text(json.dumps(results, indent=2), encoding="utf-8")
print(f"\nAll summaries -> {summary_all}")
if __name__ == "__main__":
main()
C.5 scripts/labelstudio_import.py
python
#!/usr/bin/env python3
"""
labelstudio_import.py --- 生成 Label Studio 导入任务
功能:
读取 prelabels/static 与 prelabels/video 的预标注结果,
将原图与 mask 复制到 reviewed/labelstudio_media/,
生成 Label Studio 可导入的 JSON 任务列表。
输出:
- reviewed/labelstudio_import.json 导入任务(含 /data/local-files/ URL)
- reviewed/labelstudio_media/ 图片与 mask 副本(Local Files 根目录)
- reviewed/labelstudio_config.xml 标注界面配置(从 scripts/ 复制)
Label Studio 任务字段说明:
- image: 原图 URL,格式 /data/local-files/?d=<相对路径>
- episode_id: 用于 export_coco 按 episode 分组
- source: static | video
- prelabel_mask(s): 预标注 mask URL(备用)
- annotations: SAM3 mask 转为 Brush RLE,打开任务直接显示、可编辑
用法:
python3 labelstudio_import.py
python3 labelstudio_import.py --output /path/to/import.json
下游:
start_labelstudio.sh → 浏览器 QA → Export COCO → export_coco.py --source labelstudio
"""
from __future__ import annotations
import argparse
import json
import shutil
import uuid
from pathlib import Path
from label_studio_sdk.converter.brush import image2rle
from PIL import Image
LABEL_ROOT = Path(__file__).resolve().parents[1]
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp"} # 预留,当前通过 summary 路径过滤
# Local Files 根目录的父路径;Cloud Storage 必须为其子目录(Label Studio 安全限制)
LOCAL_FILES_DOCUMENT_ROOT_NAME = "reviewed"
MEDIA_SUBDIR = "labelstudio_media"
def to_local_url(rel_under_media: str) -> str:
"""生成 /data/local-files/ URL,相对 DOCUMENT_ROOT=reviewed/。"""
return f"/data/local-files/?d={MEDIA_SUBDIR}/{rel_under_media}"
def read_prompt(prompt_file: Path) -> str:
"""读取 prompt(本脚本不直接使用,保留供扩展)。"""
if prompt_file.exists():
text = prompt_file.read_text(encoding="utf-8").strip()
if text:
return text
return "object"
def build_brush_results(image_path: Path, mask_paths: list[Path]) -> list[dict]:
"""将 mask PNG 转为 Label Studio Brush 区域(直接可编辑的 annotation)。"""
if not image_path.exists() or not mask_paths:
return []
with Image.open(image_path) as img:
width, height = img.size
results: list[dict] = []
for mask_path in mask_paths:
if not mask_path.exists():
continue
rle, _mask_w, _mask_h = image2rle(str(mask_path))
results.append(
{
"id": str(uuid.uuid4())[:8],
"from_name": "brush",
"to_name": "image",
"type": "brushlabels",
"origin": "manual",
"original_width": width,
"original_height": height,
"image_rotation": 0,
"value": {
"format": "rle",
"rle": rle,
"brushlabels": ["target"],
},
}
)
return results
def make_task(data: dict, image_path: Path, mask_paths: list[Path]) -> dict:
"""组装任务;SAM3 mask 作为 annotations 导入,打开即可见、可改。"""
task: dict = {"data": data}
brush_results = build_brush_results(image_path, mask_paths)
if brush_results:
# ground_truth 默认 True 会导致导入后难以编辑;显式设为 False
task["annotations"] = [{"result": brush_results, "ground_truth": False}]
return task
def collect_static_tasks(static_dir: Path, media_dir: Path) -> list[dict]:
"""
从 prelabels/static/seg_summary.json 构建静态图 Label Studio 任务。
每张图一个 task;多实例时 prelabel_masks 为 mask URL 列表。
"""
summary_path = static_dir / "seg_summary.json"
if not summary_path.exists():
return []
summary = json.loads(summary_path.read_text(encoding="utf-8"))
tasks: list[dict] = []
for item in summary:
image_path = Path(item["image_path"])
if not image_path.exists():
continue
# 复制原图到 media 目录,路径相对于 LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT
rel_name = f"static/{image_path.name}"
dst = media_dir / rel_name
dst.parent.mkdir(parents=True, exist_ok=True)
if not dst.exists():
shutil.copy2(image_path, dst)
# 复制各实例 mask,供标注员对照
mask_urls: list[str] = []
mask_files: list[Path] = []
for mask_path in item.get("mask_paths", []):
mp = Path(mask_path)
if mp.exists():
# 目录结构:static/masks/<图片stem>/<mask文件名>
mask_rel = f"static/masks/{mp.parent.parent.name}/{mp.name}"
mask_dst = media_dir / mask_rel
mask_dst.parent.mkdir(parents=True, exist_ok=True)
if not mask_dst.exists():
shutil.copy2(mp, mask_dst)
mask_urls.append(to_local_url(mask_rel))
mask_files.append(mask_dst)
tasks.append(
make_task(
{
"image": to_local_url(rel_name),
"episode_id": item.get("episode_id", f"static_{image_path.stem}"),
"data_source": "static",
"prelabel_masks": mask_urls,
"instances": item.get("instances", 0),
},
dst,
mask_files,
)
)
return tasks
def collect_video_tasks(video_prelabel_dir: Path, frames_dir: Path, media_dir: Path) -> list[dict]:
"""
从 prelabels/video/<episode>/tracking_summary.json 构建视频帧任务。
仅导入 has_target=True 的帧(跟踪成功且 IoU 达标的帧)。
跳过空 mask 帧,减少 Label Studio 无效任务量。
"""
tasks: list[dict] = []
if not video_prelabel_dir.exists():
return tasks
for episode_dir in sorted(video_prelabel_dir.iterdir()):
if not episode_dir.is_dir():
continue
summary_path = episode_dir / "tracking_summary.json"
if not summary_path.exists():
continue
summary = json.loads(summary_path.read_text(encoding="utf-8"))
episode_id = summary.get("episode_id", episode_dir.name)
frames_source = Path(summary.get("frames_dir", frames_dir / f"sam3_{episode_id}"))
for frame_info in summary.get("per_frame", []):
if not frame_info.get("has_target"):
continue
frame_name = frame_info["frame_name"]
frame_path = frames_source / frame_name
if not frame_path.exists():
continue
rel_image = f"video/{episode_id}/{frame_name}"
dst_image = media_dir / rel_image
dst_image.parent.mkdir(parents=True, exist_ok=True)
if not dst_image.exists():
shutil.copy2(frame_path, dst_image)
mask_name = frame_info.get("output_mask", "")
mask_path = episode_dir / "masks" / mask_name
if not mask_path.exists():
mask_path = episode_dir / mask_name
mask_files: list[Path] = []
if mask_path.exists():
mask_rel = f"video/{episode_id}/masks/{mask_name}"
dst_mask = media_dir / mask_rel
dst_mask.parent.mkdir(parents=True, exist_ok=True)
if not dst_mask.exists():
shutil.copy2(mask_path, dst_mask)
mask_files.append(dst_mask)
mask_urls = [to_local_url(m.relative_to(media_dir).as_posix()) for m in mask_files]
tasks.append(
make_task(
{
"image": to_local_url(rel_image),
"episode_id": episode_id,
"data_source": "video",
"frame_index": frame_info.get("frame_index"),
"prelabel_masks": mask_urls,
"iou_vs_ref": frame_info.get("iou_vs_ref"),
},
dst_image,
mask_files,
)
)
return tasks
def main() -> None:
parser = argparse.ArgumentParser(
description="Build Label Studio import JSON from prelabels",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--label-root",
type=Path,
default=LABEL_ROOT,
help="label 项目根目录",
)
parser.add_argument(
"--output",
type=Path,
default=LABEL_ROOT / "reviewed" / "labelstudio_import.json",
help="Label Studio 导入 JSON 输出路径",
)
args = parser.parse_args()
label_root = args.label_root
media_dir = label_root / LOCAL_FILES_DOCUMENT_ROOT_NAME / MEDIA_SUBDIR
media_dir.mkdir(parents=True, exist_ok=True)
tasks = collect_static_tasks(label_root / "prelabels" / "static", media_dir)
tasks.extend(
collect_video_tasks(
label_root / "prelabels" / "video",
label_root / "frames",
media_dir,
)
)
args.output.parent.mkdir(parents=True, exist_ok=True)
payload = json.dumps(tasks, indent=2)
args.output.write_text(payload, encoding="utf-8")
# 供 Cloud Storage「Tasks」模式 Sync 使用(路径必须是 reviewed/ 的子目录)
tasks_sync_dir = label_root / LOCAL_FILES_DOCUMENT_ROOT_NAME / "labelstudio_tasks"
tasks_sync_dir.mkdir(parents=True, exist_ok=True)
tasks_sync_path = tasks_sync_dir / "labelstudio_import.json"
tasks_sync_path.write_text(payload, encoding="utf-8")
# 同步标注界面 XML 到 reviewed/,方便在 LS 项目中粘贴
config_src = label_root / "scripts" / "labelstudio_config.xml"
config_dst = label_root / "reviewed" / "labelstudio_config.xml"
if config_src.exists():
shutil.copy2(config_src, config_dst)
print(f"Tasks: {len(tasks)}")
print(f"Import JSON (API): {args.output}")
print(f"Import JSON (Sync): {tasks_sync_path}")
print(f"Media dir: {media_dir}")
print(f"LS document root: {label_root / LOCAL_FILES_DOCUMENT_ROOT_NAME}")
print(f"LS storage path: {media_dir}")
print(f"Config: {config_dst}")
if __name__ == "__main__":
main()
C.6 scripts/labelstudio_push_import.py
python
#!/usr/bin/env python3
"""Push Label Studio tasks + SAM3 brush predictions via REST API (not Upload Files)."""
from __future__ import annotations
import argparse
import json
import sqlite3
import sys
import urllib.error
import urllib.request
from pathlib import Path
LABEL_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_IMPORT = LABEL_ROOT / "reviewed" / "labelstudio_import.json"
DEFAULT_DB = Path.home() / ".label-studio" / "label_studio.sqlite3"
DEFAULT_STORAGE_PATH = LABEL_ROOT / "reviewed" / "labelstudio_media"
def enable_legacy_api_token(db_path: Path) -> None:
if not db_path.exists():
return
with sqlite3.connect(db_path) as conn:
conn.execute(
"UPDATE jwt_auth_jwtsettings SET legacy_api_tokens_enabled = 1 WHERE organization_id = 1"
)
conn.commit()
def auth_headers(token: str) -> dict[str, str]:
token = token.strip()
if token.lower().startswith("bearer "):
return {"Authorization": token}
# JWT access token (three base64 segments)
if token.count(".") == 2:
return {"Authorization": f"Bearer {token}"}
return {"Authorization": f"Token {token}"}
def decode_jwt_payload(token: str) -> dict:
try:
part = token.split(".")[1]
part += "=" * (-len(part) % 4)
return json.loads(__import__("base64").urlsafe_b64decode(part).decode("utf-8"))
except Exception:
return {}
def resolve_access_token(host: str, port: int, token: str) -> str:
"""Accept legacy Token, JWT access, or JWT refresh (auto-exchange)."""
token = token.strip()
if token.lower().startswith("bearer "):
token = token.split(" ", 1)[1].strip()
payload = decode_jwt_payload(token)
token_type = payload.get("token_type")
if token_type == "access":
return token
if token_type == "refresh":
print("检测到 JWT refresh token,正在换取 access token ...")
resp = api_post_raw(
f"http://{host}:{port}/api/token/refresh/",
"",
{"refresh": token},
use_auth=False,
)
access = resp.get("access")
if not access:
raise RuntimeError(f"refresh 换 access 失败: {resp}")
return access
# Legacy Token (40 hex chars) --- keep as-is
return token
def api_post_raw(url: str, token: str, payload, *, use_auth: bool = True) -> dict:
body = json.dumps(payload).encode("utf-8")
headers = {"Content-Type": "application/json"}
if use_auth and token:
headers.update(auth_headers(token))
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=120) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HTTP {exc.code}: {detail}") from exc
def api_post(url: str, token: str, payload) -> dict:
return api_post_raw(url, token, payload, use_auth=True)
def api_get(url: str, token: str) -> dict | list:
req = urllib.request.Request(url, headers=auth_headers(token), method="GET")
try:
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HTTP {exc.code}: {detail}") from exc
def ensure_local_storage(host: str, port: int, token: str, project_id: int, storage_path: Path) -> None:
"""创建 Local Files Source Storage(图片 /data/local-files/ 依赖此项)。"""
base = f"http://{host}:{port}/api/storages/localfiles"
existing = api_get(f"{base}?project={project_id}", token)
target = str(storage_path.resolve())
for item in existing if isinstance(existing, list) else []:
if item.get("path") == target:
print(f"Cloud Storage 已存在: {target}")
return
print(f"创建 Cloud Storage Source: {target}")
payload = {
"project": project_id,
"title": "SAM3 media",
"path": target,
"use_blob_urls": True,
"regex_filter": r".*\.(jpg|jpeg|png|webp)",
}
api_post(f"{base}", token, payload)
print("Cloud Storage 创建成功(不要点 Sync)")
def api_delete(url: str, token: str) -> None:
req = urllib.request.Request(url, headers=auth_headers(token), method="DELETE")
try:
with urllib.request.urlopen(req, timeout=60) as resp:
resp.read()
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HTTP {exc.code}: {detail}") from exc
def delete_project_tasks(host: str, port: int, token: str, project_id: int) -> int:
"""删除项目内全部任务,便于 --replace 重新导入。"""
deleted = 0
while True:
data = api_get(
f"http://{host}:{port}/api/tasks?project={project_id}&page_size=200&fields=id",
token,
)
tasks = data.get("tasks", []) if isinstance(data, dict) else []
if not tasks:
break
for task in tasks:
api_delete(f"http://{host}:{port}/api/tasks/{task['id']}/", token)
deleted += 1
return deleted
def import_tasks(host: str, port: int, project_id: int, token: str, tasks: list[dict]) -> dict:
url = (
f"http://{host}:{port}/api/projects/{project_id}/import"
f"?commit_to_project=true&return_task_ids=true"
)
return api_post(url, token, tasks)
def import_predictions(host: str, port: int, project_id: int, token: str, items: list[dict]) -> dict:
url = f"http://{host}:{port}/api/projects/{project_id}/import/predictions"
return api_post(url, token, items)
def main() -> None:
import os
parser = argparse.ArgumentParser(description="Import Label Studio tasks with SAM3 mask predictions")
parser.add_argument("project_id", type=int, help="Label Studio project id")
default_port = int(os.environ.get("LS_PORT", "6006"))
parser.add_argument("--port", type=int, default=default_port, help="Label Studio 端口(或 export LS_PORT=6007)")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--import-json", type=Path, default=DEFAULT_IMPORT)
parser.add_argument("--token", default="", help="JWT refresh/access 或 Legacy Token;或 set LS_TOKEN")
parser.add_argument("--storage-path", type=Path, default=DEFAULT_STORAGE_PATH)
parser.add_argument("--skip-storage-setup", action="store_true", help="跳过自动创建 Cloud Storage")
parser.add_argument(
"--replace",
action="store_true",
help="导入前删除项目内全部旧任务(推荐,避免 predictions/annotations 混用)",
)
parser.add_argument("--enable-legacy-token", action="store_true", default=True)
args = parser.parse_args()
token = args.token or os.environ.get("LS_TOKEN", "")
if not token:
print("错误:请设置 LS_TOKEN 或 --token", file=sys.stderr)
sys.exit(1)
if args.enable_legacy_token:
enable_legacy_api_token(DEFAULT_DB)
if not args.import_json.exists():
print(f"错误:找不到 {args.import_json}", file=sys.stderr)
print("请先运行: python3 scripts/labelstudio_import.py", file=sys.stderr)
sys.exit(1)
tasks = json.loads(args.import_json.read_text(encoding="utf-8"))
with_annotations = sum(1 for t in tasks if t.get("annotations"))
print(f"Importing {len(tasks)} tasks ({with_annotations} with SAM3 brush annotations)...")
access_token = resolve_access_token(args.host, args.port, token)
if args.replace:
n = delete_project_tasks(args.host, args.port, access_token, args.project_id)
print(f"已删除旧任务: {n} 条")
if not args.skip_storage_setup:
ensure_local_storage(args.host, args.port, access_token, args.project_id, args.storage_path)
result = import_tasks(args.host, args.port, args.project_id, access_token, tasks)
summary = {
k: result.get(k)
for k in ("task_count", "annotation_count", "prediction_count", "duration", "file_upload_ids")
if k in result
}
if result.get("task_ids"):
summary["task_ids_count"] = len(result["task_ids"])
print("Import response:", json.dumps(summary, indent=2))
annotation_count = result.get("annotation_count") or 0
task_ids = result.get("task_ids") or []
if annotation_count:
print("\n完成。浏览器操作:")
print(" 1. 刷新页面,打开任意任务")
print(" 2. 应看到原图 + 红色 SAM3 mask(已是 annotation,无需 Accept / 齿轮)")
print(" 3. 左侧点 target → 底部 Brush/Eraser 修改 → 右上角 Submit")
else:
print("\n警告:annotations 未导入成功。", file=sys.stderr)
if __name__ == "__main__":
main()
C.7 scripts/labelstudio_reload.sh
bash
#!/usr/bin/env bash
# =============================================================================
# labelstudio_reload.sh --- 一键 reload Label Studio 任务
#
# 流程:
# 1. 从 prelabels 重新生成 import JSON + 媒体文件
# 2. 同步 Labeling Setup(labelstudio_config.xml → 项目 DB)
# 3. 删除项目旧任务,API 重新导入(SAM3 mask 为 annotations,打开即见)
#
# 用法:
# bash scripts/labelstudio_reload.sh [project_id] [port]
#
# Token(优先级):
# 1. 环境变量 LS_TOKEN
# 2. reviewed/.ls_token 文件(一行,refresh 或 access JWT 均可)
#
# 示例:
# bash scripts/labelstudio_reload.sh # 项目 1,端口 LS_PORT 或 6007
# bash scripts/labelstudio_reload.sh 1 6007
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LABEL_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
PROJECT_ID="${1:-1}"
PORT="${2:-${LS_PORT:-6007}}"
HOST="${LS_HOST:-127.0.0.1}"
TOKEN_FILE="${LABEL_ROOT}/reviewed/.ls_token"
CONFIG_SRC="${SCRIPT_DIR}/labelstudio_config.xml"
DB_PATH="${HOME}/.label-studio/label_studio.sqlite3"
if [[ -n "${LS_TOKEN:-}" ]]; then
TOKEN="${LS_TOKEN}"
elif [[ -f "${TOKEN_FILE}" ]]; then
TOKEN="$(tr -d ' \n\r' < "${TOKEN_FILE}")"
else
echo "错误:未找到 token。"
echo " 方式1: echo '<refresh token>' > ${TOKEN_FILE} && chmod 600 ${TOKEN_FILE}"
echo " 方式2: export LS_TOKEN='<refresh token>'"
exit 1
fi
if [[ -z "${TOKEN}" ]]; then
echo "错误:token 为空"
exit 1
fi
echo "== Label Studio reload =="
echo " project: ${PROJECT_ID}"
echo " port: ${PORT}"
echo ""
# 检查服务是否在跑
if ! curl -sf -o /dev/null --max-time 3 "http://${HOST}:${PORT}/health" 2>/dev/null; then
if ! curl -sf -o /dev/null --max-time 3 "http://${HOST}:${PORT}/" 2>/dev/null; then
echo "警告:Label Studio 似乎未在 http://${HOST}:${PORT} 运行"
echo " 请先: bash ${SCRIPT_DIR}/start_labelstudio.sh ${PORT}"
exit 1
fi
fi
echo "[1/3] 生成 import JSON 与媒体文件 ..."
python3 "${SCRIPT_DIR}/labelstudio_import.py" --label-root "${LABEL_ROOT}"
echo "[2/3] 同步 Labeling Setup 到项目 ${PROJECT_ID} ..."
if [[ ! -f "${CONFIG_SRC}" ]]; then
echo "错误:找不到 ${CONFIG_SRC}"
exit 1
fi
python3 - "${PROJECT_ID}" "${CONFIG_SRC}" "${DB_PATH}" <<'PY'
import sqlite3
import sys
from pathlib import Path
project_id = int(sys.argv[1])
config_path = Path(sys.argv[2])
db_path = Path(sys.argv[3])
cfg = config_path.read_text(encoding="utf-8")
if not db_path.exists():
print(f"警告:数据库不存在 {db_path},跳过 config 同步(请在 UI 手动粘贴 config)")
sys.exit(0)
conn = sqlite3.connect(db_path)
cur = conn.execute("UPDATE project SET label_config = ? WHERE id = ?", (cfg, project_id))
conn.commit()
if cur.rowcount:
print(f" label_config 已更新(project {project_id})")
else:
print(f" 警告:project {project_id} 不存在,请在 UI 新建项目后重试")
PY
echo "[3/3] 删除旧任务并 API 导入 annotations ..."
export LS_TOKEN="${TOKEN}"
export LS_PORT="${PORT}"
python3 "${SCRIPT_DIR}/labelstudio_push_import.py" "${PROJECT_ID}" --port "${PORT}" --host "${HOST}" --replace
echo ""
echo "Reload 完成。浏览器:"
echo " 1. Ctrl+F5 强制刷新"
echo " 2. 打开任务 → 应看到原图 + 红色 SAM3 mask"
echo " 3. 先点 Edit(若只读)→ 左侧点 target → 底部 Brush/Eraser → Submit"
C.8 scripts/start_labelstudio.sh
bash
#!/usr/bin/env bash
# =============================================================================
# start_labelstudio.sh --- 启动 Label Studio 人工修正服务
# =============================================================================
#
# 功能:
# 1. 调用 labelstudio_import.py 生成/更新导入任务
# 2. 配置 Local Files 存储根目录
# 3. 在指定端口启动 Label Studio Web 服务
#
# 用法:
# ./start_labelstudio.sh [port] [host]
#
# 参数:
# port 监听端口,默认 8080
# host 绑定地址,默认 0.0.0.0(AutoDL 需对外映射)
#
# AutoDL 访问步骤:
# 1. 在本机运行本脚本
# 2. AutoDL 控制台 → 自定义服务 → 映射端口 8080
# 3. 浏览器打开映射 URL
# 4. 新建项目,Labeling Setup 粘贴 reviewed/labelstudio_config.xml
# 5. Settings → Cloud Storage → Local files:
# Path: /root/autodl-tmp/label/reviewed/labelstudio_media
# (必须是 DOCUMENT_ROOT 的子目录,不能与 DOCUMENT_ROOT 相同)
# 6. 导入任务(云服务器,不要用 Upload Files):
# export LS_TOKEN="<token>"
# bash scripts/labelstudio_server_import.sh <project_id> <port>
# 读取:reviewed/labelstudio_import.json
# 7. 修正完成后导出 COCO 到服务器:
# bash scripts/labelstudio_server_export.sh <project_id>
# 输出:reviewed/labelstudio_export/result.json
#
# 注意:
# - Label Studio 安装在 .venv-labelstudio/ 独立 venv,避免与 SAM3 的 numpy 冲突
# - 不要用 label-studio --version 验证安装(会卡住初始化数据库)
#
# 下一步:python3 scripts/export_coco.py --source labelstudio
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LABEL_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
PORT="${1:-8080}"
HOST="${2:-0.0.0.0}"
# Label Studio Local Files:DOCUMENT_ROOT 为父目录,Cloud Storage 填其子目录
export LABEL_STUDIO_LOCAL_FILES_SERVING_ENABLED=true
export LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT="${LABEL_ROOT}/reviewed"
LS_STORAGE_PATH="${LABEL_ROOT}/reviewed/labelstudio_media"
# 启动前刷新导入 JSON 与媒体文件副本
python3 "${SCRIPT_DIR}/labelstudio_import.py" --label-root "${LABEL_ROOT}"
mkdir -p "${LABEL_ROOT}/reviewed/labelstudio_export" # COCO 导出目标目录
mkdir -p "${HOME}/.label-studio" # LS 数据库与配置
# 写入 .env,避免重启后 DOCUMENT_ROOT 与 Storage Path 再次撞车
ENV_FILE="${HOME}/.label-studio/.env"
if [[ -f "${ENV_FILE}" ]] && grep -q '^SECRET_KEY=' "${ENV_FILE}"; then
SECRET_KEY_LINE="$(grep '^SECRET_KEY=' "${ENV_FILE}")"
else
SECRET_KEY_LINE=""
fi
{
[[ -n "${SECRET_KEY_LINE}" ]] && echo "${SECRET_KEY_LINE}"
echo "LABEL_STUDIO_LOCAL_FILES_SERVING_ENABLED=true"
echo "LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT=${LABEL_ROOT}/reviewed"
} > "${ENV_FILE}"
echo "Label Studio starting on ${HOST}:${PORT}"
echo " Document root: ${LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT}"
echo " Storage path: ${LS_STORAGE_PATH} <-- 在 UI 里填这个路径"
echo ""
echo " 说明:你刚才填的路径没错;失败是因为旧脚本把 Document root 也设成了"
echo " labelstudio_media,两者相同才会报错。现已改为 reviewed/ 作根目录。"
echo ""
echo " Import (API): ${LABEL_ROOT}/reviewed/labelstudio_import.json"
echo " Import (Sync): ${LABEL_ROOT}/reviewed/labelstudio_tasks/labelstudio_import.json"
echo " Export COCO: ${LABEL_ROOT}/reviewed/labelstudio_export/result.json"
echo ""
echo " 导入(API,端口用 LS_PORT 或 --port):"
echo " export LS_TOKEN=\"<refresh 或 access token>\""
echo " export LS_PORT=${PORT}"
echo " python3 ${SCRIPT_DIR}/labelstudio_push_import.py <project_id> --port ${PORT}"
echo ""
echo " 导出 COCO 到服务器:"
echo " bash ${SCRIPT_DIR}/labelstudio_server_export.sh <project_id>"
echo " python3 ${SCRIPT_DIR}/export_coco.py --source labelstudio"
# 优先使用项目内独立 venv 的 label-studio,避免污染 SAM3 主环境
LS_VENV="${LABEL_ROOT}/.venv-labelstudio"
if [[ -x "${LS_VENV}/bin/label-studio" ]]; then
LS_BIN="${LS_VENV}/bin/label-studio"
else
LS_BIN="label-studio" # 回退到 PATH 中的全局安装
fi
# exec:用 label-studio 进程替换当前 shell,Ctrl+C 直接终止服务
# 注意:import JSON 需在 Web UI 中手动 Import,不能作为 start 的位置参数
exec "${LS_BIN}" start \
--no-browser \
--enable-legacy-api-token \
--internal-host "${HOST}" \
--port "${PORT}" \
--data-dir "${HOME}/.label-studio"
C.9 scripts/labelstudio_server_export.sh
bash
#!/usr/bin/env bash
# =============================================================================
# labelstudio_server_export.sh --- 导出 COCO 到云服务器本地路径
# =============================================================================
#
# 不要用浏览器 Export 下载后再手动上传;直接在服务器上写文件。
#
# 用法:
# bash scripts/labelstudio_server_export.sh <project_id>
#
# 输出(默认):
# /root/autodl-tmp/label/reviewed/labelstudio_export/result.json
#
# 下游:
# python3 scripts/export_coco.py --source labelstudio \
# --output-dir /root/autodl-tmp/sam3_lora_data/my_dataset
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LABEL_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
PROJECT_ID="${1:?用法: $0 <project_id>}"
EXPORT_DIR="${LABEL_ROOT}/reviewed/labelstudio_export"
EXPORT_PATH="${EXPORT_PATH:-${EXPORT_DIR}/result.json}"
DATA_DIR="${LABEL_STUDIO_DATA_DIR:-${HOME}/.label-studio}"
LS_VENV="${LABEL_ROOT}/.venv-labelstudio"
if [[ -x "${LS_VENV}/bin/label-studio" ]]; then
LS_BIN="${LS_VENV}/bin/label-studio"
else
LS_BIN="label-studio"
fi
mkdir -p "${EXPORT_DIR}"
echo "Exporting project ${PROJECT_ID} → ${EXPORT_PATH}"
echo " format: BRUSH_TO_COCO (Brush/mask 项目必须用此格式,普通 COCO 无 annotations)"
"${LS_BIN}" export "${PROJECT_ID}" BRUSH_TO_COCO \
--export-path "${EXPORT_PATH}" \
--data-dir "${DATA_DIR}"
echo "Done: ${EXPORT_PATH}"
echo "Next: python3 ${SCRIPT_DIR}/export_coco.py --source labelstudio --output-dir /root/autodl-tmp/sam3_lora_data/my_dataset"
C.10 scripts/labelstudio_server_import.sh
bash
#!/usr/bin/env bash
# =============================================================================
# labelstudio_server_import.sh --- 从云服务器本地路径导入 Label Studio 任务
# =============================================================================
#
# AutoDL 等云服务器上,浏览器无法直接访问 /root/... 路径,
# 不要用 Web UI 的 Import → Upload Files。
#
# 本脚本通过 REST API 读取服务器上的 JSON 并导入项目。
#
# 用法:
# export LS_TOKEN="<Access Token>" # LS 网页:Account & Settings → Access Token
# bash scripts/labelstudio_server_import.sh <project_id> [port]
#
# 示例:
# export LS_TOKEN="abc123..."
# bash scripts/labelstudio_server_import.sh 1 6006
#
# 导入文件(默认):
# /root/autodl-tmp/label/reviewed/labelstudio_import.json
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LABEL_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
IMPORT_JSON="${IMPORT_JSON:-${LABEL_ROOT}/reviewed/labelstudio_import.json}"
PROJECT_ID="${1:?用法: $0 <project_id> [port]}"
PORT="${2:-6006}"
LS_HOST="${LS_HOST:-127.0.0.1}"
if [[ -z "${LS_TOKEN:-}" ]]; then
echo "错误:请设置环境变量 LS_TOKEN(Label Studio → Account & Settings → Access Token)"
exit 1
fi
if [[ ! -f "${IMPORT_JSON}" ]]; then
echo "错误:找不到导入文件 ${IMPORT_JSON}"
echo "请先运行: python3 ${SCRIPT_DIR}/labelstudio_import.py"
exit 1
fi
echo "Importing ${IMPORT_JSON}"
echo " → project ${PROJECT_ID} @ http://${LS_HOST}:${PORT}"
response="$(curl -sf -X POST \
"http://${LS_HOST}:${PORT}/api/projects/${PROJECT_ID}/import?commit_to_project=true" \
-H "Authorization: Token ${LS_TOKEN}" \
-H "Content-Type: application/json" \
--data-binary @"${IMPORT_JSON}")"
echo "${response}"
echo "Done. Open Label Studio Data Manager to verify task count."
C.11 scripts/labelstudio_reset_import.sh
bash
#!/usr/bin/env bash
# 一键重置并重新导入(annotations 版,mask 直接可见可编辑)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ID="${1:-1}"
PORT="${LS_PORT:-6007}"
if [[ -z "${LS_TOKEN:-}" ]]; then
echo "请先: export LS_TOKEN=\"<你的 refresh token>\""
exit 1
fi
python3 "${SCRIPT_DIR}/labelstudio_import.py"
python3 "${SCRIPT_DIR}/labelstudio_push_import.py" "${PROJECT_ID}" --port "${PORT}" --replace
echo ""
echo "请在浏览器:"
echo " 1. Settings → Labeling Setup → 粘贴 reviewed/labelstudio_config.xml → Save"
echo " 2. 刷新 → 打开任务 → 应看到红色 mask"
echo " 3. 左侧点 target → Brush/Eraser → Submit"
C.12 scripts/export_coco.py
python
#!/usr/bin/env python3
"""
export_coco.py --- 导出 SAM3_LoRA 所需的 COCO 格式数据集
功能:
将 prelabels(或 Label Studio COCO 导出)转为 SAM3_LoRA 训练目录结构:
<output_dir>/
├── train/
│ ├── <episode>_<filename>.jpg
│ └── _annotations.coco.json
├── valid/
│ └── ...
└── dataset_meta.json
关键约定:
- categories[0].name = prompt.txt 内容(= SAM3 微调时的文本 query)
- segmentation 使用 RLE 格式(pycocotools 编码)
- bbox 格式 [x, y, w, h],从 mask 自动计算
- train/valid 按 episode_id 分组划分,避免同一视频帧泄漏
数据来源(--source):
prelabels 直接读 prelabels/static + prelabels/video
labelstudio 读 reviewed/labelstudio_export/(Label Studio CLI 导出为 zip,内含 COCO JSON)
auto 若存在 Label Studio 导出则优先 labelstudio,否则 prelabels
用法:
python3 export_coco.py --output-dir /root/autodl-tmp/sam3_lora_data/my_dataset
python3 export_coco.py --source prelabels --valid-ratio 0.2
下游:
SAM3_LoRA/train_sam3_lora_native.py
"""
from __future__ import annotations
import argparse
import io
import json
import shutil
import zipfile
from collections import defaultdict
from pathlib import Path
import numpy as np
from PIL import Image
from pycocotools import mask as mask_utils
LABEL_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_OUTPUT = Path("/root/autodl-tmp/sam3_lora_data/label_export")
def read_prompt(prompt_file: Path) -> str:
"""读取 prompt,作为 COCO categories[].name。"""
if prompt_file.exists():
text = prompt_file.read_text(encoding="utf-8").strip()
if text:
return text
return "object"
def mask_to_rle(mask: np.ndarray) -> dict:
"""二值 mask (H,W) → COCO RLE dict;counts 解码为 str 以便 JSON 序列化。"""
binary = (mask > 127).astype(np.uint8)
rle = mask_utils.encode(np.asfortranarray(binary))
if isinstance(rle["counts"], bytes):
rle["counts"] = rle["counts"].decode("utf-8")
return rle
def bbox_from_mask(mask: np.ndarray) -> list[float]:
"""从 mask 前景像素计算 COCO bbox [x, y, w, h]。"""
ys, xs = np.where(mask > 127)
if len(xs) == 0:
return [0.0, 0.0, 0.0, 0.0]
x_min, x_max = float(xs.min()), float(xs.max())
y_min, y_max = float(ys.min()), float(ys.max())
return [x_min, y_min, x_max - x_min + 1.0, y_max - y_min + 1.0]
def area_from_mask(mask: np.ndarray) -> float:
"""mask 前景像素数。"""
return float((mask > 127).sum())
def load_mask(path: Path) -> np.ndarray:
"""读取 PNG mask,统一为单通道灰度数组。"""
mask = np.array(Image.open(path))
if mask.ndim == 3:
mask = mask[:, :, 0]
return mask
def polygon_to_mask(segmentation: list, height: int, width: int) -> np.ndarray | None:
"""COCO polygon 列表 → 二值 mask(Label Studio 导出可能用 polygon)。"""
if not segmentation:
return None
rles = mask_utils.frPyObjects(segmentation, height, width)
if isinstance(rles, dict):
rles = [rles]
merged = mask_utils.merge(rles)
return mask_utils.decode(merged)
def segmentation_to_mask(segmentation, height: int, width: int) -> np.ndarray | None:
"""统一处理 RLE dict / polygon list 两种 COCO segmentation 格式。"""
if segmentation is None:
return None
if isinstance(segmentation, dict):
return mask_utils.decode(segmentation)
if isinstance(segmentation, list) and segmentation:
if isinstance(segmentation[0], list):
return polygon_to_mask(segmentation, height, width)
return None
def collect_from_prelabels_static(static_dir: Path) -> list[dict]:
"""
从 prelabels/static/seg_summary.json 收集样本。
跳过无 mask 的条目(如 prompt 不匹配导致 0 instance)。
"""
summary_path = static_dir / "seg_summary.json"
if not summary_path.exists():
return []
summary = json.loads(summary_path.read_text(encoding="utf-8"))
samples: list[dict] = []
for item in summary:
image_path = Path(item["image_path"])
mask_paths = [Path(p) for p in item.get("mask_paths", []) if Path(p).exists()]
if not image_path.exists() or not mask_paths:
continue
samples.append(
{
"source": "static",
"episode_id": item.get("episode_id", f"static_{image_path.stem}"),
"image_path": image_path,
"mask_paths": mask_paths,
}
)
return samples
def collect_from_prelabels_video(video_dir: Path, frames_dir: Path) -> list[dict]:
"""
从 prelabels/video/<episode>/tracking_summary.json 收集有效视频帧样本。
仅 has_target=True 的帧;每帧通常 1 个 mask。
"""
samples: list[dict] = []
if not video_dir.exists():
return samples
for episode_dir in sorted(video_dir.iterdir()):
if not episode_dir.is_dir():
continue
summary_path = episode_dir / "tracking_summary.json"
if not summary_path.exists():
continue
summary = json.loads(summary_path.read_text(encoding="utf-8"))
episode_id = str(summary.get("episode_id", episode_dir.name))
frames_source = Path(summary.get("frames_dir", frames_dir / f"sam3_{episode_id}"))
masks_dir = episode_dir / "masks"
for frame_info in summary.get("per_frame", []):
if not frame_info.get("has_target"):
continue
frame_path = frames_source / frame_info["frame_name"]
mask_name = frame_info.get("output_mask", "")
mask_path = masks_dir / mask_name
if not mask_path.exists():
mask_path = episode_dir / mask_name
if not frame_path.exists() or not mask_path.exists():
continue
samples.append(
{
"source": "video",
"episode_id": episode_id,
"image_path": frame_path,
"mask_paths": [mask_path],
}
)
return samples
def load_coco_json_from_export(path: Path) -> dict | None:
"""
从 Label Studio 导出文件加载 COCO JSON。
Label Studio CLI 的 COCO / BRUSH_TO_COCO 导出实际是 zip(即使 --export-path 以 .json 结尾)。
zip 内常见:result_coco.json(BRUSH_TO_COCO)或 result.json(COCO)。
"""
try:
raw = path.read_bytes()
except OSError:
return None
if not raw:
return None
if raw[:2] == b"PK":
try:
with zipfile.ZipFile(io.BytesIO(raw)) as zf:
for member in ("result_coco.json", "result.json"):
if member in zf.namelist():
return json.loads(zf.read(member))
for member in sorted(zf.namelist()):
if member.endswith(".json"):
data = json.loads(zf.read(member))
if isinstance(data, dict) and "images" in data:
return data
except (zipfile.BadZipFile, json.JSONDecodeError, KeyError, UnicodeDecodeError):
return None
return None
try:
return json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
return None
def is_coco_export(data: dict | None) -> bool:
return isinstance(data, dict) and "images" in data and "annotations" in data
def resolve_labelstudio_image_path(
file_name: str, media_dir: Path, coco_path: Path
) -> Path | None:
"""解析 Label Studio COCO 导出中的 file_name → 本地图片路径。"""
name = file_name.replace("\\", "/")
if "labelstudio_media/" in name:
rel = name.split("labelstudio_media/", 1)[-1]
candidate = media_dir / rel
if candidate.exists():
return candidate
basename = Path(name).name
candidates = [
media_dir / name,
media_dir / basename,
media_dir / "static" / basename,
]
if media_dir.exists():
for sub in sorted(media_dir.iterdir()):
if sub.is_dir():
candidates.append(sub / basename)
for candidate in candidates:
if candidate.exists():
return candidate
if media_dir.exists():
matches = list(media_dir.rglob(basename))
if matches:
return matches[0]
images_dir = coco_path.parent / "images"
candidate = images_dir / basename
if candidate.exists():
return candidate
return None
def find_labelstudio_coco(reviewed_dir: Path) -> Path | None:
"""
在 reviewed/ 下查找 Label Studio 导出的 COCO JSON。
搜索顺序:
1. reviewed/labelstudio_export/*.{json,zip}
2. reviewed/*coco*.{json,zip}
优先返回 annotations 最多的文件(COCO 对 Brush 项目常为空,BRUSH_TO_COCO 才有 mask)。
"""
export_dir = reviewed_dir / "labelstudio_export"
candidates: list[Path] = []
if export_dir.exists():
candidates.extend(sorted(export_dir.glob("*.json")))
candidates.extend(sorted(export_dir.glob("*.zip")))
candidates.extend(sorted(reviewed_dir.glob("*coco*.json")))
candidates.extend(sorted(reviewed_dir.glob("*coco*.zip")))
best: Path | None = None
best_anns = -1
for path in candidates:
data = load_coco_json_from_export(path)
if not is_coco_export(data):
continue
ann_count = len(data.get("annotations", []))
if ann_count > best_anns:
best = path
best_anns = ann_count
return best
def collect_from_labelstudio(coco_path: Path, media_dir: Path) -> list[dict]:
"""
从 Label Studio COCO 导出解析样本。
将 polygon/RLE segmentation 转为临时 PNG mask,
再统一走 write_split 的 RLE 导出流程。
"""
coco = load_coco_json_from_export(coco_path)
if not is_coco_export(coco):
return []
images_by_id = {img["id"]: img for img in coco.get("images", [])}
anns_by_image: dict[int, list[dict]] = defaultdict(list)
for ann in coco.get("annotations", []):
anns_by_image[ann["image_id"]].append(ann)
samples: list[dict] = []
for image_id, anns in anns_by_image.items():
img_info = images_by_id.get(image_id)
if not img_info:
continue
file_name = img_info["file_name"]
image_path = resolve_labelstudio_image_path(file_name, media_dir, coco_path)
if image_path is None:
continue
height = img_info.get("height")
width = img_info.get("width")
if height is None or width is None:
with Image.open(image_path) as im:
width, height = im.size
mask_paths: list[Path] = []
tmp_dir = coco_path.parent / "_export_masks"
tmp_dir.mkdir(exist_ok=True)
for i, ann in enumerate(anns):
mask_arr = segmentation_to_mask(ann.get("segmentation"), height, width)
if mask_arr is None or mask_arr.sum() == 0:
continue
mask_path = tmp_dir / f"{image_id}_{i}.png"
Image.fromarray((mask_arr * 255).astype(np.uint8)).save(mask_path)
mask_paths.append(mask_path)
if not mask_paths:
continue
episode_id = img_info.get(
"episode_id",
Path(file_name).parts[0] if "/" in file_name else "labelstudio",
)
samples.append(
{
"source": "labelstudio",
"episode_id": str(episode_id),
"image_path": image_path,
"mask_paths": mask_paths,
}
)
return samples
def split_by_episode(
samples: list[dict],
valid_ratio: float,
seed: int,
) -> tuple[list[dict], list[dict]]:
"""
按 episode_id 分组划分 train/valid,防止同一视频的不同帧同时出现在两个 split。
若只有 1 个 episode,则退化为按样本随机划分(仍保证 train 非空)。
"""
by_episode: dict[str, list[dict]] = defaultdict(list)
for sample in samples:
by_episode[sample["episode_id"]].append(sample)
episode_ids = sorted(by_episode.keys())
# 单 episode 或小数据集:按样本级随机划分
if len(episode_ids) <= 1:
rng = np.random.default_rng(seed)
indices = np.arange(len(samples))
rng.shuffle(indices)
if len(samples) <= 1:
return samples, []
valid_count = max(1, int(round(len(samples) * valid_ratio)))
valid_count = min(valid_count, len(samples) - 1)
valid_idx = set(indices[:valid_count].tolist())
train = [samples[i] for i in range(len(samples)) if i not in valid_idx]
valid = [samples[i] for i in range(len(samples)) if i in valid_idx]
return train, valid
# 多 episode:整组 episode 划入 train 或 valid
rng = np.random.default_rng(seed)
shuffled = episode_ids.copy()
rng.shuffle(shuffled)
valid_ep_count = max(1, int(round(len(shuffled) * valid_ratio)))
valid_ep_count = min(valid_ep_count, len(shuffled) - 1)
valid_eps = set(shuffled[:valid_ep_count])
train, valid = [], []
for ep in episode_ids:
bucket = valid if ep in valid_eps else train
bucket.extend(by_episode[ep])
return train, valid
def write_split(
split_name: str,
samples: list[dict],
output_root: Path,
category_name: str,
) -> dict:
"""
将样本列表写入一个 split 目录,生成 COCO JSON。
图片复制为 <episode_id>_<原文件名>,避免不同 episode 同名帧冲突。
每张图可有多条 annotation(多实例)。
"""
split_dir = output_root / split_name
split_dir.mkdir(parents=True, exist_ok=True)
coco = {
"images": [],
"annotations": [],
"categories": [{"id": 1, "name": category_name, "supercategory": "object"}],
}
image_id = 1
ann_id = 1
for sample in samples:
src_image = Path(sample["image_path"])
with Image.open(src_image) as im:
width, height = im.size
dst_name = f"{sample['episode_id']}_{src_image.name}"
dst_path = split_dir / dst_name
if src_image.resolve() != dst_path.resolve():
shutil.copy2(src_image, dst_path)
coco["images"].append(
{
"id": image_id,
"file_name": dst_name,
"width": width,
"height": height,
}
)
for mask_path in sample["mask_paths"]:
mask = load_mask(mask_path)
area = area_from_mask(mask)
if area <= 0:
continue
coco["annotations"].append(
{
"id": ann_id,
"image_id": image_id,
"category_id": 1,
"bbox": bbox_from_mask(mask),
"area": area,
"segmentation": mask_to_rle(mask),
"iscrowd": 0,
}
)
ann_id += 1
image_id += 1
ann_path = split_dir / "_annotations.coco.json"
ann_path.write_text(json.dumps(coco, indent=2), encoding="utf-8")
return {
"split": split_name,
"images": len(coco["images"]),
"annotations": len(coco["annotations"]),
"annotation_file": str(ann_path),
}
def main() -> None:
parser = argparse.ArgumentParser(
description="Export COCO dataset for SAM3_LoRA",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--label-root",
type=Path,
default=LABEL_ROOT,
help="label 项目根目录",
)
parser.add_argument(
"--output-dir",
type=Path,
default=DEFAULT_OUTPUT,
help="COCO 数据集输出根目录",
)
parser.add_argument(
"--source",
choices=["prelabels", "labelstudio", "auto"],
default="auto",
help="auto: 有 Label Studio 导出则优先,否则 prelabels",
)
parser.add_argument(
"--prompt-file",
type=Path,
default=LABEL_ROOT / "prompt.txt",
help="category name 来源(= SAM3 text prompt)",
)
parser.add_argument(
"--valid-ratio",
type=float,
default=0.15,
help="验证集比例(按 episode 分组)",
)
parser.add_argument("--seed", type=int, default=42, help="随机划分种子")
args = parser.parse_args()
label_root = args.label_root
category_name = read_prompt(args.prompt_file)
samples: list[dict] = []
ls_coco = find_labelstudio_coco(label_root / "reviewed")
use_labelstudio = args.source == "labelstudio" or (
args.source == "auto" and ls_coco is not None
)
if use_labelstudio:
if ls_coco is None:
raise SystemExit(
"No Label Studio COCO export found under reviewed/.\n"
" Run: bash scripts/labelstudio_server_export.sh <project_id>\n"
" Or use: --source prelabels"
)
media_dir = label_root / "reviewed" / "labelstudio_media"
samples = collect_from_labelstudio(ls_coco, media_dir)
if not samples:
coco_data = load_coco_json_from_export(ls_coco)
ann_n = len(coco_data.get("annotations", [])) if coco_data else 0
raise SystemExit(
f"Label Studio export has 0 usable samples (annotations in file: {ann_n}).\n"
" Brush 项目请用 BRUSH_TO_COCO 导出:bash scripts/labelstudio_server_export.sh <project_id>\n"
" 或跳过 LS:--source prelabels"
)
source_note = f"labelstudio:{ls_coco.name}"
else:
samples.extend(
collect_from_prelabels_static(label_root / "prelabels" / "static")
)
samples.extend(
collect_from_prelabels_video(
label_root / "prelabels" / "video",
label_root / "frames",
)
)
source_note = "prelabels"
if not samples:
raise SystemExit("No samples found. Run run_prelabel.sh first or export from Label Studio.")
train_samples, valid_samples = split_by_episode(samples, args.valid_ratio, args.seed)
args.output_dir.mkdir(parents=True, exist_ok=True)
episodes = sorted({s["episode_id"] for s in samples})
meta = {
"prompt": category_name,
"category_name": category_name,
"source": source_note,
"total_samples": len(samples),
"episodes": episodes,
"splits": [
write_split("train", train_samples, args.output_dir, category_name),
write_split("valid", valid_samples, args.output_dir, category_name),
],
}
meta_path = args.output_dir / "dataset_meta.json"
meta_path.write_text(json.dumps(meta, indent=2), encoding="utf-8")
print(json.dumps(meta, indent=2, ensure_ascii=False))
print(f"\nDataset ready at: {args.output_dir}")
if __name__ == "__main__":
main()
C.13 scripts/smoke_pipeline.sh
bash
#!/usr/bin/env bash
# =============================================================================
# smoke_pipeline.sh --- 端到端冒烟测试
# =============================================================================
#
# 功能:
# 无需自备数据,自动从 sam3/assets/test/ 复制样本,跑通完整流水线:
# 准备数据 → SAM3 预标注 → COCO 导出 → SAM3_LoRA 训练 1 epoch
#
# 用法:
# bash scripts/smoke_pipeline.sh
#
# 测试数据:
# - 10 张静态图(SAM3 测试集 4 张 + wyw 2 张 + 4 张 dup 副本)
# - 30 帧视频(来自 VIDEO/2026-07-02 191302_frames)
#
# 输出:
# 数据集:/root/autodl-tmp/sam3_lora_data/label_smoke/
# 配置: SAM3_LoRA/configs/label_smoke.yaml
# 权重: SAM3_LoRA/outputs/label_smoke/best_lora_weights.pt
# 日志: label/smoke_pipeline.log(若用 tee 重定向)
#
# 依赖:GPU、SAM3、SAM3_LoRA、decord(训练时自动 pip install)
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LABEL_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
TEST_ROOT="/root/autodl-tmp/sam3/assets/test" # 内置测试物料
SAM3_LORA="/root/autodl-tmp/SAM3_LoRA"
DATASET_NAME="label_smoke"
OUTPUT_DATA="/root/autodl-tmp/sam3_lora_data/${DATASET_NAME}"
SMOKE_CONFIG="${SAM3_LORA}/configs/${DATASET_NAME}.yaml"
# ---------------------------------------------------------------------------
# [1/5] 准备静态图:复制到 raw/images/
# ---------------------------------------------------------------------------
echo "=== [1/5] Prepare smoke raw data ==="
mkdir -p "${LABEL_ROOT}/raw/images" "${LABEL_ROOT}/frames"
# SAM3 静态测试图 4 张
for img in IMG_8086 IMG_8087 IMG_8088 IMG_8089; do
cp -n "${TEST_ROOT}/SAM3/${img}.jpg" "${LABEL_ROOT}/raw/images/" 2>/dev/null || true
done
# wyw 子目录 2 张(prompt 不匹配时可能 0 instance,属预期)
for img in IMG_8098 IMG_8099; do
cp -n "${TEST_ROOT}/wyw/${img}.jpg" "${LABEL_ROOT}/raw/images/" 2>/dev/null || true
done
# 再复制 4 张 dup 变体,凑够 10 张独立文件名
i=1
for img in IMG_8086 IMG_8087 IMG_8088 IMG_8089; do
cp -n "${TEST_ROOT}/SAM3/${img}.jpg" "${LABEL_ROOT}/raw/images/${img}_dup${i}.jpg" 2>/dev/null || true
i=$((i + 1))
done
echo " static images: $(find "${LABEL_ROOT}/raw/images" -maxdepth 1 -type f | wc -l)"
# ---------------------------------------------------------------------------
# [2/5] 准备视频帧:复制 30 帧到 frames/sam3_001/
# 注意:源目录名含空格,必须用 find -print0 安全处理
# ---------------------------------------------------------------------------
echo "=== [2/5] Prepare smoke video frames (30 frames) ==="
SMOKE_FRAMES="${LABEL_ROOT}/frames/sam3_001"
rm -rf "${SMOKE_FRAMES}"
mkdir -p "${SMOKE_FRAMES}"
SRC_FRAMES="${TEST_ROOT}/VIDEO/2026-07-02 191302_frames"
count=0
if [[ -d "${SRC_FRAMES}" ]]; then
while IFS= read -r -d '' f; do
cp "${f}" "${SMOKE_FRAMES}/${count}.jpg" # 重命名为 0.jpg, 1.jpg, ...
count=$((count + 1))
[[ "${count}" -ge 30 ]] && break
done < <(find "${SRC_FRAMES}" -maxdepth 1 -name '*.jpg' -print0 | sort -z)
fi
echo " video frames: ${count}"
# ---------------------------------------------------------------------------
# [3/5] SAM3 预标注
# ---------------------------------------------------------------------------
echo "=== [3/5] SAM3 pre-label ==="
bash "${SCRIPT_DIR}/run_prelabel.sh" all
# ---------------------------------------------------------------------------
# [4/5] 导出 COCO(跳过 Label Studio,直接用 prelabels)
# ---------------------------------------------------------------------------
echo "=== [4/5] Export COCO ==="
python3 "${SCRIPT_DIR}/export_coco.py" \
--source prelabels \
--output-dir "${OUTPUT_DATA}"
# ---------------------------------------------------------------------------
# [5/5] SAM3_LoRA 训练 1 epoch
# ---------------------------------------------------------------------------
echo "=== [5/5] SAM3_LoRA smoke train (1 epoch) ==="
# SAM3_LoRA 训练依赖 decord 读取视频相关模块,冒烟时按需安装
python3 -c "import decord" 2>/dev/null || pip install decord -q
# 生成最小 LoRA 训练配置(1 epoch 仅验证管线可通)
cat > "${SMOKE_CONFIG}" <<EOF
model:
name: "facebook/sam3"
checkpoint_path: "/root/autodl-tmp/weights/sam3_ms/sam3.pt"
load_from_HF: false
cache_dir: null
lora:
rank: 8
alpha: 16
dropout: 0.05
target_modules:
- "q_proj"
- "k_proj"
- "v_proj"
- "out_proj"
- "linear1"
- "linear2"
apply_to_vision_encoder: false
apply_to_text_encoder: false
apply_to_geometry_encoder: false
apply_to_detr_encoder: true
apply_to_detr_decoder: true
apply_to_mask_decoder: true
training:
data_dir: "${OUTPUT_DATA}"
num_negatives: 1
batch_size: 1
num_workers: 2
learning_rate: 1.0e-4
weight_decay: 0.01
adam_beta1: 0.9
adam_beta2: 0.999
adam_epsilon: 1.0e-8
max_grad_norm: 1.0
num_epochs: 1
warmup_steps: 5
lr_scheduler: "cosine"
logging_steps: 5
eval_steps: 50
save_steps: 999999
save_total_limit: 1
mixed_precision: "bf16"
seed: 42
gradient_accumulation_steps: 1
output:
output_dir: "outputs/${DATASET_NAME}"
logging_dir: "logs/${DATASET_NAME}"
save_lora_only: true
push_to_hub: false
hub_model_id: null
evaluation:
metric: "iou"
save_predictions: false
compute_metrics_during_training: true
hardware:
device: "cuda"
dataloader_pin_memory: true
use_compile: false
EOF
cd "${SAM3_LORA}"
python3 train_sam3_lora_native.py --config "${SMOKE_CONFIG}"
echo ""
echo "Smoke pipeline complete."
echo " Dataset: ${OUTPUT_DATA}"
echo " Config: ${SMOKE_CONFIG}"
echo " LoRA: ${SAM3_LORA}/outputs/${DATASET_NAME}/"
C.14 scripts/labelstudio_config.xml
Label Studio Labeling Setup 界面配置;labelstudio_reload.sh 会同步写入项目 DB,并复制到 reviewed/labelstudio_config.xml。
xml
<!--
SAM3 标注界面:mask 以 annotations 导入,打开任务即可看到红色区域。
操作:① 点 Edit 进入编辑 → ② 左侧点 target → ③ 底部 Brush/Eraser → ④ Submit
-->
<View>
<Header value="Step1: Edit | Step2: click target | Step3: Brush/Eraser | Step4: Submit"/>
<Text name="meta" value="Episode: $episode_id | Type: $data_source | SAM3 instances: $instances"/>
<Image name="image" value="$image" zoom="true" zoomControl="true"/>
<BrushLabels name="brush" toName="image">
<Label value="target" background="rgba(255, 0, 0, 0.6)"/>
</BrushLabels>
</View>
附录 D:配置与生成文件说明
D.1 prompt.txt(项目根)
| 项 | 说明 |
|---|---|
| 格式 | 单行英文描述,如 black rectangular block with a hole |
| 作用 | SAM3 推理文本 prompt;导出 COCO 时写入 categories[0].name |
| 约束 | 必须与微调/推理时使用的 prompt 完全一致 (见 <prompt_guide.md>) |
| 谁读 | prelabel_*.py、export_coco.py |
D.2 scripts/labelstudio_config.xml → reviewed/labelstudio_config.xml
Label Studio Labeling Setup 界面定义(XML)。完整源码见 [附录 C.14](#完整源码见 附录 C.14)。
| 标签 | name | 说明 |
|---|---|---|
Header |
--- | 顶部操作提示 |
Text |
meta |
显示 $episode_id、$data_source、$instances |
Image |
image |
绑定任务字段 $image(Local Files URL) |
BrushLabels |
brush |
画笔标签;toName="image" |
Label |
--- | 标签名 target (与 import JSON 中 brushlabels 一致) |
注意:
- 勿用
$source(LS 保留变量) reload.sh会写入项目 DB;改 XML 后需 reload 或手动 Save
D.3 reviewed/.ls_token
| 项 | 说明 |
|---|---|
| 内容 | 一行 JWT refresh token(Account & Settings → Access Token) |
| 权限 | chmod 600;已在 .gitignore,勿提交 git |
| 谁读 | labelstudio_reload.sh、labelstudio_push_import.py(自动 refresh → access) |
D.4 reviewed/labelstudio_import.json
API 导入用的任务数组。每条任务结构:
json
{
"data": {
"image": "/data/local-files/?d=labelstudio_media/static/IMG_8086.jpg",
"episode_id": "static_IMG_8086",
"data_source": "static",
"prelabel_masks": ["..."],
"instances": 1
},
"annotations": [{
"ground_truth": false,
"result": [{
"id": "a1b2c3d4",
"type": "brushlabels",
"from_name": "brush",
"to_name": "image",
"original_width": 3024,
"original_height": 4032,
"value": {
"format": "rle",
"rle": [2, 232, 48, ...],
"brushlabels": ["target"]
}
}]
}]
}
| 字段 | 含义 |
|---|---|
data.image |
原图 URL,相对 DOCUMENT_ROOT=reviewed/ |
data_source |
static 或 video |
annotations |
SAM3 mask(非 predictions) |
value.rle |
整数数组,Label Studio Brush 专用 RLE |
D.5 reviewed/labelstudio_media/
| 项 | 说明 |
|---|---|
| 作用 | Label Studio Local Files 实际读盘的目录 |
| Cloud Storage 配置 | Absolute path = 本目录绝对路径 |
| 结构 | static/ 静态图;static/masks/ mask;video/<episode>/ 视频帧 |
| DOCUMENT_ROOT | 父目录 reviewed/(由 start_labelstudio.sh 设置) |
D.6 .gitignore
reviewed/.ls_token
.venv-labelstudio/
防止 token 与大型 venv 误入版本库。
D.7 预标注汇总 JSON
| 文件 | 字段要点 |
|---|---|
prelabels/static/seg_summary.json |
image_path, mask_paths[], instances, episode_id |
prelabels/video/<id>/tracking_summary.json |
episode_id, frames_dir, per_frame[].has_target, output_mask |
D.8 export_coco.py 输出结构
sam3_lora_data/<name>/
├── train/
│ ├── <episode>_<file>.jpg
│ └── _annotations.coco.json # COCO instances + RLE segmentation
├── valid/
│ └── ...
└── dataset_meta.json # prompt、episode 列表、划分统计
附录 E:环境变量与 Secret 文件
| 变量 / 文件 | 默认值 | 作用 | 使用脚本 |
|---|---|---|---|
LS_TOKEN |
--- | Label Studio API 认证 | push_import, reload, server_import |
LS_PORT |
6006(reload 默认;AutoDL 6006/6007 为 TensorBoard) | LS 端口 | push_import, reload |
LS_HOST |
127.0.0.1 |
LS 主机 | push_import, reload |
PROMPT_FILE |
prompt.txt |
覆盖 prompt 路径 | run_prelabel.sh |
CONFIDENCE |
0.4 |
SAM3 置信度阈值 | run_prelabel.sh |
CHECKPOINT |
.../sam3.pt |
SAM3 权重 | run_prelabel.sh |
IMPORT_JSON |
reviewed/labelstudio_import.json |
自定义导入路径 | server_import |
LABEL_STUDIO_DATA_DIR |
~/.label-studio |
LS 数据目录 | server_export |
reviewed/.ls_token |
--- | 持久化 refresh token | reload |
Label Studio 进程环境(由 start_labelstudio.sh 设置):
| 变量 | 值 |
|---|---|
LABEL_STUDIO_LOCAL_FILES_SERVING_ENABLED |
true |
LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT |
/root/autodl-tmp/label/reviewed |
附录 F:外部关联路径
| 路径 | 说明 |
|---|---|
/root/autodl-tmp/sam3 |
SAM3 源码,pip install -e |
/root/autodl-tmp/weights/sam3_ms/sam3.pt |
默认 checkpoint |
/root/autodl-tmp/SAM3_LoRA |
LoRA 训练框架;configs、outputs |
/root/autodl-tmp/sam3_lora_data/ |
COCO 数据集默认输出根 |
~/.label-studio/label_studio.sqlite3 |
LS 项目、任务、标注 DB |
~/.label-studio/.env |
LS 持久化 SECRET_KEY、DOCUMENT_ROOT |
SAM3_LoRA 训练配置示例(非本仓库文件,但流水线终点):
yaml
# SAM3_LoRA/configs/my_dataset.yaml
training:
data_dir: "/root/autodl-tmp/sam3_lora_data/my_dataset"
num_epochs: 20
batch_size: 1
learning_rate: 1.0e-4
文档维护 :新增或修改 scripts/ 下文件时,请同步更新本附录与 README.md。