Compare commits
47 Commits
1daff5eb92
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 53e8fbb4dd | |||
| f7c73fa57e | |||
| bad6bfa34b | |||
| 054e720e39 | |||
| f8e94328a7 | |||
| aee6f8804f | |||
| 765a0aebdc | |||
| dc5a02f4ec | |||
| 4f6d7d9035 | |||
| 4667021944 | |||
| 06f2b2928b | |||
| 2d315948a2 | |||
| 34cbeb69c3 | |||
| 0ab6f52525 | |||
|
|
f6c361dd13 | ||
|
|
a4475e743d | ||
|
|
4659802f7a | ||
|
|
c3f7e93563 | ||
| 0f72cf7917 | |||
| b13f6df90e | |||
| d9db1b76db | |||
| 8ee3318ad7 | |||
| a5c5071529 | |||
| 99968f25ae | |||
| 9e6e9f98b6 | |||
| 753badd0f8 | |||
| b83a19e9c2 | |||
| 08a63807f3 | |||
| 18d207b014 | |||
| fb05cd8c71 | |||
| 882989f252 | |||
| f981a05b32 | |||
| 0cb2470e55 | |||
| 87c8006aa9 | |||
| 6dc5d17a43 | |||
| 8065619f5e | |||
| 3b5461371c | |||
| 8bb00ac928 | |||
|
|
2ec3c0711a | ||
|
|
99d02f28c8 | ||
|
|
11dec2936d | ||
|
|
7b89b8fc3f | ||
|
|
5eb25fb54b | ||
|
|
962998a167 | ||
|
|
b26a5f330e | ||
|
|
757bbb0206 | ||
|
|
2d1cbaeac7 |
480
README.md
480
README.md
@@ -1,69 +1,50 @@
|
||||
# SAM 3: Segment Anything with Concepts
|
||||
# 量迹AI · SAM3「分割一切」视觉分割服务
|
||||
|
||||
Meta Superintelligence Labs
|
||||
|
||||
[Nicolas Carion](https://www.nicolascarion.com/)\*,
|
||||
[Laura Gustafson](https://scholar.google.com/citations?user=c8IpF9gAAAAJ&hl=en)\*,
|
||||
[Yuan-Ting Hu](https://scholar.google.com/citations?user=E8DVVYQAAAAJ&hl=en)\*,
|
||||
[Shoubhik Debnath](https://scholar.google.com/citations?user=fb6FOfsAAAAJ&hl=en)\*,
|
||||
[Ronghang Hu](https://ronghanghu.com/)\*,
|
||||
[Didac Suris](https://www.didacsuris.com/)\*,
|
||||
[Chaitanya Ryali](https://scholar.google.com/citations?user=4LWx24UAAAAJ&hl=en)\*,
|
||||
[Kalyan Vasudev Alwala](https://scholar.google.co.in/citations?user=m34oaWEAAAAJ&hl=en)\*,
|
||||
[Haitham Khedr](https://hkhedr.com/)\*, Andrew Huang,
|
||||
[Jie Lei](https://jayleicn.github.io/),
|
||||
[Tengyu Ma](https://scholar.google.com/citations?user=VeTSl0wAAAAJ&hl=en),
|
||||
[Baishan Guo](https://scholar.google.com/citations?user=BC5wDu8AAAAJ&hl=en),
|
||||
Arpit Kalla, [Markus Marks](https://damaggu.github.io/),
|
||||
[Joseph Greer](https://scholar.google.com/citations?user=guL96CkAAAAJ&hl=en),
|
||||
Meng Wang, [Peize Sun](https://peizesun.github.io/),
|
||||
[Roman Rädle](https://scholar.google.com/citations?user=Tpt57v0AAAAJ&hl=en),
|
||||
[Triantafyllos Afouras](https://www.robots.ox.ac.uk/~afourast/),
|
||||
[Effrosyni Mavroudi](https://scholar.google.com/citations?user=vYRzGGEAAAAJ&hl=en),
|
||||
[Katherine Xu](https://k8xu.github.io/)°,
|
||||
[Tsung-Han Wu](https://patrickthwu.com/)°,
|
||||
[Yu Zhou](https://yu-bryan-zhou.github.io/)°,
|
||||
[Liliane Momeni](https://scholar.google.com/citations?user=Lb-KgVYAAAAJ&hl=en)°,
|
||||
[Rishi Hazra](https://rishihazra.github.io/)°,
|
||||
[Shuangrui Ding](https://mark12ding.github.io/)°,
|
||||
[Sagar Vaze](https://sgvaze.github.io/)°,
|
||||
[Francois Porcher](https://scholar.google.com/citations?user=LgHZ8hUAAAAJ&hl=en)°,
|
||||
[Feng Li](https://fengli-ust.github.io/)°,
|
||||
[Siyuan Li](https://siyuanliii.github.io/)°,
|
||||
[Aishwarya Kamath](https://ashkamath.github.io/)°,
|
||||
[Ho Kei Cheng](https://hkchengrex.com/)°,
|
||||
[Piotr Dollar](https://pdollar.github.io/)†,
|
||||
[Nikhila Ravi](https://nikhilaravi.com/)†,
|
||||
[Kate Saenko](https://ai.bu.edu/ksaenko.html)†,
|
||||
[Pengchuan Zhang](https://pzzhang.github.io/pzzhang/)†,
|
||||
[Christoph Feichtenhofer](https://feichtenhofer.github.io/)†
|
||||
# Admin Config
|
||||
ADMIN_PASSWORD = "admin_secure_password" # 可以根据需求修改
|
||||
HISTORY_FILE = "history.json"
|
||||
|
||||
\* core contributor, ° intern, † project lead, order is random within groups
|
||||
|
||||
[[`Paper`](https://ai.meta.com/research/publications/sam-3-segment-anything-with-concepts/)]
|
||||
[[`Project`](https://ai.meta.com/sam3)]
|
||||
[[`Demo`](https://segment-anything.com/)]
|
||||
[[`Blog`](https://ai.meta.com/blog/segment-anything-model-3/)]
|
||||
[[`BibTeX`](#citing-sam-3)]
|
||||
本项目在开源 SAM3(Segment Anything Model 3)能力之上,封装了面向业务的 **“分割一切”** 推理服务:通过 **FastAPI** 提供文本提示词驱动的图像分割接口,并扩展了 **塔罗牌分割/识别**、**人脸与头发分割 + 属性分析** 等场景能力。
|
||||
|
||||
 SAM 3 is a unified foundation model for promptable segmentation in images and videos. It can detect, segment, and track objects using text or visual prompts such as points, boxes, and masks. Compared to its predecessor [SAM 2](https://github.com/facebookresearch/sam2), SAM 3 introduces the ability to exhaustively segment all instances of an open-vocabulary concept specified by a short text phrase or exemplars. Unlike prior work, SAM 3 can handle a vastly larger set of open-vocabulary prompts. It achieves 75-80% of human performance on our new [SA-CO benchmark](https://github.com/facebookresearch/sam3?tab=readme-ov-file#sa-co-dataset) which contains 270K unique concepts, over 50 times more than existing benchmarks.
|
||||
本仓库定位为:**模型推理 + API 服务** 的可复用工程模板(适合在 MacOS 开发、服务器部署)。
|
||||
|
||||
This breakthrough is driven by an innovative data engine that has automatically annotated over 4 million unique concepts, creating the largest high-quality open-vocabulary segmentation dataset to date. In addition, SAM 3 introduces a new model architecture featuring a presence token that improves discrimination between closely related text prompts (e.g., “a player in white” vs. “a player in red”), as well as a decoupled detector–tracker design that minimizes task interference and scales efficiently with data.
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/dog.gif" width=380 />
|
||||
<img src="assets/player.gif" width=380 />
|
||||
</p>
|
||||
## 你能用它做什么
|
||||
|
||||
## Installation
|
||||
- 通用分割:输入图片 + 文本提示词(中文或英文),返回分割可视化结果图 URL
|
||||
- 分割子图导出:可选返回每个目标的独立裁剪图(支持透明背景抠图)
|
||||
- 塔罗牌:检测指定数量的塔罗牌 → 透视矫正裁剪 →(可选)调用多模态大模型识别牌名与正逆位
|
||||
- 人脸分析:分割人脸/头发区域 → 裁剪保存 →(可选)调用多模态大模型预测性别/年龄等属性
|
||||
|
||||
### Prerequisites
|
||||
对应实现文件:
|
||||
- API 服务入口:[fastAPI_tarot.py](fastAPI_tarot.py)
|
||||
- 人脸分析流程:[human_analysis_service.py](human_analysis_service.py)
|
||||
|
||||
- Python 3.12 or higher
|
||||
- PyTorch 2.7 or higher
|
||||
- CUDA-compatible GPU with CUDA 12.6 or higher
|
||||
---
|
||||
|
||||
1. **Create a new Conda environment:**
|
||||
## 项目结构(关键目录)
|
||||
|
||||
- sam3/:SAM3 模型核心代码(本仓库已内置,不依赖外部子模块)
|
||||
- static/results/:推理结果落盘目录(接口返回的 URL 指向这里)
|
||||
- assets/:示例素材、演示图片/视频
|
||||
- fastAPI_tarot.py:主要 API 服务(含鉴权、模型加载、推理、可视化与落盘)
|
||||
|
||||
---
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Python:建议 3.10+(SAM3 作为库最低 3.8+)
|
||||
- PyTorch:建议按你的设备(CPU/CUDA)安装匹配版本
|
||||
- 依赖:FastAPI/uvicorn 等(见 requirement.txt,另有 sam3 依赖见 pyproject.toml)
|
||||
|
||||
---
|
||||
|
||||
## 安装与准备
|
||||
|
||||
### 0) 创建虚拟环境(推荐)
|
||||
|
||||
```bash
|
||||
conda create -n sam3 python=3.12
|
||||
@@ -71,325 +52,178 @@ conda deactivate
|
||||
conda activate sam3
|
||||
```
|
||||
|
||||
2. **Install PyTorch with CUDA support:**
|
||||
### 0.1) 安装 PyTorch(按你的设备选择)
|
||||
|
||||
GPU(CUDA)环境请安装与你 CUDA 版本匹配的 PyTorch;CPU 环境可直接安装 CPU 版。
|
||||
|
||||
```bash
|
||||
pip install torch==2.7.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
|
||||
pip install torch torchvision torchaudio
|
||||
```
|
||||
|
||||
3. **Clone the repository and install the package:**
|
||||
### 1) 安装本仓库与依赖
|
||||
|
||||
```bash
|
||||
git clone https://github.com/facebookresearch/sam3.git
|
||||
cd sam3
|
||||
pip install -e .
|
||||
pip install -r requirement.txt
|
||||
```
|
||||
|
||||
4. **Install additional dependencies for example notebooks or development:**
|
||||
如果需要接口用到的第三方库(如 OpenCV、matplotlib、Pillow、requests、dashscope 等),请按你的实际场景安装(仓库中部分功能会依赖它们)。
|
||||
|
||||
### 2) 下载权重模型(必须)
|
||||
|
||||
> 说明:模型文件较大,建议预留充足磁盘空间。
|
||||
>
|
||||
> 默认将权重保存到 `./dir/`,可按需修改 `--local_dir`。
|
||||
>
|
||||
> 如需通过 Hugging Face 下载,也可以自行改用 `huggingface_hub` 的下载方式。
|
||||
|
||||
1.1 **下载权重模型:**
|
||||
|
||||
```bash
|
||||
# For running example notebooks
|
||||
pip install -e ".[notebooks]"
|
||||
|
||||
# For development
|
||||
pip install -e ".[train,dev]"
|
||||
pip install modelscope
|
||||
modelscope download --model facebook/sam3 sam3.pt --local_dir ./dir
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
下载后会得到 `./dir/sam3.pt`。
|
||||
|
||||
⚠️ Before using SAM 3, please request access to the checkpoints on the SAM 3
|
||||
Hugging Face [repo](https://huggingface.co/facebook/sam3). Once accepted, you
|
||||
need to be authenticated to download the checkpoints. You can do this by running
|
||||
the following [steps](https://huggingface.co/docs/huggingface_hub/en/quick-start#authentication)
|
||||
(e.g. `hf auth login` after generating an access token.)
|
||||
### 3) 配置 SAM3 权重路径(必须)
|
||||
|
||||
### Basic Usage
|
||||
当前 API 服务会在启动时加载 SAM3 权重。你需要确保 `fastAPI_tarot.py` 里构建模型时使用了正确的 `checkpoint_path`。
|
||||
|
||||
推荐做法:在 [fastAPI_tarot.py](fastAPI_tarot.py) 中将模型构建改为显式传入本地权重路径,例如:
|
||||
|
||||
```python
|
||||
import torch
|
||||
#################################### For Image ####################################
|
||||
from PIL import Image
|
||||
from sam3.model_builder import build_sam3_image_model
|
||||
from sam3.model.sam3_image_processor import Sam3Processor
|
||||
# Load the model
|
||||
model = build_sam3_image_model()
|
||||
processor = Sam3Processor(model)
|
||||
# Load an image
|
||||
image = Image.open("<YOUR_IMAGE_PATH.jpg>")
|
||||
inference_state = processor.set_image(image)
|
||||
# Prompt the model with text
|
||||
output = processor.set_text_prompt(state=inference_state, prompt="<YOUR_TEXT_PROMPT>")
|
||||
|
||||
# Get the masks, bounding boxes, and scores
|
||||
masks, boxes, scores = output["masks"], output["boxes"], output["scores"]
|
||||
|
||||
#################################### For Video ####################################
|
||||
|
||||
from sam3.model_builder import build_sam3_video_predictor
|
||||
|
||||
video_predictor = build_sam3_video_predictor()
|
||||
video_path = "<YOUR_VIDEO_PATH>" # a JPEG folder or an MP4 video file
|
||||
# Start a session
|
||||
response = video_predictor.handle_request(
|
||||
request=dict(
|
||||
type="start_session",
|
||||
resource_path=video_path,
|
||||
)
|
||||
)
|
||||
response = video_predictor.handle_request(
|
||||
request=dict(
|
||||
type="add_prompt",
|
||||
session_id=response["session_id"],
|
||||
frame_index=0, # Arbitrary frame index
|
||||
text="<YOUR_TEXT_PROMPT>",
|
||||
)
|
||||
)
|
||||
output = response["outputs"]
|
||||
model = build_sam3_image_model(checkpoint_path="./dir/sam3.pt")
|
||||
```
|
||||
|
||||
## Examples
|
||||
---
|
||||
|
||||
The `examples` directory contains notebooks demonstrating how to use SAM3 with
|
||||
various types of prompts:
|
||||
## 启动 API 服务(FastAPI)
|
||||
|
||||
- [`sam3_image_predictor_example.ipynb`](examples/sam3_image_predictor_example.ipynb)
|
||||
: Demonstrates how to prompt SAM 3 with text and visual box prompts on images.
|
||||
- [`sam3_video_predictor_example.ipynb`](examples/sam3_video_predictor_example.ipynb)
|
||||
: Demonstrates how to prompt SAM 3 with text prompts on videos, and doing
|
||||
further interactive refinements with points.
|
||||
- [`sam3_image_batched_inference.ipynb`](examples/sam3_image_batched_inference.ipynb)
|
||||
: Demonstrates how to run batched inference with SAM 3 on images.
|
||||
- [`sam3_agent.ipynb`](examples/sam3_agent.ipynb): Demonsterates the use of SAM
|
||||
3 Agent to segment complex text prompt on images.
|
||||
- [`saco_gold_silver_vis_example.ipynb`](examples/saco_gold_silver_vis_example.ipynb)
|
||||
: Shows a few examples from SA-Co image evaluation set.
|
||||
- [`saco_veval_vis_example.ipynb`](examples/saco_veval_vis_example.ipynb) :
|
||||
Shows a few examples from SA-Co video evaluation set.
|
||||
|
||||
There are additional notebooks in the examples directory that demonstrate how to
|
||||
use SAM 3 for interactive instance segmentation in images and videos (SAM 1/2
|
||||
tasks), or as a tool for an MLLM, and how to run evaluations on the SA-Co
|
||||
dataset.
|
||||
|
||||
To run the Jupyter notebook examples:
|
||||
服务定义在 [fastAPI_tarot.py](fastAPI_tarot.py) 中。你可以用 uvicorn 启动(示例命令仅供参考,按需调整 host/port):
|
||||
|
||||
```bash
|
||||
# Make sure you have the notebooks dependencies installed
|
||||
pip install -e ".[notebooks]"
|
||||
|
||||
# Start Jupyter notebook
|
||||
jupyter notebook examples/sam3_image_predictor_example.ipynb
|
||||
uvicorn fastAPI_tarot:app --host 127.0.0.1 --port 55600
|
||||
```
|
||||
|
||||
## Model
|
||||
启动后:
|
||||
- OpenAPI 文档:`http://127.0.0.1:55600/docs`
|
||||
- 静态结果目录:`http://127.0.0.1:55600/static/results/...`
|
||||
|
||||
SAM 3 consists of a detector and a tracker that share a vision encoder. It has 848M parameters. The
|
||||
detector is a DETR-based model conditioned on text, geometry, and image
|
||||
exemplars. The tracker inherits the SAM 2 transformer encoder-decoder
|
||||
architecture, supporting video segmentation and interactive refinement.
|
||||
---
|
||||
|
||||
## Image Results
|
||||
## 鉴权说明(API Key)
|
||||
|
||||
<div align="center">
|
||||
<table style="min-width: 80%; border: 2px solid #ddd; border-collapse: collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="3" style="border-right: 2px solid #ddd; padding: 12px 20px">Model</th>
|
||||
<th colspan="3" style="text-align: center; border-right: 2px solid #ddd; padding: 12px 20px">Instance Segmentation</th>
|
||||
<th colspan="5" style="text-align: center; padding: 12px 20px">Box Detection</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="2" style="text-align: center; border-right: 1px solid #eee; padding: 12px 20px">LVIS</th>
|
||||
<th style="text-align: center; border-right: 2px solid #ddd; padding: 12px 20px">SA-Co/Gold</th>
|
||||
<th colspan="2" style="text-align: center; border-right: 1px solid #eee; padding: 12px 20px">LVIS</th>
|
||||
<th colspan="2" style="text-align: center; border-right: 1px solid #eee; padding: 12px 20px">COCO</th>
|
||||
<th style="text-align: center; padding: 12px 20px">SA-Co/Gold</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="text-align: center; padding: 12px 20px">cgF1</th>
|
||||
<th style="text-align: center; border-right: 1px solid #eee; padding: 12px 20px">AP</th>
|
||||
<th style="text-align: center; border-right: 2px solid #ddd; padding: 12px 20px">cgF1</th>
|
||||
<th style="text-align: center; padding: 12px 20px">cgF1</th>
|
||||
<th style="text-align: center; border-right: 1px solid #eee; padding: 12px 20px">AP</th>
|
||||
<th style="text-align: center; padding: 12px 20px">AP</th>
|
||||
<th style="text-align: center; border-right: 1px solid #eee; padding: 12px 20px">AP<sub>o</sub>
|
||||
</th>
|
||||
<th style="text-align: center; padding: 12px 20px">cgF1</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-right: 2px solid #ddd; padding: 10px 20px">Human</td>
|
||||
<td style="text-align: center; padding: 10px 20px">-</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">-</td>
|
||||
<td style="text-align: center; border-right: 2px solid #ddd; padding: 10px 20px">72.8</td>
|
||||
<td style="text-align: center; padding: 10px 20px">-</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">-</td>
|
||||
<td style="text-align: center; padding: 10px 20px">-</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">-</td>
|
||||
<td style="text-align: center; padding: 10px 20px">74.0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border-right: 2px solid #ddd; padding: 10px 20px">OWLv2*</td>
|
||||
<td style="text-align: center; padding: 10px 20px; color: #999">29.3</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px; color: #999">43.4</td>
|
||||
<td style="text-align: center; border-right: 2px solid #ddd; padding: 10px 20px">24.6</td>
|
||||
<td style="text-align: center; padding: 10px 20px; color: #999">30.2</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px; color: #999">45.5</td>
|
||||
<td style="text-align: center; padding: 10px 20px">46.1</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">23.9</td>
|
||||
<td style="text-align: center; padding: 10px 20px">24.5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border-right: 2px solid #ddd; padding: 10px 20px">DINO-X</td>
|
||||
<td style="text-align: center; padding: 10px 20px">-</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">38.5</td>
|
||||
<td style="text-align: center; border-right: 2px solid #ddd; padding: 10px 20px">21.3</td>
|
||||
<td style="text-align: center; padding: 10px 20px">-</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">52.4</td>
|
||||
<td style="text-align: center; padding: 10px 20px">56.0</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">-</td>
|
||||
<td style="text-align: center; padding: 10px 20px">22.5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border-right: 2px solid #ddd; padding: 10px 20px">Gemini 2.5</td>
|
||||
<td style="text-align: center; padding: 10px 20px">13.4</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">-</td>
|
||||
<td style="text-align: center; border-right: 2px solid #ddd; padding: 10px 20px">13.0</td>
|
||||
<td style="text-align: center; padding: 10px 20px">16.1</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">-</td>
|
||||
<td style="text-align: center; padding: 10px 20px">-</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">-</td>
|
||||
<td style="text-align: center; padding: 10px 20px">14.4</td>
|
||||
</tr>
|
||||
<tr style="border-top: 2px solid #b19c9cff">
|
||||
<td style="border-right: 2px solid #ddd; padding: 10px 20px">SAM 3</td>
|
||||
<td style="text-align: center; padding: 10px 20px">37.2</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">48.5</td>
|
||||
<td style="text-align: center; border-right: 2px solid #ddd; padding: 10px 20px">54.1</td>
|
||||
<td style="text-align: center; padding: 10px 20px">40.6</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">53.6</td>
|
||||
<td style="text-align: center; padding: 10px 20px">56.4</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">55.7</td>
|
||||
<td style="text-align: center; padding: 10px 20px">55.7</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
所有接口都要求在请求 Header 中携带 API Key:
|
||||
|
||||
<p style="text-align: center; margin-top: 10px; font-size: 0.9em; color: #ddd;">* Partially trained on LVIS, AP<sub>o</sub> refers to COCO-O accuracy</p>
|
||||
- Header 名:`X-API-Key`
|
||||
- 值:请使用你服务端配置的 Key(建议用环境变量或配置文件管理,不要在代码里硬编码)
|
||||
|
||||
</div>
|
||||
---
|
||||
|
||||
## Video Results
|
||||
## API 接口一览
|
||||
|
||||
<div align="center">
|
||||
<table style="min-width: 80%; border: 2px solid #ddd; border-collapse: collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2" style="border-right: 2px solid #ddd; padding: 12px 20px">Model</th>
|
||||
<th colspan="2" style="text-align: center; border-right: 1px solid #eee; padding: 12px 20px">SA-V test</th>
|
||||
<th colspan="2" style="text-align: center; border-right: 1px solid #eee; padding: 12px 20px">YT-Temporal-1B test</th>
|
||||
<th colspan="2" style="text-align: center; border-right: 1px solid #eee; padding: 12px 20px">SmartGlasses test</th>
|
||||
<th style="text-align: center; border-right: 1px solid #eee; padding: 12px 20px">LVVIS test</th>
|
||||
<th style="text-align: center; padding: 12px 20px">BURST test</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="text-align: center; padding: 12px 20px">cgF1</th>
|
||||
<th style="text-align: center; border-right: 1px solid #eee; padding: 12px 20px">pHOTA</th>
|
||||
<th style="text-align: center; padding: 12px 20px">cgF1</th>
|
||||
<th style="text-align: center; border-right: 1px solid #eee; padding: 12px 20px">pHOTA</th>
|
||||
<th style="text-align: center; padding: 12px 20px">cgF1</th>
|
||||
<th style="text-align: center; border-right: 1px solid #eee; padding: 12px 20px">pHOTA</th>
|
||||
<th style="text-align: center; border-right: 1px solid #eee; padding: 12px 20px">mAP</th>
|
||||
<th style="text-align: center; padding: 12px 20px">HOTA</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-right: 2px solid #ddd; padding: 10px 20px">Human</td>
|
||||
<td style="text-align: center; padding: 10px 20px">53.1</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">70.5</td>
|
||||
<td style="text-align: center; padding: 10px 20px">71.2</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">78.4</td>
|
||||
<td style="text-align: center; padding: 10px 20px">58.5</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">72.3</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">-</td>
|
||||
<td style="text-align: center; padding: 10px 20px">-</td>
|
||||
</tr>
|
||||
<tr style="border-top: 2px solid #b19c9cff">
|
||||
<td style="border-right: 2px solid #ddd; padding: 10px 20px">SAM 3</td>
|
||||
<td style="text-align: center; padding: 10px 20px">30.3</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">58.0</td>
|
||||
<td style="text-align: center; padding: 10px 20px">50.8</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">69.9</td>
|
||||
<td style="text-align: center; padding: 10px 20px">36.4</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">63.6</td>
|
||||
<td style="text-align: center; border-right: 1px solid #eee; padding: 10px 20px">36.3</td>
|
||||
<td style="text-align: center; padding: 10px 20px">44.5</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
以下接口均在 [fastAPI_tarot.py](fastAPI_tarot.py) 中实现,返回体为 JSON,并通过静态文件服务输出结果图片 URL。
|
||||
|
||||
## SA-Co Dataset
|
||||
### 1) POST /segment(通用分割)
|
||||
|
||||
We release 2 image benchmarks, [SA-Co/Gold](scripts/eval/gold/README.md) and
|
||||
[SA-Co/Silver](scripts/eval/silver/README.md), and a video benchmark
|
||||
[SA-Co/VEval](scripts/eval/veval/README.md). The datasets contain images (or videos) with annotated noun phrases. Each image/video and noun phrase pair is annotated with instance masks and unique IDs of each object matching the phrase. Phrases that have no matching objects (negative prompts) have no masks, shown in red font in the figure. See the linked READMEs for more details on how to download and run evaluations on the datasets.
|
||||
功能:对图片做文本提示词分割(中文会自动翻译为英文提示词)。
|
||||
|
||||
* HuggingFace host: [SA-Co/Gold](https://huggingface.co/datasets/facebook/SACo-Gold), [SA-Co/Silver](https://huggingface.co/datasets/facebook/SACo-Silver) and [SA-Co/VEval](https://huggingface.co/datasets/facebook/SACo-VEval)
|
||||
* Roboflow host: [SA-Co/Gold](https://universe.roboflow.com/sa-co-gold), [SA-Co/Silver](https://universe.roboflow.com/sa-co-silver) and [SA-Co/VEval](https://universe.roboflow.com/sa-co-veval)
|
||||
参数(表单):
|
||||
- prompt:提示词(必填,例:`cat` / `人` / `白色连衣裙`)
|
||||
- file:上传图片(二选一)
|
||||
- image_url:图片 URL(二选一)
|
||||
- save_segment_images:是否保存并返回每个目标的独立图片(默认 false)
|
||||
- cutout:导出子图时是否透明背景抠图(默认 false)
|
||||
- highlight:是否启用“周边变黑突出主体”的效果(默认 false)
|
||||
- confidence:置信度阈值(0.0-1.0,默认 0.7)
|
||||
|
||||

|
||||
返回(示例字段):
|
||||
- result_image_url:分割可视化结果图
|
||||
- detected_count:检测到的目标数量
|
||||
- segmented_images:可选的分割子图 URL 列表
|
||||
|
||||
## Development
|
||||
### 2) POST /segment_tarot(塔罗牌分割)
|
||||
|
||||
To set up the development environment:
|
||||
功能:检测塔罗牌并对每张牌做透视矫正与裁剪。
|
||||
|
||||
参数(表单):
|
||||
- file / image_url(二选一)
|
||||
- expected_count:期望严格检测到的塔罗牌数量(默认 3)
|
||||
|
||||
返回:
|
||||
- tarot_cards:每张牌的裁剪图 URL、是否被算法旋转矫正等信息
|
||||
- full_visualization:整体分割可视化图 URL(可选)
|
||||
|
||||
### 3) POST /recognize_tarot(塔罗牌全流程:分割 + 识别)
|
||||
|
||||
功能:在 `/segment_tarot` 基础上,进一步调用多模态大模型识别每张牌的名称与正逆位,并尝试识别牌阵。
|
||||
|
||||
说明:
|
||||
- 该流程依赖多模态大模型服务(例如 Qwen-VL / DashScope),请自行配置密钥与可用模型
|
||||
- 强烈建议将密钥通过环境变量注入,避免写入仓库
|
||||
|
||||
### 4) POST /segment_face(人脸/头发分割 + 属性分析)
|
||||
|
||||
功能:分割“face and hair”等区域,并可选调用多模态大模型进行性别/年龄等属性识别。
|
||||
|
||||
参数(表单):
|
||||
- file / image_url(二选一)
|
||||
- prompt:默认 `face and hair`(中文会尝试翻译)
|
||||
|
||||
---
|
||||
|
||||
## 请求示例(curl)
|
||||
|
||||
### 通用分割(上传图片)
|
||||
|
||||
```bash
|
||||
pip install -e ".[dev,train]"
|
||||
curl -X POST "http://127.0.0.1:55600/segment" \
|
||||
-H "X-API-Key: <YOUR_API_KEY>" \
|
||||
-F "prompt=猫" \
|
||||
-F "file=@/path/to/image.jpg"
|
||||
```
|
||||
|
||||
To format the code:
|
||||
### 通用分割(图片 URL)
|
||||
|
||||
```bash
|
||||
ufmt format .
|
||||
curl -X POST "http://127.0.0.1:55600/segment" \
|
||||
-H "X-API-Key: <YOUR_API_KEY>" \
|
||||
-F "prompt=person" \
|
||||
-F "image_url=https://example.com/image.jpg"
|
||||
```
|
||||
|
||||
## Contributing
|
||||
---
|
||||
|
||||
See [contributing](CONTRIBUTING.md) and the
|
||||
[code of conduct](CODE_OF_CONDUCT.md).
|
||||
## 结果输出与落盘
|
||||
|
||||
## License
|
||||
- 结果图会写入:`static/results/`
|
||||
- 接口返回的 URL 形如:`/static/results/<filename>` 或 `/static/results/<request_id>/<filename>`
|
||||
- 服务可选开启后台清理任务,定期删除过期结果文件(见 `fastAPI_tarot.py` 末尾的环境变量配置逻辑)
|
||||
|
||||
This project is licensed under the SAM License - see the [LICENSE](LICENSE) file
|
||||
for details.
|
||||
---
|
||||
|
||||
## Acknowledgements
|
||||
## 常见问题
|
||||
|
||||
We would like to thank the following people for their contributions to the SAM 3 project: Alex He, Alexander Kirillov,
|
||||
Alyssa Newcomb, Ana Paula Kirschner Mofarrej, Andrea Madotto, Andrew Westbury, Ashley Gabriel, Azita Shokpour,
|
||||
Ben Samples, Bernie Huang, Carleigh Wood, Ching-Feng Yeh, Christian Puhrsch, Claudette Ward, Daniel Bolya,
|
||||
Daniel Li, Facundo Figueroa, Fazila Vhora, George Orlin, Hanzi Mao, Helen Klein, Hu Xu, Ida Cheng, Jake Kinney,
|
||||
Jiale Zhi, Jo Sampaio, Joel Schlosser, Justin Johnson, Kai Brown, Karen Bergan, Karla Martucci, Kenny Lehmann,
|
||||
Maddie Mintz, Mallika Malhotra, Matt Ward, Michelle Chan, Michelle Restrepo, Miranda Hartley, Muhammad Maaz,
|
||||
Nisha Deo, Peter Park, Phillip Thomas, Raghu Nayani, Rene Martinez Doehner, Robbie Adkins, Ross Girshik, Sasha
|
||||
Mitts, Shashank Jain, Spencer Whitehead, Ty Toledano, Valentin Gabeur, Vincent Cho, Vivian Lee, William Ngan,
|
||||
Xuehai He, Yael Yungster, Ziqi Pang, Ziyi Dou, Zoe Quake.
|
||||
### 1) 启动时报找不到权重文件?
|
||||
|
||||
## Citing SAM 3
|
||||
请确认:
|
||||
- 已按“下载权重模型”得到 `sam3.pt`
|
||||
- `fastAPI_tarot.py` 中 `build_sam3_image_model(checkpoint_path=...)` 指向正确路径
|
||||
|
||||
If you use SAM 3 or the SA-Co dataset in your research, please use the following BibTeX entry.
|
||||
### 2) 返回 401/403?
|
||||
|
||||
```bibtex
|
||||
@misc{carion2025sam3segmentconcepts,
|
||||
title={SAM 3: Segment Anything with Concepts},
|
||||
author={Nicolas Carion and Laura Gustafson and Yuan-Ting Hu and Shoubhik Debnath and Ronghang Hu and Didac Suris and Chaitanya Ryali and Kalyan Vasudev Alwala and Haitham Khedr and Andrew Huang and Jie Lei and Tengyu Ma and Baishan Guo and Arpit Kalla and Markus Marks and Joseph Greer and Meng Wang and Peize Sun and Roman Rädle and Triantafyllos Afouras and Effrosyni Mavroudi and Katherine Xu and Tsung-Han Wu and Yu Zhou and Liliane Momeni and Rishi Hazra and Shuangrui Ding and Sagar Vaze and Francois Porcher and Feng Li and Siyuan Li and Aishwarya Kamath and Ho Kei Cheng and Piotr Dollár and Nikhila Ravi and Kate Saenko and Pengchuan Zhang and Christoph Feichtenhofer},
|
||||
year={2025},
|
||||
eprint={2511.16719},
|
||||
archivePrefix={arXiv},
|
||||
primaryClass={cs.CV},
|
||||
url={https://arxiv.org/abs/2511.16719},
|
||||
}
|
||||
```
|
||||
请确认请求头是否带了 `X-API-Key`,且值与服务端配置一致。
|
||||
|
||||
---
|
||||
|
||||
# Admin Config
|
||||
ADMIN_PASSWORD = "admin_secure_password" # 可以根据需求修改
|
||||
HISTORY_FILE = "history.json"
|
||||
|
||||
API_KEY = "123quant-speed" # 可以根据需求修改
|
||||
|
||||
## 免责声明与致谢
|
||||
|
||||
- 本项目为量迹AI内部/业务化工程封装示例,**不是官方 SAM3 仓库**。
|
||||
- 模型与核心算法能力来自开源的 SAM3 实现与相关依赖,感谢社区贡献者与原作者团队。
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"from sam3 import build_sam3_image_model\n",
|
||||
"from sam3.model.sam3_image_processor import Sam3Processor\n",
|
||||
"\n",
|
||||
"sam3_root = os.path.join(os.path.dirname(sam3.__file__), \"..\")\n",
|
||||
"sam3_root = os.path.dirname(sam3.__file__)\n",
|
||||
"bpe_path = f\"{sam3_root}/assets/bpe_simple_vocab_16e6.txt.gz\"\n",
|
||||
"model = build_sam3_image_model(bpe_path=bpe_path)\n",
|
||||
"processor = Sam3Processor(model, confidence_threshold=0.5)"
|
||||
@@ -115,7 +115,7 @@
|
||||
" \"qwen3_vl_8b_thinking\": {\n",
|
||||
" \"provider\": \"vllm\",\n",
|
||||
" \"model\": \"Qwen/Qwen3-VL-8B-Thinking\",\n",
|
||||
" }, \n",
|
||||
" },\n",
|
||||
" # models served via external APIs\n",
|
||||
" # add your own\n",
|
||||
"}\n",
|
||||
@@ -191,7 +191,7 @@
|
||||
"send_generate_request = partial(send_generate_request_orig, server_url=LLM_SERVER_URL, model=llm_config[\"model\"], api_key=llm_config[\"api_key\"])\n",
|
||||
"call_sam_service = partial(call_sam_service_orig, sam3_processor=processor)\n",
|
||||
"output_image_path = run_single_image_inference(\n",
|
||||
" image, prompt, llm_config, send_generate_request, call_sam_service, \n",
|
||||
" image, prompt, llm_config, send_generate_request, call_sam_service,\n",
|
||||
" debug=True, output_dir=\"agent_output\"\n",
|
||||
")\n",
|
||||
"\n",
|
||||
@@ -238,5 +238,5 @@
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
|
||||
159
fastAPI_main.py
Normal file
159
fastAPI_main.py
Normal file
@@ -0,0 +1,159 @@
|
||||
import os
|
||||
import uuid
|
||||
import requests
|
||||
from typing import Optional
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import torch
|
||||
import matplotlib
|
||||
# 关键:设置非交互式后端,避免服务器环境下报错
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Request
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import JSONResponse
|
||||
from PIL import Image
|
||||
|
||||
# SAM3 相关导入 (请确保你的环境中已正确安装 sam3)
|
||||
from sam3.model_builder import build_sam3_image_model
|
||||
from sam3.model.sam3_image_processor import Sam3Processor
|
||||
from sam3.visualization_utils import plot_results
|
||||
|
||||
# ------------------- 配置与路径 -------------------
|
||||
STATIC_DIR = "static"
|
||||
RESULT_IMAGE_DIR = os.path.join(STATIC_DIR, "results")
|
||||
os.makedirs(RESULT_IMAGE_DIR, exist_ok=True)
|
||||
|
||||
# ------------------- 核心修改:图片压缩函数 -------------------
|
||||
def compress_image(image: Image.Image, max_size: int = 1920, quality: int = 85) -> Image.Image:
|
||||
"""
|
||||
如果图片边长超过 max_size,则按比例压缩。
|
||||
:param image: PIL Image 对象
|
||||
:param max_size: 图片最大边长 (宽或高)
|
||||
:param quality: 仅用于保存时的参考,这里主要做尺寸压缩
|
||||
:return: 压缩后的 PIL Image 对象
|
||||
"""
|
||||
width, height = image.size
|
||||
|
||||
# 如果图片本身就很小,直接返回
|
||||
if width <= max_size and height <= max_size:
|
||||
return image
|
||||
|
||||
# 计算缩放比例
|
||||
if width > height:
|
||||
new_width = max_size
|
||||
new_height = int(height * (max_size / width))
|
||||
else:
|
||||
new_height = max_size
|
||||
new_width = int(width * (max_size / height))
|
||||
|
||||
# 使用 LANCZOS 滤镜进行高质量下采样
|
||||
print(f"压缩图片: {width}x{height} -> {new_width}x{new_height}")
|
||||
return image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# ------------------- 生命周期管理 -------------------
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
print("正在加载 SAM3 模型到 GPU...")
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
if not torch.cuda.is_available():
|
||||
print("警告: 未检测到 GPU,将使用 CPU,速度会较慢。")
|
||||
|
||||
model = build_sam3_image_model()
|
||||
model = model.to(device)
|
||||
model.eval()
|
||||
processor = Sam3Processor(model)
|
||||
|
||||
app.state.model = model
|
||||
app.state.processor = processor
|
||||
app.state.device = device
|
||||
|
||||
print(f"模型加载完成,设备: {device}")
|
||||
yield
|
||||
print("正在清理资源...")
|
||||
|
||||
# ------------------- FastAPI 初始化 -------------------
|
||||
app = FastAPI(lifespan=lifespan, title="SAM3 Segmentation API")
|
||||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||
|
||||
# ------------------- 辅助函数 -------------------
|
||||
def load_image_from_url(url: str) -> Image.Image:
|
||||
try:
|
||||
headers = {'User-Agent': 'Mozilla/5.0'}
|
||||
response = requests.get(url, headers=headers, stream=True, timeout=10)
|
||||
response.raise_for_status()
|
||||
image = Image.open(response.raw).convert("RGB")
|
||||
return image
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"无法下载图片: {str(e)}")
|
||||
|
||||
def generate_and_save_result(image: Image.Image, inference_state) -> str:
|
||||
filename = f"seg_{uuid.uuid4().hex}.jpg"
|
||||
save_path = os.path.join(RESULT_IMAGE_DIR, filename)
|
||||
plot_results(image, inference_state)
|
||||
plt.savefig(save_path, dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
return filename
|
||||
|
||||
# ------------------- API 接口 -------------------
|
||||
@app.post("/segment")
|
||||
async def segment(
|
||||
request: Request,
|
||||
prompt: str = Form(...),
|
||||
file: Optional[UploadFile] = File(None),
|
||||
image_url: Optional[str] = Form(None)
|
||||
):
|
||||
if not file and not image_url:
|
||||
raise HTTPException(status_code=400, detail="必须提供 file 或 image_url")
|
||||
|
||||
# 1. 获取图片对象
|
||||
try:
|
||||
if file:
|
||||
image = Image.open(file.file).convert("RGB")
|
||||
elif image_url:
|
||||
image = load_image_from_url(image_url)
|
||||
|
||||
# ========== 关键修改位置 ==========
|
||||
# 在送入模型前,强制压缩图片
|
||||
image = compress_image(image, max_size=1920)
|
||||
# ===================================
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"图片解析失败: {str(e)}")
|
||||
|
||||
# 2. 获取模型
|
||||
processor = request.app.state.processor
|
||||
|
||||
# 3. 执行推理
|
||||
try:
|
||||
inference_state = processor.set_image(image)
|
||||
output = processor.set_text_prompt(state=inference_state, prompt=prompt)
|
||||
masks, boxes, scores = output["masks"], output["boxes"], output["scores"]
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"模型推理错误: {str(e)}")
|
||||
|
||||
# 4. 生成可视化并保存
|
||||
try:
|
||||
filename = generate_and_save_result(image, inference_state)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"绘图保存错误: {str(e)}")
|
||||
|
||||
file_url = request.url_for("static", path=f"results/{filename}")
|
||||
|
||||
return JSONResponse(content={
|
||||
"status": "success",
|
||||
"result_image_url": str(file_url),
|
||||
"detected_count": len(masks),
|
||||
"scores": scores.tolist() if torch.is_tensor(scores) else scores
|
||||
})
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"fastAPI_main:app",
|
||||
host="127.0.0.1",
|
||||
port=55600,
|
||||
proxy_headers=True,
|
||||
forwarded_allow_ips="*"
|
||||
)
|
||||
1576
fastAPI_tarot.py
Normal file
1576
fastAPI_tarot.py
Normal file
File diff suppressed because it is too large
Load Diff
0
history.json
Normal file
0
history.json
Normal file
472
human_analysis_service.py
Normal file
472
human_analysis_service.py
Normal file
@@ -0,0 +1,472 @@
|
||||
import os
|
||||
import uuid
|
||||
import time
|
||||
import requests
|
||||
import numpy as np
|
||||
import json
|
||||
import torch
|
||||
import cv2
|
||||
import ast
|
||||
import re
|
||||
from PIL import Image
|
||||
from dashscope import MultiModalConversation
|
||||
|
||||
# 配置 (与 fastAPI_tarot.py 保持一致或通过参数传入)
|
||||
# 这里的常量可以根据需要调整,或者从主文件传入
|
||||
QWEN_MODEL = 'qwen-vl-max'
|
||||
|
||||
def load_image_from_url(url: str) -> Image.Image:
|
||||
"""
|
||||
从 URL 下载图片并转换为 RGB 格式
|
||||
"""
|
||||
try:
|
||||
headers = {'User-Agent': 'Mozilla/5.0'}
|
||||
response = requests.get(url, headers=headers, stream=True, timeout=10)
|
||||
response.raise_for_status()
|
||||
image = Image.open(response.raw).convert("RGB")
|
||||
return image
|
||||
except Exception as e:
|
||||
raise Exception(f"无法下载图片: {str(e)}")
|
||||
|
||||
def crop_head_with_padding(image: Image.Image, box, padding_ratio=0.1) -> Image.Image:
|
||||
"""
|
||||
根据 bounding box 裁剪图片,并添加一定的 padding
|
||||
box格式: [x1, y1, x2, y2]
|
||||
"""
|
||||
img_w, img_h = image.size
|
||||
x1, y1, x2, y2 = box
|
||||
|
||||
w = x2 - x1
|
||||
h = y2 - y1
|
||||
|
||||
# 计算 padding
|
||||
pad_w = w * padding_ratio
|
||||
pad_h = h * padding_ratio
|
||||
|
||||
# 应用 padding 并确保不越界
|
||||
new_x1 = max(0, int(x1 - pad_w))
|
||||
new_y1 = max(0, int(y1 - pad_h))
|
||||
new_x2 = min(img_w, int(x2 + pad_w))
|
||||
new_y2 = min(img_h, int(y2 + pad_h))
|
||||
|
||||
return image.crop((new_x1, new_y1, new_x2, new_y2))
|
||||
|
||||
def create_highlighted_visualization(image: Image.Image, masks, output_path: str):
|
||||
"""
|
||||
创建一个突出显示头部(Mask区域)的可视化图,背景变暗
|
||||
"""
|
||||
# Convert PIL to numpy RGB
|
||||
img_np = np.array(image)
|
||||
|
||||
# Create darkened background (e.g., 30% brightness)
|
||||
darkened_np = (img_np * 0.3).astype(np.uint8)
|
||||
|
||||
# Combine all masks
|
||||
if len(masks) > 0:
|
||||
combined_mask = np.zeros(img_np.shape[:2], dtype=bool)
|
||||
for mask in masks:
|
||||
# Handle tensor/numpy conversions
|
||||
if isinstance(mask, torch.Tensor):
|
||||
m = mask.cpu().numpy().squeeze()
|
||||
else:
|
||||
m = mask.squeeze()
|
||||
|
||||
# Ensure 2D
|
||||
if m.ndim > 2:
|
||||
m = m[0]
|
||||
|
||||
# Threshold if probability or float
|
||||
if m.dtype != bool:
|
||||
m = m > 0.5
|
||||
|
||||
# Resize mask if it doesn't match image size (rare but possible with some internal resizing)
|
||||
if m.shape != img_np.shape[:2]:
|
||||
# resize to match image
|
||||
m = cv2.resize(m.astype(np.uint8), (img_np.shape[1], img_np.shape[0]), interpolation=cv2.INTER_NEAREST).astype(bool)
|
||||
|
||||
combined_mask = np.logical_or(combined_mask, m)
|
||||
|
||||
# Expand mask to 3 channels for broadcasting
|
||||
mask_3ch = np.stack([combined_mask]*3, axis=-1)
|
||||
|
||||
# Composite: Original where mask is True, Darkened where False
|
||||
result_np = np.where(mask_3ch, img_np, darkened_np)
|
||||
else:
|
||||
result_np = darkened_np # No masks, just dark
|
||||
|
||||
# Save
|
||||
Image.fromarray(result_np).save(output_path)
|
||||
|
||||
def extract_json_from_response(text: str) -> dict:
|
||||
"""
|
||||
Robustly extract JSON from text, handling:
|
||||
1. Markdown code blocks (```json ... ```)
|
||||
2. Single quotes (Python dict style) via ast.literal_eval
|
||||
"""
|
||||
try:
|
||||
# 1. Try to find JSON block
|
||||
json_match = re.search(r'```json\s*(.*?)\s*```', text, re.DOTALL)
|
||||
if json_match:
|
||||
clean_text = json_match.group(1).strip()
|
||||
else:
|
||||
# Try to find { ... } block if no markdown
|
||||
match = re.search(r'\{.*\}', text, re.DOTALL)
|
||||
if match:
|
||||
clean_text = match.group(0).strip()
|
||||
else:
|
||||
clean_text = text.strip()
|
||||
|
||||
# 2. Try standard JSON
|
||||
return json.loads(clean_text)
|
||||
except Exception as e1:
|
||||
# 3. Try ast.literal_eval for single quotes
|
||||
try:
|
||||
return ast.literal_eval(clean_text)
|
||||
except Exception as e2:
|
||||
# 4. Fail
|
||||
raise ValueError(f"Could not parse JSON: {e1} | {e2} | Content: {text[:100]}...")
|
||||
|
||||
def analyze_demographics_with_qwen(image_path: str, model_name: str = 'qwen-vl-max', prompt_template: str = None) -> dict:
|
||||
"""
|
||||
调用 Qwen-VL 模型分析人物的年龄和性别
|
||||
"""
|
||||
try:
|
||||
# 确保路径是绝对路径
|
||||
abs_path = os.path.abspath(image_path)
|
||||
file_url = f"file://{abs_path}"
|
||||
|
||||
# 默认 Prompt
|
||||
default_prompt = """请仔细观察这张图片中的人物头部/面部特写:
|
||||
1. 识别性别 (Gender):男性/女性
|
||||
2. 预估年龄 (Age):请给出一个合理的年龄范围,例如 "25-30岁"
|
||||
3. 简要描述:发型、发色、是否有眼镜等显著特征。
|
||||
|
||||
请以 JSON 格式返回,包含 'gender', 'age', 'description' 字段。
|
||||
不要包含 Markdown 标记。"""
|
||||
|
||||
final_prompt = prompt_template if prompt_template else default_prompt
|
||||
|
||||
# 构造 Prompt
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"image": file_url},
|
||||
{"text": final_prompt}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
# 调用模型
|
||||
response = MultiModalConversation.call(model=model_name, messages=messages)
|
||||
|
||||
if response.status_code == 200:
|
||||
content = response.output.choices[0].message.content[0]['text']
|
||||
try:
|
||||
result = extract_json_from_response(content)
|
||||
result["model_used"] = model_name
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"JSON Parse Error in face analysis: {e}")
|
||||
return {"raw_analysis": content, "error": str(e), "model_used": model_name}
|
||||
else:
|
||||
return {"error": f"API Error: {response.code} - {response.message}"}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": f"分析失败: {str(e)}"}
|
||||
|
||||
import asyncio
|
||||
|
||||
def process_face_segmentation_and_analysis(
|
||||
processor,
|
||||
image: Image.Image,
|
||||
prompt: str = "head",
|
||||
output_base_dir: str = "static/results",
|
||||
qwen_model: str = "qwen-vl-max",
|
||||
analysis_prompt: str = None
|
||||
) -> dict:
|
||||
"""
|
||||
核心处理逻辑:
|
||||
1. SAM3 分割 (默认提示词 "head" 以包含头发)
|
||||
2. 裁剪图片
|
||||
3. Qwen-VL 识别性别年龄 (并发)
|
||||
4. 返回结果
|
||||
"""
|
||||
|
||||
# 1. SAM3 推理 (同步,因为涉及 GPU 操作)
|
||||
inference_state = processor.set_image(image)
|
||||
output = processor.set_text_prompt(state=inference_state, prompt=prompt)
|
||||
masks, boxes, scores = output["masks"], output["boxes"], output["scores"]
|
||||
|
||||
detected_count = len(masks)
|
||||
if detected_count == 0:
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "未检测到目标",
|
||||
"detected_count": 0,
|
||||
"results": []
|
||||
}
|
||||
|
||||
# 准备结果目录
|
||||
request_id = f"{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
||||
output_dir = os.path.join(output_base_dir, request_id)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# --- 生成可视化图 ---
|
||||
vis_filename = f"seg_{uuid.uuid4().hex}.jpg"
|
||||
vis_path = os.path.join(output_dir, vis_filename)
|
||||
try:
|
||||
create_highlighted_visualization(image, masks, vis_path)
|
||||
full_vis_relative_path = f"results/{request_id}/{vis_filename}"
|
||||
except Exception as e:
|
||||
print(f"可视化生成失败: {e}")
|
||||
full_vis_relative_path = None
|
||||
# ------------------
|
||||
|
||||
# 转换 boxes 和 scores
|
||||
if isinstance(boxes, torch.Tensor):
|
||||
boxes_np = boxes.cpu().numpy()
|
||||
else:
|
||||
boxes_np = boxes
|
||||
|
||||
if isinstance(scores, torch.Tensor):
|
||||
scores_list = scores.tolist()
|
||||
else:
|
||||
scores_list = scores if isinstance(scores, list) else [float(scores)]
|
||||
|
||||
# 准备异步任务
|
||||
async def run_analysis_tasks():
|
||||
loop = asyncio.get_event_loop()
|
||||
tasks = []
|
||||
temp_results = [] # 存储 (index, filename, score) 以便后续排序组合
|
||||
|
||||
for i, box in enumerate(boxes_np):
|
||||
# 2. 裁剪 (同步)
|
||||
cropped_img = crop_head_with_padding(image, box, padding_ratio=0.1)
|
||||
filename = f"face_{i}.jpg"
|
||||
save_path = os.path.join(output_dir, filename)
|
||||
cropped_img.save(save_path)
|
||||
|
||||
# 3. 准备识别任务
|
||||
task = loop.run_in_executor(
|
||||
None,
|
||||
analyze_demographics_with_qwen,
|
||||
save_path,
|
||||
qwen_model,
|
||||
analysis_prompt
|
||||
)
|
||||
tasks.append(task)
|
||||
temp_results.append({
|
||||
"filename": filename,
|
||||
"relative_path": f"results/{request_id}/{filename}",
|
||||
"score": float(scores_list[i]) if i < len(scores_list) else 0.0
|
||||
})
|
||||
|
||||
# 等待所有任务完成
|
||||
if tasks:
|
||||
analysis_results = await asyncio.gather(*tasks)
|
||||
else:
|
||||
analysis_results = []
|
||||
|
||||
# 组合结果
|
||||
final_results = []
|
||||
for i, item in enumerate(temp_results):
|
||||
item["analysis"] = analysis_results[i]
|
||||
final_results.append(item)
|
||||
|
||||
return final_results
|
||||
|
||||
# 运行异步任务
|
||||
# 注意:由于本函数被 FastAPI (异步环境) 中的同步或异步函数调用,
|
||||
# 如果上层是 async def,我们可以直接 await。
|
||||
# 但由于这个函数定义没有 async,且之前的调用是同步的,
|
||||
# 为了兼容性,我们需要检查当前是否在事件循环中。
|
||||
|
||||
# 然而,查看 fastAPI_tarot.py,这个函数是在 async def segment_face 中被调用的。
|
||||
# 但它是作为普通函数被导入和调用的。
|
||||
# 为了不破坏现有签名,我们可以使用 asyncio.run() 或者在新循环中运行,
|
||||
# 但这在已经运行的 loop 中是不允许的。
|
||||
|
||||
# 最佳方案:修改本函数为 async,并在 fastAPI_tarot.py 中 await 它。
|
||||
# 但这需要修改 fastAPI_tarot.py 的调用处。
|
||||
|
||||
# 既然我们已经修改了 fastAPI_tarot.py,我们也可以顺便修改这里的签名。
|
||||
# 但为了稳妥,我们可以用一种 hack:
|
||||
# 如果在一个正在运行的 loop 中调用,我们必须返回 awaitable 或者使用 loop.run_until_complete (会报错)
|
||||
|
||||
# 让我们先把这个函数改成 async,然后去修改 fastAPI_tarot.py 的调用。
|
||||
# 这是最正确的做法。
|
||||
pass # 占位,实际代码在下面
|
||||
|
||||
async def process_face_segmentation_and_analysis_async(
|
||||
processor,
|
||||
image: Image.Image,
|
||||
prompt: str = "head",
|
||||
output_base_dir: str = "static/results",
|
||||
qwen_model: str = "qwen-vl-max",
|
||||
analysis_prompt: str = None
|
||||
) -> dict:
|
||||
# ... (同上逻辑,只是是 async)
|
||||
|
||||
# 1. SAM3 推理
|
||||
inference_state = processor.set_image(image)
|
||||
output = processor.set_text_prompt(state=inference_state, prompt=prompt)
|
||||
masks, boxes, scores = output["masks"], output["boxes"], output["scores"]
|
||||
|
||||
detected_count = len(masks)
|
||||
if detected_count == 0:
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "未检测到目标",
|
||||
"detected_count": 0,
|
||||
"results": []
|
||||
}
|
||||
|
||||
request_id = f"{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
||||
output_dir = os.path.join(output_base_dir, request_id)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
vis_filename = f"seg_{uuid.uuid4().hex}.jpg"
|
||||
vis_path = os.path.join(output_dir, vis_filename)
|
||||
try:
|
||||
create_highlighted_visualization(image, masks, vis_path)
|
||||
full_vis_relative_path = f"results/{request_id}/{vis_filename}"
|
||||
except Exception as e:
|
||||
print(f"可视化生成失败: {e}")
|
||||
full_vis_relative_path = None
|
||||
|
||||
if isinstance(boxes, torch.Tensor):
|
||||
boxes_np = boxes.cpu().numpy()
|
||||
else:
|
||||
boxes_np = boxes
|
||||
|
||||
if isinstance(scores, torch.Tensor):
|
||||
scores_list = scores.tolist()
|
||||
else:
|
||||
scores_list = scores if isinstance(scores, list) else [float(scores)]
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
tasks = []
|
||||
results = []
|
||||
|
||||
for i, box in enumerate(boxes_np):
|
||||
cropped_img = crop_head_with_padding(image, box, padding_ratio=0.1)
|
||||
filename = f"face_{i}.jpg"
|
||||
save_path = os.path.join(output_dir, filename)
|
||||
cropped_img.save(save_path)
|
||||
|
||||
task = loop.run_in_executor(
|
||||
None,
|
||||
analyze_demographics_with_qwen,
|
||||
save_path,
|
||||
qwen_model,
|
||||
analysis_prompt
|
||||
)
|
||||
tasks.append(task)
|
||||
|
||||
results.append({
|
||||
"filename": filename,
|
||||
"relative_path": f"results/{request_id}/{filename}",
|
||||
"score": float(scores_list[i]) if i < len(scores_list) else 0.0
|
||||
})
|
||||
|
||||
if tasks:
|
||||
analysis_results = await asyncio.gather(*tasks)
|
||||
else:
|
||||
analysis_results = []
|
||||
|
||||
for i, item in enumerate(results):
|
||||
item["analysis"] = analysis_results[i]
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"成功检测并分析 {detected_count} 个人脸",
|
||||
"detected_count": detected_count,
|
||||
"request_id": request_id,
|
||||
"full_visualization": full_vis_relative_path,
|
||||
"scores": scores_list,
|
||||
"results": results
|
||||
}
|
||||
|
||||
# 保留旧的同步接口以兼容其他潜在调用者,但内部实现可能会有问题如果它在 loop 中运行
|
||||
# 既然我们主要关注 fastAPI_tarot.py,我们可以直接替换 process_face_segmentation_and_analysis
|
||||
# 或者让它只是一个 wrapper
|
||||
def process_face_segmentation_and_analysis(
|
||||
processor,
|
||||
image: Image.Image,
|
||||
prompt: str = "head",
|
||||
output_base_dir: str = "static/results",
|
||||
qwen_model: str = "qwen-vl-max",
|
||||
analysis_prompt: str = None
|
||||
) -> dict:
|
||||
"""
|
||||
同步版本 (保留以兼容)
|
||||
注意:如果在 async loop 中调用此函数,且此函数内部没有异步操作,则会阻塞 loop。
|
||||
如果需要异步并发,请使用 process_face_segmentation_and_analysis_async
|
||||
"""
|
||||
# 这里我们简单地复用逻辑,但去除异步部分,退化为串行
|
||||
|
||||
# 1. SAM3 推理
|
||||
inference_state = processor.set_image(image)
|
||||
output = processor.set_text_prompt(state=inference_state, prompt=prompt)
|
||||
masks, boxes, scores = output["masks"], output["boxes"], output["scores"]
|
||||
|
||||
detected_count = len(masks)
|
||||
if detected_count == 0:
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "未检测到目标",
|
||||
"detected_count": 0,
|
||||
"results": []
|
||||
}
|
||||
|
||||
request_id = f"{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
||||
output_dir = os.path.join(output_base_dir, request_id)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
vis_filename = f"seg_{uuid.uuid4().hex}.jpg"
|
||||
vis_path = os.path.join(output_dir, vis_filename)
|
||||
try:
|
||||
create_highlighted_visualization(image, masks, vis_path)
|
||||
full_vis_relative_path = f"results/{request_id}/{vis_filename}"
|
||||
except Exception as e:
|
||||
print(f"可视化生成失败: {e}")
|
||||
full_vis_relative_path = None
|
||||
|
||||
if isinstance(boxes, torch.Tensor):
|
||||
boxes_np = boxes.cpu().numpy()
|
||||
else:
|
||||
boxes_np = boxes
|
||||
|
||||
if isinstance(scores, torch.Tensor):
|
||||
scores_list = scores.tolist()
|
||||
else:
|
||||
scores_list = scores if isinstance(scores, list) else [float(scores)]
|
||||
|
||||
results = []
|
||||
for i, box in enumerate(boxes_np):
|
||||
cropped_img = crop_head_with_padding(image, box, padding_ratio=0.1)
|
||||
filename = f"face_{i}.jpg"
|
||||
save_path = os.path.join(output_dir, filename)
|
||||
cropped_img.save(save_path)
|
||||
|
||||
# 同步调用
|
||||
analysis = analyze_demographics_with_qwen(save_path, model_name=qwen_model, prompt_template=analysis_prompt)
|
||||
|
||||
results.append({
|
||||
"filename": filename,
|
||||
"relative_path": f"results/{request_id}/{filename}",
|
||||
"analysis": analysis,
|
||||
"score": float(scores_list[i]) if i < len(scores_list) else 0.0
|
||||
})
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"成功检测并分析 {detected_count} 个人脸",
|
||||
"detected_count": detected_count,
|
||||
"request_id": request_id,
|
||||
"full_visualization": full_vis_relative_path,
|
||||
"scores": scores_list,
|
||||
"results": results
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"timm>=1.0.17",
|
||||
"numpy==1.26",
|
||||
"numpy>=1.26,<2",
|
||||
"tqdm",
|
||||
"ftfy==6.1.1",
|
||||
"regex",
|
||||
@@ -82,8 +82,12 @@ train = [
|
||||
"Homepage" = "https://github.com/facebookresearch/sam3"
|
||||
"Bug Tracker" = "https://github.com/facebookresearch/sam3/issues"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["sam3", "sam3.model"]
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["sam3*"]
|
||||
exclude = ["build*", "scripts*", "examples*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
sam3 = ["assets/*.txt.gz"]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "sam3.__version__"}
|
||||
|
||||
4
requirement.txt
Normal file
4
requirement.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
uvicorn
|
||||
python-multipart
|
||||
fastapi
|
||||
dashscope
|
||||
164
run_monitor.sh
Executable file
164
run_monitor.sh
Executable file
@@ -0,0 +1,164 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==============================================================================
|
||||
# SAM3 项目启动与监控脚本
|
||||
# 功能:启动 Python FastAPI 服务,并持续监控健康状态
|
||||
# 作者:Trae AI
|
||||
# 日期:2026-02-17
|
||||
# ==============================================================================
|
||||
|
||||
# 配置部分
|
||||
PROJECT_DIR="/home/quant/data/dev/sam3" # 项目根目录
|
||||
SCRIPT_NAME="fastAPI_tarot.py" # Python 启动脚本
|
||||
LOG_FILE="${PROJECT_DIR}/log/monitor.log" # 监控日志文件
|
||||
APP_LOG_FILE="${PROJECT_DIR}/log/app.log" # 应用输出日志文件
|
||||
PORT=55600 # 服务端口
|
||||
CHECK_INTERVAL=60 # 检查间隔(秒)
|
||||
MAX_FAILURES=3 # 最大连续失败次数,超过则重启
|
||||
STARTUP_TIMEOUT=300 # 启动超时时间(秒),等待模型加载
|
||||
PYTHON_CMD="python" # Python 命令,根据环境可能是 python3
|
||||
|
||||
# 切换到项目目录
|
||||
cd "$PROJECT_DIR" || exit 1
|
||||
|
||||
# 初始化变量
|
||||
APP_PID=0
|
||||
FAIL_COUNT=0
|
||||
|
||||
# ==============================================================================
|
||||
# 函数:记录日志 (log_message)
|
||||
# 功能:将带有时间戳的信息写入日志文件并输出到控制台
|
||||
# 参数:$1 - 日志内容
|
||||
# ==============================================================================
|
||||
log_message() {
|
||||
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
|
||||
echo "[$timestamp] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# 函数:启动应用 (start_app)
|
||||
# 功能:启动 FastAPI 服务,并记录 PID
|
||||
# ==============================================================================
|
||||
start_app() {
|
||||
log_message "正在启动项目: $SCRIPT_NAME ..."
|
||||
log_message "应用日志将输出到: $APP_LOG_FILE"
|
||||
|
||||
# 后台启动 Python 脚本,将 stdout 和 stderr 重定向到日志
|
||||
# 使用 -u 参数启用无缓冲输出,确保日志实时更新
|
||||
nohup $PYTHON_CMD -u "$SCRIPT_NAME" > "$APP_LOG_FILE" 2>&1 &
|
||||
|
||||
APP_PID=$!
|
||||
log_message "项目已启动,PID: $APP_PID"
|
||||
|
||||
log_message "正在等待服务初始化 (最多等待 ${STARTUP_TIMEOUT} 秒)..."
|
||||
|
||||
# 循环检查服务是否就绪
|
||||
local elapsed=0
|
||||
while [ $elapsed -lt $STARTUP_TIMEOUT ]; do
|
||||
# 检查进程是否还活着
|
||||
if ! kill -0 $APP_PID 2>/dev/null; then
|
||||
log_message "错误: 进程在启动过程中退出。请检查应用日志。"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 检查端口响应
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:$PORT/docs")
|
||||
if [ "$HTTP_CODE" == "200" ]; then
|
||||
log_message "服务启动成功!"
|
||||
return 0
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
elapsed=$((elapsed + 5))
|
||||
|
||||
# 每30秒打印一次等待日志
|
||||
if [ $((elapsed % 30)) -eq 0 ]; then
|
||||
log_message "仍在等待服务启动... (已耗时 ${elapsed} 秒)"
|
||||
fi
|
||||
done
|
||||
|
||||
log_message "错误: 服务启动超时 (${STARTUP_TIMEOUT} 秒)。正在终止进程..."
|
||||
kill -9 $APP_PID 2>/dev/null
|
||||
return 1
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# 函数:停止应用 (stop_app)
|
||||
# 功能:通过 PID 停止应用,如果失败则强制杀死
|
||||
# ==============================================================================
|
||||
stop_app() {
|
||||
if [ $APP_PID -gt 0 ]; then
|
||||
log_message "正在停止项目 (PID: $APP_PID)..."
|
||||
kill $APP_PID 2>/dev/null
|
||||
|
||||
# 等待进程结束
|
||||
for i in {1..5}; do
|
||||
if ! kill -0 $APP_PID 2>/dev/null; then
|
||||
log_message "项目已停止"
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 如果还在运行,强制杀死
|
||||
log_message "项目未响应,正在强制终止..."
|
||||
kill -9 $APP_PID 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# 函数:检查健康状态 (check_health)
|
||||
# 功能:检查进程是否存在以及端口是否响应
|
||||
# 返回:0 (正常) / 1 (异常)
|
||||
# ==============================================================================
|
||||
check_health() {
|
||||
# 1. 检查进程是否存在
|
||||
if ! kill -0 $APP_PID 2>/dev/null; then
|
||||
log_message "警告: 进程 $APP_PID 不存在"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 2. 检查端口响应 (请求 /docs 接口)
|
||||
# 使用 curl 获取 HTTP 状态码
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:$PORT/docs")
|
||||
|
||||
if [ "$HTTP_CODE" == "200" ]; then
|
||||
return 0
|
||||
else
|
||||
log_message "警告: 健康检查失败,HTTP 状态码: $HTTP_CODE"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# 主循环
|
||||
# ==============================================================================
|
||||
|
||||
# 初始启动
|
||||
start_app
|
||||
|
||||
while true; do
|
||||
if check_health; then
|
||||
# 健康检查通过
|
||||
FAIL_COUNT=0
|
||||
# log_message "健康检查通过" # 可选:为了减少日志量,可以注释掉这行
|
||||
else
|
||||
# 健康检查失败
|
||||
((FAIL_COUNT++))
|
||||
log_message "健康检查失败 ($FAIL_COUNT/$MAX_FAILURES)"
|
||||
|
||||
if [ $FAIL_COUNT -ge $MAX_FAILURES ]; then
|
||||
log_message "错误: 连续检测失败次数过多,准备重启项目..."
|
||||
stop_app
|
||||
start_app
|
||||
FAIL_COUNT=0
|
||||
elif ! kill -0 $APP_PID 2>/dev/null; then
|
||||
# 如果进程直接没了,立即重启
|
||||
log_message "错误: 进程意外退出,立即重启..."
|
||||
start_app
|
||||
FAIL_COUNT=0
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep $CHECK_INTERVAL
|
||||
done
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from .model_builder import build_sam3_image_model
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
@@ -294,9 +296,9 @@ def agent_inference(
|
||||
assert LATEST_SAM3_TEXT_PROMPT != ""
|
||||
|
||||
# Make sure that the last message is a image
|
||||
assert (
|
||||
messages[-1]["content"][1]["type"] == "image"
|
||||
), "Second content element should be an image"
|
||||
assert messages[-1]["content"][1]["type"] == "image", (
|
||||
"Second content element should be an image"
|
||||
)
|
||||
messages.pop() # Remove the last user message
|
||||
# Add simplified replacement message
|
||||
simplified_message = {
|
||||
@@ -316,7 +318,7 @@ def agent_inference(
|
||||
|
||||
# MLLM check the mask one by one
|
||||
for i in range(num_masks):
|
||||
print(f"🔍 Checking mask {i+1}/{num_masks}...")
|
||||
print(f"🔍 Checking mask {i + 1}/{num_masks}...")
|
||||
image_w_mask_i, image_w_zoomed_in_mask_i = visualize(current_outputs, i)
|
||||
|
||||
image_w_zoomed_in_mask_i_path = os.path.join(
|
||||
@@ -361,7 +363,7 @@ def agent_inference(
|
||||
raise ValueError(
|
||||
"Generated text is None, which is unexpected. Please check the Qwen server and the input parameters."
|
||||
)
|
||||
print(f"Generated text for mask {i+1}: {checking_generated_text}")
|
||||
print(f"Generated text for mask {i + 1}: {checking_generated_text}")
|
||||
verdict = (
|
||||
checking_generated_text.split("<verdict>")[-1]
|
||||
.split("</verdict>")[0]
|
||||
@@ -369,11 +371,11 @@ def agent_inference(
|
||||
)
|
||||
if "Accept" in verdict:
|
||||
assert not "Reject" in verdict
|
||||
print(f"Mask {i+1} accepted, keeping it in the outputs.")
|
||||
print(f"Mask {i + 1} accepted, keeping it in the outputs.")
|
||||
masks_to_keep.append(i)
|
||||
elif "Reject" in verdict:
|
||||
assert not "Accept" in verdict
|
||||
print(f"Mask {i+1} rejected, removing it from the outputs.")
|
||||
print(f"Mask {i + 1} rejected, removing it from the outputs.")
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unexpected verdict in generated text: {checking_generated_text}. Expected 'Accept' or 'Reject'."
|
||||
@@ -395,7 +397,7 @@ def agent_inference(
|
||||
sam_output_dir, rf"{LATEST_SAM3_TEXT_PROMPT}.png"
|
||||
).replace(
|
||||
".png",
|
||||
f"_selected_masks_{'-'.join(map(str, [i+1 for i in masks_to_keep]))}.png".replace(
|
||||
f"_selected_masks_{'-'.join(map(str, [i + 1 for i in masks_to_keep]))}.png".replace(
|
||||
"/", "_"
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import base64
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import torch
|
||||
from PIL import Image
|
||||
|
||||
from sam3.model.box_ops import box_xyxy_to_xywh
|
||||
from sam3.train.masks_ops import rle_encode
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import math
|
||||
from enum import IntEnum, unique
|
||||
from typing import List, Tuple, Union
|
||||
@@ -82,9 +84,9 @@ class BoxMode(IntEnum):
|
||||
], "Relative mode not yet supported!"
|
||||
|
||||
if from_mode == BoxMode.XYWHA_ABS and to_mode == BoxMode.XYXY_ABS:
|
||||
assert (
|
||||
arr.shape[-1] == 5
|
||||
), "The last dimension of input shape must be 5 for XYWHA format"
|
||||
assert arr.shape[-1] == 5, (
|
||||
"The last dimension of input shape must be 5 for XYWHA format"
|
||||
)
|
||||
original_dtype = arr.dtype
|
||||
arr = arr.double()
|
||||
|
||||
@@ -242,9 +244,9 @@ class Boxes:
|
||||
if isinstance(item, int):
|
||||
return Boxes(self.tensor[item].view(1, -1))
|
||||
b = self.tensor[item]
|
||||
assert (
|
||||
b.dim() == 2
|
||||
), "Indexing on Boxes with {} failed to return a matrix!".format(item)
|
||||
assert b.dim() == 2, (
|
||||
"Indexing on Boxes with {} failed to return a matrix!".format(item)
|
||||
)
|
||||
return Boxes(b)
|
||||
|
||||
def __len__(self) -> int:
|
||||
@@ -423,7 +425,7 @@ def matched_pairwise_iou(boxes1: Boxes, boxes2: Boxes) -> torch.Tensor:
|
||||
Tensor: iou, sized [N].
|
||||
"""
|
||||
assert len(boxes1) == len(boxes2), (
|
||||
"boxlists should have the same" "number of entries, got {}, {}".format(
|
||||
"boxlists should have the samenumber of entries, got {}, {}".format(
|
||||
len(boxes1), len(boxes2)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""
|
||||
An awesome colormap for really neat visualizations.
|
||||
Copied from Detectron, and removed gray colors.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from typing import Any, List, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import copy
|
||||
import itertools
|
||||
from typing import Any, Iterator, List, Union
|
||||
@@ -11,7 +13,6 @@ from torch import device
|
||||
|
||||
from .boxes import Boxes
|
||||
from .memory import retry_if_cuda_oom
|
||||
|
||||
from .roi_align import ROIAlign
|
||||
|
||||
|
||||
@@ -140,11 +141,11 @@ class BitMasks:
|
||||
if isinstance(item, int):
|
||||
return BitMasks(self.tensor[item].unsqueeze(0))
|
||||
m = self.tensor[item]
|
||||
assert (
|
||||
m.dim() == 3
|
||||
), "Indexing on BitMasks with {} returns a tensor with shape {}!".format(
|
||||
assert m.dim() == 3, (
|
||||
"Indexing on BitMasks with {} returns a tensor with shape {}!".format(
|
||||
item, m.shape
|
||||
)
|
||||
)
|
||||
return BitMasks(m)
|
||||
|
||||
@torch.jit.unused
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""Some utilities for RLE encoding that doesn't require downloading the masks to the cpu"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from torch import nn
|
||||
from torchvision.ops import roi_align
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import math
|
||||
@@ -361,9 +363,9 @@ class RotatedBoxes(Boxes):
|
||||
if isinstance(item, int):
|
||||
return RotatedBoxes(self.tensor[item].view(1, -1))
|
||||
b = self.tensor[item]
|
||||
assert (
|
||||
b.dim() == 2
|
||||
), "Indexing on RotatedBoxes with {} failed to return a matrix!".format(item)
|
||||
assert b.dim() == 2, (
|
||||
"Indexing on RotatedBoxes with {} failed to return a matrix!".format(item)
|
||||
)
|
||||
return RotatedBoxes(b)
|
||||
|
||||
def __len__(self) -> int:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import colorsys
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Tuple
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import colorsys
|
||||
import logging
|
||||
import math
|
||||
@@ -18,7 +20,6 @@ from matplotlib.backends.backend_agg import FigureCanvasAgg
|
||||
from PIL import Image
|
||||
|
||||
from .boxes import Boxes, BoxMode
|
||||
|
||||
from .color_map import random_color
|
||||
from .keypoints import Keypoints
|
||||
from .masks import BitMasks, PolygonMasks
|
||||
@@ -220,9 +221,9 @@ class _PanopticPrediction:
|
||||
empty_ids.append(id)
|
||||
if len(empty_ids) == 0:
|
||||
return np.zeros(self._seg.shape, dtype=np.uint8)
|
||||
assert (
|
||||
len(empty_ids) == 1
|
||||
), ">1 ids corresponds to no labels. This is currently not supported"
|
||||
assert len(empty_ids) == 1, (
|
||||
">1 ids corresponds to no labels. This is currently not supported"
|
||||
)
|
||||
return (self._seg != empty_ids[0]).numpy().astype(np.bool)
|
||||
|
||||
def semantic_masks(self):
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import io
|
||||
import math
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
@@ -39,7 +41,7 @@ def run_single_image_inference(
|
||||
print(f"Output JSON {output_json_path} already exists. Skipping.")
|
||||
return
|
||||
|
||||
print(f"{'-'*30} Starting SAM 3 Agent Session... {'-'*30} ")
|
||||
print(f"{'-' * 30} Starting SAM 3 Agent Session... {'-' * 30} ")
|
||||
agent_history, final_output_dict, rendered_final_output = agent_inference(
|
||||
image_path,
|
||||
text_prompt,
|
||||
@@ -48,7 +50,7 @@ def run_single_image_inference(
|
||||
output_dir=output_dir,
|
||||
debug=debug,
|
||||
)
|
||||
print(f"{'-'*30} End of SAM 3 Agent Session... {'-'*30} ")
|
||||
print(f"{'-' * 30} End of SAM 3 Agent Session... {'-' * 30} ")
|
||||
|
||||
final_output_dict["text_prompt"] = text_prompt
|
||||
final_output_dict["image_path"] = image_path
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pycocotools.mask as mask_utils
|
||||
@@ -71,7 +73,9 @@ def visualize(
|
||||
idx = int(zoom_in_index)
|
||||
num_masks = len(input_json.get("pred_masks", []))
|
||||
if idx < 0 or idx >= num_masks:
|
||||
raise ValueError(f"zoom_in_index {idx} is out of range (0..{num_masks-1}).")
|
||||
raise ValueError(
|
||||
f"zoom_in_index {idx} is out of range (0..{num_masks - 1})."
|
||||
)
|
||||
|
||||
# (1) Replicate zoom_in_and_visualize
|
||||
object_data = {
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import contextlib
|
||||
import copy
|
||||
import json
|
||||
@@ -124,9 +126,9 @@ class COCOCustom(COCO):
|
||||
# MODIFICATION: faster and cached subset check
|
||||
if not hasattr(self, "img_id_set"):
|
||||
self.img_id_set = set(self.getImgIds())
|
||||
assert set(annsImgIds).issubset(
|
||||
self.img_id_set
|
||||
), "Results do not correspond to current coco set"
|
||||
assert set(annsImgIds).issubset(self.img_id_set), (
|
||||
"Results do not correspond to current coco set"
|
||||
)
|
||||
# END MODIFICATION
|
||||
if "caption" in anns[0]:
|
||||
imgIds = set([img["id"] for img in res.dataset["images"]]) & set(
|
||||
@@ -299,9 +301,9 @@ class CGF1Eval(COCOeval):
|
||||
TP = (match_scores >= thresh).sum()
|
||||
FP = len(dt) - TP
|
||||
FN = len(gt) - TP
|
||||
assert (
|
||||
FP >= 0 and FN >= 0
|
||||
), f"FP: {FP}, FN: {FN}, TP: {TP}, match_scores: {match_scores}, len(dt): {len(dt)}, len(gt): {len(gt)}, ious: {ious}"
|
||||
assert FP >= 0 and FN >= 0, (
|
||||
f"FP: {FP}, FN: {FN}, TP: {TP}, match_scores: {match_scores}, len(dt): {len(dt)}, len(gt): {len(gt)}, ious: {ious}"
|
||||
)
|
||||
TPs.append(TP)
|
||||
FPs.append(FP)
|
||||
FNs.append(FN)
|
||||
@@ -597,9 +599,9 @@ class CGF1Evaluator:
|
||||
|
||||
"""
|
||||
assert len(self.coco_gts) > 0, "No ground truth provided for evaluation."
|
||||
assert len(self.coco_gts) == len(
|
||||
self.coco_evals
|
||||
), "Mismatch in number of ground truths and evaluators."
|
||||
assert len(self.coco_gts) == len(self.coco_evals), (
|
||||
"Mismatch in number of ground truths and evaluators."
|
||||
)
|
||||
|
||||
if self.verbose:
|
||||
print(f"Loading predictions from {pred_file}")
|
||||
@@ -666,17 +668,17 @@ class CGF1Evaluator:
|
||||
if len(scorings) == 1:
|
||||
return scorings[0]
|
||||
|
||||
assert (
|
||||
scorings[0].ndim == 3
|
||||
), f"Expecting results in [numCats, numAreas, numImgs] format, got {scorings[0].shape}"
|
||||
assert (
|
||||
scorings[0].shape[0] == 1
|
||||
), f"Expecting a single category, got {scorings[0].shape[0]}"
|
||||
assert scorings[0].ndim == 3, (
|
||||
f"Expecting results in [numCats, numAreas, numImgs] format, got {scorings[0].shape}"
|
||||
)
|
||||
assert scorings[0].shape[0] == 1, (
|
||||
f"Expecting a single category, got {scorings[0].shape[0]}"
|
||||
)
|
||||
|
||||
for scoring in scorings:
|
||||
assert (
|
||||
scoring.shape == scorings[0].shape
|
||||
), f"Shape mismatch: {scoring.shape}, {scorings[0].shape}"
|
||||
assert scoring.shape == scorings[0].shape, (
|
||||
f"Shape mismatch: {scoring.shape}, {scorings[0].shape}"
|
||||
)
|
||||
|
||||
selected_imgs = []
|
||||
for img_id in range(scorings[0].shape[-1]):
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""
|
||||
COCO evaluator that works in distributed mode.
|
||||
|
||||
@@ -16,19 +18,15 @@ import os
|
||||
import pickle
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
import pycocotools.mask as mask_utils
|
||||
import torch
|
||||
from iopath.common.file_io import g_pathmgr
|
||||
from pycocotools.coco import COCO
|
||||
from pycocotools.cocoeval import COCOeval
|
||||
|
||||
from sam3.train.masks_ops import rle_encode
|
||||
|
||||
from sam3.train.utils.distributed import (
|
||||
all_gather,
|
||||
gather_to_rank_0_via_filesys,
|
||||
@@ -753,9 +751,9 @@ def loadRes(self, resFile):
|
||||
anns = resFile
|
||||
assert type(anns) == list, "results in not an array of objects"
|
||||
annsImgIds = [ann["image_id"] for ann in anns]
|
||||
assert set(annsImgIds) == (
|
||||
set(annsImgIds) & set(self.getImgIds())
|
||||
), "Results do not correspond to current coco set"
|
||||
assert set(annsImgIds) == (set(annsImgIds) & set(self.getImgIds())), (
|
||||
"Results do not correspond to current coco set"
|
||||
)
|
||||
if "caption" in anns[0]:
|
||||
imgIds = set([img["id"] for img in res.dataset["images"]]) & set(
|
||||
[ann["image_id"] for ann in anns]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""
|
||||
This evaluator is meant for regular COCO mAP evaluation, for example on the COCO val set.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""
|
||||
Self-contained COCO JSON re-indexing function that creates temporary files.
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""
|
||||
COCO prediction dumper for distributed training.
|
||||
|
||||
@@ -81,9 +83,9 @@ class PredictionDumper:
|
||||
self.merge_predictions = merge_predictions
|
||||
self.pred_file_evaluators = pred_file_evaluators
|
||||
if self.pred_file_evaluators is not None:
|
||||
assert (
|
||||
merge_predictions
|
||||
), "merge_predictions must be True if pred_file_evaluators are provided"
|
||||
assert merge_predictions, (
|
||||
"merge_predictions must be True if pred_file_evaluators are provided"
|
||||
)
|
||||
assert self.dump_dir is not None, "dump_dir must be provided"
|
||||
|
||||
if is_main_process():
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
import json
|
||||
import os
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""
|
||||
This evaluator is based upon COCO evaluation, but evaluates the model in a "demo" setting.
|
||||
This means that the model's predictions are thresholded and evaluated as "hard" predictions.
|
||||
@@ -11,11 +13,9 @@ from typing import Optional
|
||||
import numpy as np
|
||||
import pycocotools.mask as maskUtils
|
||||
from pycocotools.cocoeval import COCOeval
|
||||
|
||||
from sam3.eval.coco_eval import CocoEvaluator
|
||||
from sam3.train.masks_ops import compute_F_measure
|
||||
from sam3.train.utils.distributed import is_main_process
|
||||
|
||||
from scipy.optimize import linear_sum_assignment
|
||||
|
||||
|
||||
@@ -154,9 +154,9 @@ class DemoEval(COCOeval):
|
||||
TP = (match_scores >= thresh).sum()
|
||||
FP = len(dt) - TP
|
||||
FN = len(gt) - TP
|
||||
assert (
|
||||
FP >= 0 and FN >= 0
|
||||
), f"FP: {FP}, FN: {FN}, TP: {TP}, match_scores: {match_scores}, len(dt): {len(dt)}, len(gt): {len(gt)}, ious: {ious}"
|
||||
assert FP >= 0 and FN >= 0, (
|
||||
f"FP: {FP}, FN: {FN}, TP: {TP}, match_scores: {match_scores}, len(dt): {len(dt)}, len(gt): {len(gt)}, ious: {ious}"
|
||||
)
|
||||
TPs.append(TP)
|
||||
FPs.append(FP)
|
||||
FNs.append(FN)
|
||||
@@ -526,17 +526,17 @@ class DemoEvaluator(CocoEvaluator):
|
||||
if len(scorings) == 1:
|
||||
return scorings[0]
|
||||
|
||||
assert (
|
||||
scorings[0].ndim == 3
|
||||
), f"Expecting results in [numCats, numAreas, numImgs] format, got {scorings[0].shape}"
|
||||
assert (
|
||||
scorings[0].shape[0] == 1
|
||||
), f"Expecting a single category, got {scorings[0].shape[0]}"
|
||||
assert scorings[0].ndim == 3, (
|
||||
f"Expecting results in [numCats, numAreas, numImgs] format, got {scorings[0].shape}"
|
||||
)
|
||||
assert scorings[0].shape[0] == 1, (
|
||||
f"Expecting a single category, got {scorings[0].shape[0]}"
|
||||
)
|
||||
|
||||
for scoring in scorings:
|
||||
assert (
|
||||
scoring.shape == scorings[0].shape
|
||||
), f"Shape mismatch: {scoring.shape}, {scorings[0].shape}"
|
||||
assert scoring.shape == scorings[0].shape, (
|
||||
f"Shape mismatch: {scoring.shape}, {scorings[0].shape}"
|
||||
)
|
||||
|
||||
selected_imgs = []
|
||||
for img_id in range(scorings[0].shape[-1]):
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""run_youtube_vis.py
|
||||
Run example:
|
||||
run_youtube_vis.py --USE_PARALLEL False --METRICS HOTA --TRACKERS_TO_EVAL STEm_Seg
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from . import datasets, metrics, utils
|
||||
from .eval import Evaluator
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import inspect
|
||||
from functools import wraps
|
||||
from time import perf_counter
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from .tao_ow import TAO_OW
|
||||
from .youtube_vis import YouTubeVIS
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
# note: this file has been modified from its original version in TrackEval in
|
||||
# https://github.com/JonathonLuiten/TrackEval/blob/master/trackeval/datasets/youtube_vis.py
|
||||
# to support the following:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
@@ -253,9 +255,10 @@ class Evaluator:
|
||||
if show_progressbar and TQDM_IMPORTED:
|
||||
seq_list_sorted = sorted(seq_list)
|
||||
|
||||
with Pool(config["NUM_PARALLEL_CORES"]) as pool, tqdm.tqdm(
|
||||
total=len(seq_list)
|
||||
) as pbar:
|
||||
with (
|
||||
Pool(config["NUM_PARALLEL_CORES"]) as pool,
|
||||
tqdm.tqdm(total=len(seq_list)) as pbar,
|
||||
):
|
||||
_eval_sequence = partial(
|
||||
eval_sequence,
|
||||
dataset=dataset,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from .count import Count
|
||||
from .hota import HOTA
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from .. import _timing
|
||||
from ._base_metric import _BaseMetric
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import os
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""Postprocessors class to transform MDETR output according to the downstream task"""
|
||||
|
||||
import dataclasses
|
||||
@@ -81,9 +83,9 @@ class PostProcessImage(nn.Module):
|
||||
ret_tensordict: Experimental argument. If true, return a tensordict.TensorDict instead of a list of dictionaries for easier manipulation.
|
||||
"""
|
||||
if ret_tensordict:
|
||||
assert (
|
||||
consistent is True
|
||||
), "We don't support returning TensorDict if the outputs have different shapes" # NOTE: It's possible but we don't support it.
|
||||
assert consistent is True, (
|
||||
"We don't support returning TensorDict if the outputs have different shapes"
|
||||
) # NOTE: It's possible but we don't support it.
|
||||
assert self.detection_threshold <= 0.0, "TODO: implement?"
|
||||
try:
|
||||
from tensordict import TensorDict
|
||||
@@ -116,7 +118,9 @@ class PostProcessImage(nn.Module):
|
||||
|
||||
if boxes is None:
|
||||
assert out_masks is not None
|
||||
assert not ret_tensordict, "We don't support returning TensorDict if the output does not contain boxes"
|
||||
assert not ret_tensordict, (
|
||||
"We don't support returning TensorDict if the output does not contain boxes"
|
||||
)
|
||||
B = len(out_masks)
|
||||
boxes = [None] * B
|
||||
scores = [None] * B
|
||||
@@ -416,9 +420,9 @@ class PostProcessAPIVideo(PostProcessImage):
|
||||
if video_id == -1:
|
||||
video_id = unique_vid_id.item()
|
||||
else:
|
||||
assert (
|
||||
video_id == unique_vid_id.item()
|
||||
), "We can only postprocess one video per datapoint"
|
||||
assert video_id == unique_vid_id.item(), (
|
||||
"We can only postprocess one video per datapoint"
|
||||
)
|
||||
# keeping track of which objects appear in the current frame
|
||||
obj_ids_per_frame = frame_outs["pred_object_ids"]
|
||||
assert obj_ids_per_frame.size(-1) == frame_outs["pred_logits"].size(-2)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# fmt: off
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from . import config, datasets, metrics, utils
|
||||
from .eval import Evaluator
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# fmt: off
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import inspect
|
||||
from functools import wraps
|
||||
from time import perf_counter
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# fmt: off
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""Config."""
|
||||
import argparse
|
||||
import os
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# fmt: off
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
"""Datasets."""
|
||||
from .coco import COCO
|
||||
from .tao import TAO
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# fmt: off
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# fmt: off
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""COCO Dataset."""
|
||||
import copy
|
||||
import itertools
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# fmt: off
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""TAO Dataset."""
|
||||
import copy
|
||||
import itertools
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# fmt: off
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import copy
|
||||
import os
|
||||
import pickle
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# fmt: off
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from .teta import TETA
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# fmt: off
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# fmt: off
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""Track Every Thing Accuracy metric."""
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# fmt: off
|
||||
# flake8: noqa
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import csv
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
# (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
@@ -93,9 +95,9 @@ class YTVIS(COCO):
|
||||
anns = resFile
|
||||
assert type(anns) == list, "results is not an array of objects"
|
||||
annsImgIds = [ann["image_id"] for ann in anns]
|
||||
assert set(annsImgIds) == (
|
||||
set(annsImgIds) & set(self.getImgIds())
|
||||
), "Results do not correspond to current coco set"
|
||||
assert set(annsImgIds) == (set(annsImgIds) & set(self.getImgIds())), (
|
||||
"Results do not correspond to current coco set"
|
||||
)
|
||||
if "bboxes" in anns[0] and not anns[0]["bboxes"] == []:
|
||||
res.dataset["categories"] = copy.deepcopy(self.dataset["categories"])
|
||||
for id, ann in enumerate(anns):
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
import copy
|
||||
import gc
|
||||
import logging
|
||||
@@ -107,9 +109,7 @@ class YTVISevalMixin:
|
||||
) # Num preds x Num GTS x Num frames
|
||||
inter = inter.sum(-1)
|
||||
union = union.sum(-1)
|
||||
assert (
|
||||
union > 0
|
||||
).all(), (
|
||||
assert (union > 0).all(), (
|
||||
"There exists a tracklet with zero GTs across time. This is suspicious"
|
||||
)
|
||||
return inter / union
|
||||
@@ -134,9 +134,9 @@ class YTVISevalMixin:
|
||||
iou = inter / union
|
||||
assert iou >= 0 and iou <= 1, "Encountered an error in IoU computation"
|
||||
else:
|
||||
assert np.isclose(inter, 0) and np.isclose(
|
||||
union, 0
|
||||
), "Encountered an error in IoU computation"
|
||||
assert np.isclose(inter, 0) and np.isclose(union, 0), (
|
||||
"Encountered an error in IoU computation"
|
||||
)
|
||||
iou = 1
|
||||
return iou
|
||||
|
||||
@@ -204,16 +204,16 @@ class YTVISResultsWriter:
|
||||
if len(prediction) == 0:
|
||||
continue
|
||||
for k in ["boxes", "scores", "labels"]:
|
||||
assert (
|
||||
k in prediction
|
||||
), f"Expected predictions to have `{k}` key, available keys are {prediction.keys()}"
|
||||
assert k in prediction, (
|
||||
f"Expected predictions to have `{k}` key, available keys are {prediction.keys()}"
|
||||
)
|
||||
if self.save_per_frame_scores:
|
||||
assert (
|
||||
"per_frame_scores" in prediction
|
||||
), f"Expected predictions to have `per_frame_scores` key, available keys are {prediction.keys()}"
|
||||
assert xor(
|
||||
"masks" in prediction, "masks_rle" in prediction
|
||||
), f"Expected predictions to have either `masks` key or `masks_rle` key, available keys are {prediction.keys()}"
|
||||
assert "per_frame_scores" in prediction, (
|
||||
f"Expected predictions to have `per_frame_scores` key, available keys are {prediction.keys()}"
|
||||
)
|
||||
assert xor("masks" in prediction, "masks_rle" in prediction), (
|
||||
f"Expected predictions to have either `masks` key or `masks_rle` key, available keys are {prediction.keys()}"
|
||||
)
|
||||
|
||||
boxes = prediction["boxes"]
|
||||
boxes = convert_to_xywh(boxes).tolist()
|
||||
@@ -221,9 +221,9 @@ class YTVISResultsWriter:
|
||||
labels = prediction["labels"].tolist()
|
||||
if "masks" in prediction:
|
||||
masks = prediction["masks"].squeeze(2)
|
||||
assert (
|
||||
masks.ndim == 4
|
||||
), "Expected masks to be of shape(N_preds,T_frames,H,W)"
|
||||
assert masks.ndim == 4, (
|
||||
"Expected masks to be of shape(N_preds,T_frames,H,W)"
|
||||
)
|
||||
|
||||
areas = [mask.flatten(1).sum(1).tolist() for mask in masks]
|
||||
rles = [rle_encode(masklet) for masklet in masks]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -40,9 +42,9 @@ def get_logger(name, level=logging.INFO):
|
||||
"""A command line logger."""
|
||||
if "LOG_LEVEL" in os.environ:
|
||||
level = os.environ["LOG_LEVEL"].upper()
|
||||
assert (
|
||||
level in LOG_LEVELS
|
||||
), f"Invalid LOG_LEVEL: {level}, must be one of {list(LOG_LEVELS.keys())}"
|
||||
assert level in LOG_LEVELS, (
|
||||
f"Invalid LOG_LEVEL: {level}, must be one of {list(LOG_LEVELS.keys())}"
|
||||
)
|
||||
level = LOG_LEVELS[level]
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(level)
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import inspect
|
||||
from functools import wraps
|
||||
from typing import Callable, TypeVar, Union
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
"""
|
||||
Utilities for bounding box manipulation and GIoU.
|
||||
"""
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
"""
|
||||
Misc functions, including distributed helpers.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import re
|
||||
|
||||
from dataclasses import dataclass, field as field_ptr_behaviour, fields, is_dataclass
|
||||
from typing import Any, get_args, get_origin, List, Mapping, Optional, Sequence, Union
|
||||
|
||||
@@ -27,9 +28,9 @@ def interpolate(
|
||||
input, size, scale_factor, mode, align_corners
|
||||
)
|
||||
|
||||
assert (
|
||||
input.shape[0] != 0 or input.shape[1] != 0
|
||||
), "At least one of the two first dimensions must be non zero"
|
||||
assert input.shape[0] != 0 or input.shape[1] != 0, (
|
||||
"At least one of the two first dimensions must be non zero"
|
||||
)
|
||||
|
||||
if input.shape[1] == 0:
|
||||
# Pytorch doesn't support null dimension on the channel dimension, so we transpose to fake a null batch dim
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
"""
|
||||
Transformer decoder.
|
||||
Inspired from Pytorch's version, adds the pre-norm variant
|
||||
@@ -7,18 +9,13 @@ Inspired from Pytorch's version, adds the pre-norm variant
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
|
||||
from sam3.sam.transformer import RoPEAttention
|
||||
|
||||
from torch import nn, Tensor
|
||||
from torchvision.ops.roi_align import RoIAlign
|
||||
|
||||
from .act_ckpt_utils import activation_ckpt_wrapper
|
||||
|
||||
from .box_ops import box_cxcywh_to_xyxy
|
||||
|
||||
from .model_misc import (
|
||||
gen_sineembed_for_position,
|
||||
get_activation_fn,
|
||||
@@ -442,9 +439,9 @@ class TransformerDecoder(nn.Module):
|
||||
- valid_ratios/spatial_shapes: bs, nlevel, 2
|
||||
"""
|
||||
if memory_mask is not None:
|
||||
assert (
|
||||
self.boxRPB == "none"
|
||||
), "inputting a memory_mask in the presence of boxRPB is unexpected/not implemented"
|
||||
assert self.boxRPB == "none", (
|
||||
"inputting a memory_mask in the presence of boxRPB is unexpected/not implemented"
|
||||
)
|
||||
|
||||
apply_dac = apply_dac if apply_dac is not None else self.dac
|
||||
if apply_dac:
|
||||
@@ -514,18 +511,18 @@ class TransformerDecoder(nn.Module):
|
||||
query_pos = self.ref_point_head(query_sine_embed) # nq, bs, d_model
|
||||
|
||||
if self.boxRPB != "none" and reference_boxes is not None:
|
||||
assert (
|
||||
spatial_shapes.shape[0] == 1
|
||||
), "only single scale support implemented"
|
||||
assert spatial_shapes.shape[0] == 1, (
|
||||
"only single scale support implemented"
|
||||
)
|
||||
memory_mask = self._get_rpb_matrix(
|
||||
reference_boxes,
|
||||
(spatial_shapes[0, 0], spatial_shapes[0, 1]),
|
||||
)
|
||||
memory_mask = memory_mask.flatten(0, 1) # (bs*n_heads, nq, H*W)
|
||||
if self.training:
|
||||
assert (
|
||||
self.use_act_checkpoint
|
||||
), "Activation checkpointing not enabled in the decoder"
|
||||
assert self.use_act_checkpoint, (
|
||||
"Activation checkpointing not enabled in the decoder"
|
||||
)
|
||||
output, presence_out = activation_ckpt_wrapper(layer)(
|
||||
tgt=output,
|
||||
tgt_query_pos=query_pos,
|
||||
@@ -674,9 +671,9 @@ class TransformerEncoderCrossAttention(nn.Module):
|
||||
src_pos[0],
|
||||
)
|
||||
|
||||
assert (
|
||||
src.shape[1] == prompt.shape[1]
|
||||
), "Batch size must be the same for src and prompt"
|
||||
assert src.shape[1] == prompt.shape[1], (
|
||||
"Batch size must be the same for src and prompt"
|
||||
)
|
||||
|
||||
output = src
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""Triton kernel for euclidean distance transform (EDT)"""
|
||||
|
||||
import torch
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
# Based on https://github.com/IDEA-Research/GroundingDINO
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
@@ -320,9 +322,9 @@ class TransformerEncoder(nn.Module):
|
||||
return reference_points
|
||||
|
||||
def _prepare_multilevel_features(self, srcs, masks, pos_embeds):
|
||||
assert (
|
||||
len(srcs) == self.num_feature_levels
|
||||
), "mismatch between expected and received # of feature levels"
|
||||
assert len(srcs) == self.num_feature_levels, (
|
||||
"mismatch between expected and received # of feature levels"
|
||||
)
|
||||
|
||||
src_flatten = []
|
||||
mask_flatten = []
|
||||
@@ -404,9 +406,9 @@ class TransformerEncoder(nn.Module):
|
||||
- spatial_shapes: Spatial dimensions of each feature level
|
||||
- valid_ratios: Valid ratios for each feature level
|
||||
"""
|
||||
assert (
|
||||
len(src) == self.num_feature_levels
|
||||
), "must be equal to num_feature_levels"
|
||||
assert len(src) == self.num_feature_levels, (
|
||||
"must be equal to num_feature_levels"
|
||||
)
|
||||
if src_key_padding_masks is not None:
|
||||
assert len(src_key_padding_masks) == self.num_feature_levels
|
||||
if pos is not None:
|
||||
@@ -536,9 +538,9 @@ class TransformerEncoderFusion(TransformerEncoder):
|
||||
else None
|
||||
)
|
||||
else:
|
||||
assert all(
|
||||
x.dim == 4 for x in src
|
||||
), "expected list of (bs, c, h, w) tensors"
|
||||
assert all(x.dim == 4 for x in src), (
|
||||
"expected list of (bs, c, h, w) tensors"
|
||||
)
|
||||
|
||||
if self.add_pooled_text_to_img_feat:
|
||||
# Fusion: Add mean pooled text to image features
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
import torch
|
||||
@@ -9,7 +11,6 @@ from typing_extensions import override
|
||||
|
||||
from .act_ckpt_utils import activation_ckpt_wrapper
|
||||
from .box_ops import box_cxcywh_to_xyxy
|
||||
|
||||
from .model_misc import get_clones
|
||||
|
||||
|
||||
@@ -146,54 +147,42 @@ class Prompt:
|
||||
)
|
||||
|
||||
# Dimension checks
|
||||
assert (
|
||||
box_embeddings is not None
|
||||
and list(box_embeddings.shape[:2])
|
||||
== [
|
||||
assert box_embeddings is not None and list(box_embeddings.shape[:2]) == [
|
||||
box_seq_len,
|
||||
bs,
|
||||
]
|
||||
), f"Wrong dimension for box embeddings. Expected [{box_seq_len}, {bs}, *] got {box_embeddings.shape}"
|
||||
assert (
|
||||
box_mask is not None
|
||||
and list(box_mask.shape)
|
||||
== [
|
||||
], (
|
||||
f"Wrong dimension for box embeddings. Expected [{box_seq_len}, {bs}, *] got {box_embeddings.shape}"
|
||||
)
|
||||
assert box_mask is not None and list(box_mask.shape) == [
|
||||
bs,
|
||||
box_seq_len,
|
||||
]
|
||||
), f"Wrong dimension for box mask. Expected [{bs}, {box_seq_len}] got {box_mask.shape}"
|
||||
assert (
|
||||
point_embeddings is not None
|
||||
and list(point_embeddings.shape[:2])
|
||||
== [
|
||||
], (
|
||||
f"Wrong dimension for box mask. Expected [{bs}, {box_seq_len}] got {box_mask.shape}"
|
||||
)
|
||||
assert point_embeddings is not None and list(point_embeddings.shape[:2]) == [
|
||||
point_seq_len,
|
||||
bs,
|
||||
]
|
||||
), f"Wrong dimension for point embeddings. Expected [{point_seq_len}, {bs}, *] got {point_embeddings.shape}"
|
||||
assert (
|
||||
point_mask is not None
|
||||
and list(point_mask.shape)
|
||||
== [
|
||||
], (
|
||||
f"Wrong dimension for point embeddings. Expected [{point_seq_len}, {bs}, *] got {point_embeddings.shape}"
|
||||
)
|
||||
assert point_mask is not None and list(point_mask.shape) == [
|
||||
bs,
|
||||
point_seq_len,
|
||||
]
|
||||
), f"Wrong dimension for point mask. Expected [{bs}, {point_seq_len}] got {point_mask.shape}"
|
||||
assert (
|
||||
box_labels is not None
|
||||
and list(box_labels.shape)
|
||||
== [
|
||||
], (
|
||||
f"Wrong dimension for point mask. Expected [{bs}, {point_seq_len}] got {point_mask.shape}"
|
||||
)
|
||||
assert box_labels is not None and list(box_labels.shape) == [
|
||||
box_seq_len,
|
||||
bs,
|
||||
]
|
||||
), f"Wrong dimension for box labels. Expected [{box_seq_len}, {bs}] got {box_labels.shape}"
|
||||
assert (
|
||||
point_labels is not None
|
||||
and list(point_labels.shape)
|
||||
== [
|
||||
], (
|
||||
f"Wrong dimension for box labels. Expected [{box_seq_len}, {bs}] got {box_labels.shape}"
|
||||
)
|
||||
assert point_labels is not None and list(point_labels.shape) == [
|
||||
point_seq_len,
|
||||
bs,
|
||||
]
|
||||
), f"Wrong dimension for point labels. Expected [{point_seq_len}, {bs}] got {point_labels.shape}"
|
||||
], (
|
||||
f"Wrong dimension for point labels. Expected [{point_seq_len}, {bs}] got {point_labels.shape}"
|
||||
)
|
||||
assert (
|
||||
# Allowed to be None, we leave it to the encoder to check for validity before encoding.
|
||||
mask_embeddings is None
|
||||
@@ -202,41 +191,41 @@ class Prompt:
|
||||
mask_seq_len,
|
||||
bs,
|
||||
]
|
||||
), f"Wrong dimension for mask embeddings. Expected [{mask_seq_len}, {bs}, *] got {mask_embeddings.shape}"
|
||||
assert (
|
||||
mask_mask is None
|
||||
or list(mask_mask.shape)
|
||||
== [
|
||||
), (
|
||||
f"Wrong dimension for mask embeddings. Expected [{mask_seq_len}, {bs}, *] got {mask_embeddings.shape}"
|
||||
)
|
||||
assert mask_mask is None or list(mask_mask.shape) == [
|
||||
bs,
|
||||
mask_seq_len,
|
||||
]
|
||||
), f"Wrong dimension for mask attn. mask. Expected [{bs}, {mask_seq_len}] got {mask_mask.shape}"
|
||||
], (
|
||||
f"Wrong dimension for mask attn. mask. Expected [{bs}, {mask_seq_len}] got {mask_mask.shape}"
|
||||
)
|
||||
|
||||
# Device checks
|
||||
assert (
|
||||
box_embeddings is not None and box_embeddings.device == device
|
||||
), f"Expected box embeddings to be on device {device}, got {box_embeddings.device}"
|
||||
assert (
|
||||
box_mask is not None and box_mask.device == device
|
||||
), f"Expected box mask to be on device {device}, got {box_mask.device}"
|
||||
assert (
|
||||
box_labels is not None and box_labels.device == device
|
||||
), f"Expected box labels to be on device {device}, got {box_labels.device}"
|
||||
assert (
|
||||
point_embeddings is not None and point_embeddings.device == device
|
||||
), f"Expected point embeddings to be on device {device}, got {point_embeddings.device}"
|
||||
assert (
|
||||
point_mask is not None and point_mask.device == device
|
||||
), f"Expected point mask to be on device {device}, got {point_mask.device}"
|
||||
assert (
|
||||
point_labels is not None and point_labels.device == device
|
||||
), f"Expected point labels to be on device {device}, got {point_labels.device}"
|
||||
assert (
|
||||
mask_embeddings is None or mask_embeddings.device == device
|
||||
), f"Expected mask embeddings to be on device {device}, got {mask_embeddings.device}"
|
||||
assert (
|
||||
mask_mask is None or mask_mask.device == device
|
||||
), f"Expected mask attn. mask to be on device {device}, got {mask_mask.device}"
|
||||
assert box_embeddings is not None and box_embeddings.device == device, (
|
||||
f"Expected box embeddings to be on device {device}, got {box_embeddings.device}"
|
||||
)
|
||||
assert box_mask is not None and box_mask.device == device, (
|
||||
f"Expected box mask to be on device {device}, got {box_mask.device}"
|
||||
)
|
||||
assert box_labels is not None and box_labels.device == device, (
|
||||
f"Expected box labels to be on device {device}, got {box_labels.device}"
|
||||
)
|
||||
assert point_embeddings is not None and point_embeddings.device == device, (
|
||||
f"Expected point embeddings to be on device {device}, got {point_embeddings.device}"
|
||||
)
|
||||
assert point_mask is not None and point_mask.device == device, (
|
||||
f"Expected point mask to be on device {device}, got {point_mask.device}"
|
||||
)
|
||||
assert point_labels is not None and point_labels.device == device, (
|
||||
f"Expected point labels to be on device {device}, got {point_labels.device}"
|
||||
)
|
||||
assert mask_embeddings is None or mask_embeddings.device == device, (
|
||||
f"Expected mask embeddings to be on device {device}, got {mask_embeddings.device}"
|
||||
)
|
||||
assert mask_mask is None or mask_mask.device == device, (
|
||||
f"Expected mask attn. mask to be on device {device}, got {mask_mask.device}"
|
||||
)
|
||||
|
||||
self.box_embeddings = box_embeddings
|
||||
self.point_embeddings = point_embeddings
|
||||
@@ -262,30 +251,30 @@ class Prompt:
|
||||
if point_embeddings is not None:
|
||||
point_seq_len = point_embeddings.shape[0]
|
||||
if bs is not None:
|
||||
assert (
|
||||
bs == point_embeddings.shape[1]
|
||||
), f"Batch size mismatch between box and point embeddings. Got {bs} and {point_embeddings.shape[1]}."
|
||||
assert bs == point_embeddings.shape[1], (
|
||||
f"Batch size mismatch between box and point embeddings. Got {bs} and {point_embeddings.shape[1]}."
|
||||
)
|
||||
else:
|
||||
bs = point_embeddings.shape[1]
|
||||
if device is not None:
|
||||
assert (
|
||||
device == point_embeddings.device
|
||||
), "Device mismatch between box and point embeddings"
|
||||
assert device == point_embeddings.device, (
|
||||
"Device mismatch between box and point embeddings"
|
||||
)
|
||||
else:
|
||||
device = point_embeddings.device
|
||||
|
||||
if mask_embeddings is not None:
|
||||
mask_seq_len = mask_embeddings.shape[0]
|
||||
if bs is not None:
|
||||
assert (
|
||||
bs == mask_embeddings.shape[1]
|
||||
), f"Batch size mismatch between box/point and mask embedding. Got {bs} and {mask_embeddings.shape[1]}"
|
||||
assert bs == mask_embeddings.shape[1], (
|
||||
f"Batch size mismatch between box/point and mask embedding. Got {bs} and {mask_embeddings.shape[1]}"
|
||||
)
|
||||
else:
|
||||
bs = mask_embeddings.shape[1]
|
||||
if device is not None:
|
||||
assert (
|
||||
device == mask_embeddings.device
|
||||
), "Device mismatch between box/point and mask embeddings."
|
||||
assert device == mask_embeddings.device, (
|
||||
"Device mismatch between box/point and mask embeddings."
|
||||
)
|
||||
else:
|
||||
device = mask_embeddings.device
|
||||
|
||||
@@ -537,9 +526,9 @@ class SequenceGeometryEncoder(nn.Module):
|
||||
if add_cls:
|
||||
self.cls_embed = torch.nn.Embedding(1, self.d_model)
|
||||
|
||||
assert (
|
||||
points_direct_project or points_pos_enc or points_pool
|
||||
), "Error: need at least one way to encode points"
|
||||
assert points_direct_project or points_pos_enc or points_pool, (
|
||||
"Error: need at least one way to encode points"
|
||||
)
|
||||
assert (
|
||||
encode_boxes_as_points
|
||||
or boxes_direct_project
|
||||
@@ -581,16 +570,16 @@ class SequenceGeometryEncoder(nn.Module):
|
||||
|
||||
self.encode = None
|
||||
if num_layers > 0:
|
||||
assert (
|
||||
add_cls
|
||||
), "It's currently highly recommended to add a CLS when using a transformer"
|
||||
assert add_cls, (
|
||||
"It's currently highly recommended to add a CLS when using a transformer"
|
||||
)
|
||||
self.encode = get_clones(layer, num_layers)
|
||||
self.encode_norm = nn.LayerNorm(self.d_model)
|
||||
|
||||
if mask_encoder is not None:
|
||||
assert isinstance(
|
||||
mask_encoder, MaskEncoder
|
||||
), f"Expected mask_encoder of type MaskEncoder. Got {type(mask_encoder)}."
|
||||
assert isinstance(mask_encoder, MaskEncoder), (
|
||||
f"Expected mask_encoder of type MaskEncoder. Got {type(mask_encoder)}."
|
||||
)
|
||||
if add_mask_label:
|
||||
self.mask_label_embed = torch.nn.Embedding(2, self.d_model)
|
||||
self.add_mask_label = add_mask_label
|
||||
@@ -699,16 +688,15 @@ class SequenceGeometryEncoder(nn.Module):
|
||||
img_feats: torch.Tensor = None,
|
||||
):
|
||||
n_masks, bs = masks.shape[:2]
|
||||
assert (
|
||||
n_masks == 1
|
||||
), "We assume one mask per prompt for now. Code should still be functional if this assertion is removed."
|
||||
assert (
|
||||
list(attn_mask.shape)
|
||||
== [
|
||||
assert n_masks == 1, (
|
||||
"We assume one mask per prompt for now. Code should still be functional if this assertion is removed."
|
||||
)
|
||||
assert list(attn_mask.shape) == [
|
||||
bs,
|
||||
n_masks,
|
||||
]
|
||||
), f"Expected attn_mask to be of shape {bs}x{n_masks}. Got {list(attn_mask.shape)}."
|
||||
], (
|
||||
f"Expected attn_mask to be of shape {bs}x{n_masks}. Got {list(attn_mask.shape)}."
|
||||
)
|
||||
masks, pos = self.mask_encoder(
|
||||
masks=masks.flatten(0, 1).float(),
|
||||
pix_feat=img_feats,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import queue
|
||||
@@ -11,9 +13,7 @@ import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import torchvision.transforms.functional as TF
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from sam3.logger import get_logger
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import math
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
@@ -246,7 +248,9 @@ class UniversalSegmentationHead(SegmentationHead):
|
||||
self.d_model = hidden_dim
|
||||
|
||||
if dot_product_scorer is not None:
|
||||
assert presence_head, "Specifying a dot product scorer without a presence head is likely a mistake"
|
||||
assert presence_head, (
|
||||
"Specifying a dot product scorer without a presence head is likely a mistake"
|
||||
)
|
||||
|
||||
self.presence_head = None
|
||||
if presence_head:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import math
|
||||
from typing import Tuple
|
||||
|
||||
@@ -60,9 +62,9 @@ class SimpleMaskDownSampler(nn.Module):
|
||||
self.encoder.append(nn.Conv2d(mask_out_chans, embed_dim, kernel_size=1))
|
||||
self.interpol_size = interpol_size
|
||||
if self.interpol_size is not None:
|
||||
assert isinstance(
|
||||
self.interpol_size, (list, tuple)
|
||||
), f"Unsupported type {type(self.interpol_size)}. Should be a list or tuple."
|
||||
assert isinstance(self.interpol_size, (list, tuple)), (
|
||||
f"Unsupported type {type(self.interpol_size)}. Should be a list or tuple."
|
||||
)
|
||||
self.interpol_size = list(interpol_size)
|
||||
assert len(self.interpol_size) == 2
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""Various utility models"""
|
||||
|
||||
import copy
|
||||
@@ -328,9 +330,9 @@ class SAM3Output(list):
|
||||
self.output = output
|
||||
else:
|
||||
self.output = []
|
||||
assert isinstance(
|
||||
iter_mode, SAM3Output.IterMode
|
||||
), f"iter_mode shoulf be of enum type 'SAM3Output.IterMode'. Got {type(iter_mode)}"
|
||||
assert isinstance(iter_mode, SAM3Output.IterMode), (
|
||||
f"iter_mode shoulf be of enum type 'SAM3Output.IterMode'. Got {type(iter_mode)}"
|
||||
)
|
||||
|
||||
self.iter_mode = iter_mode
|
||||
# We create a weak reference to self to be used in the lambda functions.
|
||||
@@ -409,9 +411,9 @@ class SAM3Output(list):
|
||||
return SAM3Output._IterationMode(model_output=model_output, iter_mode=iter_mode)
|
||||
|
||||
def append(self, item: list):
|
||||
assert isinstance(
|
||||
item, list
|
||||
), f"Only list items are supported. Got {type(item)}"
|
||||
assert isinstance(item, list), (
|
||||
f"Only list items are supported. Got {type(item)}"
|
||||
)
|
||||
self.output.append(item)
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""Necks are the interface between a vision backbone and the rest of the detection model"""
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import math
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
# All rights reserved.
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
import torch.nn as nn
|
||||
from PIL.Image import Image
|
||||
|
||||
from sam3.model.sam3_tracker_base import Sam3TrackerBase
|
||||
from sam3.model.utils.sam1_utils import SAM2Transforms
|
||||
|
||||
@@ -95,9 +94,9 @@ class SAM3InteractiveImagePredictor(nn.Module):
|
||||
input_image = self._transforms(image)
|
||||
input_image = input_image[None, ...].to(self.device)
|
||||
|
||||
assert (
|
||||
len(input_image.shape) == 4 and input_image.shape[1] == 3
|
||||
), f"input_image must be of size 1x3xHxW, got {input_image.shape}"
|
||||
assert len(input_image.shape) == 4 and input_image.shape[1] == 3, (
|
||||
f"input_image must be of size 1x3xHxW, got {input_image.shape}"
|
||||
)
|
||||
logging.info("Computing image embeddings for the provided image...")
|
||||
backbone_out = self.model.forward_image(input_image)
|
||||
(
|
||||
@@ -134,17 +133,17 @@ class SAM3InteractiveImagePredictor(nn.Module):
|
||||
assert isinstance(image_list, list)
|
||||
self._orig_hw = []
|
||||
for image in image_list:
|
||||
assert isinstance(
|
||||
image, np.ndarray
|
||||
), "Images are expected to be an np.ndarray in RGB format, and of shape HWC"
|
||||
assert isinstance(image, np.ndarray), (
|
||||
"Images are expected to be an np.ndarray in RGB format, and of shape HWC"
|
||||
)
|
||||
self._orig_hw.append(image.shape[:2])
|
||||
# Transform the image to the form expected by the model
|
||||
img_batch = self._transforms.forward_batch(image_list)
|
||||
img_batch = img_batch.to(self.device)
|
||||
batch_size = img_batch.shape[0]
|
||||
assert (
|
||||
len(img_batch.shape) == 4 and img_batch.shape[1] == 3
|
||||
), f"img_batch must be of size Bx3xHxW, got {img_batch.shape}"
|
||||
assert len(img_batch.shape) == 4 and img_batch.shape[1] == 3, (
|
||||
f"img_batch must be of size Bx3xHxW, got {img_batch.shape}"
|
||||
)
|
||||
logging.info("Computing image embeddings for the provided images...")
|
||||
backbone_out = self.model.forward_image(img_batch)
|
||||
(
|
||||
@@ -300,9 +299,9 @@ class SAM3InteractiveImagePredictor(nn.Module):
|
||||
):
|
||||
unnorm_coords, labels, unnorm_box, mask_input = None, None, None, None
|
||||
if point_coords is not None:
|
||||
assert (
|
||||
point_labels is not None
|
||||
), "point_labels must be supplied if point_coords is supplied."
|
||||
assert point_labels is not None, (
|
||||
"point_labels must be supplied if point_coords is supplied."
|
||||
)
|
||||
point_coords = torch.as_tensor(
|
||||
point_coords, dtype=torch.float, device=self.device
|
||||
)
|
||||
@@ -439,9 +438,9 @@ class SAM3InteractiveImagePredictor(nn.Module):
|
||||
raise RuntimeError(
|
||||
"An image must be set with .set_image(...) to generate an embedding."
|
||||
)
|
||||
assert (
|
||||
self._features is not None
|
||||
), "Features must exist if an image has been set."
|
||||
assert self._features is not None, (
|
||||
"Features must exist if an image has been set."
|
||||
)
|
||||
return self._features["image_embed"]
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import os
|
||||
from copy import deepcopy
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from sam3.model.model_misc import SAM3Output
|
||||
|
||||
from sam3.model.sam1_task_predictor import SAM3InteractiveImagePredictor
|
||||
from sam3.model.vl_combiner import SAM3VLBackbone
|
||||
from sam3.perflib.nms import nms_masks
|
||||
|
||||
from sam3.train.data.collator import BatchedDatapoint
|
||||
|
||||
from .act_ckpt_utils import activation_ckpt_wrapper
|
||||
|
||||
from .box_ops import box_cxcywh_to_xyxy
|
||||
|
||||
from .geometry_encoders import Prompt
|
||||
from .model_misc import inverse_sigmoid
|
||||
|
||||
@@ -659,9 +656,9 @@ class Sam3Image(torch.nn.Module):
|
||||
inference_state["original_heights"],
|
||||
inference_state["original_widths"],
|
||||
)
|
||||
assert (
|
||||
batch_size == len(orig_heights) == len(orig_widths)
|
||||
), f"Batch size mismatch in predict_inst_batch. Got {batch_size}, {len(orig_heights)}, {len(orig_widths)}"
|
||||
assert batch_size == len(orig_heights) == len(orig_widths), (
|
||||
f"Batch size mismatch in predict_inst_batch. Got {batch_size}, {len(orig_heights)}, {len(orig_widths)}"
|
||||
)
|
||||
feats = [
|
||||
feat.permute(1, 2, 0).view(batch_size, -1, *feat_size)
|
||||
for feat, feat_size in zip(
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
from typing import Dict, List
|
||||
|
||||
import numpy as np
|
||||
import PIL
|
||||
import torch
|
||||
|
||||
from sam3.model import box_ops
|
||||
|
||||
from sam3.model.data_misc import FindStage, interpolate
|
||||
from torchvision.transforms import v2
|
||||
|
||||
@@ -81,9 +81,9 @@ class Sam3Processor:
|
||||
if not isinstance(images, list):
|
||||
raise ValueError("Images must be a list of PIL images or tensors")
|
||||
assert len(images) > 0, "Images list must not be empty"
|
||||
assert isinstance(
|
||||
images[0], PIL.Image.Image
|
||||
), "Images must be a list of PIL images"
|
||||
assert isinstance(images[0], PIL.Image.Image), (
|
||||
"Images must be a list of PIL images"
|
||||
)
|
||||
|
||||
state["original_heights"] = [image.height for image in images]
|
||||
state["original_widths"] = [image.width for image in images]
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import logging
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from sam3.model.memory import SimpleMaskEncoder
|
||||
|
||||
from sam3.model.sam3_tracker_utils import get_1d_sine_pe, select_closest_cond_frames
|
||||
|
||||
from sam3.sam.mask_decoder import MaskDecoder, MLP
|
||||
from sam3.sam.prompt_encoder import PromptEncoder
|
||||
from sam3.sam.transformer import TwoWayTransformer
|
||||
@@ -900,8 +899,6 @@ class Sam3TrackerBase(torch.nn.Module):
|
||||
image=current_image,
|
||||
point_inputs=backbone_out["point_inputs_per_frame"].get(stage_id, None),
|
||||
mask_inputs=backbone_out["mask_inputs_per_frame"].get(stage_id, None),
|
||||
gt_masks=backbone_out["gt_masks_per_frame"].get(stage_id, None),
|
||||
frames_to_add_correction_pt=frames_to_add_correction_pt,
|
||||
output_dict=output_dict,
|
||||
num_frames=num_frames,
|
||||
)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from sam3.model.edt import edt_triton
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
import torch
|
||||
|
||||
from sam3.model.sam3_tracker_base import concat_points, NO_OBJ_SCORE, Sam3TrackerBase
|
||||
from sam3.model.sam3_tracker_utils import fill_holes_in_mask_scores
|
||||
from sam3.model.utils.sam2_utils import load_video_frames
|
||||
@@ -657,8 +658,6 @@ class Sam3TrackerPredictor(Sam3TrackerBase):
|
||||
image=image,
|
||||
point_inputs=None,
|
||||
mask_inputs=mask_inputs,
|
||||
gt_masks=None,
|
||||
frames_to_add_correction_pt=[],
|
||||
output_dict={
|
||||
"cond_frame_outputs": {},
|
||||
"non_cond_frame_outputs": {},
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
@@ -14,7 +16,6 @@ import numpy.typing as npt
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.nn.functional as F
|
||||
|
||||
from sam3 import perflib
|
||||
from sam3.logger import get_logger
|
||||
from sam3.model.box_ops import fast_diag_box_iou
|
||||
@@ -618,9 +619,9 @@ class Sam3VideoBase(nn.Module):
|
||||
num_obj_dropped_due_to_limit,
|
||||
trk_id_to_max_iou_high_conf_det,
|
||||
]
|
||||
assert (
|
||||
len(update_plan) == NUM_BROADCAST_ITEMS
|
||||
), f"Manually update NUM_BROADCAST_ITEMS to be: {len(update_plan)}"
|
||||
assert len(update_plan) == NUM_BROADCAST_ITEMS, (
|
||||
f"Manually update NUM_BROADCAST_ITEMS to be: {len(update_plan)}"
|
||||
)
|
||||
self.broadcast_python_obj_cpu(update_plan, src=0)
|
||||
elif self.rank > 0 and self.world_size > 1:
|
||||
update_plan = [
|
||||
@@ -840,9 +841,9 @@ class Sam3VideoBase(nn.Module):
|
||||
binary_tracker_low_res_masks_global = tracker_low_res_masks_global > 0
|
||||
batch_size = tracker_low_res_masks_global.size(0)
|
||||
if batch_size > 0:
|
||||
assert (
|
||||
len(obj_ids_global) == batch_size
|
||||
), f"Mismatch in number of objects: {len(obj_ids_global)} vs {batch_size}"
|
||||
assert len(obj_ids_global) == batch_size, (
|
||||
f"Mismatch in number of objects: {len(obj_ids_global)} vs {batch_size}"
|
||||
)
|
||||
NEVER_OCCLUDED = -1
|
||||
ALWAYS_OCCLUDED = 100000 # This value should be larger than any possible frame index, indicates that the object was removed by hotstart logic
|
||||
last_occluded_prev = torch.cat(
|
||||
@@ -1021,9 +1022,9 @@ class Sam3VideoBase(nn.Module):
|
||||
reverse: bool = False,
|
||||
):
|
||||
# Suppress overlapping masks for objects that were most recently occluded
|
||||
assert (
|
||||
binary_low_res_masks.dtype == torch.bool
|
||||
), f"Expected boolean tensor, got {binary_low_res_masks.dtype}"
|
||||
assert binary_low_res_masks.dtype == torch.bool, (
|
||||
f"Expected boolean tensor, got {binary_low_res_masks.dtype}"
|
||||
)
|
||||
to_suppress = torch.zeros(
|
||||
binary_low_res_masks.size(0),
|
||||
device=binary_low_res_masks.device,
|
||||
@@ -1128,9 +1129,9 @@ class Sam3VideoBase(nn.Module):
|
||||
num_frames_propagated += 1
|
||||
|
||||
# only 1 frames should be propagated
|
||||
assert (
|
||||
num_frames_propagated == 1 and out_frame_idx == frame_idx
|
||||
), f"num_frames_propagated: {num_frames_propagated}, out_frame_idx: {out_frame_idx}, frame_idx: {frame_idx}"
|
||||
assert num_frames_propagated == 1 and out_frame_idx == frame_idx, (
|
||||
f"num_frames_propagated: {num_frames_propagated}, out_frame_idx: {out_frame_idx}, frame_idx: {frame_idx}"
|
||||
)
|
||||
assert isinstance(out_obj_ids, list)
|
||||
obj_ids_local.extend(out_obj_ids)
|
||||
low_res_masks_list.append(out_low_res_masks.squeeze(1))
|
||||
@@ -1187,9 +1188,9 @@ class Sam3VideoBase(nn.Module):
|
||||
|
||||
assert det_masks.is_floating_point(), "float tensor expected (do not binarize)"
|
||||
assert trk_masks.is_floating_point(), "float tensor expected (do not binarize)"
|
||||
assert (
|
||||
trk_masks.size(0) == len(trk_obj_ids)
|
||||
), f"trk_masks and trk_obj_ids should have the same length, {trk_masks.size(0)} vs {len(trk_obj_ids)}"
|
||||
assert trk_masks.size(0) == len(trk_obj_ids), (
|
||||
f"trk_masks and trk_obj_ids should have the same length, {trk_masks.size(0)} vs {len(trk_obj_ids)}"
|
||||
)
|
||||
if trk_masks.size(0) == 0:
|
||||
# all detections are new
|
||||
new_det_fa_inds = np.arange(det_masks.size(0))
|
||||
@@ -1653,9 +1654,9 @@ class Sam3VideoBase(nn.Module):
|
||||
# a) first, expand "confirmation_data" to include new masklets added in this frame
|
||||
status_prev = confirmation_data["status"]
|
||||
consecutive_det_num_prev = confirmation_data["consecutive_det_num"]
|
||||
assert (
|
||||
status_prev.shape == obj_ids_all_gpu_prev.shape
|
||||
), f"Got {status_prev.shape} vs {obj_ids_all_gpu_prev.shape}"
|
||||
assert status_prev.shape == obj_ids_all_gpu_prev.shape, (
|
||||
f"Got {status_prev.shape} vs {obj_ids_all_gpu_prev.shape}"
|
||||
)
|
||||
|
||||
obj_id_to_updated_idx = {
|
||||
obj_id: idx for idx, obj_id in enumerate(obj_ids_all_gpu_updated)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -7,7 +9,6 @@ import numpy as np
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.nn.functional as F
|
||||
|
||||
from sam3 import perflib
|
||||
from sam3.logger import get_logger
|
||||
from sam3.model.act_ckpt_utils import clone_output_wrapper
|
||||
@@ -553,7 +554,9 @@ class Sam3VideoInference(Sam3VideoBase):
|
||||
assert (
|
||||
"cached_frame_outputs" in inference_state
|
||||
and frame_idx in inference_state["cached_frame_outputs"]
|
||||
), "No cached outputs found. Ensure normal propagation has run first to populate the cache."
|
||||
), (
|
||||
"No cached outputs found. Ensure normal propagation has run first to populate the cache."
|
||||
)
|
||||
cached_outputs = inference_state["cached_frame_outputs"][frame_idx]
|
||||
|
||||
obj_id_to_mask = cached_outputs.copy()
|
||||
@@ -561,9 +564,9 @@ class Sam3VideoInference(Sam3VideoBase):
|
||||
# Update with refined masks if provided
|
||||
if refined_obj_id_to_mask is not None:
|
||||
for obj_id, refined_mask in refined_obj_id_to_mask.items():
|
||||
assert (
|
||||
refined_mask is not None
|
||||
), f"Refined mask data must be provided for obj_id {obj_id}"
|
||||
assert refined_mask is not None, (
|
||||
f"Refined mask data must be provided for obj_id {obj_id}"
|
||||
)
|
||||
obj_id_to_mask[obj_id] = refined_mask
|
||||
|
||||
return obj_id_to_mask
|
||||
@@ -658,12 +661,12 @@ class Sam3VideoInference(Sam3VideoBase):
|
||||
for i, thresh in enumerate(new_det_score_thresh_list):
|
||||
self.new_det_thresh = thresh
|
||||
for num_objects in num_objects_list:
|
||||
logger.info(f"{i+1}/{num_rounds} warming up model compilation")
|
||||
logger.info(f"{i + 1}/{num_rounds} warming up model compilation")
|
||||
self.add_prompt(
|
||||
inference_state, frame_idx=start_frame_idx, text_str="cat"
|
||||
)
|
||||
logger.info(
|
||||
f"{i+1}/{num_rounds} warming up model compilation -- simulating {num_objects}/{self.num_obj_for_compile} objects"
|
||||
f"{i + 1}/{num_rounds} warming up model compilation -- simulating {num_objects}/{self.num_obj_for_compile} objects"
|
||||
)
|
||||
inference_state = self.add_fake_objects_to_inference_state(
|
||||
inference_state, num_objects, frame_idx=start_frame_idx
|
||||
@@ -688,7 +691,7 @@ class Sam3VideoInference(Sam3VideoBase):
|
||||
pass
|
||||
self.reset_state(inference_state)
|
||||
logger.info(
|
||||
f"{i+1}/{num_rounds} warming up model compilation -- completed round {i+1} out of {num_rounds}"
|
||||
f"{i + 1}/{num_rounds} warming up model compilation -- completed round {i + 1} out of {num_rounds}"
|
||||
)
|
||||
|
||||
# Warm up Tracker memory encoder with varying input shapes
|
||||
@@ -852,12 +855,12 @@ class Sam3VideoInference(Sam3VideoBase):
|
||||
logger.debug("Running add_prompt on frame %d", frame_idx)
|
||||
|
||||
num_frames = inference_state["num_frames"]
|
||||
assert (
|
||||
text_str is not None or boxes_xywh is not None
|
||||
), "at least one type of prompt (text, boxes) must be provided"
|
||||
assert (
|
||||
0 <= frame_idx < num_frames
|
||||
), f"{frame_idx=} is out of range for a total of {num_frames} frames"
|
||||
assert text_str is not None or boxes_xywh is not None, (
|
||||
"at least one type of prompt (text, boxes) must be provided"
|
||||
)
|
||||
assert 0 <= frame_idx < num_frames, (
|
||||
f"{frame_idx=} is out of range for a total of {num_frames} frames"
|
||||
)
|
||||
|
||||
# since it's a semantic prompt, we start over
|
||||
self.reset_state(inference_state)
|
||||
@@ -1198,9 +1201,9 @@ class Sam3VideoInferenceWithInstanceInteractivity(Sam3VideoInference):
|
||||
"propagation_partial",
|
||||
"propagation_fetch",
|
||||
]
|
||||
assert (
|
||||
action_type in instance_actions + propagation_actions
|
||||
), f"Invalid action type: {action_type}, must be one of {instance_actions + propagation_actions}"
|
||||
assert action_type in instance_actions + propagation_actions, (
|
||||
f"Invalid action type: {action_type}, must be one of {instance_actions + propagation_actions}"
|
||||
)
|
||||
action = {
|
||||
"type": action_type,
|
||||
"frame_idx": frame_idx,
|
||||
@@ -1368,12 +1371,12 @@ class Sam3VideoInferenceWithInstanceInteractivity(Sam3VideoInference):
|
||||
):
|
||||
if points is not None:
|
||||
# Tracker instance prompts
|
||||
assert (
|
||||
text_str is None and boxes_xywh is None
|
||||
), "When points are provided, text_str and boxes_xywh must be None."
|
||||
assert (
|
||||
obj_id is not None
|
||||
), "When points are provided, obj_id must be provided."
|
||||
assert text_str is None and boxes_xywh is None, (
|
||||
"When points are provided, text_str and boxes_xywh must be None."
|
||||
)
|
||||
assert obj_id is not None, (
|
||||
"When points are provided, obj_id must be provided."
|
||||
)
|
||||
return self.add_tracker_new_points(
|
||||
inference_state,
|
||||
frame_idx,
|
||||
@@ -1489,9 +1492,9 @@ class Sam3VideoInferenceWithInstanceInteractivity(Sam3VideoInference):
|
||||
tracker_states = self._get_tracker_inference_states_by_obj_ids(
|
||||
inference_state, [obj_id]
|
||||
)
|
||||
assert (
|
||||
len(tracker_states) == 1
|
||||
), f"[rank={self.rank}] Multiple Tracker inference states found for the same object id."
|
||||
assert len(tracker_states) == 1, (
|
||||
f"[rank={self.rank}] Multiple Tracker inference states found for the same object id."
|
||||
)
|
||||
tracker_state = tracker_states[0]
|
||||
|
||||
# log
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
import datetime
|
||||
import gc
|
||||
import multiprocessing as mp
|
||||
@@ -14,7 +16,6 @@ from typing import List, Optional
|
||||
|
||||
import psutil
|
||||
import torch
|
||||
|
||||
from sam3.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -168,7 +169,7 @@ class Sam3VideoPredictor:
|
||||
):
|
||||
"""Remove an object from tracking."""
|
||||
logger.debug(
|
||||
f"remove object {obj_id} in session {session_id}: " f"{is_user_action=}"
|
||||
f"remove object {obj_id} in session {session_id}: {is_user_action=}"
|
||||
)
|
||||
session = self._get_session(session_id)
|
||||
inference_state = session["state"]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Callable, List, Optional, Tuple, Union
|
||||
|
||||
@@ -316,9 +318,9 @@ class VETextEncoder(nn.Module):
|
||||
# The text is already encoded, use as is.
|
||||
text_attention_mask, text_memory_resized, tokenized = text
|
||||
inputs_embeds = tokenized["inputs_embeds"]
|
||||
assert (
|
||||
input_boxes is None or len(input_boxes) == 0
|
||||
), "Can't replace boxes in text if it's already encoded"
|
||||
assert input_boxes is None or len(input_boxes) == 0, (
|
||||
"Can't replace boxes in text if it's already encoded"
|
||||
)
|
||||
|
||||
# Note that the input_embeds are returned in pytorch's convention (sequence first)
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""
|
||||
Text Tokenizer.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
# All rights reserved.
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import fields, is_dataclass
|
||||
from typing import Any, Mapping, Protocol, runtime_checkable
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
# All rights reserved.
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
# All rights reserved.
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
# pyre-unsafe
|
||||
|
||||
"""
|
||||
ViTDet backbone adapted from Detectron2.
|
||||
This module implements Vision Transformer (ViT) backbone for object detection.
|
||||
@@ -706,9 +708,9 @@ class ViT(nn.Module):
|
||||
self.retain_cls_token = retain_cls_token
|
||||
if self.retain_cls_token:
|
||||
assert pretrain_use_cls_token
|
||||
assert (
|
||||
len(window_block_indexes) == 0
|
||||
), "windowing not supported with cls token"
|
||||
assert len(window_block_indexes) == 0, (
|
||||
"windowing not supported with cls token"
|
||||
)
|
||||
|
||||
assert sum(self.rel_pos_blocks) == 0, "rel pos not supported with cls token"
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user