Image Segmentation
Transformers
Safetensors
English
concor1
feature-extraction
vision-language
multimodal
vision-language-grounding
concept-correspondence
phrase-grounding
referring-expression-segmentation
open-vocabulary-segmentation
custom_code
Instructions to use UWGZQ/ConCor-1 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use UWGZQ/ConCor-1 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-segmentation", model="UWGZQ/ConCor-1", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("UWGZQ/ConCor-1", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
ConCor-1: weights, remote code, processor and model card
Browse files- .gitattributes +4 -0
- README.md +206 -0
- config.json +169 -0
- configuration_concor1.py +202 -0
- example.png +3 -0
- example_2.png +3 -0
- example_inference.py +129 -0
- merges.txt +0 -0
- model.jpg +3 -0
- model.safetensors +3 -0
- modeling_concor1.py +804 -0
- preprocessor_config.json +23 -0
- processing_concor1.py +489 -0
- processor_config.json +17 -0
- requirements.txt +22 -0
- tokenizer.json +3 -0
- tokenizer_config.json +306 -0
- vocab.json +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
example.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
example_2.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
model.jpg filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: transformers
|
| 3 |
+
license: apache-2.0
|
| 4 |
+
language:
|
| 5 |
+
- en
|
| 6 |
+
pipeline_tag: image-segmentation
|
| 7 |
+
base_model:
|
| 8 |
+
- Qwen/Qwen3.5-0.8B
|
| 9 |
+
tags:
|
| 10 |
+
- vision-language
|
| 11 |
+
- multimodal
|
| 12 |
+
- vision-language-grounding
|
| 13 |
+
- concept-correspondence
|
| 14 |
+
- phrase-grounding
|
| 15 |
+
- referring-expression-segmentation
|
| 16 |
+
- open-vocabulary-segmentation
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
# ConCor-1
|
| 20 |
+
|
| 21 |
+
## Vision-Language Grounding as Bidirectional Concept Correspondence
|
| 22 |
+
|
| 23 |
+
[Jieyu Zhang](https://jieyuz2.github.io)<sup>1</sup>\*,
|
| 24 |
+
[Ziqi Gao](https://uwgzq.github.io)<sup>1,2</sup>\*,
|
| 25 |
+
[Luke Zettlemoyer](https://homes.cs.washington.edu/~lsz/)<sup>1,3</sup>,
|
| 26 |
+
[Ranjay Krishna](https://www.ranjaykrishna.com/)<sup>1</sup>
|
| 27 |
+
|
| 28 |
+
<sup>1</sup>University of Washington · <sup>2</sup>Allen Institute for AI · <sup>3</sup>FAIR at Meta
|
| 29 |
+
\*Equal contribution
|
| 30 |
+
|
| 31 |
+
**Quick links**
|
| 32 |
+
|
| 33 |
+
📄 [Paper](https://arxiv.org) ·
|
| 34 |
+
🌐 [Project page](https://uwgzq.github.io/papers/ConCor-1/) ·
|
| 35 |
+
💻 [GitHub](https://github.com/uwGZQ/ConCor-1) ·
|
| 36 |
+
🤗 [Data](https://huggingface.co/datasets/UWGZQ/ConCor-1-Data) ·
|
| 37 |
+
🎨 [Demo](https://huggingface.co/spaces/UWGZQ/ConCor-1-demo)
|
| 38 |
+
|
| 39 |
+
## Model Summary
|
| 40 |
+
|
| 41 |
+
ConCor-1 is a vision-language grounding model designed to explicitly predict **bidirectional concept correspondences** between language and visual content. Built on a pretrained Qwen3.5-0.8B vision-language backbone, it introduces a set of learnable bridge tokens, each representing a candidate text–image correspondence. By jointly attending to visual and textual tokens, these bridge tokens aggregate multimodal context to represent potential alignments between image regions and text spans. For each bridge token, ConCor-1 simultaneously predicts a text mask identifying the grounded text segment, an image mask localizing the corresponding visual instance, and a presence score indicating whether the proposed pairing is a valid correspondence.
|
| 42 |
+
|
| 43 |
+

|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
## Usage
|
| 47 |
+
|
| 48 |
+
### Environment
|
| 49 |
+
|
| 50 |
+
The released checkpoint is supported with the following core inference stack:
|
| 51 |
+
|
| 52 |
+
| Dependency | Supported version |
|
| 53 |
+
|---|---|
|
| 54 |
+
| Python | >=3.11 |
|
| 55 |
+
| PyTorch | 2.8.0 |
|
| 56 |
+
| torchvision | 0.23.0 |
|
| 57 |
+
| Transformers | 5.3.0 |
|
| 58 |
+
|
| 59 |
+
Install a PyTorch/torchvision build compatible with your CUDA environment first. Then install the remaining inference dependencies:
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
pip install "transformers==5.3.0" "safetensors>=0.4.0" \
|
| 63 |
+
"huggingface_hub>=0.30.0" "numpy>=1.24" "pillow>=10.0" \
|
| 64 |
+
"flash-linear-attention>=0.4.1"
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
The example below uses PyTorch SDPA for the backbone's full-attention layers. To use FlashAttention-2, install `flash-attn>=2.8.0` and set `attn_implementation="flash_attention_2"`. `causal-conv1d>=1.4.0` is an optional acceleration dependency for the linear-attention layers.
|
| 68 |
+
|
| 69 |
+
### Image + caption
|
| 70 |
+
|
| 71 |
+
```python
|
| 72 |
+
import torch
|
| 73 |
+
from PIL import Image
|
| 74 |
+
from transformers import AutoModel, AutoProcessor
|
| 75 |
+
|
| 76 |
+
model_id = "UWGZQ/ConCor-1" # or a local path to this repository
|
| 77 |
+
|
| 78 |
+
processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
|
| 79 |
+
model = AutoModel.from_pretrained(
|
| 80 |
+
model_id,
|
| 81 |
+
trust_remote_code=True,
|
| 82 |
+
dtype=torch.bfloat16,
|
| 83 |
+
attn_implementation="sdpa", # "flash_attention_2" also works
|
| 84 |
+
).to("cuda").eval()
|
| 85 |
+
|
| 86 |
+
image = Image.open("example.png").convert("RGB")
|
| 87 |
+
text = (
|
| 88 |
+
"This image depicts a close-up of a brown bear in a natural outdoor setting. "
|
| 89 |
+
"The background consists of lush green grass. In the foreground, a large brown bear "
|
| 90 |
+
"is positioned centrally."
|
| 91 |
+
)
|
| 92 |
+
# A referring expression works the same way:
|
| 93 |
+
# text = "the large brown bear in the foreground"
|
| 94 |
+
|
| 95 |
+
inputs = processor(images=image, text=text, return_tensors="pt").to("cuda")
|
| 96 |
+
with torch.inference_mode(), torch.autocast("cuda", dtype=torch.bfloat16):
|
| 97 |
+
outputs = model(**inputs)
|
| 98 |
+
|
| 99 |
+
correspondences = processor.post_process_correspondences(
|
| 100 |
+
outputs, text=text, target_sizes=[(image.height, image.width)]
|
| 101 |
+
)[0]
|
| 102 |
+
|
| 103 |
+
for correspondence in correspondences:
|
| 104 |
+
print(
|
| 105 |
+
f"{correspondence['presence_score']:.3f}",
|
| 106 |
+
correspondence["text_phrases"], # phrases of this correspondence's text mask
|
| 107 |
+
correspondence["text_spans"], # character spans into `text`
|
| 108 |
+
correspondence["mask"].shape, # bool array, (height, width)
|
| 109 |
+
)
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
`post_process_correspondences` also takes `presence_threshold`, `text_threshold`, `image_threshold` and `nms_iou_threshold` (defaults `0.1`, `0.45`, `0.45`, `0.5`).
|
| 114 |
+
|
| 115 |
+
`example_inference.py` wraps this up as a script, including a mask-overlay renderer:
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
python example_inference.py \
|
| 119 |
+
--image example.png \
|
| 120 |
+
--text "This image depicts a close-up of a brown bear in a natural outdoor setting. The background consists of lush green grass. In the foreground, a large brown bear is positioned centrally." \
|
| 121 |
+
--output overlay.png
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
### Image + category list
|
| 126 |
+
|
| 127 |
+
```python
|
| 128 |
+
image = Image.open("example_2.png").convert("RGB")
|
| 129 |
+
text = "baseball player . baseball glove . grass . fence . dog"
|
| 130 |
+
|
| 131 |
+
inputs = processor(images=image, text=text, return_tensors="pt").to("cuda")
|
| 132 |
+
with torch.inference_mode(), torch.autocast("cuda", dtype=torch.bfloat16):
|
| 133 |
+
outputs = model(**inputs)
|
| 134 |
+
|
| 135 |
+
for correspondence in processor.post_process_correspondences(
|
| 136 |
+
outputs, text=text, target_sizes=[(image.height, image.width)]
|
| 137 |
+
)[0]:
|
| 138 |
+
print(f"{correspondence['presence_score']:.3f}", correspondence["text_phrases"])
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
### Batched image–text pairs
|
| 143 |
+
|
| 144 |
+
```python
|
| 145 |
+
images = [Image.open("example.png").convert("RGB"), Image.open("example_2.png").convert("RGB")]
|
| 146 |
+
texts = [
|
| 147 |
+
"This image depicts a close-up of a brown bear in a natural outdoor setting. "
|
| 148 |
+
"The background consists of lush green grass. In the foreground, a large brown bear "
|
| 149 |
+
"is positioned centrally.",
|
| 150 |
+
"baseball player . baseball glove . grass . fence . dog",
|
| 151 |
+
]
|
| 152 |
+
|
| 153 |
+
inputs = processor(images=images, text=texts, return_tensors="pt").to("cuda")
|
| 154 |
+
with torch.inference_mode(), torch.autocast("cuda", dtype=torch.bfloat16):
|
| 155 |
+
outputs = model(**inputs)
|
| 156 |
+
|
| 157 |
+
results = processor.post_process_correspondences(
|
| 158 |
+
outputs,
|
| 159 |
+
text=texts, # the same list of texts
|
| 160 |
+
target_sizes=[(image.height, image.width) for image in images], # one (height, width) per sample
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
for index, correspondences in enumerate(results):
|
| 164 |
+
print(f"sample {index}: {len(correspondences)} correspondence(s)")
|
| 165 |
+
for correspondence in correspondences:
|
| 166 |
+
print(f" {correspondence['presence_score']:.3f}", correspondence["text_phrases"])
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
## Files
|
| 172 |
+
|
| 173 |
+
| File | Purpose |
|
| 174 |
+
|---|---|
|
| 175 |
+
| `configuration_concor1.py` | `ConCor1Config` |
|
| 176 |
+
| `modeling_concor1.py` | `ConCor1ForConceptCorrespondence` and its heads |
|
| 177 |
+
| `processing_concor1.py` | `ConCor1Processor`: sequence construction + correspondence post-processing |
|
| 178 |
+
| `example_inference.py` | image + text → correspondences, with mask overlay |
|
| 179 |
+
| `example.png`, `example_2.png` | the images used in the examples above |
|
| 180 |
+
| `model.jpg` | the architecture figure above |
|
| 181 |
+
| `model.safetensors` | weights: bf16 backbone, fp32 prediction heads |
|
| 182 |
+
| `requirements.txt` | the inference dependencies listed above |
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
## Demo
|
| 187 |
+
|
| 188 |
+
An interactive ZeroGPU demo lives at
|
| 189 |
+
[`UWGZQ/ConCor-1-demo`](https://huggingface.co/spaces/UWGZQ/ConCor-1-demo).
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
## License and Use
|
| 193 |
+
|
| 194 |
+
The weights and the code in this repository are released under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. The backbone is [Qwen3.5-0.8B](https://huggingface.co/Qwen/Qwen3.5-0.8B), also Apache 2.0.
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
## Citation
|
| 199 |
+
|
| 200 |
+
```bibtex
|
| 201 |
+
@article{zhang2026concor,
|
| 202 |
+
title = {Vision-Language Grounding as Bidirectional Concept Correspondence},
|
| 203 |
+
author = {Zhang, Jieyu and Gao, Ziqi and Zettlemoyer, Luke and Krishna, Ranjay},
|
| 204 |
+
year = {2026}
|
| 205 |
+
}
|
| 206 |
+
```
|
config.json
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"ConCor1ForConceptCorrespondence"
|
| 4 |
+
],
|
| 5 |
+
"auto_map": {
|
| 6 |
+
"AutoConfig": "configuration_concor1.ConCor1Config",
|
| 7 |
+
"AutoModel": "modeling_concor1.ConCor1ForConceptCorrespondence",
|
| 8 |
+
"AutoProcessor": "processing_concor1.ConCor1Processor"
|
| 9 |
+
},
|
| 10 |
+
"backbone_config": {
|
| 11 |
+
"image_token_id": 248056,
|
| 12 |
+
"model_type": "qwen3_5",
|
| 13 |
+
"text_config": {
|
| 14 |
+
"_name_or_path": "",
|
| 15 |
+
"architectures": null,
|
| 16 |
+
"attention_bias": false,
|
| 17 |
+
"attention_dropout": 0.0,
|
| 18 |
+
"attn_output_gate": true,
|
| 19 |
+
"bos_token_id": null,
|
| 20 |
+
"chunk_size_feed_forward": 0,
|
| 21 |
+
"dtype": "bfloat16",
|
| 22 |
+
"eos_token_id": 248044,
|
| 23 |
+
"full_attention_interval": 4,
|
| 24 |
+
"head_dim": 256,
|
| 25 |
+
"hidden_act": "silu",
|
| 26 |
+
"hidden_size": 1024,
|
| 27 |
+
"id2label": {
|
| 28 |
+
"0": "LABEL_0",
|
| 29 |
+
"1": "LABEL_1"
|
| 30 |
+
},
|
| 31 |
+
"initializer_range": 0.02,
|
| 32 |
+
"intermediate_size": 3584,
|
| 33 |
+
"is_encoder_decoder": false,
|
| 34 |
+
"label2id": {
|
| 35 |
+
"LABEL_0": 0,
|
| 36 |
+
"LABEL_1": 1
|
| 37 |
+
},
|
| 38 |
+
"layer_types": [
|
| 39 |
+
"linear_attention",
|
| 40 |
+
"linear_attention",
|
| 41 |
+
"linear_attention",
|
| 42 |
+
"full_attention",
|
| 43 |
+
"linear_attention",
|
| 44 |
+
"linear_attention",
|
| 45 |
+
"linear_attention",
|
| 46 |
+
"full_attention",
|
| 47 |
+
"linear_attention",
|
| 48 |
+
"linear_attention",
|
| 49 |
+
"linear_attention",
|
| 50 |
+
"full_attention",
|
| 51 |
+
"linear_attention",
|
| 52 |
+
"linear_attention",
|
| 53 |
+
"linear_attention",
|
| 54 |
+
"full_attention",
|
| 55 |
+
"linear_attention",
|
| 56 |
+
"linear_attention",
|
| 57 |
+
"linear_attention",
|
| 58 |
+
"full_attention",
|
| 59 |
+
"linear_attention",
|
| 60 |
+
"linear_attention",
|
| 61 |
+
"linear_attention",
|
| 62 |
+
"full_attention"
|
| 63 |
+
],
|
| 64 |
+
"linear_conv_kernel_dim": 4,
|
| 65 |
+
"linear_key_head_dim": 128,
|
| 66 |
+
"linear_num_key_heads": 16,
|
| 67 |
+
"linear_num_value_heads": 16,
|
| 68 |
+
"linear_value_head_dim": 128,
|
| 69 |
+
"mamba_ssm_dtype": "float32",
|
| 70 |
+
"max_position_embeddings": 262144,
|
| 71 |
+
"mlp_only_layers": [],
|
| 72 |
+
"model_type": "qwen3_5_text",
|
| 73 |
+
"mtp_num_hidden_layers": 1,
|
| 74 |
+
"mtp_use_dedicated_embeddings": false,
|
| 75 |
+
"num_attention_heads": 8,
|
| 76 |
+
"num_hidden_layers": 24,
|
| 77 |
+
"num_key_value_heads": 2,
|
| 78 |
+
"output_attentions": false,
|
| 79 |
+
"output_hidden_states": false,
|
| 80 |
+
"pad_token_id": null,
|
| 81 |
+
"partial_rotary_factor": 0.25,
|
| 82 |
+
"problem_type": null,
|
| 83 |
+
"return_dict": true,
|
| 84 |
+
"rms_norm_eps": 1e-06,
|
| 85 |
+
"rope_parameters": {
|
| 86 |
+
"mrope_interleaved": true,
|
| 87 |
+
"mrope_section": [
|
| 88 |
+
11,
|
| 89 |
+
11,
|
| 90 |
+
10
|
| 91 |
+
],
|
| 92 |
+
"partial_rotary_factor": 0.25,
|
| 93 |
+
"rope_theta": 10000000,
|
| 94 |
+
"rope_type": "default"
|
| 95 |
+
},
|
| 96 |
+
"tie_word_embeddings": true,
|
| 97 |
+
"use_cache": true,
|
| 98 |
+
"vocab_size": 248462
|
| 99 |
+
},
|
| 100 |
+
"tie_word_embeddings": true,
|
| 101 |
+
"video_token_id": 248057,
|
| 102 |
+
"vision_config": {
|
| 103 |
+
"_name_or_path": "",
|
| 104 |
+
"architectures": null,
|
| 105 |
+
"chunk_size_feed_forward": 0,
|
| 106 |
+
"deepstack_visual_indexes": [],
|
| 107 |
+
"depth": 12,
|
| 108 |
+
"dtype": null,
|
| 109 |
+
"hidden_act": "gelu_pytorch_tanh",
|
| 110 |
+
"hidden_size": 768,
|
| 111 |
+
"id2label": {
|
| 112 |
+
"0": "LABEL_0",
|
| 113 |
+
"1": "LABEL_1"
|
| 114 |
+
},
|
| 115 |
+
"in_channels": 3,
|
| 116 |
+
"initializer_range": 0.02,
|
| 117 |
+
"intermediate_size": 3072,
|
| 118 |
+
"is_encoder_decoder": false,
|
| 119 |
+
"label2id": {
|
| 120 |
+
"LABEL_0": 0,
|
| 121 |
+
"LABEL_1": 1
|
| 122 |
+
},
|
| 123 |
+
"model_type": "qwen3_5",
|
| 124 |
+
"num_heads": 12,
|
| 125 |
+
"num_position_embeddings": 2304,
|
| 126 |
+
"out_hidden_size": 1024,
|
| 127 |
+
"output_attentions": false,
|
| 128 |
+
"output_hidden_states": false,
|
| 129 |
+
"patch_size": 16,
|
| 130 |
+
"problem_type": null,
|
| 131 |
+
"return_dict": true,
|
| 132 |
+
"spatial_merge_size": 2,
|
| 133 |
+
"temporal_patch_size": 2
|
| 134 |
+
},
|
| 135 |
+
"vision_end_token_id": 248054,
|
| 136 |
+
"vision_start_token_id": 248053,
|
| 137 |
+
"vocab_size": 248462
|
| 138 |
+
},
|
| 139 |
+
"bidirectional_full_attention": true,
|
| 140 |
+
"bridge_grid_levels": [
|
| 141 |
+
1,
|
| 142 |
+
2,
|
| 143 |
+
3,
|
| 144 |
+
4,
|
| 145 |
+
5,
|
| 146 |
+
6,
|
| 147 |
+
7,
|
| 148 |
+
8,
|
| 149 |
+
9,
|
| 150 |
+
10
|
| 151 |
+
],
|
| 152 |
+
"bridge_token_id_start": 248077,
|
| 153 |
+
"correspondence_dim": 256,
|
| 154 |
+
"dtype": "bfloat16",
|
| 155 |
+
"fuse_vision_encoder_features": true,
|
| 156 |
+
"image_max_pixels": 1003520,
|
| 157 |
+
"image_min_pixels": 1003520,
|
| 158 |
+
"image_threshold": 0.45,
|
| 159 |
+
"merge_size": 2,
|
| 160 |
+
"model_type": "concor1",
|
| 161 |
+
"nms_iou_threshold": 0.5,
|
| 162 |
+
"num_bridge_tokens": 385,
|
| 163 |
+
"num_mask_upsample_blocks": 2,
|
| 164 |
+
"patch_size": 16,
|
| 165 |
+
"presence_hidden_dim": 256,
|
| 166 |
+
"presence_threshold": 0.1,
|
| 167 |
+
"text_threshold": 0.45,
|
| 168 |
+
"transformers_version": "5.3.0"
|
| 169 |
+
}
|
configuration_concor1.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
# Copyright 2026 The ConCor-1 authors.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
"""ConCor-1 configuration.
|
| 16 |
+
|
| 17 |
+
ConCor-1 is the concept-correspondence model of *Vision-Language Grounding as
|
| 18 |
+
Bidirectional Concept Correspondence*. It wraps a pretrained Qwen3.5-0.8B
|
| 19 |
+
vision-language backbone, appends ``num_bridge_tokens`` learnable **bridge
|
| 20 |
+
tokens** to the multimodal sequence, and predicts, for every bridge token, an
|
| 21 |
+
image mask, a text mask, and a correspondence presence score.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
from typing import List, Optional, Sequence, Union
|
| 27 |
+
|
| 28 |
+
from transformers.configuration_utils import PretrainedConfig
|
| 29 |
+
from transformers.models.qwen3_5.configuration_qwen3_5 import Qwen3_5Config
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class ConCor1Config(PretrainedConfig):
|
| 33 |
+
r"""Configuration class for [`ConCor1ForConceptCorrespondence`].
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
backbone_config (`Union[Qwen3_5Config, dict]`, *optional*):
|
| 37 |
+
Configuration of the Qwen3.5 vision-language backbone used as a
|
| 38 |
+
contextual image-text encoder. Defaults to the Qwen3.5-0.8B
|
| 39 |
+
architecture.
|
| 40 |
+
num_bridge_tokens (`int`, *optional*, defaults to 385):
|
| 41 |
+
Number of bridge tokens `Q` appended after the image and text
|
| 42 |
+
tokens. Each bridge token stands for one candidate image-text
|
| 43 |
+
correspondence. With the default multi-scale grid levels,
|
| 44 |
+
`Q = sum(s**2 for s in 1..10) = 385`.
|
| 45 |
+
bridge_grid_levels (`Sequence[int]`, *optional*, defaults to `(1, ..., 10)`):
|
| 46 |
+
Multi-scale spatial grid levels the bridge tokens are organised
|
| 47 |
+
into. Level `s` contributes `s**2` bridge tokens, ordered in raster
|
| 48 |
+
order over an `s x s` grid. Only used for the training-time
|
| 49 |
+
Hungarian assignment and for interpreting a bridge index; the
|
| 50 |
+
forward pass itself does not depend on it.
|
| 51 |
+
bridge_token_id_start (`int`, *optional*, defaults to 248077):
|
| 52 |
+
First token id used for bridge tokens. Qwen3.5's tokenizer defines
|
| 53 |
+
248077 tokens while its embedding table has 248320 rows, so ids
|
| 54 |
+
from 248077 on are unused vocabulary slots; the embedding table is
|
| 55 |
+
grown when `num_bridge_tokens` needs more than the reserved slots.
|
| 56 |
+
correspondence_dim (`int`, *optional*, defaults to 256):
|
| 57 |
+
Dimension of the shared correspondence space that bridge, text and
|
| 58 |
+
visual features are projected into before the bilinear scorers.
|
| 59 |
+
presence_hidden_dim (`int`, *optional*, defaults to 256):
|
| 60 |
+
Hidden dimension of the presence head's SwiGLU MLP.
|
| 61 |
+
patch_size (`int`, *optional*, defaults to 16):
|
| 62 |
+
Vision-encoder patch size, in pixels.
|
| 63 |
+
merge_size (`int`, *optional*, defaults to 2):
|
| 64 |
+
Spatial merge factor of the backbone's patch merger. One merged
|
| 65 |
+
visual token therefore covers `patch_size * merge_size` pixels per
|
| 66 |
+
side (32 px for the released model).
|
| 67 |
+
num_mask_upsample_blocks (`int`, *optional*, defaults to 2):
|
| 68 |
+
Number of transposed-convolution blocks in the vision segmentation
|
| 69 |
+
head's convolutional decoder. Each block upsamples by 2, so image
|
| 70 |
+
masks are predicted on a grid of
|
| 71 |
+
`patch_size / 2**num_mask_upsample_blocks` pixel cells (4 px).
|
| 72 |
+
fuse_vision_encoder_features (`bool`, *optional*, defaults to `True`):
|
| 73 |
+
Fuse pre-merger ViT patch features into the expanded patch-level
|
| 74 |
+
features to restore local spatial detail.
|
| 75 |
+
bidirectional_full_attention (`bool`, *optional*, defaults to `True`):
|
| 76 |
+
Run the backbone's full-attention layers with a bidirectional mask
|
| 77 |
+
so bridge tokens can access the complete multimodal context. The
|
| 78 |
+
linear-attention layers keep their original behaviour. The released
|
| 79 |
+
checkpoint was trained this way; setting this to `False` reproduces
|
| 80 |
+
stock causal attention and will degrade predictions badly.
|
| 81 |
+
image_min_pixels (`int`, *optional*, defaults to 1003520):
|
| 82 |
+
`min_pixels` passed to the image processor; the released checkpoint
|
| 83 |
+
was trained and evaluated with `min_pixels == max_pixels`, i.e. a
|
| 84 |
+
fixed ~1.0 M pixel budget (1024 visual tokens).
|
| 85 |
+
image_max_pixels (`int`, *optional*, defaults to 1003520):
|
| 86 |
+
`max_pixels` passed to the image processor.
|
| 87 |
+
presence_threshold (`float`, *optional*, defaults to 0.1):
|
| 88 |
+
Default presence-score threshold used by the processor's
|
| 89 |
+
post-processing (paper: 0.1).
|
| 90 |
+
text_threshold (`float`, *optional*, defaults to 0.45):
|
| 91 |
+
Default text-mask probability threshold (paper: 0.45).
|
| 92 |
+
image_threshold (`float`, *optional*, defaults to 0.45):
|
| 93 |
+
Default image-mask probability threshold (paper: 0.45).
|
| 94 |
+
nms_iou_threshold (`float`, *optional*, defaults to 0.5):
|
| 95 |
+
Default IoU threshold for the correspondence NMS that removes
|
| 96 |
+
duplicate bridge predictions (paper: 0.5).
|
| 97 |
+
|
| 98 |
+
Example:
|
| 99 |
+
|
| 100 |
+
```python
|
| 101 |
+
>>> from transformers import AutoConfig, AutoModel
|
| 102 |
+
>>> config = AutoConfig.from_pretrained("UWGZQ/ConCor-1", trust_remote_code=True)
|
| 103 |
+
>>> config.num_bridge_tokens
|
| 104 |
+
385
|
| 105 |
+
```
|
| 106 |
+
"""
|
| 107 |
+
|
| 108 |
+
model_type = "concor1"
|
| 109 |
+
sub_configs = {"backbone_config": Qwen3_5Config}
|
| 110 |
+
|
| 111 |
+
def __init__(
|
| 112 |
+
self,
|
| 113 |
+
backbone_config: Optional[Union[Qwen3_5Config, dict]] = None,
|
| 114 |
+
num_bridge_tokens: int = 385,
|
| 115 |
+
bridge_grid_levels: Sequence[int] = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
|
| 116 |
+
bridge_token_id_start: int = 248077,
|
| 117 |
+
correspondence_dim: int = 256,
|
| 118 |
+
presence_hidden_dim: int = 256,
|
| 119 |
+
patch_size: int = 16,
|
| 120 |
+
merge_size: int = 2,
|
| 121 |
+
num_mask_upsample_blocks: int = 2,
|
| 122 |
+
fuse_vision_encoder_features: bool = True,
|
| 123 |
+
bidirectional_full_attention: bool = True,
|
| 124 |
+
image_min_pixels: int = 1003520,
|
| 125 |
+
image_max_pixels: int = 1003520,
|
| 126 |
+
presence_threshold: float = 0.1,
|
| 127 |
+
text_threshold: float = 0.45,
|
| 128 |
+
image_threshold: float = 0.45,
|
| 129 |
+
nms_iou_threshold: float = 0.5,
|
| 130 |
+
**kwargs,
|
| 131 |
+
):
|
| 132 |
+
if backbone_config is None:
|
| 133 |
+
backbone_config = Qwen3_5Config()
|
| 134 |
+
elif isinstance(backbone_config, dict):
|
| 135 |
+
backbone_config = Qwen3_5Config(**backbone_config)
|
| 136 |
+
self.backbone_config = backbone_config
|
| 137 |
+
|
| 138 |
+
self.num_bridge_tokens = num_bridge_tokens
|
| 139 |
+
self.bridge_grid_levels = list(bridge_grid_levels)
|
| 140 |
+
self.bridge_token_id_start = bridge_token_id_start
|
| 141 |
+
self.correspondence_dim = correspondence_dim
|
| 142 |
+
self.presence_hidden_dim = presence_hidden_dim
|
| 143 |
+
self.patch_size = patch_size
|
| 144 |
+
self.merge_size = merge_size
|
| 145 |
+
self.num_mask_upsample_blocks = num_mask_upsample_blocks
|
| 146 |
+
self.fuse_vision_encoder_features = fuse_vision_encoder_features
|
| 147 |
+
self.bidirectional_full_attention = bidirectional_full_attention
|
| 148 |
+
self.image_min_pixels = image_min_pixels
|
| 149 |
+
self.image_max_pixels = image_max_pixels
|
| 150 |
+
self.presence_threshold = presence_threshold
|
| 151 |
+
self.text_threshold = text_threshold
|
| 152 |
+
self.image_threshold = image_threshold
|
| 153 |
+
self.nms_iou_threshold = nms_iou_threshold
|
| 154 |
+
|
| 155 |
+
super().__init__(**kwargs)
|
| 156 |
+
|
| 157 |
+
if self.bridge_grid_levels:
|
| 158 |
+
expected = sum(s * s for s in self.bridge_grid_levels)
|
| 159 |
+
if expected != self.num_bridge_tokens:
|
| 160 |
+
raise ValueError(
|
| 161 |
+
f"num_bridge_tokens={self.num_bridge_tokens} does not match "
|
| 162 |
+
f"bridge_grid_levels={self.bridge_grid_levels} (sum of s^2 = {expected})."
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
@property
|
| 166 |
+
def hidden_size(self) -> int:
|
| 167 |
+
"""Backbone hidden size (bridge / text token feature dim)."""
|
| 168 |
+
return self.backbone_config.text_config.hidden_size
|
| 169 |
+
|
| 170 |
+
@property
|
| 171 |
+
def vision_hidden_size(self) -> int:
|
| 172 |
+
"""Pre-merger ViT hidden size (patch-level feature dim)."""
|
| 173 |
+
return self.backbone_config.vision_config.hidden_size
|
| 174 |
+
|
| 175 |
+
@property
|
| 176 |
+
def bridge_token_ids(self) -> List[int]:
|
| 177 |
+
"""Token ids of the `num_bridge_tokens` bridge slots, in order."""
|
| 178 |
+
start = self.bridge_token_id_start
|
| 179 |
+
return list(range(start, start + self.num_bridge_tokens))
|
| 180 |
+
|
| 181 |
+
@property
|
| 182 |
+
def merged_token_size(self) -> int:
|
| 183 |
+
"""Pixels per side covered by one merged visual token (32 px)."""
|
| 184 |
+
return self.patch_size * self.merge_size
|
| 185 |
+
|
| 186 |
+
@property
|
| 187 |
+
def mask_cell_size(self) -> int:
|
| 188 |
+
"""Pixels per side of one image-mask cell (4 px for the released model)."""
|
| 189 |
+
return self.patch_size // (2 ** self.num_mask_upsample_blocks)
|
| 190 |
+
|
| 191 |
+
@property
|
| 192 |
+
def bridge_cells(self) -> List[tuple]:
|
| 193 |
+
"""`(level, row, col)` of every bridge token, in bridge order."""
|
| 194 |
+
cells = []
|
| 195 |
+
for level in self.bridge_grid_levels:
|
| 196 |
+
for row in range(level):
|
| 197 |
+
for col in range(level):
|
| 198 |
+
cells.append((level, row, col))
|
| 199 |
+
return cells
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
__all__ = ["ConCor1Config"]
|
example.png
ADDED
|
Git LFS Details
|
example_2.png
ADDED
|
Git LFS Details
|
example_inference.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# coding=utf-8
|
| 3 |
+
"""ConCor-1 inference example: an image + its text in, correspondences out.
|
| 4 |
+
|
| 5 |
+
Given an image and a paired text, ConCor-1 predicts the full set of
|
| 6 |
+
correspondences between visually referential text spans and instance-level
|
| 7 |
+
image segments — the text spans are *not* given as queries; the model decides
|
| 8 |
+
which parts of the text are grounded.
|
| 9 |
+
|
| 10 |
+
Usage (the bundled `example.png` with its COCONut-PanCap caption):
|
| 11 |
+
python example_inference.py --image example.png \
|
| 12 |
+
--text "This image depicts a close-up of a brown bear in a natural outdoor setting. \
|
| 13 |
+
The background consists of lush green grass. In the foreground, a large brown bear is \
|
| 14 |
+
positioned centrally."
|
| 15 |
+
|
| 16 |
+
# a category list works just as well as a caption
|
| 17 |
+
python example_inference.py --image example.png --text "bear . grass . tree . person"
|
| 18 |
+
|
| 19 |
+
# write a mask overlay next to the printed correspondences
|
| 20 |
+
python example_inference.py --image example.png --text "..." --output overlay.png
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import argparse
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
from typing import List
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
import torch
|
| 31 |
+
from PIL import Image, ImageDraw
|
| 32 |
+
from transformers import AutoModel, AutoProcessor
|
| 33 |
+
|
| 34 |
+
# Visually distinct overlay colours.
|
| 35 |
+
COLORS = [
|
| 36 |
+
(239, 64, 64), (64, 204, 64), (64, 115, 255), (255, 204, 0), (255, 115, 0),
|
| 37 |
+
(217, 51, 217), (0, 204, 217), (153, 89, 13), (102, 255, 102), (140, 26, 255),
|
| 38 |
+
(255, 153, 204), (0, 140, 0), (179, 179, 0), (0, 26, 153), (204, 140, 51),
|
| 39 |
+
(128, 128, 128), (255, 0, 128), (0, 255, 140), (140, 0, 0), (0, 140, 140),
|
| 40 |
+
]
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def overlay_masks(image: Image.Image, correspondences: List[dict], alpha: float = 0.5) -> Image.Image:
|
| 44 |
+
"""Blend each correspondence's mask over the image and label it with its phrases."""
|
| 45 |
+
canvas = np.array(image.convert("RGB"), dtype=np.float32)
|
| 46 |
+
for index, correspondence in enumerate(correspondences):
|
| 47 |
+
mask = correspondence.get("mask")
|
| 48 |
+
if mask is None or not mask.any():
|
| 49 |
+
continue
|
| 50 |
+
color = np.array(COLORS[index % len(COLORS)], dtype=np.float32)
|
| 51 |
+
canvas[mask] = (1.0 - alpha) * canvas[mask] + alpha * color
|
| 52 |
+
|
| 53 |
+
overlaid = Image.fromarray(canvas.astype(np.uint8))
|
| 54 |
+
draw = ImageDraw.Draw(overlaid)
|
| 55 |
+
for index, correspondence in enumerate(correspondences):
|
| 56 |
+
mask = correspondence.get("mask")
|
| 57 |
+
if mask is None or not mask.any():
|
| 58 |
+
continue
|
| 59 |
+
rows, columns = np.nonzero(mask)
|
| 60 |
+
label = " / ".join(correspondence["text_phrases"]) or "(no text span)"
|
| 61 |
+
label = f"{label} {correspondence['presence_score']:.2f}"
|
| 62 |
+
anchor = (int(columns.min()), max(int(rows.min()) - 12, 0))
|
| 63 |
+
draw.text(anchor, label, fill=COLORS[index % len(COLORS)])
|
| 64 |
+
return overlaid
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def main() -> None:
|
| 68 |
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 69 |
+
parser.add_argument("--model", default=str(Path(__file__).parent), help="model repo id or local path")
|
| 70 |
+
parser.add_argument("--image", required=True, type=Path)
|
| 71 |
+
parser.add_argument("--text", required=True, help="the text to ground (caption, category list, referring expression)")
|
| 72 |
+
parser.add_argument("--output", type=Path, default=None, help="write a mask overlay here (PNG)")
|
| 73 |
+
parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 74 |
+
parser.add_argument(
|
| 75 |
+
"--attn_implementation",
|
| 76 |
+
default="sdpa",
|
| 77 |
+
choices=["flash_attention_2", "sdpa", "eager"],
|
| 78 |
+
help="sdpa needs no extra dependency; flash_attention_2 reproduces the paper's numbers exactly",
|
| 79 |
+
)
|
| 80 |
+
parser.add_argument("--presence_threshold", type=float, default=None, help="default 0.1")
|
| 81 |
+
parser.add_argument("--text_threshold", type=float, default=None, help="default 0.45")
|
| 82 |
+
parser.add_argument("--image_threshold", type=float, default=None, help="default 0.45")
|
| 83 |
+
parser.add_argument("--nms_iou_threshold", type=float, default=None, help="default 0.5")
|
| 84 |
+
args = parser.parse_args()
|
| 85 |
+
|
| 86 |
+
processor = AutoProcessor.from_pretrained(args.model, trust_remote_code=True)
|
| 87 |
+
model = AutoModel.from_pretrained(
|
| 88 |
+
args.model,
|
| 89 |
+
trust_remote_code=True,
|
| 90 |
+
dtype=torch.bfloat16,
|
| 91 |
+
attn_implementation=args.attn_implementation,
|
| 92 |
+
).to(args.device).eval()
|
| 93 |
+
|
| 94 |
+
image = Image.open(args.image).convert("RGB")
|
| 95 |
+
inputs = processor(images=image, text=args.text, return_tensors="pt").to(args.device)
|
| 96 |
+
|
| 97 |
+
with torch.inference_mode(), torch.autocast(args.device, dtype=torch.bfloat16):
|
| 98 |
+
outputs = model(**inputs)
|
| 99 |
+
|
| 100 |
+
correspondences = processor.post_process_correspondences(
|
| 101 |
+
outputs,
|
| 102 |
+
text=args.text,
|
| 103 |
+
target_sizes=[(image.height, image.width)],
|
| 104 |
+
presence_threshold=args.presence_threshold,
|
| 105 |
+
text_threshold=args.text_threshold,
|
| 106 |
+
image_threshold=args.image_threshold,
|
| 107 |
+
nms_iou_threshold=args.nms_iou_threshold,
|
| 108 |
+
)[0]
|
| 109 |
+
|
| 110 |
+
print(f"\nimage: {args.image} ({image.width}x{image.height})")
|
| 111 |
+
print(f"text: {args.text}")
|
| 112 |
+
print(f"\n{len(correspondences)} correspondence(s):")
|
| 113 |
+
for correspondence in correspondences:
|
| 114 |
+
mask = correspondence["mask"]
|
| 115 |
+
phrases = " / ".join(correspondence["text_phrases"]) or "(no text span)"
|
| 116 |
+
area = 100.0 * mask.mean() if mask is not None else 0.0
|
| 117 |
+
print(
|
| 118 |
+
f" presence={correspondence['presence_score']:.3f} "
|
| 119 |
+
f"bridge={correspondence['bridge_index']:3d} "
|
| 120 |
+
f"mask={area:5.2f}% of image spans={correspondence['text_spans']} | {phrases}"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
if args.output is not None:
|
| 124 |
+
overlay_masks(image, correspondences).save(args.output)
|
| 125 |
+
print(f"\nwrote {args.output}")
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
if __name__ == "__main__":
|
| 129 |
+
main()
|
merges.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
model.jpg
ADDED
|
Git LFS Details
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4bdd5c7f935c7288241191066d0d3860d82ffe0d371071dd3f010e5317d9aab4
|
| 3 |
+
size 1760946056
|
modeling_concor1.py
ADDED
|
@@ -0,0 +1,804 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
# Copyright 2026 The ConCor-1 authors.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
"""PyTorch ConCor-1 model — vision-language grounding as bidirectional concept
|
| 16 |
+
correspondence.
|
| 17 |
+
|
| 18 |
+
ConCor-1 uses a pretrained Qwen3.5-0.8B vision-language model as a *contextual
|
| 19 |
+
image-text encoder* (not as a generator) and appends `Q` learnable **bridge
|
| 20 |
+
tokens** to the multimodal sequence:
|
| 21 |
+
|
| 22 |
+
[<vision_start>, <image_pad> x N_v, <vision_end>, text tokens x N_t, bridge tokens x Q]
|
| 23 |
+
|
| 24 |
+
For every bridge token, three lightweight heads predict one candidate
|
| 25 |
+
image-text correspondence:
|
| 26 |
+
|
| 27 |
+
* `presence_head` — a scalar presence score: is this a valid correspondence?
|
| 28 |
+
* `text_segmentation_head` — a binary mask over the input text tokens
|
| 29 |
+
* `vision_segmentation_head` — a binary mask over the image, on a 4-pixel cell grid
|
| 30 |
+
|
| 31 |
+
The backbone's full-attention layers are run with a **bidirectional** mask so
|
| 32 |
+
that bridge tokens see the complete multimodal context and can differentiate
|
| 33 |
+
from one another; the linear-attention (gated delta-net) layers keep their
|
| 34 |
+
original behaviour. This model is therefore not autoregressive: it does not
|
| 35 |
+
support `use_cache`, `past_key_values` or `generate()`.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
from __future__ import annotations
|
| 39 |
+
|
| 40 |
+
import math
|
| 41 |
+
from dataclasses import dataclass
|
| 42 |
+
from typing import List, Optional, Tuple
|
| 43 |
+
|
| 44 |
+
import torch
|
| 45 |
+
import torch.nn as nn
|
| 46 |
+
import torch.nn.functional as F
|
| 47 |
+
|
| 48 |
+
from transformers.masking_utils import create_bidirectional_mask
|
| 49 |
+
from transformers.modeling_outputs import ModelOutput
|
| 50 |
+
from transformers.modeling_utils import PreTrainedModel
|
| 51 |
+
from transformers.models.qwen3_5 import modeling_qwen3_5 as qwen3_5
|
| 52 |
+
from transformers.models.qwen3_5.modeling_qwen3_5 import (
|
| 53 |
+
Qwen3_5Model,
|
| 54 |
+
Qwen3_5ModelOutputWithPast,
|
| 55 |
+
Qwen3_5TextModel,
|
| 56 |
+
)
|
| 57 |
+
from transformers.utils import logging
|
| 58 |
+
|
| 59 |
+
from .configuration_concor1 import ConCor1Config
|
| 60 |
+
|
| 61 |
+
logger = logging.get_logger(__name__)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 65 |
+
# Backbone: Qwen3.5 with bidirectional full-attention layers
|
| 66 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class ConCor1BidirectionalTextModel(Qwen3_5TextModel):
|
| 70 |
+
"""Qwen3.5 text model whose *full-attention* layers are bidirectional.
|
| 71 |
+
|
| 72 |
+
Identical to [`Qwen3_5TextModel`] except that the mask handed to the
|
| 73 |
+
full-attention layers is built with `create_bidirectional_mask` instead of
|
| 74 |
+
`create_causal_mask`, and `is_causal=False` is forced so the attention
|
| 75 |
+
backend cannot silently fall back to causal masking. The linear-attention
|
| 76 |
+
layers are untouched, preserving Qwen3.5's pretrained hybrid-attention
|
| 77 |
+
structure.
|
| 78 |
+
|
| 79 |
+
Bidirectional attention is only defined for full-sequence forward passes,
|
| 80 |
+
so KV caching / incremental decoding is rejected.
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
@qwen3_5.merge_with_config_defaults
|
| 84 |
+
@qwen3_5.capture_outputs
|
| 85 |
+
def forward(
|
| 86 |
+
self,
|
| 87 |
+
input_ids: Optional[torch.LongTensor] = None,
|
| 88 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 89 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 90 |
+
past_key_values=None,
|
| 91 |
+
inputs_embeds: Optional[torch.FloatTensor] = None,
|
| 92 |
+
use_cache: Optional[bool] = None,
|
| 93 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 94 |
+
**kwargs,
|
| 95 |
+
) -> Qwen3_5ModelOutputWithPast:
|
| 96 |
+
bidirectional = kwargs.pop("bidirectional_full_attention", True)
|
| 97 |
+
|
| 98 |
+
if (input_ids is None) ^ (inputs_embeds is not None):
|
| 99 |
+
raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
|
| 100 |
+
|
| 101 |
+
if bidirectional:
|
| 102 |
+
if past_key_values is not None:
|
| 103 |
+
raise ValueError(
|
| 104 |
+
"ConCor-1's bidirectional attention does not support `past_key_values`; "
|
| 105 |
+
"use a full-sequence forward pass."
|
| 106 |
+
)
|
| 107 |
+
if use_cache:
|
| 108 |
+
raise ValueError(
|
| 109 |
+
"ConCor-1's bidirectional attention does not support `use_cache=True` or "
|
| 110 |
+
"autoregressive `generate()`. Pass `use_cache=False`."
|
| 111 |
+
)
|
| 112 |
+
use_cache = False
|
| 113 |
+
|
| 114 |
+
if inputs_embeds is None:
|
| 115 |
+
inputs_embeds = self.embed_tokens(input_ids)
|
| 116 |
+
|
| 117 |
+
if use_cache and past_key_values is None:
|
| 118 |
+
past_key_values = qwen3_5.Qwen3_5DynamicCache(config=self.config)
|
| 119 |
+
|
| 120 |
+
if cache_position is None:
|
| 121 |
+
past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
|
| 122 |
+
cache_position = torch.arange(
|
| 123 |
+
past_seen_tokens,
|
| 124 |
+
past_seen_tokens + inputs_embeds.shape[1],
|
| 125 |
+
device=inputs_embeds.device,
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
if position_ids is None:
|
| 129 |
+
position_ids = cache_position.view(1, 1, -1).expand(4, inputs_embeds.shape[0], -1)
|
| 130 |
+
elif position_ids.ndim == 2:
|
| 131 |
+
position_ids = position_ids[None, ...].expand(4, position_ids.shape[0], -1)
|
| 132 |
+
|
| 133 |
+
if position_ids.ndim == 3 and position_ids.shape[0] == 4:
|
| 134 |
+
text_position_ids = position_ids[0]
|
| 135 |
+
position_ids = position_ids[1:]
|
| 136 |
+
else:
|
| 137 |
+
text_position_ids = None
|
| 138 |
+
|
| 139 |
+
# Linear-attention layers keep the original mask path.
|
| 140 |
+
linear_attn_mask = self._update_linear_attn_mask(attention_mask, cache_position)
|
| 141 |
+
|
| 142 |
+
if bidirectional:
|
| 143 |
+
full_attn_mask = create_bidirectional_mask(
|
| 144 |
+
config=self.config,
|
| 145 |
+
inputs_embeds=inputs_embeds,
|
| 146 |
+
attention_mask=attention_mask,
|
| 147 |
+
)
|
| 148 |
+
else:
|
| 149 |
+
full_attn_mask = qwen3_5.create_causal_mask(
|
| 150 |
+
config=self.config,
|
| 151 |
+
inputs_embeds=inputs_embeds,
|
| 152 |
+
attention_mask=attention_mask,
|
| 153 |
+
cache_position=cache_position,
|
| 154 |
+
past_key_values=past_key_values,
|
| 155 |
+
position_ids=text_position_ids,
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
hidden_states = inputs_embeds
|
| 159 |
+
position_embeddings = self.rotary_emb(hidden_states, position_ids)
|
| 160 |
+
|
| 161 |
+
for decoder_layer in self.layers[: self.config.num_hidden_layers]:
|
| 162 |
+
if decoder_layer.layer_type == "linear_attention":
|
| 163 |
+
layer_mask = linear_attn_mask
|
| 164 |
+
layer_kwargs = kwargs
|
| 165 |
+
else:
|
| 166 |
+
layer_mask = full_attn_mask
|
| 167 |
+
layer_kwargs = dict(kwargs)
|
| 168 |
+
if bidirectional:
|
| 169 |
+
layer_kwargs["is_causal"] = False
|
| 170 |
+
|
| 171 |
+
hidden_states = decoder_layer(
|
| 172 |
+
hidden_states,
|
| 173 |
+
position_embeddings=position_embeddings,
|
| 174 |
+
attention_mask=layer_mask,
|
| 175 |
+
position_ids=text_position_ids,
|
| 176 |
+
past_key_values=past_key_values,
|
| 177 |
+
use_cache=use_cache,
|
| 178 |
+
cache_position=cache_position,
|
| 179 |
+
**layer_kwargs,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
hidden_states = self.norm(hidden_states)
|
| 183 |
+
|
| 184 |
+
return Qwen3_5ModelOutputWithPast(
|
| 185 |
+
last_hidden_state=hidden_states,
|
| 186 |
+
past_key_values=past_key_values,
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
class ConCor1VisionLanguageBackbone(Qwen3_5Model):
|
| 191 |
+
"""Qwen3.5 vision-language backbone with a bidirectional text model."""
|
| 192 |
+
|
| 193 |
+
def __init__(self, config):
|
| 194 |
+
super().__init__(config)
|
| 195 |
+
# Same weights and layout as Qwen3_5TextModel; only the attention mask of
|
| 196 |
+
# the full-attention layers differs (see ConCor1BidirectionalTextModel).
|
| 197 |
+
self.language_model.__class__ = ConCor1BidirectionalTextModel
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 201 |
+
# Building blocks
|
| 202 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
class ConCor1SwiGLUProjection(nn.Module):
|
| 206 |
+
"""SwiGLU projection `norm(silu(W_g x) * W_u x)`, following the backbone's FFN style."""
|
| 207 |
+
|
| 208 |
+
def __init__(self, in_features: int, out_features: int):
|
| 209 |
+
super().__init__()
|
| 210 |
+
self.gate_proj = nn.Linear(in_features, out_features, bias=False)
|
| 211 |
+
self.up_proj = nn.Linear(in_features, out_features, bias=False)
|
| 212 |
+
self.norm = nn.RMSNorm(out_features)
|
| 213 |
+
|
| 214 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 215 |
+
return self.norm(F.silu(self.gate_proj(hidden_states)) * self.up_proj(hidden_states))
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
class ConCor1LayerNorm2d(nn.LayerNorm):
|
| 219 |
+
"""LayerNorm over the channel dimension of `(B, C, H, W)` tensors."""
|
| 220 |
+
|
| 221 |
+
def __init__(self, num_channels: int, eps: float = 1e-6):
|
| 222 |
+
super().__init__(num_channels, eps=eps, elementwise_affine=True)
|
| 223 |
+
|
| 224 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 225 |
+
hidden_states = hidden_states.permute(0, 2, 3, 1)
|
| 226 |
+
hidden_states = F.layer_norm(
|
| 227 |
+
hidden_states, self.normalized_shape, self.weight, self.bias, self.eps
|
| 228 |
+
)
|
| 229 |
+
return hidden_states.permute(0, 3, 1, 2)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
class ConCor1PresenceHead(nn.Module):
|
| 233 |
+
"""Presence head: SwiGLU MLP over a bridge token, projected to a scalar logit.
|
| 234 |
+
|
| 235 |
+
`z_pres[j] = W_o · norm(silu(W_g b_j) * W_u b_j)`
|
| 236 |
+
"""
|
| 237 |
+
|
| 238 |
+
def __init__(self, hidden_size: int, intermediate_size: int):
|
| 239 |
+
super().__init__()
|
| 240 |
+
self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
|
| 241 |
+
self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
|
| 242 |
+
self.norm = nn.RMSNorm(intermediate_size)
|
| 243 |
+
self.out_proj = nn.Linear(intermediate_size, 1, bias=False)
|
| 244 |
+
|
| 245 |
+
def forward(self, bridge_features: torch.Tensor) -> torch.Tensor:
|
| 246 |
+
bridge_features = bridge_features.to(self.gate_proj.weight.dtype)
|
| 247 |
+
hidden = self.norm(
|
| 248 |
+
F.silu(self.gate_proj(bridge_features)) * self.up_proj(bridge_features)
|
| 249 |
+
)
|
| 250 |
+
return self.out_proj(hidden)
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
class ConCor1BilinearCorrespondenceScorer(nn.Module):
|
| 254 |
+
"""Bilinear scorer between bridge tokens and a sequence of features.
|
| 255 |
+
|
| 256 |
+
Both sides are projected into a shared `correspondence_dim` space with
|
| 257 |
+
independent SwiGLU MLPs, then scored with a learnable bilinear form:
|
| 258 |
+
|
| 259 |
+
z[j, n] = phi_f(f_n)^T W phi_b(b_j)
|
| 260 |
+
|
| 261 |
+
Used both as the text segmentation head (features = text tokens) and as the
|
| 262 |
+
mask predictor of the vision segmentation head (features = decoded spatial
|
| 263 |
+
cells).
|
| 264 |
+
"""
|
| 265 |
+
|
| 266 |
+
def __init__(self, feature_dim: int, bridge_dim: int, correspondence_dim: int = 256):
|
| 267 |
+
super().__init__()
|
| 268 |
+
self.correspondence_dim = correspondence_dim
|
| 269 |
+
self.feature_proj = ConCor1SwiGLUProjection(feature_dim, correspondence_dim)
|
| 270 |
+
self.bridge_proj = ConCor1SwiGLUProjection(bridge_dim, correspondence_dim)
|
| 271 |
+
self.bilinear = nn.Bilinear(correspondence_dim, correspondence_dim, 1, bias=False)
|
| 272 |
+
|
| 273 |
+
def forward(self, features: torch.Tensor, bridge_features: torch.Tensor) -> torch.Tensor:
|
| 274 |
+
"""
|
| 275 |
+
Args:
|
| 276 |
+
features: `(B, N, feature_dim)`
|
| 277 |
+
bridge_features: `(B, Q, bridge_dim)`
|
| 278 |
+
|
| 279 |
+
Returns:
|
| 280 |
+
`(B, Q, N)` correspondence logits.
|
| 281 |
+
"""
|
| 282 |
+
parameter_dtype = self.bilinear.weight.dtype
|
| 283 |
+
features = features.to(parameter_dtype)
|
| 284 |
+
bridge_features = bridge_features.to(parameter_dtype)
|
| 285 |
+
projected_features = self.feature_proj(features) # (B, N, C)
|
| 286 |
+
projected_bridges = self.bridge_proj(bridge_features) # (B, Q, C)
|
| 287 |
+
weight = self.bilinear.weight.squeeze(0) # (C, C)
|
| 288 |
+
logits = torch.einsum("bni, ij, bqj -> bqn", projected_features, weight, projected_bridges)
|
| 289 |
+
return logits.to(projected_features.dtype)
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
class ConCor1PatchExpander(nn.Module):
|
| 293 |
+
"""Expand each merged visual token back into its `merge_size**2` patch features.
|
| 294 |
+
|
| 295 |
+
The backbone merges 2x2 patch neighbourhoods into one visual token, so mask
|
| 296 |
+
prediction over merged tokens would be spatially coarse. Four independent
|
| 297 |
+
SwiGLU MLPs map one merged token (`hidden_size`) to the top-left, top-right,
|
| 298 |
+
bottom-left and bottom-right pre-merger patch features (`vision_hidden_size`).
|
| 299 |
+
|
| 300 |
+
Output order is interleaved: the four children of a merged token are
|
| 301 |
+
contiguous, matching the pre-merger ViT patch order.
|
| 302 |
+
"""
|
| 303 |
+
|
| 304 |
+
def __init__(self, hidden_size: int, patch_dim: int, num_children: int = 4):
|
| 305 |
+
super().__init__()
|
| 306 |
+
self.patch_dim = patch_dim
|
| 307 |
+
self.num_children = num_children
|
| 308 |
+
self.branches = nn.ModuleList(
|
| 309 |
+
[ConCor1SwiGLUProjection(hidden_size, patch_dim) for _ in range(num_children)]
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
def forward(
|
| 313 |
+
self,
|
| 314 |
+
visual_features: torch.Tensor,
|
| 315 |
+
patch_features: Optional[torch.Tensor] = None,
|
| 316 |
+
) -> torch.Tensor:
|
| 317 |
+
"""
|
| 318 |
+
Args:
|
| 319 |
+
visual_features: `(B, N_vis, hidden_size)` merged visual tokens.
|
| 320 |
+
patch_features: `(B, N_vis * num_children, patch_dim)`, optional
|
| 321 |
+
pre-merger ViT patch features fused into the expanded features.
|
| 322 |
+
|
| 323 |
+
Returns:
|
| 324 |
+
`(B, N_vis * num_children, patch_dim)`, interleaved.
|
| 325 |
+
"""
|
| 326 |
+
visual_features = visual_features.to(self.branches[0].gate_proj.weight.dtype)
|
| 327 |
+
batch_size, num_visual, _ = visual_features.shape
|
| 328 |
+
|
| 329 |
+
if patch_features is not None:
|
| 330 |
+
expected = num_visual * self.num_children
|
| 331 |
+
if patch_features.shape[1] != expected:
|
| 332 |
+
raise ValueError(
|
| 333 |
+
f"patch_features.shape[1]={patch_features.shape[1]} != "
|
| 334 |
+
f"N_vis * num_children = {expected}"
|
| 335 |
+
)
|
| 336 |
+
if patch_features.shape[2] != self.patch_dim:
|
| 337 |
+
raise ValueError(
|
| 338 |
+
f"patch_features.shape[2]={patch_features.shape[2]} != patch_dim={self.patch_dim}"
|
| 339 |
+
)
|
| 340 |
+
patch_features = patch_features.view(
|
| 341 |
+
batch_size, num_visual, self.num_children, self.patch_dim
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
children = []
|
| 345 |
+
for index, branch in enumerate(self.branches):
|
| 346 |
+
child = branch(visual_features)
|
| 347 |
+
if patch_features is not None:
|
| 348 |
+
child = child + patch_features[:, :, index, :].to(
|
| 349 |
+
device=child.device, dtype=child.dtype
|
| 350 |
+
)
|
| 351 |
+
children.append(child)
|
| 352 |
+
|
| 353 |
+
stacked = torch.stack(children, dim=2) # (B, N_vis, num_children, patch_dim)
|
| 354 |
+
return stacked.reshape(batch_size, num_visual * self.num_children, self.patch_dim)
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
class ConCor1UpsampleBlock(nn.Module):
|
| 358 |
+
"""2x upsampling block: transposed conv → SiLU → depthwise 3x3 refine → LayerNorm2d."""
|
| 359 |
+
|
| 360 |
+
def __init__(self, dim: int):
|
| 361 |
+
super().__init__()
|
| 362 |
+
self.up = nn.ConvTranspose2d(dim, dim, kernel_size=2, stride=2)
|
| 363 |
+
self.act = nn.SiLU()
|
| 364 |
+
self.refine = nn.Conv2d(dim, dim, kernel_size=3, padding=1, groups=dim, bias=False)
|
| 365 |
+
self.norm = ConCor1LayerNorm2d(dim)
|
| 366 |
+
|
| 367 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 368 |
+
hidden_states = self.up(hidden_states)
|
| 369 |
+
hidden_states = self.act(hidden_states)
|
| 370 |
+
hidden_states = self.refine(hidden_states)
|
| 371 |
+
return self.norm(hidden_states)
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
class ConCor1ConvolutionalDecoder(nn.Module):
|
| 375 |
+
"""Lightweight convolutional decoder: `num_blocks` successive 2x upsamplings."""
|
| 376 |
+
|
| 377 |
+
def __init__(self, dim: int, num_blocks: int = 2):
|
| 378 |
+
super().__init__()
|
| 379 |
+
self.blocks = nn.ModuleList([ConCor1UpsampleBlock(dim) for _ in range(num_blocks)])
|
| 380 |
+
|
| 381 |
+
@property
|
| 382 |
+
def scale_factor(self) -> int:
|
| 383 |
+
return 2 ** len(self.blocks)
|
| 384 |
+
|
| 385 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 386 |
+
for block in self.blocks:
|
| 387 |
+
hidden_states = block(hidden_states)
|
| 388 |
+
return hidden_states
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
class ConCor1VisionSegmentationHead(nn.Module):
|
| 392 |
+
"""Predict one image mask per bridge token.
|
| 393 |
+
|
| 394 |
+
Two components, as described in the paper:
|
| 395 |
+
|
| 396 |
+
1. a *feature decoder* that reconstructs dense visual features from the
|
| 397 |
+
spatially compressed backbone visual tokens — `patch_expander`
|
| 398 |
+
(+ optional fusion of pre-merger ViT features) followed by
|
| 399 |
+
`convolutional_decoder`;
|
| 400 |
+
2. a *mask predictor* that scores every decoded spatial cell against every
|
| 401 |
+
bridge token with a bilinear form — `mask_predictor`.
|
| 402 |
+
"""
|
| 403 |
+
|
| 404 |
+
def __init__(self, config: ConCor1Config):
|
| 405 |
+
super().__init__()
|
| 406 |
+
self.merge_size = config.merge_size
|
| 407 |
+
self.patch_expander = ConCor1PatchExpander(
|
| 408 |
+
hidden_size=config.hidden_size,
|
| 409 |
+
patch_dim=config.vision_hidden_size,
|
| 410 |
+
num_children=config.merge_size ** 2,
|
| 411 |
+
)
|
| 412 |
+
self.convolutional_decoder = ConCor1ConvolutionalDecoder(
|
| 413 |
+
dim=config.vision_hidden_size,
|
| 414 |
+
num_blocks=config.num_mask_upsample_blocks,
|
| 415 |
+
)
|
| 416 |
+
self.mask_predictor = ConCor1BilinearCorrespondenceScorer(
|
| 417 |
+
feature_dim=config.vision_hidden_size,
|
| 418 |
+
bridge_dim=config.hidden_size,
|
| 419 |
+
correspondence_dim=config.correspondence_dim,
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
def decode_features(
|
| 423 |
+
self,
|
| 424 |
+
visual_features: torch.Tensor,
|
| 425 |
+
patch_features: Optional[torch.Tensor],
|
| 426 |
+
image_grid_thw: torch.Tensor,
|
| 427 |
+
visual_token_mask: torch.Tensor,
|
| 428 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 429 |
+
"""Expand, fuse and upsample the visual tokens into a dense feature map.
|
| 430 |
+
|
| 431 |
+
Returns:
|
| 432 |
+
features: `(B, N_cells_max, patch_dim)` in row-major order, zero-padded.
|
| 433 |
+
grid_hw: `(B, 2)` — the `(height, width)` of each sample's cell grid.
|
| 434 |
+
"""
|
| 435 |
+
expanded = self.patch_expander(visual_features, patch_features=patch_features)
|
| 436 |
+
|
| 437 |
+
batch_size = expanded.shape[0]
|
| 438 |
+
dim = expanded.shape[-1]
|
| 439 |
+
merge_size = self.merge_size
|
| 440 |
+
scale = self.convolutional_decoder.scale_factor
|
| 441 |
+
|
| 442 |
+
per_sample: List[torch.Tensor] = []
|
| 443 |
+
grid_hw = expanded.new_zeros((batch_size, 2), dtype=torch.long)
|
| 444 |
+
|
| 445 |
+
for index in range(batch_size):
|
| 446 |
+
num_visual = int(visual_token_mask[index].sum().item())
|
| 447 |
+
if num_visual == 0: # text-only sample
|
| 448 |
+
per_sample.append(expanded.new_zeros(1, dim))
|
| 449 |
+
continue
|
| 450 |
+
|
| 451 |
+
patch_h = int(image_grid_thw[index, 1].item())
|
| 452 |
+
patch_w = int(image_grid_thw[index, 2].item())
|
| 453 |
+
merged_h, merged_w = patch_h // merge_size, patch_w // merge_size
|
| 454 |
+
|
| 455 |
+
# Interleaved children → row-major 2D patch grid.
|
| 456 |
+
features = expanded[index, : num_visual * merge_size ** 2]
|
| 457 |
+
features = features.reshape(merged_h, merged_w, merge_size, merge_size, dim)
|
| 458 |
+
features = features.permute(0, 2, 1, 3, 4).reshape(patch_h, patch_w, dim)
|
| 459 |
+
|
| 460 |
+
# (1, D, patch_h, patch_w) → conv decoder → (1, D, patch_h * s, patch_w * s)
|
| 461 |
+
feature_map = features.permute(2, 0, 1).unsqueeze(0)
|
| 462 |
+
feature_map = self.convolutional_decoder(feature_map)
|
| 463 |
+
|
| 464 |
+
grid_hw[index, 0] = patch_h * scale
|
| 465 |
+
grid_hw[index, 1] = patch_w * scale
|
| 466 |
+
per_sample.append(feature_map.squeeze(0).permute(1, 2, 0).reshape(-1, dim))
|
| 467 |
+
|
| 468 |
+
num_cells = max(max(f.shape[0] for f in per_sample), 1)
|
| 469 |
+
padded = expanded.new_zeros(batch_size, num_cells, dim)
|
| 470 |
+
for index, features in enumerate(per_sample):
|
| 471 |
+
padded[index, : features.shape[0]] = features
|
| 472 |
+
return padded, grid_hw
|
| 473 |
+
|
| 474 |
+
def forward(
|
| 475 |
+
self,
|
| 476 |
+
visual_features: torch.Tensor,
|
| 477 |
+
bridge_features: torch.Tensor,
|
| 478 |
+
patch_features: Optional[torch.Tensor],
|
| 479 |
+
image_grid_thw: torch.Tensor,
|
| 480 |
+
visual_token_mask: torch.Tensor,
|
| 481 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 482 |
+
features, grid_hw = self.decode_features(
|
| 483 |
+
visual_features, patch_features, image_grid_thw, visual_token_mask
|
| 484 |
+
)
|
| 485 |
+
return self.mask_predictor(features, bridge_features), grid_hw
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
def extract_tokens_by_mask(hidden_states: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 489 |
+
"""Gather the hidden states where `mask` is True, keeping left-to-right order.
|
| 490 |
+
|
| 491 |
+
Rows with fewer selected positions than the batch maximum are zero-padded.
|
| 492 |
+
|
| 493 |
+
Args:
|
| 494 |
+
hidden_states: `(B, S, D)`
|
| 495 |
+
mask: `(B, S)` boolean
|
| 496 |
+
|
| 497 |
+
Returns:
|
| 498 |
+
`(B, max_selected, D)`
|
| 499 |
+
"""
|
| 500 |
+
_, _, dim = hidden_states.shape
|
| 501 |
+
is_valid = mask.bool()
|
| 502 |
+
|
| 503 |
+
max_count = max(int(is_valid.sum(dim=1).max().item()), 1)
|
| 504 |
+
|
| 505 |
+
sorted_indices = torch.argsort(is_valid.int(), dim=1, descending=True, stable=True)
|
| 506 |
+
selected = sorted_indices[:, :max_count]
|
| 507 |
+
|
| 508 |
+
extracted = torch.gather(hidden_states, 1, selected.unsqueeze(-1).expand(-1, -1, dim))
|
| 509 |
+
valid = torch.gather(is_valid, 1, selected)
|
| 510 |
+
return extracted * valid.unsqueeze(-1).to(extracted.dtype)
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 514 |
+
# Model
|
| 515 |
+
# ══════════════════════════════════════════════════════════════════════════════
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
@dataclass
|
| 519 |
+
class ConCor1Output(ModelOutput):
|
| 520 |
+
"""Correspondence predictions of [`ConCor1ForConceptCorrespondence`].
|
| 521 |
+
|
| 522 |
+
Args:
|
| 523 |
+
presence_logits: `(B, Q)` — logit that bridge `q` holds a valid
|
| 524 |
+
image-text correspondence.
|
| 525 |
+
text_mask_logits: `(B, Q, N_text)` — per-bridge logits over the text
|
| 526 |
+
tokens selected by `text_token_mask` (the bridge's text mask).
|
| 527 |
+
image_mask_logits: `(B, Q, N_cells)` — per-bridge logits over the decoded
|
| 528 |
+
image cells, row-major, zero-padded across the batch.
|
| 529 |
+
image_mask_grid_hw: `(B, 2)` — `(height, width)` of every sample's cell
|
| 530 |
+
grid, so `image_mask_logits[b, q, : h * w].reshape(h, w)` is the mask
|
| 531 |
+
map. One cell covers `config.mask_cell_size` pixels per side of the
|
| 532 |
+
(resized) image.
|
| 533 |
+
last_hidden_state: `(B, S, D)` — the backbone's final hidden states over
|
| 534 |
+
the full multimodal sequence.
|
| 535 |
+
"""
|
| 536 |
+
|
| 537 |
+
presence_logits: Optional[torch.FloatTensor] = None
|
| 538 |
+
text_mask_logits: Optional[torch.FloatTensor] = None
|
| 539 |
+
image_mask_logits: Optional[torch.FloatTensor] = None
|
| 540 |
+
image_mask_grid_hw: Optional[torch.LongTensor] = None
|
| 541 |
+
last_hidden_state: Optional[torch.FloatTensor] = None
|
| 542 |
+
|
| 543 |
+
|
| 544 |
+
class ConCor1PreTrainedModel(PreTrainedModel):
|
| 545 |
+
config_class = ConCor1Config
|
| 546 |
+
base_model_prefix = "concor1"
|
| 547 |
+
supports_gradient_checkpointing = True
|
| 548 |
+
_no_split_modules = ["Qwen3_5DecoderLayer", "Qwen3_5VisionBlock"]
|
| 549 |
+
_keep_in_fp32_modules_strict = [
|
| 550 |
+
"presence_head",
|
| 551 |
+
"text_segmentation_head",
|
| 552 |
+
"vision_segmentation_head",
|
| 553 |
+
]
|
| 554 |
+
_supports_flash_attn = True
|
| 555 |
+
_supports_sdpa = True
|
| 556 |
+
_supports_flex_attn = False
|
| 557 |
+
_can_compile_fullgraph = False
|
| 558 |
+
|
| 559 |
+
def _init_weights(self, module):
|
| 560 |
+
if isinstance(module, (nn.Linear, nn.Bilinear, nn.Conv2d, nn.ConvTranspose2d)):
|
| 561 |
+
nn.init.kaiming_uniform_(module.weight, a=math.sqrt(5))
|
| 562 |
+
if getattr(module, "bias", None) is not None:
|
| 563 |
+
nn.init.zeros_(module.bias)
|
| 564 |
+
elif isinstance(module, (nn.LayerNorm, nn.RMSNorm)):
|
| 565 |
+
if module.weight is not None:
|
| 566 |
+
nn.init.ones_(module.weight)
|
| 567 |
+
if getattr(module, "bias", None) is not None:
|
| 568 |
+
nn.init.zeros_(module.bias)
|
| 569 |
+
|
| 570 |
+
|
| 571 |
+
class ConCor1ForConceptCorrespondence(ConCor1PreTrainedModel):
|
| 572 |
+
"""ConCor-1: bidirectional concept correspondence over an image-text pair.
|
| 573 |
+
|
| 574 |
+
Example:
|
| 575 |
+
|
| 576 |
+
```python
|
| 577 |
+
>>> import requests
|
| 578 |
+
>>> import torch
|
| 579 |
+
>>> from PIL import Image
|
| 580 |
+
>>> from transformers import AutoModel, AutoProcessor
|
| 581 |
+
|
| 582 |
+
>>> processor = AutoProcessor.from_pretrained("UWGZQ/ConCor-1", trust_remote_code=True)
|
| 583 |
+
>>> model = AutoModel.from_pretrained("UWGZQ/ConCor-1", trust_remote_code=True, dtype=torch.bfloat16).cuda().eval()
|
| 584 |
+
|
| 585 |
+
>>> url = "http://images.cocodataset.org/val2017/000000000285.jpg"
|
| 586 |
+
>>> image = Image.open(requests.get(url, stream=True).raw).convert("RGB")
|
| 587 |
+
>>> text = (
|
| 588 |
+
... "This image depicts a close-up of a brown bear in a natural outdoor setting. "
|
| 589 |
+
... "The background consists of lush green grass. In the foreground, a large brown "
|
| 590 |
+
... "bear is positioned centrally."
|
| 591 |
+
... )
|
| 592 |
+
>>> inputs = processor(images=image, text=text, return_tensors="pt").to("cuda")
|
| 593 |
+
>>> with torch.inference_mode():
|
| 594 |
+
... outputs = model(**inputs)
|
| 595 |
+
>>> correspondences = processor.post_process_correspondences(
|
| 596 |
+
... outputs, text=text, target_sizes=[(image.height, image.width)]
|
| 597 |
+
... )[0]
|
| 598 |
+
>>> [(round(c["presence_score"], 3), c["text_phrases"]) for c in correspondences]
|
| 599 |
+
```
|
| 600 |
+
"""
|
| 601 |
+
|
| 602 |
+
def __init__(self, config: ConCor1Config):
|
| 603 |
+
super().__init__(config)
|
| 604 |
+
|
| 605 |
+
if not config.bidirectional_full_attention:
|
| 606 |
+
logger.warning(
|
| 607 |
+
"bidirectional_full_attention=False runs the backbone's full-attention layers "
|
| 608 |
+
"causally. ConCor-1 was trained with bidirectional full attention; predictions "
|
| 609 |
+
"will be badly degraded."
|
| 610 |
+
)
|
| 611 |
+
|
| 612 |
+
backbone_config = config.backbone_config
|
| 613 |
+
backbone_config._attn_implementation = config._attn_implementation
|
| 614 |
+
self.backbone = ConCor1VisionLanguageBackbone(backbone_config)
|
| 615 |
+
|
| 616 |
+
self.presence_head = ConCor1PresenceHead(
|
| 617 |
+
hidden_size=config.hidden_size,
|
| 618 |
+
intermediate_size=config.presence_hidden_dim,
|
| 619 |
+
)
|
| 620 |
+
self.text_segmentation_head = ConCor1BilinearCorrespondenceScorer(
|
| 621 |
+
feature_dim=config.hidden_size,
|
| 622 |
+
bridge_dim=config.hidden_size,
|
| 623 |
+
correspondence_dim=config.correspondence_dim,
|
| 624 |
+
)
|
| 625 |
+
self.vision_segmentation_head = ConCor1VisionSegmentationHead(config)
|
| 626 |
+
|
| 627 |
+
self.post_init()
|
| 628 |
+
|
| 629 |
+
def get_input_embeddings(self) -> nn.Module:
|
| 630 |
+
return self.backbone.get_input_embeddings()
|
| 631 |
+
|
| 632 |
+
def set_input_embeddings(self, value: nn.Module) -> None:
|
| 633 |
+
self.backbone.set_input_embeddings(value)
|
| 634 |
+
|
| 635 |
+
def _encode(
|
| 636 |
+
self,
|
| 637 |
+
input_ids: torch.LongTensor,
|
| 638 |
+
pixel_values: Optional[torch.FloatTensor],
|
| 639 |
+
image_grid_thw: Optional[torch.LongTensor],
|
| 640 |
+
visual_token_mask: torch.BoolTensor,
|
| 641 |
+
attention_mask: Optional[torch.Tensor],
|
| 642 |
+
) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
|
| 643 |
+
"""Run the backbone and return `(hidden_states, pre_merger_patch_features)`.
|
| 644 |
+
|
| 645 |
+
This inlines `Qwen3_5Model.forward` so that the pre-merger ViT patch
|
| 646 |
+
features (needed by the vision segmentation head) can be kept without
|
| 647 |
+
running the vision tower twice.
|
| 648 |
+
"""
|
| 649 |
+
inputs_embeds = self.backbone.get_input_embeddings()(input_ids)
|
| 650 |
+
|
| 651 |
+
patch_features = None
|
| 652 |
+
if pixel_values is not None:
|
| 653 |
+
vision_output = self.backbone.get_image_features(
|
| 654 |
+
pixel_values, image_grid_thw, return_dict=True
|
| 655 |
+
)
|
| 656 |
+
if self.config.fuse_vision_encoder_features:
|
| 657 |
+
patch_features = vision_output.last_hidden_state # (total_patches, patch_dim)
|
| 658 |
+
image_embeds = torch.cat(vision_output.pooler_output, dim=0).to(
|
| 659 |
+
inputs_embeds.device, inputs_embeds.dtype
|
| 660 |
+
)
|
| 661 |
+
image_mask, _ = self.backbone.get_placeholder_mask(
|
| 662 |
+
input_ids, inputs_embeds=inputs_embeds, image_features=image_embeds
|
| 663 |
+
)
|
| 664 |
+
inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds)
|
| 665 |
+
|
| 666 |
+
mm_token_type_ids = torch.zeros_like(input_ids, dtype=torch.int32)
|
| 667 |
+
mm_token_type_ids[visual_token_mask] = 1
|
| 668 |
+
position_ids = self.backbone.compute_3d_position_ids(
|
| 669 |
+
input_ids=input_ids,
|
| 670 |
+
image_grid_thw=image_grid_thw,
|
| 671 |
+
video_grid_thw=None,
|
| 672 |
+
inputs_embeds=inputs_embeds,
|
| 673 |
+
attention_mask=attention_mask,
|
| 674 |
+
past_key_values=None,
|
| 675 |
+
mm_token_type_ids=mm_token_type_ids,
|
| 676 |
+
)
|
| 677 |
+
|
| 678 |
+
outputs = self.backbone.language_model(
|
| 679 |
+
input_ids=None,
|
| 680 |
+
inputs_embeds=inputs_embeds,
|
| 681 |
+
position_ids=position_ids,
|
| 682 |
+
attention_mask=attention_mask,
|
| 683 |
+
use_cache=False,
|
| 684 |
+
bidirectional_full_attention=self.config.bidirectional_full_attention,
|
| 685 |
+
)
|
| 686 |
+
return outputs.last_hidden_state, patch_features
|
| 687 |
+
|
| 688 |
+
def _gather_patch_features(
|
| 689 |
+
self,
|
| 690 |
+
patch_features: torch.Tensor,
|
| 691 |
+
image_grid_thw: torch.LongTensor,
|
| 692 |
+
visual_token_mask: torch.BoolTensor,
|
| 693 |
+
num_visual_tokens: int,
|
| 694 |
+
) -> torch.Tensor:
|
| 695 |
+
"""Batch and zero-pad per-image pre-merger patch features.
|
| 696 |
+
|
| 697 |
+
The vision tower flattens all images into one sequence; split it per
|
| 698 |
+
image and pad to `num_visual_tokens * merge_size**2`, keeping the
|
| 699 |
+
interleaved child order the patch expander produces.
|
| 700 |
+
"""
|
| 701 |
+
batch_size = visual_token_mask.shape[0]
|
| 702 |
+
patch_dim = patch_features.shape[-1]
|
| 703 |
+
num_children = self.config.merge_size ** 2
|
| 704 |
+
padded = patch_features.new_zeros((batch_size, num_visual_tokens * num_children, patch_dim))
|
| 705 |
+
|
| 706 |
+
per_image = patch_features.split(image_grid_thw.prod(dim=1).tolist())
|
| 707 |
+
for index in range(batch_size):
|
| 708 |
+
num_visual = int(visual_token_mask[index].sum().item())
|
| 709 |
+
if num_visual == 0:
|
| 710 |
+
continue
|
| 711 |
+
num_patches = num_visual * num_children
|
| 712 |
+
padded[index, :num_patches] = per_image[index][:num_patches]
|
| 713 |
+
return padded
|
| 714 |
+
|
| 715 |
+
def forward(
|
| 716 |
+
self,
|
| 717 |
+
input_ids: torch.LongTensor,
|
| 718 |
+
bridge_token_mask: torch.BoolTensor,
|
| 719 |
+
text_token_mask: torch.BoolTensor,
|
| 720 |
+
visual_token_mask: Optional[torch.BoolTensor] = None,
|
| 721 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 722 |
+
pixel_values: Optional[torch.FloatTensor] = None,
|
| 723 |
+
image_grid_thw: Optional[torch.LongTensor] = None,
|
| 724 |
+
**kwargs,
|
| 725 |
+
) -> ConCor1Output:
|
| 726 |
+
r"""
|
| 727 |
+
Args:
|
| 728 |
+
input_ids (`torch.LongTensor` of shape `(B, S)`):
|
| 729 |
+
Flat multimodal sequence: `<vision_start>`, `<image_pad>` x N_v,
|
| 730 |
+
`<vision_end>`, text tokens, then the `Q` bridge token ids.
|
| 731 |
+
bridge_token_mask (`torch.BoolTensor` of shape `(B, S)`):
|
| 732 |
+
True at the bridge-token positions.
|
| 733 |
+
text_token_mask (`torch.BoolTensor` of shape `(B, S)`):
|
| 734 |
+
True at the text positions the text masks are predicted over
|
| 735 |
+
(the input text, excluding the vision and bridge tokens).
|
| 736 |
+
visual_token_mask (`torch.BoolTensor` of shape `(B, S)`, *optional*):
|
| 737 |
+
True at the `<image_pad>` positions. Required with an image.
|
| 738 |
+
attention_mask (`torch.Tensor` of shape `(B, S)`, *optional*):
|
| 739 |
+
1 for real tokens, 0 for padding.
|
| 740 |
+
pixel_values (`torch.FloatTensor`, *optional*):
|
| 741 |
+
Flattened image patches from the Qwen3.5 image processor.
|
| 742 |
+
image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*):
|
| 743 |
+
Temporal / height / width patch counts per image.
|
| 744 |
+
|
| 745 |
+
Returns:
|
| 746 |
+
[`ConCor1Output`]
|
| 747 |
+
"""
|
| 748 |
+
if kwargs.get("use_cache") or kwargs.get("past_key_values") is not None:
|
| 749 |
+
raise ValueError(
|
| 750 |
+
"ConCor-1 is not autoregressive: `use_cache` / `past_key_values` are not supported."
|
| 751 |
+
)
|
| 752 |
+
if pixel_values is not None and visual_token_mask is None:
|
| 753 |
+
raise ValueError("`visual_token_mask` is required when `pixel_values` is passed.")
|
| 754 |
+
if visual_token_mask is None:
|
| 755 |
+
visual_token_mask = torch.zeros_like(input_ids, dtype=torch.bool)
|
| 756 |
+
if attention_mask is None:
|
| 757 |
+
attention_mask = torch.ones_like(input_ids)
|
| 758 |
+
|
| 759 |
+
hidden_states, patch_features = self._encode(
|
| 760 |
+
input_ids=input_ids,
|
| 761 |
+
pixel_values=pixel_values,
|
| 762 |
+
image_grid_thw=image_grid_thw,
|
| 763 |
+
visual_token_mask=visual_token_mask,
|
| 764 |
+
attention_mask=attention_mask,
|
| 765 |
+
)
|
| 766 |
+
|
| 767 |
+
bridge_features = extract_tokens_by_mask(hidden_states, bridge_token_mask) # (B, Q, D)
|
| 768 |
+
text_features = extract_tokens_by_mask(hidden_states, text_token_mask) # (B, N_text, D)
|
| 769 |
+
|
| 770 |
+
presence_logits = self.presence_head(bridge_features).squeeze(-1) # (B, Q)
|
| 771 |
+
text_mask_logits = self.text_segmentation_head(text_features, bridge_features) # (B, Q, N_text)
|
| 772 |
+
|
| 773 |
+
image_mask_logits, image_mask_grid_hw = None, None
|
| 774 |
+
if pixel_values is not None:
|
| 775 |
+
visual_features = extract_tokens_by_mask(hidden_states, visual_token_mask)
|
| 776 |
+
if patch_features is not None:
|
| 777 |
+
patch_features = self._gather_patch_features(
|
| 778 |
+
patch_features,
|
| 779 |
+
image_grid_thw=image_grid_thw,
|
| 780 |
+
visual_token_mask=visual_token_mask,
|
| 781 |
+
num_visual_tokens=visual_features.shape[1],
|
| 782 |
+
)
|
| 783 |
+
image_mask_logits, image_mask_grid_hw = self.vision_segmentation_head(
|
| 784 |
+
visual_features=visual_features,
|
| 785 |
+
bridge_features=bridge_features,
|
| 786 |
+
patch_features=patch_features,
|
| 787 |
+
image_grid_thw=image_grid_thw,
|
| 788 |
+
visual_token_mask=visual_token_mask,
|
| 789 |
+
)
|
| 790 |
+
|
| 791 |
+
return ConCor1Output(
|
| 792 |
+
presence_logits=presence_logits,
|
| 793 |
+
text_mask_logits=text_mask_logits,
|
| 794 |
+
image_mask_logits=image_mask_logits,
|
| 795 |
+
image_mask_grid_hw=image_mask_grid_hw,
|
| 796 |
+
last_hidden_state=hidden_states,
|
| 797 |
+
)
|
| 798 |
+
|
| 799 |
+
|
| 800 |
+
__all__ = [
|
| 801 |
+
"ConCor1ForConceptCorrespondence",
|
| 802 |
+
"ConCor1PreTrainedModel",
|
| 803 |
+
"ConCor1Output",
|
| 804 |
+
]
|
preprocessor_config.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"size": {
|
| 3 |
+
"longest_edge": 16777216,
|
| 4 |
+
"shortest_edge": 65536
|
| 5 |
+
},
|
| 6 |
+
"patch_size": 16,
|
| 7 |
+
"temporal_patch_size": 2,
|
| 8 |
+
"merge_size": 2,
|
| 9 |
+
"image_mean": [
|
| 10 |
+
0.5,
|
| 11 |
+
0.5,
|
| 12 |
+
0.5
|
| 13 |
+
],
|
| 14 |
+
"image_std": [
|
| 15 |
+
0.5,
|
| 16 |
+
0.5,
|
| 17 |
+
0.5
|
| 18 |
+
],
|
| 19 |
+
"processor_class": "ConCor1Processor",
|
| 20 |
+
"image_processor_type": "Qwen2VLImageProcessorFast",
|
| 21 |
+
"min_pixels": 1003520,
|
| 22 |
+
"max_pixels": 1003520
|
| 23 |
+
}
|
processing_concor1.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
# Copyright 2026 The ConCor-1 authors.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
"""Processor for ConCor-1.
|
| 16 |
+
|
| 17 |
+
Turns an image-text pair into the flat multimodal sequence ConCor-1 expects
|
| 18 |
+
(image tokens, text tokens, bridge tokens) and turns the model's per-bridge
|
| 19 |
+
logits back into a set of image-text correspondences: a character-level text
|
| 20 |
+
span set and a binary image mask per correspondence.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import string
|
| 26 |
+
from typing import Dict, List, Optional, Sequence, Tuple, Union
|
| 27 |
+
|
| 28 |
+
import numpy as np
|
| 29 |
+
import torch
|
| 30 |
+
import torch.nn.functional as F
|
| 31 |
+
|
| 32 |
+
from transformers.feature_extraction_utils import BatchFeature
|
| 33 |
+
from transformers.processing_utils import ProcessorMixin
|
| 34 |
+
|
| 35 |
+
ImageInput = Union["PIL.Image.Image", Sequence["PIL.Image.Image"]]
|
| 36 |
+
|
| 37 |
+
_TRIM_CHARACTERS = frozenset(string.punctuation)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def compute_span_iou(spans_a: List[List[int]], spans_b: List[List[int]]) -> float:
|
| 41 |
+
"""IoU of two character-span sets, treated as 1-D binary masks over the text."""
|
| 42 |
+
characters_a = {index for start, end in spans_a for index in range(start, end)}
|
| 43 |
+
characters_b = {index for start, end in spans_b for index in range(start, end)}
|
| 44 |
+
union = len(characters_a | characters_b)
|
| 45 |
+
if union == 0:
|
| 46 |
+
return 0.0
|
| 47 |
+
return len(characters_a & characters_b) / union
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def compute_mask_iou(mask_a: np.ndarray, mask_b: np.ndarray) -> float:
|
| 51 |
+
"""IoU of two binary masks."""
|
| 52 |
+
flat_a = mask_a.astype(bool).ravel()
|
| 53 |
+
flat_b = mask_b.astype(bool).ravel()
|
| 54 |
+
union = np.count_nonzero(flat_a | flat_b)
|
| 55 |
+
if union == 0:
|
| 56 |
+
return 0.0
|
| 57 |
+
return np.count_nonzero(flat_a & flat_b) / union
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class ConCor1Processor(ProcessorMixin):
|
| 61 |
+
r"""Constructs a ConCor-1 processor from a Qwen3.5 image processor and tokenizer.
|
| 62 |
+
|
| 63 |
+
Args:
|
| 64 |
+
image_processor: the Qwen3.5 (`Qwen2VLImageProcessorFast`) image processor.
|
| 65 |
+
tokenizer: the Qwen3.5 tokenizer.
|
| 66 |
+
num_bridge_tokens (`int`, *optional*, defaults to 385):
|
| 67 |
+
Number of bridge tokens appended to every sequence.
|
| 68 |
+
bridge_token_id_start (`int`, *optional*, defaults to 248077):
|
| 69 |
+
Token id of the first bridge token.
|
| 70 |
+
patch_size (`int`, *optional*, defaults to 16): vision patch size.
|
| 71 |
+
merge_size (`int`, *optional*, defaults to 2): patch-merger factor.
|
| 72 |
+
min_pixels (`int`, *optional*, defaults to 1003520):
|
| 73 |
+
`min_pixels` forwarded to the image processor.
|
| 74 |
+
max_pixels (`int`, *optional*, defaults to 1003520):
|
| 75 |
+
`max_pixels` forwarded to the image processor. The released
|
| 76 |
+
checkpoint was trained with `min_pixels == max_pixels`, i.e. a fixed
|
| 77 |
+
~1.0 M pixel budget.
|
| 78 |
+
presence_threshold (`float`, *optional*, defaults to 0.1)
|
| 79 |
+
text_threshold (`float`, *optional*, defaults to 0.45)
|
| 80 |
+
image_threshold (`float`, *optional*, defaults to 0.45)
|
| 81 |
+
nms_iou_threshold (`float`, *optional*, defaults to 0.5)
|
| 82 |
+
|
| 83 |
+
Example:
|
| 84 |
+
|
| 85 |
+
```python
|
| 86 |
+
>>> text = "A close-up of a brown bear sitting in lush green grass."
|
| 87 |
+
>>> inputs = processor(images=image, text=text, return_tensors="pt")
|
| 88 |
+
>>> outputs = model(**inputs)
|
| 89 |
+
>>> correspondences = processor.post_process_correspondences(
|
| 90 |
+
... outputs, text=text, target_sizes=[(image.height, image.width)]
|
| 91 |
+
... )
|
| 92 |
+
```
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
attributes = ["image_processor", "tokenizer"]
|
| 96 |
+
|
| 97 |
+
image_processor_class = "Qwen2VLImageProcessorFast"
|
| 98 |
+
tokenizer_class = "AutoTokenizer"
|
| 99 |
+
|
| 100 |
+
def __init__(
|
| 101 |
+
self,
|
| 102 |
+
image_processor=None,
|
| 103 |
+
tokenizer=None,
|
| 104 |
+
num_bridge_tokens: int = 385,
|
| 105 |
+
bridge_token_id_start: int = 248077,
|
| 106 |
+
patch_size: int = 16,
|
| 107 |
+
merge_size: int = 2,
|
| 108 |
+
num_mask_upsample_blocks: int = 2,
|
| 109 |
+
min_pixels: int = 1003520,
|
| 110 |
+
max_pixels: int = 1003520,
|
| 111 |
+
presence_threshold: float = 0.1,
|
| 112 |
+
text_threshold: float = 0.45,
|
| 113 |
+
image_threshold: float = 0.45,
|
| 114 |
+
nms_iou_threshold: float = 0.5,
|
| 115 |
+
**kwargs,
|
| 116 |
+
):
|
| 117 |
+
self.num_bridge_tokens = num_bridge_tokens
|
| 118 |
+
self.bridge_token_id_start = bridge_token_id_start
|
| 119 |
+
self.patch_size = patch_size
|
| 120 |
+
self.merge_size = merge_size
|
| 121 |
+
self.num_mask_upsample_blocks = num_mask_upsample_blocks
|
| 122 |
+
self.min_pixels = min_pixels
|
| 123 |
+
self.max_pixels = max_pixels
|
| 124 |
+
self.presence_threshold = presence_threshold
|
| 125 |
+
self.text_threshold = text_threshold
|
| 126 |
+
self.image_threshold = image_threshold
|
| 127 |
+
self.nms_iou_threshold = nms_iou_threshold
|
| 128 |
+
super().__init__(image_processor, tokenizer, **kwargs)
|
| 129 |
+
|
| 130 |
+
@property
|
| 131 |
+
def bridge_token_ids(self) -> List[int]:
|
| 132 |
+
start = self.bridge_token_id_start
|
| 133 |
+
return list(range(start, start + self.num_bridge_tokens))
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def __call__(
|
| 137 |
+
self,
|
| 138 |
+
images: Optional[ImageInput] = None,
|
| 139 |
+
text: Optional[Union[str, List[str]]] = None,
|
| 140 |
+
return_tensors: str = "pt",
|
| 141 |
+
**kwargs,
|
| 142 |
+
) -> BatchFeature:
|
| 143 |
+
"""Build ConCor-1's flat multimodal sequences for one or more image-text pairs.
|
| 144 |
+
|
| 145 |
+
The text is *not* wrapped in a chat template: ConCor-1 consumes the raw
|
| 146 |
+
text (a caption, a list of category names, a referring expression, ...)
|
| 147 |
+
directly, and predicts its text masks over exactly these tokens.
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
images: one PIL image, or a list with one image per text.
|
| 151 |
+
text: the paired text, or a list of texts.
|
| 152 |
+
return_tensors: only `"pt"` is supported.
|
| 153 |
+
|
| 154 |
+
Returns:
|
| 155 |
+
[`BatchFeature`] with `input_ids`, `attention_mask`,
|
| 156 |
+
`visual_token_mask`, `text_token_mask`, `bridge_token_mask` and —
|
| 157 |
+
when images are given — `pixel_values` and `image_grid_thw`.
|
| 158 |
+
"""
|
| 159 |
+
if return_tensors != "pt":
|
| 160 |
+
raise ValueError(f"ConCor1Processor only supports return_tensors='pt', got {return_tensors!r}")
|
| 161 |
+
if text is None:
|
| 162 |
+
raise ValueError(
|
| 163 |
+
"ConCor-1 always grounds text: pass the image's paired `text` "
|
| 164 |
+
"(a caption, category list or referring expression)."
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
texts = [text] if isinstance(text, str) else list(text)
|
| 168 |
+
if images is None:
|
| 169 |
+
image_list = []
|
| 170 |
+
elif isinstance(images, (list, tuple)):
|
| 171 |
+
image_list = list(images)
|
| 172 |
+
else:
|
| 173 |
+
image_list = [images]
|
| 174 |
+
|
| 175 |
+
if image_list and len(image_list) != len(texts):
|
| 176 |
+
raise ValueError(
|
| 177 |
+
f"Got {len(image_list)} image(s) and {len(texts)} text(s); pass one image per text."
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
pixel_values, image_grid_thw = None, None
|
| 181 |
+
num_visual_tokens = [0] * len(texts)
|
| 182 |
+
if image_list:
|
| 183 |
+
image_inputs = self.image_processor(
|
| 184 |
+
images=image_list,
|
| 185 |
+
return_tensors="pt",
|
| 186 |
+
min_pixels=kwargs.pop("min_pixels", self.min_pixels),
|
| 187 |
+
max_pixels=kwargs.pop("max_pixels", self.max_pixels),
|
| 188 |
+
)
|
| 189 |
+
pixel_values = image_inputs["pixel_values"]
|
| 190 |
+
image_grid_thw = image_inputs["image_grid_thw"]
|
| 191 |
+
num_visual_tokens = [
|
| 192 |
+
int(grid.prod().item()) // (self.merge_size ** 2) for grid in image_grid_thw
|
| 193 |
+
]
|
| 194 |
+
|
| 195 |
+
vision_start_id = self.tokenizer.convert_tokens_to_ids("<|vision_start|>")
|
| 196 |
+
vision_end_id = self.tokenizer.convert_tokens_to_ids("<|vision_end|>")
|
| 197 |
+
image_pad_id = self.tokenizer.convert_tokens_to_ids("<|image_pad|>")
|
| 198 |
+
bridge_ids = self.bridge_token_ids
|
| 199 |
+
|
| 200 |
+
sequences, visual_masks, text_masks, bridge_masks = [], [], [], []
|
| 201 |
+
for index, sample_text in enumerate(texts):
|
| 202 |
+
ids: List[int] = []
|
| 203 |
+
visual_mask: List[bool] = []
|
| 204 |
+
text_mask: List[bool] = []
|
| 205 |
+
|
| 206 |
+
if num_visual_tokens[index] > 0:
|
| 207 |
+
ids.append(vision_start_id)
|
| 208 |
+
ids.extend([image_pad_id] * num_visual_tokens[index])
|
| 209 |
+
ids.append(vision_end_id)
|
| 210 |
+
visual_mask.extend([False] + [True] * num_visual_tokens[index] + [False])
|
| 211 |
+
text_mask.extend([False] * (num_visual_tokens[index] + 2))
|
| 212 |
+
|
| 213 |
+
text_ids = self.tokenizer.encode(sample_text, add_special_tokens=False)
|
| 214 |
+
if not text_ids:
|
| 215 |
+
raise ValueError(f"Text {index} is empty after tokenization: {sample_text!r}")
|
| 216 |
+
ids.extend(text_ids)
|
| 217 |
+
visual_mask.extend([False] * len(text_ids))
|
| 218 |
+
text_mask.extend([True] * len(text_ids))
|
| 219 |
+
|
| 220 |
+
ids.extend(bridge_ids)
|
| 221 |
+
visual_mask.extend([False] * len(bridge_ids))
|
| 222 |
+
text_mask.extend([False] * len(bridge_ids))
|
| 223 |
+
|
| 224 |
+
bridge_mask = [False] * (len(ids) - len(bridge_ids)) + [True] * len(bridge_ids)
|
| 225 |
+
|
| 226 |
+
sequences.append(ids)
|
| 227 |
+
visual_masks.append(visual_mask)
|
| 228 |
+
text_masks.append(text_mask)
|
| 229 |
+
bridge_masks.append(bridge_mask)
|
| 230 |
+
|
| 231 |
+
# Right-pad to the longest sequence, as in training.
|
| 232 |
+
max_length = max(len(ids) for ids in sequences)
|
| 233 |
+
pad_token_id = self.tokenizer.pad_token_id or 0
|
| 234 |
+
batch_size = len(sequences)
|
| 235 |
+
|
| 236 |
+
input_ids = torch.full((batch_size, max_length), pad_token_id, dtype=torch.long)
|
| 237 |
+
attention_mask = torch.zeros((batch_size, max_length), dtype=torch.long)
|
| 238 |
+
visual_token_mask = torch.zeros((batch_size, max_length), dtype=torch.bool)
|
| 239 |
+
text_token_mask = torch.zeros((batch_size, max_length), dtype=torch.bool)
|
| 240 |
+
bridge_token_mask = torch.zeros((batch_size, max_length), dtype=torch.bool)
|
| 241 |
+
|
| 242 |
+
for index, ids in enumerate(sequences):
|
| 243 |
+
length = len(ids)
|
| 244 |
+
input_ids[index, :length] = torch.tensor(ids, dtype=torch.long)
|
| 245 |
+
attention_mask[index, :length] = 1
|
| 246 |
+
visual_token_mask[index, :length] = torch.tensor(visual_masks[index], dtype=torch.bool)
|
| 247 |
+
text_token_mask[index, :length] = torch.tensor(text_masks[index], dtype=torch.bool)
|
| 248 |
+
bridge_token_mask[index, :length] = torch.tensor(bridge_masks[index], dtype=torch.bool)
|
| 249 |
+
|
| 250 |
+
data = {
|
| 251 |
+
"input_ids": input_ids,
|
| 252 |
+
"attention_mask": attention_mask,
|
| 253 |
+
"visual_token_mask": visual_token_mask,
|
| 254 |
+
"text_token_mask": text_token_mask,
|
| 255 |
+
"bridge_token_mask": bridge_token_mask,
|
| 256 |
+
}
|
| 257 |
+
if pixel_values is not None:
|
| 258 |
+
data["pixel_values"] = pixel_values
|
| 259 |
+
data["image_grid_thw"] = image_grid_thw
|
| 260 |
+
return BatchFeature(data=data)
|
| 261 |
+
|
| 262 |
+
# ── Post-processing ──────────────────────────────────────────────────────
|
| 263 |
+
|
| 264 |
+
def text_logits_to_spans(
|
| 265 |
+
self,
|
| 266 |
+
text_logits: np.ndarray,
|
| 267 |
+
offset_mapping: Sequence[Tuple[int, int]],
|
| 268 |
+
text: str,
|
| 269 |
+
text_threshold: float,
|
| 270 |
+
) -> List[List[int]]:
|
| 271 |
+
"""Convert one bridge token's text-mask logits into character spans.
|
| 272 |
+
|
| 273 |
+
Active text tokens are mapped to their character ranges, merged into
|
| 274 |
+
maximal contiguous spans and trimmed of leading/trailing whitespace and
|
| 275 |
+
punctuation. A text mask may consist of several disjoint spans, which is
|
| 276 |
+
how discontinuous and co-referring mentions are represented.
|
| 277 |
+
"""
|
| 278 |
+
probabilities = 1.0 / (1.0 + np.exp(-text_logits.astype(np.float64)))
|
| 279 |
+
active = probabilities >= text_threshold
|
| 280 |
+
|
| 281 |
+
characters = set()
|
| 282 |
+
for token_index in np.where(active)[0]:
|
| 283 |
+
if token_index < len(offset_mapping):
|
| 284 |
+
start, end = offset_mapping[token_index]
|
| 285 |
+
characters.update(range(int(start), int(end)))
|
| 286 |
+
if not characters:
|
| 287 |
+
return []
|
| 288 |
+
|
| 289 |
+
ordered = sorted(characters)
|
| 290 |
+
spans: List[List[int]] = []
|
| 291 |
+
start = ordered[0]
|
| 292 |
+
end = start + 1
|
| 293 |
+
for character in ordered[1:]:
|
| 294 |
+
if character == end:
|
| 295 |
+
end += 1
|
| 296 |
+
else:
|
| 297 |
+
spans.append([start, end])
|
| 298 |
+
start, end = character, character + 1
|
| 299 |
+
spans.append([start, end])
|
| 300 |
+
|
| 301 |
+
trimmed: List[List[int]] = []
|
| 302 |
+
for span_start, span_end in spans:
|
| 303 |
+
span_start = max(0, span_start)
|
| 304 |
+
span_end = min(len(text), span_end)
|
| 305 |
+
while span_start < span_end and (
|
| 306 |
+
text[span_start].isspace() or text[span_start] in _TRIM_CHARACTERS
|
| 307 |
+
):
|
| 308 |
+
span_start += 1
|
| 309 |
+
while span_end > span_start and (
|
| 310 |
+
text[span_end - 1].isspace() or text[span_end - 1] in _TRIM_CHARACTERS
|
| 311 |
+
):
|
| 312 |
+
span_end -= 1
|
| 313 |
+
if span_start < span_end:
|
| 314 |
+
trimmed.append([span_start, span_end])
|
| 315 |
+
return trimmed
|
| 316 |
+
|
| 317 |
+
@staticmethod
|
| 318 |
+
def suppress_duplicate_correspondences(
|
| 319 |
+
masks: List[Optional[np.ndarray]],
|
| 320 |
+
spans: List[List[List[int]]],
|
| 321 |
+
presence_scores: np.ndarray,
|
| 322 |
+
nms_iou_threshold: float,
|
| 323 |
+
) -> List[int]:
|
| 324 |
+
"""Greedy NMS over correspondences, ranked by presence score.
|
| 325 |
+
|
| 326 |
+
A candidate is suppressed only when it duplicates a higher-scoring one on
|
| 327 |
+
*both* sides of the correspondence: image-mask IoU and text-span IoU both
|
| 328 |
+
exceed `nms_iou_threshold`.
|
| 329 |
+
"""
|
| 330 |
+
count = len(spans)
|
| 331 |
+
if nms_iou_threshold <= 0.0 or count <= 1:
|
| 332 |
+
return list(range(count))
|
| 333 |
+
|
| 334 |
+
kept: List[int] = []
|
| 335 |
+
for candidate in np.argsort(-presence_scores):
|
| 336 |
+
candidate = int(candidate)
|
| 337 |
+
suppressed = False
|
| 338 |
+
for reference in kept:
|
| 339 |
+
if masks[candidate] is not None and masks[reference] is not None:
|
| 340 |
+
if compute_mask_iou(masks[candidate], masks[reference]) < nms_iou_threshold:
|
| 341 |
+
continue
|
| 342 |
+
if compute_span_iou(spans[candidate], spans[reference]) >= nms_iou_threshold:
|
| 343 |
+
suppressed = True
|
| 344 |
+
break
|
| 345 |
+
if not suppressed:
|
| 346 |
+
kept.append(candidate)
|
| 347 |
+
return sorted(kept)
|
| 348 |
+
|
| 349 |
+
def post_process_correspondences(
|
| 350 |
+
self,
|
| 351 |
+
outputs,
|
| 352 |
+
text: Union[str, List[str]],
|
| 353 |
+
target_sizes: Optional[Sequence[Tuple[int, int]]] = None,
|
| 354 |
+
presence_threshold: Optional[float] = None,
|
| 355 |
+
text_threshold: Optional[float] = None,
|
| 356 |
+
image_threshold: Optional[float] = None,
|
| 357 |
+
nms_iou_threshold: Optional[float] = None,
|
| 358 |
+
return_masks: bool = True,
|
| 359 |
+
) -> List[List[Dict]]:
|
| 360 |
+
"""Turn per-bridge logits into a set of image-text correspondences.
|
| 361 |
+
|
| 362 |
+
Bridge tokens whose presence probability passes `presence_threshold` are
|
| 363 |
+
kept; each keeps a character-span text mask and a binary image mask
|
| 364 |
+
(logits bilinearly upsampled to `target_sizes`, then thresholded), and
|
| 365 |
+
duplicates are removed with NMS.
|
| 366 |
+
|
| 367 |
+
Args:
|
| 368 |
+
outputs: the [`ConCor1Output`] returned by the model.
|
| 369 |
+
text: the same text that was passed to `__call__`.
|
| 370 |
+
target_sizes: `(height, width)` per sample the image masks are
|
| 371 |
+
resized to — normally the original image size. Defaults to the
|
| 372 |
+
processed image resolution.
|
| 373 |
+
presence_threshold / text_threshold / image_threshold /
|
| 374 |
+
nms_iou_threshold: override the defaults (0.1 / 0.45 / 0.45 / 0.5).
|
| 375 |
+
return_masks: set to `False` to skip mask upsampling and return text
|
| 376 |
+
masks only.
|
| 377 |
+
|
| 378 |
+
Returns:
|
| 379 |
+
One list of correspondences per sample, sorted by decreasing
|
| 380 |
+
presence score. Each correspondence is a dict with
|
| 381 |
+
`bridge_index`, `presence_score`, `text_spans`, `text_phrases` and
|
| 382 |
+
(unless disabled) `mask`.
|
| 383 |
+
"""
|
| 384 |
+
presence_threshold = (
|
| 385 |
+
self.presence_threshold if presence_threshold is None else presence_threshold
|
| 386 |
+
)
|
| 387 |
+
text_threshold = self.text_threshold if text_threshold is None else text_threshold
|
| 388 |
+
image_threshold = self.image_threshold if image_threshold is None else image_threshold
|
| 389 |
+
nms_iou_threshold = (
|
| 390 |
+
self.nms_iou_threshold if nms_iou_threshold is None else nms_iou_threshold
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
texts = [text] if isinstance(text, str) else list(text)
|
| 394 |
+
presence_probabilities = torch.sigmoid(outputs.presence_logits.float()).cpu().numpy()
|
| 395 |
+
batch_size = presence_probabilities.shape[0]
|
| 396 |
+
if len(texts) != batch_size:
|
| 397 |
+
raise ValueError(f"Got {len(texts)} text(s) for a batch of {batch_size}.")
|
| 398 |
+
|
| 399 |
+
results: List[List[Dict]] = []
|
| 400 |
+
for sample in range(batch_size):
|
| 401 |
+
sample_text = texts[sample]
|
| 402 |
+
offset_mapping = self.tokenizer(
|
| 403 |
+
sample_text, return_offsets_mapping=True, add_special_tokens=False
|
| 404 |
+
)["offset_mapping"]
|
| 405 |
+
|
| 406 |
+
active = np.where(presence_probabilities[sample] >= presence_threshold)[0]
|
| 407 |
+
|
| 408 |
+
spans_per_bridge: List[List[List[int]]] = []
|
| 409 |
+
masks_per_bridge: List[Optional[np.ndarray]] = []
|
| 410 |
+
for bridge_index in active:
|
| 411 |
+
text_logits = outputs.text_mask_logits[sample, bridge_index].float().cpu().numpy()
|
| 412 |
+
spans_per_bridge.append(
|
| 413 |
+
self.text_logits_to_spans(
|
| 414 |
+
text_logits, offset_mapping, sample_text, text_threshold
|
| 415 |
+
)
|
| 416 |
+
)
|
| 417 |
+
masks_per_bridge.append(
|
| 418 |
+
self._decode_image_mask(
|
| 419 |
+
outputs,
|
| 420 |
+
sample=sample,
|
| 421 |
+
bridge_index=int(bridge_index),
|
| 422 |
+
target_size=None if target_sizes is None else tuple(target_sizes[sample]),
|
| 423 |
+
image_threshold=image_threshold,
|
| 424 |
+
)
|
| 425 |
+
if return_masks
|
| 426 |
+
else None
|
| 427 |
+
)
|
| 428 |
+
|
| 429 |
+
kept = self.suppress_duplicate_correspondences(
|
| 430 |
+
masks_per_bridge,
|
| 431 |
+
spans_per_bridge,
|
| 432 |
+
presence_probabilities[sample][active],
|
| 433 |
+
nms_iou_threshold,
|
| 434 |
+
)
|
| 435 |
+
|
| 436 |
+
correspondences = []
|
| 437 |
+
for position in kept:
|
| 438 |
+
bridge_index = int(active[position])
|
| 439 |
+
spans = spans_per_bridge[position]
|
| 440 |
+
correspondence = {
|
| 441 |
+
"bridge_index": bridge_index,
|
| 442 |
+
"presence_score": float(presence_probabilities[sample, bridge_index]),
|
| 443 |
+
"text_spans": spans,
|
| 444 |
+
"text_phrases": [sample_text[start:end] for start, end in spans],
|
| 445 |
+
}
|
| 446 |
+
if return_masks:
|
| 447 |
+
correspondence["mask"] = masks_per_bridge[position]
|
| 448 |
+
correspondences.append(correspondence)
|
| 449 |
+
|
| 450 |
+
correspondences.sort(key=lambda item: -item["presence_score"])
|
| 451 |
+
results.append(correspondences)
|
| 452 |
+
return results
|
| 453 |
+
|
| 454 |
+
def _decode_image_mask(
|
| 455 |
+
self,
|
| 456 |
+
outputs,
|
| 457 |
+
sample: int,
|
| 458 |
+
bridge_index: int,
|
| 459 |
+
target_size: Optional[Tuple[int, int]],
|
| 460 |
+
image_threshold: float,
|
| 461 |
+
) -> Optional[np.ndarray]:
|
| 462 |
+
"""Reshape, upsample and threshold one bridge token's image-mask logits."""
|
| 463 |
+
if outputs.image_mask_logits is None:
|
| 464 |
+
return None
|
| 465 |
+
|
| 466 |
+
height = int(outputs.image_mask_grid_hw[sample, 0].item())
|
| 467 |
+
width = int(outputs.image_mask_grid_hw[sample, 1].item())
|
| 468 |
+
if height == 0 or width == 0:
|
| 469 |
+
return None
|
| 470 |
+
|
| 471 |
+
logits = outputs.image_mask_logits[sample, bridge_index, : height * width]
|
| 472 |
+
logits = logits.float().reshape(1, 1, height, width)
|
| 473 |
+
|
| 474 |
+
if target_size is None:
|
| 475 |
+
target_size = (height * self.mask_cell_size, width * self.mask_cell_size)
|
| 476 |
+
|
| 477 |
+
upsampled = F.interpolate(
|
| 478 |
+
logits, size=tuple(int(value) for value in target_size), mode="bilinear", align_corners=False
|
| 479 |
+
)
|
| 480 |
+
probabilities = torch.sigmoid(upsampled)[0, 0].cpu().numpy()
|
| 481 |
+
return probabilities >= image_threshold
|
| 482 |
+
|
| 483 |
+
@property
|
| 484 |
+
def mask_cell_size(self) -> int:
|
| 485 |
+
"""Pixels per side of one image-mask cell in the processed image (4 px)."""
|
| 486 |
+
return self.patch_size // (2 ** self.num_mask_upsample_blocks)
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
__all__ = ["ConCor1Processor"]
|
processor_config.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"processor_class": "ConCor1Processor",
|
| 3 |
+
"auto_map": {
|
| 4 |
+
"AutoProcessor": "processing_concor1.ConCor1Processor"
|
| 5 |
+
},
|
| 6 |
+
"num_bridge_tokens": 385,
|
| 7 |
+
"bridge_token_id_start": 248077,
|
| 8 |
+
"patch_size": 16,
|
| 9 |
+
"merge_size": 2,
|
| 10 |
+
"num_mask_upsample_blocks": 2,
|
| 11 |
+
"min_pixels": 1003520,
|
| 12 |
+
"max_pixels": 1003520,
|
| 13 |
+
"presence_threshold": 0.1,
|
| 14 |
+
"text_threshold": 0.45,
|
| 15 |
+
"image_threshold": 0.45,
|
| 16 |
+
"nms_iou_threshold": 0.5
|
| 17 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ConCor-1 inference requirements.
|
| 2 |
+
# Versions the released checkpoint was trained and evaluated with are pinned as
|
| 3 |
+
# lower bounds; transformers must be new enough to ship the Qwen3.5 architecture.
|
| 4 |
+
torch>=2.8.0
|
| 5 |
+
torchvision>=0.23.0 # required by the fast image processor
|
| 6 |
+
transformers==5.3.0
|
| 7 |
+
safetensors>=0.4.0
|
| 8 |
+
numpy>=1.24
|
| 9 |
+
pillow>=10.0
|
| 10 |
+
|
| 11 |
+
# Qwen3.5's gated-delta-net (linear-attention) kernels.
|
| 12 |
+
flash-linear-attention>=0.4.1
|
| 13 |
+
|
| 14 |
+
# Optional, and both need a CUDA compiler. Neither changes the set of
|
| 15 |
+
# correspondences the model returns.
|
| 16 |
+
# causal-conv1d: a fused causal convolution for the linear-attention layers;
|
| 17 |
+
# without it transformers falls back to a pure-torch path.
|
| 18 |
+
# flash-attn: exact reproduction of the paper's logits, with
|
| 19 |
+
# attn_implementation="flash_attention_2". The default,
|
| 20 |
+
# attn_implementation="sdpa", needs no extra dependency.
|
| 21 |
+
# causal-conv1d>=1.4.0
|
| 22 |
+
# flash-attn>=2.8.0
|
tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5f9e4d4901a92b997e463c1f46055088b6cca5ca61a6522d1b9f64c4bb81cb42
|
| 3 |
+
size 12807982
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": false,
|
| 3 |
+
"added_tokens_decoder": {
|
| 4 |
+
"248044": {
|
| 5 |
+
"content": "<|endoftext|>",
|
| 6 |
+
"lstrip": false,
|
| 7 |
+
"normalized": false,
|
| 8 |
+
"rstrip": false,
|
| 9 |
+
"single_word": false,
|
| 10 |
+
"special": true
|
| 11 |
+
},
|
| 12 |
+
"248045": {
|
| 13 |
+
"content": "<|im_start|>",
|
| 14 |
+
"lstrip": false,
|
| 15 |
+
"normalized": false,
|
| 16 |
+
"rstrip": false,
|
| 17 |
+
"single_word": false,
|
| 18 |
+
"special": true
|
| 19 |
+
},
|
| 20 |
+
"248046": {
|
| 21 |
+
"content": "<|im_end|>",
|
| 22 |
+
"lstrip": false,
|
| 23 |
+
"normalized": false,
|
| 24 |
+
"rstrip": false,
|
| 25 |
+
"single_word": false,
|
| 26 |
+
"special": true
|
| 27 |
+
},
|
| 28 |
+
"248047": {
|
| 29 |
+
"content": "<|object_ref_start|>",
|
| 30 |
+
"lstrip": false,
|
| 31 |
+
"normalized": false,
|
| 32 |
+
"rstrip": false,
|
| 33 |
+
"single_word": false,
|
| 34 |
+
"special": true
|
| 35 |
+
},
|
| 36 |
+
"248048": {
|
| 37 |
+
"content": "<|object_ref_end|>",
|
| 38 |
+
"lstrip": false,
|
| 39 |
+
"normalized": false,
|
| 40 |
+
"rstrip": false,
|
| 41 |
+
"single_word": false,
|
| 42 |
+
"special": true
|
| 43 |
+
},
|
| 44 |
+
"248049": {
|
| 45 |
+
"content": "<|box_start|>",
|
| 46 |
+
"lstrip": false,
|
| 47 |
+
"normalized": false,
|
| 48 |
+
"rstrip": false,
|
| 49 |
+
"single_word": false,
|
| 50 |
+
"special": true
|
| 51 |
+
},
|
| 52 |
+
"248050": {
|
| 53 |
+
"content": "<|box_end|>",
|
| 54 |
+
"lstrip": false,
|
| 55 |
+
"normalized": false,
|
| 56 |
+
"rstrip": false,
|
| 57 |
+
"single_word": false,
|
| 58 |
+
"special": true
|
| 59 |
+
},
|
| 60 |
+
"248051": {
|
| 61 |
+
"content": "<|quad_start|>",
|
| 62 |
+
"lstrip": false,
|
| 63 |
+
"normalized": false,
|
| 64 |
+
"rstrip": false,
|
| 65 |
+
"single_word": false,
|
| 66 |
+
"special": true
|
| 67 |
+
},
|
| 68 |
+
"248052": {
|
| 69 |
+
"content": "<|quad_end|>",
|
| 70 |
+
"lstrip": false,
|
| 71 |
+
"normalized": false,
|
| 72 |
+
"rstrip": false,
|
| 73 |
+
"single_word": false,
|
| 74 |
+
"special": true
|
| 75 |
+
},
|
| 76 |
+
"248053": {
|
| 77 |
+
"content": "<|vision_start|>",
|
| 78 |
+
"lstrip": false,
|
| 79 |
+
"normalized": false,
|
| 80 |
+
"rstrip": false,
|
| 81 |
+
"single_word": false,
|
| 82 |
+
"special": true
|
| 83 |
+
},
|
| 84 |
+
"248054": {
|
| 85 |
+
"content": "<|vision_end|>",
|
| 86 |
+
"lstrip": false,
|
| 87 |
+
"normalized": false,
|
| 88 |
+
"rstrip": false,
|
| 89 |
+
"single_word": false,
|
| 90 |
+
"special": true
|
| 91 |
+
},
|
| 92 |
+
"248055": {
|
| 93 |
+
"content": "<|vision_pad|>",
|
| 94 |
+
"lstrip": false,
|
| 95 |
+
"normalized": false,
|
| 96 |
+
"rstrip": false,
|
| 97 |
+
"single_word": false,
|
| 98 |
+
"special": true
|
| 99 |
+
},
|
| 100 |
+
"248056": {
|
| 101 |
+
"content": "<|image_pad|>",
|
| 102 |
+
"lstrip": false,
|
| 103 |
+
"normalized": false,
|
| 104 |
+
"rstrip": false,
|
| 105 |
+
"single_word": false,
|
| 106 |
+
"special": true
|
| 107 |
+
},
|
| 108 |
+
"248057": {
|
| 109 |
+
"content": "<|video_pad|>",
|
| 110 |
+
"lstrip": false,
|
| 111 |
+
"normalized": false,
|
| 112 |
+
"rstrip": false,
|
| 113 |
+
"single_word": false,
|
| 114 |
+
"special": true
|
| 115 |
+
},
|
| 116 |
+
"248058": {
|
| 117 |
+
"content": "<tool_call>",
|
| 118 |
+
"lstrip": false,
|
| 119 |
+
"normalized": false,
|
| 120 |
+
"rstrip": false,
|
| 121 |
+
"single_word": false,
|
| 122 |
+
"special": false
|
| 123 |
+
},
|
| 124 |
+
"248059": {
|
| 125 |
+
"content": "</tool_call>",
|
| 126 |
+
"lstrip": false,
|
| 127 |
+
"normalized": false,
|
| 128 |
+
"rstrip": false,
|
| 129 |
+
"single_word": false,
|
| 130 |
+
"special": false
|
| 131 |
+
},
|
| 132 |
+
"248060": {
|
| 133 |
+
"content": "<|fim_prefix|>",
|
| 134 |
+
"lstrip": false,
|
| 135 |
+
"normalized": false,
|
| 136 |
+
"rstrip": false,
|
| 137 |
+
"single_word": false,
|
| 138 |
+
"special": false
|
| 139 |
+
},
|
| 140 |
+
"248061": {
|
| 141 |
+
"content": "<|fim_middle|>",
|
| 142 |
+
"lstrip": false,
|
| 143 |
+
"normalized": false,
|
| 144 |
+
"rstrip": false,
|
| 145 |
+
"single_word": false,
|
| 146 |
+
"special": false
|
| 147 |
+
},
|
| 148 |
+
"248062": {
|
| 149 |
+
"content": "<|fim_suffix|>",
|
| 150 |
+
"lstrip": false,
|
| 151 |
+
"normalized": false,
|
| 152 |
+
"rstrip": false,
|
| 153 |
+
"single_word": false,
|
| 154 |
+
"special": false
|
| 155 |
+
},
|
| 156 |
+
"248063": {
|
| 157 |
+
"content": "<|fim_pad|>",
|
| 158 |
+
"lstrip": false,
|
| 159 |
+
"normalized": false,
|
| 160 |
+
"rstrip": false,
|
| 161 |
+
"single_word": false,
|
| 162 |
+
"special": false
|
| 163 |
+
},
|
| 164 |
+
"248064": {
|
| 165 |
+
"content": "<|repo_name|>",
|
| 166 |
+
"lstrip": false,
|
| 167 |
+
"normalized": false,
|
| 168 |
+
"rstrip": false,
|
| 169 |
+
"single_word": false,
|
| 170 |
+
"special": false
|
| 171 |
+
},
|
| 172 |
+
"248065": {
|
| 173 |
+
"content": "<|file_sep|>",
|
| 174 |
+
"lstrip": false,
|
| 175 |
+
"normalized": false,
|
| 176 |
+
"rstrip": false,
|
| 177 |
+
"single_word": false,
|
| 178 |
+
"special": false
|
| 179 |
+
},
|
| 180 |
+
"248066": {
|
| 181 |
+
"content": "<tool_response>",
|
| 182 |
+
"lstrip": false,
|
| 183 |
+
"normalized": false,
|
| 184 |
+
"rstrip": false,
|
| 185 |
+
"single_word": false,
|
| 186 |
+
"special": false
|
| 187 |
+
},
|
| 188 |
+
"248067": {
|
| 189 |
+
"content": "</tool_response>",
|
| 190 |
+
"lstrip": false,
|
| 191 |
+
"normalized": false,
|
| 192 |
+
"rstrip": false,
|
| 193 |
+
"single_word": false,
|
| 194 |
+
"special": false
|
| 195 |
+
},
|
| 196 |
+
"248068": {
|
| 197 |
+
"content": "<think>",
|
| 198 |
+
"lstrip": false,
|
| 199 |
+
"normalized": false,
|
| 200 |
+
"rstrip": false,
|
| 201 |
+
"single_word": false,
|
| 202 |
+
"special": false
|
| 203 |
+
},
|
| 204 |
+
"248069": {
|
| 205 |
+
"content": "</think>",
|
| 206 |
+
"lstrip": false,
|
| 207 |
+
"normalized": false,
|
| 208 |
+
"rstrip": false,
|
| 209 |
+
"single_word": false,
|
| 210 |
+
"special": false
|
| 211 |
+
},
|
| 212 |
+
"248070": {
|
| 213 |
+
"content": "<|audio_start|>",
|
| 214 |
+
"lstrip": false,
|
| 215 |
+
"normalized": false,
|
| 216 |
+
"rstrip": false,
|
| 217 |
+
"single_word": false,
|
| 218 |
+
"special": true
|
| 219 |
+
},
|
| 220 |
+
"248071": {
|
| 221 |
+
"content": "<|audio_end|>",
|
| 222 |
+
"lstrip": false,
|
| 223 |
+
"normalized": false,
|
| 224 |
+
"rstrip": false,
|
| 225 |
+
"single_word": false,
|
| 226 |
+
"special": true
|
| 227 |
+
},
|
| 228 |
+
"248072": {
|
| 229 |
+
"content": "<tts_pad>",
|
| 230 |
+
"lstrip": false,
|
| 231 |
+
"normalized": false,
|
| 232 |
+
"rstrip": false,
|
| 233 |
+
"single_word": false,
|
| 234 |
+
"special": true
|
| 235 |
+
},
|
| 236 |
+
"248073": {
|
| 237 |
+
"content": "<tts_text_bos>",
|
| 238 |
+
"lstrip": false,
|
| 239 |
+
"normalized": false,
|
| 240 |
+
"rstrip": false,
|
| 241 |
+
"single_word": false,
|
| 242 |
+
"special": true
|
| 243 |
+
},
|
| 244 |
+
"248074": {
|
| 245 |
+
"content": "<tts_text_eod>",
|
| 246 |
+
"lstrip": false,
|
| 247 |
+
"normalized": false,
|
| 248 |
+
"rstrip": false,
|
| 249 |
+
"single_word": false,
|
| 250 |
+
"special": true
|
| 251 |
+
},
|
| 252 |
+
"248075": {
|
| 253 |
+
"content": "<tts_text_bos_single>",
|
| 254 |
+
"lstrip": false,
|
| 255 |
+
"normalized": false,
|
| 256 |
+
"rstrip": false,
|
| 257 |
+
"single_word": false,
|
| 258 |
+
"special": true
|
| 259 |
+
},
|
| 260 |
+
"248076": {
|
| 261 |
+
"content": "<|audio_pad|>",
|
| 262 |
+
"lstrip": false,
|
| 263 |
+
"normalized": false,
|
| 264 |
+
"rstrip": false,
|
| 265 |
+
"single_word": false,
|
| 266 |
+
"special": true
|
| 267 |
+
}
|
| 268 |
+
},
|
| 269 |
+
"additional_special_tokens": [
|
| 270 |
+
"<|im_start|>",
|
| 271 |
+
"<|im_end|>",
|
| 272 |
+
"<|object_ref_start|>",
|
| 273 |
+
"<|object_ref_end|>",
|
| 274 |
+
"<|box_start|>",
|
| 275 |
+
"<|box_end|>",
|
| 276 |
+
"<|quad_start|>",
|
| 277 |
+
"<|quad_end|>",
|
| 278 |
+
"<|vision_start|>",
|
| 279 |
+
"<|vision_end|>",
|
| 280 |
+
"<|vision_pad|>",
|
| 281 |
+
"<|image_pad|>",
|
| 282 |
+
"<|video_pad|>"
|
| 283 |
+
],
|
| 284 |
+
"bos_token": null,
|
| 285 |
+
"chat_template": "{%- set image_count = namespace(value=0) %}\n{%- set video_count = namespace(value=0) %}\n{%- macro render_content(content, do_vision_count, is_system_content=false) %}\n {%- if content is string %}\n {{- content }}\n {%- elif content is iterable and content is not mapping %}\n {%- for item in content %}\n {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}\n {%- if is_system_content %}\n {{- raise_exception('System message cannot contain images.') }}\n {%- endif %}\n {%- if do_vision_count %}\n {%- set image_count.value = image_count.value + 1 %}\n {%- endif %}\n {%- if add_vision_id %}\n {{- 'Picture ' ~ image_count.value ~ ': ' }}\n {%- endif %}\n {{- '<|vision_start|><|image_pad|><|vision_end|>' }}\n {%- elif 'video' in item or item.type == 'video' %}\n {%- if is_system_content %}\n {{- raise_exception('System message cannot contain videos.') }}\n {%- endif %}\n {%- if do_vision_count %}\n {%- set video_count.value = video_count.value + 1 %}\n {%- endif %}\n {%- if add_vision_id %}\n {{- 'Video ' ~ video_count.value ~ ': ' }}\n {%- endif %}\n {{- '<|vision_start|><|video_pad|><|vision_end|>' }}\n {%- elif 'text' in item %}\n {{- item.text }}\n {%- else %}\n {{- raise_exception('Unexpected item type in content.') }}\n {%- endif %}\n {%- endfor %}\n {%- elif content is none or content is undefined %}\n {{- '' }}\n {%- else %}\n {{- raise_exception('Unexpected content type.') }}\n {%- endif %}\n{%- endmacro %}\n{%- if not messages %}\n {{- raise_exception('No messages provided.') }}\n{%- endif %}\n{%- if tools and tools is iterable and tools is not mapping %}\n {{- '<|im_start|>system\\n' }}\n {{- \"# Tools\\n\\nYou have access to the following functions:\\n\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\" }}\n {{- '\\n\\nIf you choose to call a function ONLY reply in the following format with NO suffix:\\n\\n<tool_call>\\n<function=example_function_name>\\n<parameter=example_parameter_1>\\nvalue_1\\n</parameter>\\n<parameter=example_parameter_2>\\nThis is the value for the second parameter\\nthat can span\\nmultiple lines\\n</parameter>\\n</function>\\n</tool_call>\\n\\n<IMPORTANT>\\nReminder:\\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\\n- Required parameters MUST be specified\\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\\n</IMPORTANT>' }}\n {%- if messages[0].role == 'system' %}\n {%- set content = render_content(messages[0].content, false, true)|trim %}\n {%- if content %}\n {{- '\\n\\n' + content }}\n {%- endif %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {%- set content = render_content(messages[0].content, false, true)|trim %}\n {{- '<|im_start|>system\\n' + content + '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n{%- for message in messages[::-1] %}\n {%- set index = (messages|length - 1) - loop.index0 %}\n {%- if ns.multi_step_tool and message.role == \"user\" %}\n {%- set content = render_content(message.content, false)|trim %}\n {%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}\n {%- set ns.multi_step_tool = false %}\n {%- set ns.last_query_index = index %}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if ns.multi_step_tool %}\n {{- raise_exception('No user query found in messages.') }}\n{%- endif %}\n{%- for message in messages %}\n {%- set content = render_content(message.content, true)|trim %}\n {%- if message.role == \"system\" %}\n {%- if not loop.first %}\n {{- raise_exception('System message must be at the beginning.') }}\n {%- endif %}\n {%- elif message.role == \"user\" %}\n {{- '<|im_start|>' + message.role + '\\n' + content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {%- set reasoning_content = '' %}\n {%- if message.reasoning_content is string %}\n {%- set reasoning_content = message.reasoning_content %}\n {%- else %}\n {%- if '</think>' in content %}\n {%- set reasoning_content = content.split('</think>')[0].rstrip('\\n').split('<think>')[-1].lstrip('\\n') %}\n {%- set content = content.split('</think>')[-1].lstrip('\\n') %}\n {%- endif %}\n {%- endif %}\n {%- set reasoning_content = reasoning_content|trim %}\n {%- if loop.index0 > ns.last_query_index %}\n {{- '<|im_start|>' + message.role + '\\n<think>\\n' + reasoning_content + '\\n</think>\\n\\n' + content }}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {%- if loop.first %}\n {%- if content|trim %}\n {{- '\\n\\n<tool_call>\\n<function=' + tool_call.name + '>\\n' }}\n {%- else %}\n {{- '<tool_call>\\n<function=' + tool_call.name + '>\\n' }}\n {%- endif %}\n {%- else %}\n {{- '\\n<tool_call>\\n<function=' + tool_call.name + '>\\n' }}\n {%- endif %}\n {%- if tool_call.arguments is defined %}\n {%- for args_name, args_value in tool_call.arguments|items %}\n {{- '<parameter=' + args_name + '>\\n' }}\n {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}\n {{- args_value }}\n {{- '\\n</parameter>\\n' }}\n {%- endfor %}\n {%- endif %}\n {{- '</function>\\n</tool_call>' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.previtem and loop.previtem.role != \"tool\" %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- content }}\n {{- '\\n</tool_response>' }}\n {%- if not loop.last and loop.nextitem.role != \"tool\" %}\n {{- '<|im_end|>\\n' }}\n {%- elif loop.last %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- else %}\n {{- raise_exception('Unexpected message role.') }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n {%- if enable_thinking is defined and enable_thinking is true %}\n {{- '<think>\\n' }}\n {%- else %}\n {{- '<think>\\n\\n</think>\\n\\n' }}\n {%- endif %}\n{%- endif %}",
|
| 286 |
+
"clean_up_tokenization_spaces": false,
|
| 287 |
+
"eos_token": "<|im_end|>",
|
| 288 |
+
"errors": "replace",
|
| 289 |
+
"model_max_length": 262144,
|
| 290 |
+
"pad_token": "<|endoftext|>",
|
| 291 |
+
"split_special_tokens": false,
|
| 292 |
+
"tokenizer_class": "Qwen2Tokenizer",
|
| 293 |
+
"unk_token": null,
|
| 294 |
+
"add_bos_token": false,
|
| 295 |
+
"pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
|
| 296 |
+
"extra_special_tokens": {
|
| 297 |
+
"audio_bos_token": "<|audio_start|>",
|
| 298 |
+
"audio_eos_token": "<|audio_end|>",
|
| 299 |
+
"audio_token": "<|audio_pad|>",
|
| 300 |
+
"image_token": "<|image_pad|>",
|
| 301 |
+
"video_token": "<|video_pad|>",
|
| 302 |
+
"vision_bos_token": "<|vision_start|>",
|
| 303 |
+
"vision_eos_token": "<|vision_end|>"
|
| 304 |
+
},
|
| 305 |
+
"processor_class": "ConCor1Processor"
|
| 306 |
+
}
|
vocab.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|