ConCor-1 / example_inference.py
UWGZQ's picture
ConCor-1: release checkpoint, remote code, processor, example
4f08932 verified
Raw
History Blame Contribute Delete
5.75 kB
#!/usr/bin/env python3
# coding=utf-8
"""ConCor-1 inference example: an image + its text in, correspondences out.
Given an image and a paired text, ConCor-1 predicts the full set of
correspondences between visually referential text spans and instance-level
image segments — the text spans are *not* given as queries; the model decides
which parts of the text are grounded.
Usage (the bundled `example.png` with its COCONut-PanCap caption):
python example_inference.py --image example.png \
--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."
# a category list works just as well as a caption
python example_inference.py --image example.png --text "bear . grass . tree . person"
# write a mask overlay next to the printed correspondences
python example_inference.py --image example.png --text "..." --output overlay.png
"""
from __future__ import annotations
import argparse
from pathlib import Path
from typing import List
import numpy as np
import torch
from PIL import Image, ImageDraw
from transformers import AutoModel, AutoProcessor
# Visually distinct overlay colours.
COLORS = [
(239, 64, 64), (64, 204, 64), (64, 115, 255), (255, 204, 0), (255, 115, 0),
(217, 51, 217), (0, 204, 217), (153, 89, 13), (102, 255, 102), (140, 26, 255),
(255, 153, 204), (0, 140, 0), (179, 179, 0), (0, 26, 153), (204, 140, 51),
(128, 128, 128), (255, 0, 128), (0, 255, 140), (140, 0, 0), (0, 140, 140),
]
def overlay_masks(image: Image.Image, correspondences: List[dict], alpha: float = 0.5) -> Image.Image:
"""Blend each correspondence's mask over the image and label it with its phrases."""
canvas = np.array(image.convert("RGB"), dtype=np.float32)
for index, correspondence in enumerate(correspondences):
mask = correspondence.get("mask")
if mask is None or not mask.any():
continue
color = np.array(COLORS[index % len(COLORS)], dtype=np.float32)
canvas[mask] = (1.0 - alpha) * canvas[mask] + alpha * color
overlaid = Image.fromarray(canvas.astype(np.uint8))
draw = ImageDraw.Draw(overlaid)
for index, correspondence in enumerate(correspondences):
mask = correspondence.get("mask")
if mask is None or not mask.any():
continue
rows, columns = np.nonzero(mask)
label = " / ".join(correspondence["text_phrases"]) or "(no text span)"
label = f"{label} {correspondence['presence_score']:.2f}"
anchor = (int(columns.min()), max(int(rows.min()) - 12, 0))
draw.text(anchor, label, fill=COLORS[index % len(COLORS)])
return overlaid
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--model", default=str(Path(__file__).parent), help="model repo id or local path")
parser.add_argument("--image", required=True, type=Path)
parser.add_argument("--text", required=True, help="the text to ground (caption, category list, referring expression)")
parser.add_argument("--output", type=Path, default=None, help="write a mask overlay here (PNG)")
parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
parser.add_argument(
"--attn_implementation",
default="flash_attention_2",
choices=["flash_attention_2", "sdpa", "eager"],
help="flash_attention_2 reproduces the paper's numbers exactly; sdpa needs no extra dependency",
)
parser.add_argument("--presence_threshold", type=float, default=None, help="default 0.1")
parser.add_argument("--text_threshold", type=float, default=None, help="default 0.45")
parser.add_argument("--image_threshold", type=float, default=None, help="default 0.45")
parser.add_argument("--nms_iou_threshold", type=float, default=None, help="default 0.5")
args = parser.parse_args()
processor = AutoProcessor.from_pretrained(args.model, trust_remote_code=True)
model = AutoModel.from_pretrained(
args.model,
trust_remote_code=True,
dtype=torch.bfloat16,
attn_implementation=args.attn_implementation,
).to(args.device).eval()
image = Image.open(args.image).convert("RGB")
inputs = processor(images=image, text=args.text, return_tensors="pt").to(args.device)
with torch.inference_mode(), torch.autocast(args.device, dtype=torch.bfloat16):
outputs = model(**inputs)
correspondences = processor.post_process_correspondences(
outputs,
text=args.text,
target_sizes=[(image.height, image.width)],
presence_threshold=args.presence_threshold,
text_threshold=args.text_threshold,
image_threshold=args.image_threshold,
nms_iou_threshold=args.nms_iou_threshold,
)[0]
print(f"\nimage: {args.image} ({image.width}x{image.height})")
print(f"text: {args.text}")
print(f"\n{len(correspondences)} correspondence(s):")
for correspondence in correspondences:
mask = correspondence["mask"]
phrases = " / ".join(correspondence["text_phrases"]) or "(no text span)"
area = 100.0 * mask.mean() if mask is not None else 0.0
print(
f" presence={correspondence['presence_score']:.3f} "
f"bridge={correspondence['bridge_index']:3d} "
f"mask={area:5.2f}% of image spans={correspondence['text_spans']} | {phrases}"
)
if args.output is not None:
overlay_masks(image, correspondences).save(args.output)
print(f"\nwrote {args.output}")
if __name__ == "__main__":
main()