siglip2-toolkit 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- siglip2_toolkit/__init__.py +71 -0
- siglip2_toolkit/__main__.py +6 -0
- siglip2_toolkit/classification.py +161 -0
- siglip2_toolkit/cli.py +557 -0
- siglip2_toolkit/defaults.py +3 -0
- siglip2_toolkit/embedder.py +324 -0
- siglip2_toolkit/exporter.py +272 -0
- siglip2_toolkit/filters.py +283 -0
- siglip2_toolkit/formats.py +293 -0
- siglip2_toolkit/models.py +421 -0
- siglip2_toolkit/retrieval.py +155 -0
- siglip2_toolkit/server.py +197 -0
- siglip2_toolkit/utils.py +245 -0
- siglip2_toolkit-0.0.1.dist-info/METADATA +153 -0
- siglip2_toolkit-0.0.1.dist-info/RECORD +18 -0
- siglip2_toolkit-0.0.1.dist-info/WHEEL +4 -0
- siglip2_toolkit-0.0.1.dist-info/entry_points.txt +2 -0
- siglip2_toolkit-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""siglip2-toolkit — local-first toolkit around Google's SigLIP 2 models.
|
|
2
|
+
|
|
3
|
+
The package is deliberately lazy: importing :mod:`siglip2_toolkit` does not
|
|
4
|
+
import torch, transformers or FastAPI. Heavy modules are loaded on first
|
|
5
|
+
attribute access, so tooling and metadata inspection stay fast::
|
|
6
|
+
|
|
7
|
+
from siglip2_toolkit import Embedder
|
|
8
|
+
|
|
9
|
+
embedder = Embedder(model="google/siglip2-base-patch16-naflex", device="auto")
|
|
10
|
+
vectors = embedder.embed_texts(["a wedding at sunset"])
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from importlib import import_module
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from importlib.metadata import version as _dist_version
|
|
17
|
+
|
|
18
|
+
__version__ = _dist_version("siglip2-toolkit")
|
|
19
|
+
except Exception: # pragma: no cover - source checkout without installed metadata
|
|
20
|
+
__version__ = "0.0.1"
|
|
21
|
+
|
|
22
|
+
#: Lazy attributes: name -> (relative module, attribute or None for the module itself)
|
|
23
|
+
_LAZY_ATTRS: dict[str, tuple[str, str | None]] = {
|
|
24
|
+
# Public API
|
|
25
|
+
"Embedder": (".embedder", "Embedder"),
|
|
26
|
+
"Exporter": (".exporter", "Exporter"),
|
|
27
|
+
"ExportConfig": (".exporter", "ExportConfig"),
|
|
28
|
+
"ExportResult": (".exporter", "ExportResult"),
|
|
29
|
+
"EmbeddingSpace": (".formats", "EmbeddingSpace"),
|
|
30
|
+
"ImageClassifier": (".classification", "ImageClassifier"),
|
|
31
|
+
"Filter": (".filters", "Filter"),
|
|
32
|
+
"field": (".filters", "field"),
|
|
33
|
+
# Submodules
|
|
34
|
+
"formats": (".formats", None),
|
|
35
|
+
"retrieval": (".retrieval", None),
|
|
36
|
+
"filters": (".filters", None),
|
|
37
|
+
"models": (".models", None),
|
|
38
|
+
"utils": (".utils", None),
|
|
39
|
+
"embedder": (".embedder", None),
|
|
40
|
+
"exporter": (".exporter", None),
|
|
41
|
+
"classification": (".classification", None),
|
|
42
|
+
"cli": (".cli", None),
|
|
43
|
+
"server": (".server", None),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"Embedder",
|
|
48
|
+
"Exporter",
|
|
49
|
+
"ExportConfig",
|
|
50
|
+
"ExportResult",
|
|
51
|
+
"EmbeddingSpace",
|
|
52
|
+
"ImageClassifier",
|
|
53
|
+
"Filter",
|
|
54
|
+
"field",
|
|
55
|
+
"__version__",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def __getattr__(name: str):
|
|
60
|
+
try:
|
|
61
|
+
module_name, attr = _LAZY_ATTRS[name]
|
|
62
|
+
except KeyError:
|
|
63
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
|
|
64
|
+
module = import_module(module_name, __name__)
|
|
65
|
+
value = module if attr is None else getattr(module, attr)
|
|
66
|
+
globals()[name] = value # cache for subsequent lookups
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def __dir__() -> list[str]:
|
|
71
|
+
return sorted(set(globals()) | set(_LAZY_ATTRS))
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Fine-tuned image classification with SigLIP-family checkpoints.
|
|
2
|
+
|
|
3
|
+
Zero-shot classification (prompt-based) lives on the embedder:
|
|
4
|
+
:meth:`siglip2_toolkit.embedder.Embedder.classify`. This module covers
|
|
5
|
+
checkpoints that ship a trained classification head, consumed through
|
|
6
|
+
``AutoModelForImageClassification`` (e.g. ``SiglipForImageClassification``).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import threading
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Optional, Sequence
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from . import utils
|
|
19
|
+
from .embedder import ImageInput, softmax_rows
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ImageClassifier:
|
|
25
|
+
"""Classifies images with a fine-tuned image-classification model.
|
|
26
|
+
|
|
27
|
+
The model and its image processor are loaded lazily on first use. Labels
|
|
28
|
+
come from the checkpoint's ``config.id2label``; ``num_labels`` overrides
|
|
29
|
+
the head size when the checkpoint requires it.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
model: str | Path,
|
|
35
|
+
device: str = "auto",
|
|
36
|
+
dtype: Optional[str] = None,
|
|
37
|
+
num_labels: Optional[int] = None,
|
|
38
|
+
trust_remote_code: bool = False,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.model_path = str(Path(model).expanduser())
|
|
41
|
+
self.model_name = self.model_path
|
|
42
|
+
self.requested_device = device
|
|
43
|
+
self.requested_dtype = dtype
|
|
44
|
+
self.num_labels = num_labels
|
|
45
|
+
self.trust_remote_code = trust_remote_code
|
|
46
|
+
|
|
47
|
+
self._model: Any = None
|
|
48
|
+
self._processor: Any = None
|
|
49
|
+
self._torch: Any = None
|
|
50
|
+
self._device: Any = None
|
|
51
|
+
self._id2label: dict[int, str] = {}
|
|
52
|
+
self._lock = threading.Lock()
|
|
53
|
+
|
|
54
|
+
# ------------------------------------------------------------------ #
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def is_loaded(self) -> bool:
|
|
58
|
+
return self._model is not None
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def labels(self) -> dict[int, str]:
|
|
62
|
+
"""``id -> label`` mapping (empty until the model is loaded)."""
|
|
63
|
+
return dict(self._id2label)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def device(self) -> Optional[str]:
|
|
67
|
+
return str(self._device) if self._device is not None else None
|
|
68
|
+
|
|
69
|
+
def load(self) -> "ImageClassifier":
|
|
70
|
+
"""Load the model and its image processor from a local directory (idempotent)."""
|
|
71
|
+
if self._model is not None:
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
path = Path(self.model_path)
|
|
75
|
+
if not path.is_dir():
|
|
76
|
+
raise FileNotFoundError(
|
|
77
|
+
f"ImageClassifier expects a local model directory, got: "
|
|
78
|
+
f"'{self.model_path}'. Get one with `siglip2 models download` or "
|
|
79
|
+
"`siglip2 models import`, or pass the path to one."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
import torch
|
|
83
|
+
from transformers import AutoImageProcessor, AutoModelForImageClassification
|
|
84
|
+
|
|
85
|
+
device, dtype = utils.resolve_torch_device(
|
|
86
|
+
torch, self.requested_device, self.requested_dtype
|
|
87
|
+
)
|
|
88
|
+
logger.info("Loading classifier '%s' on %s (%s)...", self.model_name, device, dtype)
|
|
89
|
+
|
|
90
|
+
extra: dict[str, Any] = {}
|
|
91
|
+
if self.num_labels is not None:
|
|
92
|
+
extra["num_labels"] = self.num_labels
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
model = AutoModelForImageClassification.from_pretrained(
|
|
96
|
+
self.model_path,
|
|
97
|
+
trust_remote_code=self.trust_remote_code,
|
|
98
|
+
torch_dtype=dtype,
|
|
99
|
+
**extra,
|
|
100
|
+
)
|
|
101
|
+
processor = AutoImageProcessor.from_pretrained(
|
|
102
|
+
self.model_path,
|
|
103
|
+
trust_remote_code=self.trust_remote_code,
|
|
104
|
+
)
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
raise OSError(
|
|
107
|
+
f"Could not load a classifier from '{self.model_path}': {exc}"
|
|
108
|
+
) from exc
|
|
109
|
+
model = model.to(device).eval()
|
|
110
|
+
|
|
111
|
+
self._torch = torch
|
|
112
|
+
self._model = model
|
|
113
|
+
self._processor = processor
|
|
114
|
+
self._device = device
|
|
115
|
+
self._id2label = {
|
|
116
|
+
int(key): str(value)
|
|
117
|
+
for key, value in (getattr(model.config, "id2label", None) or {}).items()
|
|
118
|
+
}
|
|
119
|
+
logger.info("Classifier ready: %d label(s) ✅", len(self._id2label))
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
# ------------------------------------------------------------------ #
|
|
123
|
+
|
|
124
|
+
def classify(
|
|
125
|
+
self,
|
|
126
|
+
images: Sequence[ImageInput],
|
|
127
|
+
top_k: Optional[int] = None,
|
|
128
|
+
batch_size: int = 16,
|
|
129
|
+
) -> list[list[tuple[str, float]]]:
|
|
130
|
+
"""Classify images; returns (label, probability) pairs sorted by score."""
|
|
131
|
+
self.load()
|
|
132
|
+
images = list(images)
|
|
133
|
+
results: list[list[tuple[str, float]]] = []
|
|
134
|
+
|
|
135
|
+
for start in range(0, len(images), batch_size):
|
|
136
|
+
batch = [utils.load_image(item) for item in images[start : start + batch_size]]
|
|
137
|
+
if not batch:
|
|
138
|
+
continue
|
|
139
|
+
inputs = self._processor(images=batch, return_tensors="pt").to(self._device)
|
|
140
|
+
with self._lock, self._torch.no_grad():
|
|
141
|
+
logits = self._model(**inputs).logits
|
|
142
|
+
probabilities = softmax_rows(
|
|
143
|
+
logits.detach().cpu().to(self._torch.float32).numpy()
|
|
144
|
+
)
|
|
145
|
+
for row in probabilities:
|
|
146
|
+
order = np.argsort(row)[::-1]
|
|
147
|
+
if top_k is not None:
|
|
148
|
+
order = order[: max(0, int(top_k))]
|
|
149
|
+
results.append(
|
|
150
|
+
[
|
|
151
|
+
(self._id2label.get(int(index), f"LABEL_{int(index)}"), float(row[index]))
|
|
152
|
+
for index in order
|
|
153
|
+
]
|
|
154
|
+
)
|
|
155
|
+
return results
|
|
156
|
+
|
|
157
|
+
def __repr__(self) -> str:
|
|
158
|
+
return (
|
|
159
|
+
f"ImageClassifier(model={self.model_name!r}, "
|
|
160
|
+
f"device={self.requested_device!r}, loaded={self.is_loaded})"
|
|
161
|
+
)
|