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()}
|