File size: 8,006 Bytes
383a563
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d6e10eb
383a563
 
d6e10eb
 
 
383a563
d6e10eb
383a563
 
 
d6e10eb
 
383a563
 
 
 
d6e10eb
 
383a563
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
"""CountGD++ on the Hugging Face Hub.

    from countgdpp import CountGDPlusPlus, count
    model = CountGDPlusPlus.from_pretrained("nikigoli/CountGDPlusPlus").to("cuda").eval()
    result = count(model, "img/336.jpg", "strawberry", negative_texts=["blueberry"])
    print(result["count"])

`CountGDPlusPlus` is the CountGD++ GroundingDINO model with Hugging Face's
PyTorchModelHubMixin added, so `from_pretrained`, `save_pretrained` and
`push_to_hub` work. Its state dict keys are identical to the original
`countgd_plusplus.pth` checkpoint.
"""
import os
import sys
from types import SimpleNamespace
from typing import List, Optional, Sequence, Union

import torch
from PIL import Image
from huggingface_hub import PyTorchModelHubMixin

_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
    sys.path.insert(0, _HERE)

from util.slconfig import SLConfig  # noqa: E402
from util.misc import nested_tensor_from_tensor_list  # noqa: E402
import datasets.transforms_app as T  # noqa: E402
from models.GroundingDINO.groundingdino_app import GroundingDINO  # noqa: E402
from models.GroundingDINO.backbone import build_backbone  # noqa: E402
from models.GroundingDINO.transformer import build_transformer  # noqa: E402

# Default score threshold used in the paper's demo.
CONF_THRESH = 0.23
# BERT token id of "." — separates the positive prompt from negative prompts in the caption.
_DOT_TOKEN_ID = 1012
_DROP_KEYS = {"label_list", "val_label_list"}


def default_config() -> dict:
    """Model configuration read from cfg_app.py (the config used by the CountGD++ demo)."""
    cfg = SLConfig.fromfile(os.path.join(_HERE, "cfg_app.py"))._cfg_dict.to_dict()
    cfg = {k: v for k, v in cfg.items() if k not in _DROP_KEYS}
    cfg["text_encoder_type"] = "bert-base-uncased"  # fetched from the Hub by transformers
    cfg["device"] = "cpu"
    return cfg


class CountGDPlusPlus(
    GroundingDINO,
    PyTorchModelHubMixin,
    library_name="pytorch",
    repo_url="https://github.com/niki-amini-naieni/CountGDPlusPlus",
    license="mit",
    tags=["CountGD++", "computer-vision", "counting", "grounding-dino", "multi-modal", "open-vocabulary"],
):
    """CountGD++ model. Build with `CountGDPlusPlus.from_pretrained(repo_id)`."""

    def __init__(self, config: Optional[dict] = None):
        config = dict(config or default_config())
        args = SimpleNamespace(**config)
        backbone = build_backbone(args)
        transformer = build_transformer(args)
        # Same arguments as build_groundingdino() in groundingdino_app.py
        super().__init__(
            backbone,
            transformer,
            num_queries=args.num_queries,
            aux_loss=args.aux_loss,
            iter_update=True,
            query_dim=4,
            num_feature_levels=args.num_feature_levels,
            nheads=args.nheads,
            dec_pred_bbox_embed_share=args.dec_pred_bbox_embed_share,
            two_stage_type=args.two_stage_type,
            two_stage_bbox_embed_share=args.two_stage_bbox_embed_share,
            two_stage_class_embed_share=args.two_stage_class_embed_share,
            num_patterns=args.num_patterns,
            dn_number=0,
            dn_box_noise_scale=args.dn_box_noise_scale,
            dn_label_noise_ratio=args.dn_label_noise_ratio,
            dn_labelbook_size=args.dn_labelbook_size,
            text_encoder_type=args.text_encoder_type,
            sub_sentence_present=args.sub_sentence_present,
            max_text_len=args.max_text_len,
        )
        self.countgd_config = config


# ----------------------------------------------------------------------------
# Inference (same pre/post-processing as the CountGD++ demo app)
# ----------------------------------------------------------------------------
def build_transform():
    normalize = T.Compose([T.ToTensor(), T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
    return T.Compose([T.RandomResize([800], max_size=1333), normalize])


def _load(image: Union[str, Image.Image]) -> Image.Image:
    return (Image.open(image) if isinstance(image, str) else image).convert("RGB")


def _prep_exemplars(transform, image, boxes):
    boxes = torch.tensor(boxes, dtype=torch.float32) if boxes else torch.tensor([])
    img, target = transform(image, {"exemplars": boxes})
    return img, target["exemplars"]


@torch.no_grad()
def count(
    model: CountGDPlusPlus,
    image: Union[str, Image.Image],
    text: str,
    exemplar_boxes: Optional[Sequence[Sequence[float]]] = None,
    exemplar_image: Optional[Union[str, Image.Image]] = None,
    negative_texts: Optional[List[str]] = None,
    negative_exemplar_boxes: Optional[List[Sequence[Sequence[float]]]] = None,
    negative_exemplar_images: Optional[List[Union[str, Image.Image]]] = None,
    conf_thresh: float = CONF_THRESH,
):
    """Count objects in `image`.

    text: what to count, e.g. "strawberry". Use "" for exemplars only.
    exemplar_boxes: optional visual exemplars [[x1, y1, x2, y2], ...] in pixels,
        drawn on `exemplar_image` (defaults to `image`).
    negative_texts: optional things NOT to count, e.g. ["blueberry"]. Use "" for
        a negative given by exemplars only. If omitted while negative exemplars
        are given, each negative gets "" as its text.
    negative_exemplar_boxes / negative_exemplar_images: optional, one entry per
        negative, same format as the positive ones.

    Returns dict with "count", "boxes" (normalized cx, cy, w, h) and "scores".
    """
    if not text and not exemplar_boxes:
        raise ValueError("Give a positive text, positive exemplar boxes, or both.")
    device = next(model.parameters()).device
    transform = build_transform()
    image = _load(image)
    pos_img = _load(exemplar_image) if exemplar_image is not None else image
    if negative_texts is None and negative_exemplar_boxes:
        negative_texts = [""] * len(negative_exemplar_boxes)
    negative_texts = list(negative_texts or [])
    n_neg = len(negative_texts)
    neg_boxes = list(negative_exemplar_boxes or [None] * n_neg)
    neg_imgs = list(negative_exemplar_images or [None] * n_neg)
    if len(neg_boxes) != n_neg or len(neg_imgs) != n_neg:
        raise ValueError("Give one negative exemplar entry (or None) per negative text.")

    input_image, _ = transform(image, None)
    pos_exemplar_image, pos_exemplars = _prep_exemplars(transform, pos_img, exemplar_boxes)

    neg_images, neg_exemplars = [], []
    for boxes, img in zip(neg_boxes, neg_imgs):
        img = _load(img) if img is not None else image
        ex_img, ex = _prep_exemplars(transform, img, boxes)
        neg_images.append(nested_tensor_from_tensor_list(ex_img.unsqueeze(0).to(device)))
        neg_exemplars.append([ex.to(device)])

    caption = text + " . " + "".join(t + " . " for t in negative_texts)
    out = model(
        nested_tensor_from_tensor_list(input_image.unsqueeze(0).to(device)),
        nested_tensor_from_tensor_list(pos_exemplar_image.unsqueeze(0).to(device)),
        [pos_exemplars.to(device)],
        neg_images,
        neg_exemplars,
        captions=[caption],
    )

    input_ids = out["input_ids"][0]
    logits = out["pred_logits"].sigmoid()[0]
    boxes = out["pred_boxes"][0]
    split = next(i for i, t in enumerate(input_ids) if int(t) == _DOT_TOKEN_ID)
    pos_logits, neg_logits = logits[:, : split + 1], logits[:, split + 1 :]

    # Stage 1: keep queries confident about the positive prompt.
    keep = pos_logits.max(dim=-1).values > conf_thresh
    boxes, scores = boxes[keep], logits[keep].max(dim=-1).values
    pos_logits, neg_logits = pos_logits[keep], neg_logits[keep]
    # Stage 2: drop queries that match a negative prompt better than the positive one.
    if neg_logits.shape[-1] > 0:
        keep = pos_logits.max(dim=-1).values > neg_logits.max(dim=-1).values
        boxes, scores = boxes[keep], scores[keep]

    return {"count": int(boxes.shape[0]), "boxes": boxes.cpu().numpy(), "scores": scores.cpu().numpy()}