panopticml 0.0.2__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.
- panopticml/__init__.py +0 -0
- panopticml/compute/__init__.py +1 -0
- panopticml/compute/clustering.py +122 -0
- panopticml/compute/faiss_tree.py +72 -0
- panopticml/compute/similarity.py +11 -0
- panopticml/compute/transformers/__init__.py +131 -0
- panopticml/compute_vector_task.py +64 -0
- panopticml/models.py +6 -0
- panopticml/panoptic_ml.py +340 -0
- panopticml/utils.py +72 -0
- panopticml-0.0.2.dist-info/METADATA +55 -0
- panopticml-0.0.2.dist-info/RECORD +13 -0
- panopticml-0.0.2.dist-info/WHEEL +4 -0
panopticml/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .clustering import make_clusters
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import faiss
|
|
2
|
+
import numpy as np
|
|
3
|
+
import torch
|
|
4
|
+
|
|
5
|
+
from panoptic.models import Vector
|
|
6
|
+
from panoptic.models.results import ScoreList, Score, Group
|
|
7
|
+
from ..utils import similarity_matrix
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def make_clusters(vectors: list[Vector], **kwargs) -> (list[list[str]], list[int]):
|
|
11
|
+
res_clusters = []
|
|
12
|
+
res_distances = []
|
|
13
|
+
vectors, sha1 = zip(*[(i.data, i.sha1) for i in vectors])
|
|
14
|
+
sha1 = np.asarray(sha1)
|
|
15
|
+
clusters: np.ndarray
|
|
16
|
+
|
|
17
|
+
clusters, distances = _make_clusters_faiss(vectors, **kwargs)
|
|
18
|
+
|
|
19
|
+
for cluster in list(set(clusters)):
|
|
20
|
+
sha1_cluster = sha1[clusters == cluster]
|
|
21
|
+
current_cluster_distances = distances[clusters == cluster]
|
|
22
|
+
if distances is not None:
|
|
23
|
+
res_distances.append(np.mean(current_cluster_distances))
|
|
24
|
+
res_clusters.append(list(sha1_cluster))
|
|
25
|
+
# sort clusters by distances
|
|
26
|
+
sorted_clusters = [cluster for _, cluster in sorted(zip(res_distances, res_clusters))]
|
|
27
|
+
return sorted_clusters, sorted(res_distances)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _make_clusters_faiss(vectors, nb_clusters=6, **kwargs) -> (np.ndarray, np.ndarray):
|
|
31
|
+
def _make_single_kmean(vectors, nb_clusters):
|
|
32
|
+
kmean = faiss.Kmeans(vectors.shape[1], nb_clusters, niter=20, verbose=False)
|
|
33
|
+
kmean.train(vectors)
|
|
34
|
+
return kmean.index.search(vectors, 1)
|
|
35
|
+
|
|
36
|
+
vectors = np.asarray(vectors)
|
|
37
|
+
if nb_clusters == -1:
|
|
38
|
+
import hdbscan
|
|
39
|
+
clusterer = hdbscan.HDBSCAN(min_cluster_size=5, gen_min_span_tree=True)
|
|
40
|
+
clusterer.fit(vectors)
|
|
41
|
+
indices = clusterer.labels_
|
|
42
|
+
probabilities = clusterer.probabilities_
|
|
43
|
+
distances = np.zeros_like(probabilities, dtype=np.float32)
|
|
44
|
+
unique_clusters = np.unique(indices)
|
|
45
|
+
# compute distances just like the one returned by kmeans to have consistent metrics
|
|
46
|
+
for cluster_id in unique_clusters:
|
|
47
|
+
if cluster_id == -1:
|
|
48
|
+
distances[indices == -1] = 100.0
|
|
49
|
+
continue
|
|
50
|
+
cluster_mask = (indices == cluster_id)
|
|
51
|
+
cluster_vectors = vectors[cluster_mask]
|
|
52
|
+
cluster_probabilities = probabilities[cluster_mask]
|
|
53
|
+
center_local_index = np.argmax(cluster_probabilities)
|
|
54
|
+
center_vector = cluster_vectors[center_local_index].reshape(1, -1)
|
|
55
|
+
dists = faiss.pairwise_distances(center_vector, cluster_vectors)[0]
|
|
56
|
+
distances[cluster_mask] = dists
|
|
57
|
+
else:
|
|
58
|
+
distances, indices = _make_single_kmean(vectors, nb_clusters)
|
|
59
|
+
return indices.flatten(), distances.flatten()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def cluster_by_text(image_vectors: list[Vector], text_vectors: list[np.array], text_labels: list[str]) -> list[Group]:
|
|
63
|
+
vectors, sha1s = zip(*[(i.data, i.sha1) for i in image_vectors])
|
|
64
|
+
sha1s_array = np.asarray(sha1s)
|
|
65
|
+
|
|
66
|
+
similarities, closest_text_indices = similarity_matrix(vectors, text_vectors)
|
|
67
|
+
|
|
68
|
+
clusters = []
|
|
69
|
+
distances = []
|
|
70
|
+
clusters_text = []
|
|
71
|
+
cluster_sims = []
|
|
72
|
+
|
|
73
|
+
for text_index in closest_text_indices.unique():
|
|
74
|
+
clusters_text.append(text_labels[text_index])
|
|
75
|
+
cluster = sha1s_array[closest_text_indices == text_index]
|
|
76
|
+
|
|
77
|
+
# similarities of each image inside the cluster and the text
|
|
78
|
+
cluster_sim = similarities[closest_text_indices == text_index]
|
|
79
|
+
sorted_sim = cluster_sim.sort(descending=True).values
|
|
80
|
+
cluster_sims.append([round(x, 2) for x in sorted_sim.tolist()])
|
|
81
|
+
|
|
82
|
+
# sort cluster by descending similarity
|
|
83
|
+
sorting_index = cluster_sim.argsort(descending=True)
|
|
84
|
+
sorted_cluster = cluster[sorting_index]
|
|
85
|
+
if type(sorted_cluster) is not np.ndarray:
|
|
86
|
+
sorted_cluster = np.array([sorted_cluster])
|
|
87
|
+
clusters.append(sorted_cluster)
|
|
88
|
+
|
|
89
|
+
# compute mean distance of the images
|
|
90
|
+
distance = float((1 - torch.mean(cluster_sim)) * 100)
|
|
91
|
+
distances.append(distance)
|
|
92
|
+
|
|
93
|
+
groups = []
|
|
94
|
+
for cluster, distance, name, cluster_sim in zip(clusters, distances, clusters_text, cluster_sims):
|
|
95
|
+
similarities = ScoreList(min=0, max=1, max_is_best=True, values=cluster_sim)
|
|
96
|
+
group = Group(score=Score(distance, min=0, max=200, max_is_best=False,
|
|
97
|
+
description="Mean distance between the images of this cluster and the queried text"),
|
|
98
|
+
scores=similarities)
|
|
99
|
+
group.sha1s = list(cluster)
|
|
100
|
+
group.name = f"Cluster {name}"
|
|
101
|
+
groups.append(group)
|
|
102
|
+
|
|
103
|
+
return groups
|
|
104
|
+
|
|
105
|
+
def custom_range(min_i, max_i, steps, increments):
|
|
106
|
+
"""
|
|
107
|
+
Generate a range of values from min_i to max_i with a variable increment for each step
|
|
108
|
+
:param min_i: first value
|
|
109
|
+
:param max_i: last value
|
|
110
|
+
:param steps: values for which increment should change
|
|
111
|
+
:param increments: increments values
|
|
112
|
+
:return:
|
|
113
|
+
"""
|
|
114
|
+
i = min_i
|
|
115
|
+
current_step = 0
|
|
116
|
+
current_incr = 1
|
|
117
|
+
while i < max_i:
|
|
118
|
+
yield i
|
|
119
|
+
if i >= steps[current_step] and current_step < len(steps) - 1:
|
|
120
|
+
current_incr = increments[current_step]
|
|
121
|
+
current_step += 1
|
|
122
|
+
i += current_incr
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pickle
|
|
3
|
+
|
|
4
|
+
import faiss
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .similarity import get_text_vectors
|
|
8
|
+
from ..models import VectorType
|
|
9
|
+
from panoptic.core.plugin.plugin import APlugin
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FaissTree:
|
|
13
|
+
def __init__(self, index: faiss.IndexFlatIP, labels: list[str]):
|
|
14
|
+
self.index = index
|
|
15
|
+
self.labels = labels
|
|
16
|
+
|
|
17
|
+
def query(self, vectors: list[np.ndarray], k=999999):
|
|
18
|
+
vector_center = np.mean(vectors, axis=0)
|
|
19
|
+
vector = np.asarray([vector_center])
|
|
20
|
+
|
|
21
|
+
real_k = min(k, len(self.labels))
|
|
22
|
+
vector = vector.reshape(1, -1)
|
|
23
|
+
dist, ind = self.index.search(vector, real_k)
|
|
24
|
+
indices = [x for x in ind[0]]
|
|
25
|
+
distances = [x for x in dist[0]] # avoid some strange overflow behavior
|
|
26
|
+
return [{'sha1': self.labels[i], 'dist': float('%.2f' % (distances[index]))} for index, i in
|
|
27
|
+
enumerate(indices)]
|
|
28
|
+
|
|
29
|
+
def query_texts(self, texts: list[str], transformer):
|
|
30
|
+
text_vectors = get_text_vectors(texts, transformer)
|
|
31
|
+
return self.query(text_vectors)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def gen_tree_file_name(vec_type: VectorType):
|
|
35
|
+
return f"{vec_type.value}_faiss_tree.pkl"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def create_faiss_tree(plugin: APlugin, vec_type: VectorType):
|
|
39
|
+
project = plugin.project
|
|
40
|
+
name = gen_tree_file_name(vec_type)
|
|
41
|
+
vectors = await project.get_vectors(plugin.name, vector_type=vec_type.value)
|
|
42
|
+
|
|
43
|
+
if vectors is None or len(vectors) == 0:
|
|
44
|
+
return
|
|
45
|
+
vec_data, sha1_list = zip(*[(i.data, i.sha1) for i in vectors])
|
|
46
|
+
vec_np_arr = np.asarray(vec_data)
|
|
47
|
+
faiss.normalize_L2(vec_np_arr)
|
|
48
|
+
|
|
49
|
+
# create the faiss index based on this post: https://anttihavanko.medium.com/building-image-search-with-openai-clip-5a1deaa7a6e2
|
|
50
|
+
vector_size = vec_np_arr.shape[1]
|
|
51
|
+
index = faiss.IndexFlatIP(vector_size)
|
|
52
|
+
# faiss.ParameterSpace().set_index_parameter(index, 'nprobe', 100)
|
|
53
|
+
index.add(np.asarray(vec_np_arr))
|
|
54
|
+
|
|
55
|
+
tree = FaissTree(index, sha1_list)
|
|
56
|
+
|
|
57
|
+
with open(os.path.join(plugin.data_path, name), 'wb') as f:
|
|
58
|
+
pickle.dump(tree, f)
|
|
59
|
+
|
|
60
|
+
return FaissTree(index=index, labels=sha1_list)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_faiss_tree(plugin: APlugin, vec_type: VectorType) -> FaissTree | None:
|
|
64
|
+
name = gen_tree_file_name(vec_type)
|
|
65
|
+
path = os.path.join(plugin.data_path, name)
|
|
66
|
+
if not os.path.exists(path):
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
with open(path, 'rb') as f:
|
|
70
|
+
return pickle.load(f)
|
|
71
|
+
except ModuleNotFoundError:
|
|
72
|
+
return None
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_text_vectors(texts: [str], transformer):
|
|
5
|
+
vectors = []
|
|
6
|
+
if transformer.can_handle_text:
|
|
7
|
+
for text in texts:
|
|
8
|
+
vectors.append(transformer.to_text_vector(text))
|
|
9
|
+
else:
|
|
10
|
+
raise ValueError(f"The selected transformer {transformer.name_or_path} does not support text vectors.")
|
|
11
|
+
return np.asarray(vectors)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
from PIL import Image
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ....panopticml.utils import resolve_device
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TransformerName(Enum):
|
|
11
|
+
mobilenet = "mobilenet"
|
|
12
|
+
clip = "clip"
|
|
13
|
+
siglip = "siglip"
|
|
14
|
+
dinov2 = "dinov2"
|
|
15
|
+
auto = "auto"
|
|
16
|
+
|
|
17
|
+
def get_transformer(model: TransformerName=TransformerName.clip, hugging_face_model=None):
|
|
18
|
+
match model:
|
|
19
|
+
case TransformerName.mobilenet:
|
|
20
|
+
return GoogleTransformer()
|
|
21
|
+
case TransformerName.clip:
|
|
22
|
+
return CLIPTransformer()
|
|
23
|
+
case TransformerName.siglip:
|
|
24
|
+
return SIGLIPTransformer()
|
|
25
|
+
case TransformerName.dinov2:
|
|
26
|
+
return Dinov2Transformer()
|
|
27
|
+
case TransformerName.auto:
|
|
28
|
+
return AutoTransformer(hugging_face_model)
|
|
29
|
+
|
|
30
|
+
class Transformer(object):
|
|
31
|
+
def __init__(self):
|
|
32
|
+
import torch
|
|
33
|
+
from transformers import logging
|
|
34
|
+
logging.set_verbosity_error()
|
|
35
|
+
self.device = resolve_device()
|
|
36
|
+
self.tokenizer = None
|
|
37
|
+
self.processor = None
|
|
38
|
+
self.model = None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def can_handle_text(self):
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
class AutoTransformer(Transformer):
|
|
45
|
+
def __init__(self, hugging_face_model=None):
|
|
46
|
+
super().__init__()
|
|
47
|
+
from transformers import AutoModel, AutoProcessor
|
|
48
|
+
if hugging_face_model:
|
|
49
|
+
self.model = AutoModel.from_pretrained(hugging_face_model).to(self.device)
|
|
50
|
+
self.processor = AutoProcessor.from_pretrained(hugging_face_model)
|
|
51
|
+
self.name = hugging_face_model
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def can_handle_text(self):
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
def to_vector(self, image: Image) -> np.ndarray:
|
|
58
|
+
inputs = self.processor(images=[image], return_tensors="pt")
|
|
59
|
+
inputs = {k: v.to(self.device) for k, v in inputs.items()}
|
|
60
|
+
with torch.no_grad():
|
|
61
|
+
image_features = self.model.get_image_features(**inputs)
|
|
62
|
+
image_features = image_features / image_features.norm(dim=-1, keepdim=True)
|
|
63
|
+
return image_features.cpu().numpy().flatten()
|
|
64
|
+
|
|
65
|
+
def to_text_vector(self, text: str) -> np.ndarray:
|
|
66
|
+
inputs = self.processor(text=[text], return_tensors="pt")
|
|
67
|
+
inputs = {k: v.to(self.device) for k, v in inputs.items()}
|
|
68
|
+
with torch.no_grad():
|
|
69
|
+
text_features = self.model.get_text_features(**inputs)
|
|
70
|
+
text_features = text_features / text_features.norm(dim=-1, keepdim=True)
|
|
71
|
+
return text_features.cpu().numpy().flatten()
|
|
72
|
+
|
|
73
|
+
class GoogleTransformer(Transformer):
|
|
74
|
+
def __init__(self):
|
|
75
|
+
super().__init__()
|
|
76
|
+
from transformers import MobileNetV2Model, AutoImageProcessor
|
|
77
|
+
self.model = MobileNetV2Model.from_pretrained("google/mobilenet_v2_1.0_224")
|
|
78
|
+
self.processor = AutoImageProcessor.from_pretrained("google/mobilenet_v2_1.0_224")
|
|
79
|
+
self.name = "MobileNetV2"
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def can_handle_text(self):
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
def to_vector(self, image: Image) -> np.ndarray:
|
|
86
|
+
input1 = self.processor(images=image, return_tensors="pt")
|
|
87
|
+
output1 = self.model(**input1)
|
|
88
|
+
pooled_output1 = output1[1].detach().numpy()
|
|
89
|
+
vector = pooled_output1.flatten()
|
|
90
|
+
return vector
|
|
91
|
+
|
|
92
|
+
class CLIPTransformer(AutoTransformer):
|
|
93
|
+
def __init__(self):
|
|
94
|
+
model_name = "openai/clip-vit-base-patch32"
|
|
95
|
+
super().__init__(model_name)
|
|
96
|
+
self.name = "CLIP"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def can_handle_text(self):
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
class SIGLIPTransformer(AutoTransformer):
|
|
104
|
+
def __init__(self):
|
|
105
|
+
model_name = "google/siglip2-so400m-patch16-naflex"
|
|
106
|
+
super().__init__(model_name)
|
|
107
|
+
self.name = "SIGLIP"
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def can_handle_text(self):
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Dinov2Transformer(Transformer):
|
|
115
|
+
def __init__(self):
|
|
116
|
+
super().__init__()
|
|
117
|
+
from transformers import AutoModel, AutoImageProcessor
|
|
118
|
+
ckpt = "facebook/dinov2-base"
|
|
119
|
+
self.model = AutoModel.from_pretrained(ckpt).to(self.device)
|
|
120
|
+
self.processor = AutoImageProcessor.from_pretrained(ckpt, use_fast=True)
|
|
121
|
+
self.name = "Dinov2"
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def can_handle_text(self):
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
def to_vector(self, image: Image) -> np.ndarray:
|
|
128
|
+
inputs = self.processor(images=image, return_tensors="pt").to(self.device)
|
|
129
|
+
with torch.no_grad():
|
|
130
|
+
outputs = self.model(**inputs)
|
|
131
|
+
return outputs.last_hidden_state.mean(dim=1).cpu().numpy()[0]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from .utils import preprocess_image
|
|
7
|
+
from .models import VectorType
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from . import PanopticML
|
|
11
|
+
|
|
12
|
+
import aiofiles
|
|
13
|
+
|
|
14
|
+
from panoptic.core.task.task import Task
|
|
15
|
+
from panoptic.models import Instance, Vector
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger('PanopticML:VectorTask')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ComputeVectorTask(Task):
|
|
21
|
+
def __init__(self, plugin: PanopticML, source: str, type_: VectorType, instance: Instance,
|
|
22
|
+
data_path: str):
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.plugin = plugin
|
|
25
|
+
self.project = plugin.project
|
|
26
|
+
self.source = source
|
|
27
|
+
self.type = type_
|
|
28
|
+
self.instance = instance
|
|
29
|
+
self.transformer = self.plugin.transformer
|
|
30
|
+
self.name = f'{self.transformer.name} Vectors ({type_.value})'
|
|
31
|
+
self.data_path = data_path
|
|
32
|
+
self.key += f"_{type_.value}"
|
|
33
|
+
|
|
34
|
+
async def run(self):
|
|
35
|
+
instance = self.instance
|
|
36
|
+
exist = await self.project.vector_exist(self.source, self.type.value, instance.sha1)
|
|
37
|
+
if exist:
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
image_data = await self.project.get_project().db.get_large_image(instance.sha1)
|
|
41
|
+
if not image_data:
|
|
42
|
+
file = instance.url
|
|
43
|
+
async with aiofiles.open(file, mode='rb') as file:
|
|
44
|
+
image_data = await file.read()
|
|
45
|
+
|
|
46
|
+
vector_data = None
|
|
47
|
+
vector_data = await self._async(self.compute_image_clip, image_data)
|
|
48
|
+
|
|
49
|
+
if vector_data is None:
|
|
50
|
+
return
|
|
51
|
+
vector = Vector(self.source, self.type.name, instance.sha1, vector_data)
|
|
52
|
+
res = await self.project.add_vector(vector)
|
|
53
|
+
del vector
|
|
54
|
+
return res
|
|
55
|
+
|
|
56
|
+
async def run_if_last(self):
|
|
57
|
+
await self.plugin._update_tree(self.type)
|
|
58
|
+
|
|
59
|
+
def compute_image_clip(self, image_data: bytes):
|
|
60
|
+
image = preprocess_image(image_data, self.type)
|
|
61
|
+
vector = self.transformer.to_vector(image)
|
|
62
|
+
|
|
63
|
+
del image
|
|
64
|
+
return vector
|
panopticml/models.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import requests
|
|
5
|
+
from PIL import Image
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from panoptic.core.plugin.plugin import APlugin
|
|
9
|
+
from panoptic.core.plugin.plugin_project_interface import PluginProjectInterface
|
|
10
|
+
from panoptic.models import Instance, ActionContext, PropertyId, PropertyType
|
|
11
|
+
from panoptic.models.results import Group, ActionResult, Notif, NotifType, NotifFunction, ScoreList, Score
|
|
12
|
+
from panoptic.utils import group_by_sha1
|
|
13
|
+
|
|
14
|
+
from .compute import make_clusters
|
|
15
|
+
from .compute.clustering import cluster_by_text
|
|
16
|
+
from .compute.faiss_tree import load_faiss_tree, create_faiss_tree, FaissTree
|
|
17
|
+
from .compute.similarity import get_text_vectors
|
|
18
|
+
from .compute.transformers import get_transformer, TransformerName
|
|
19
|
+
from .compute_vector_task import ComputeVectorTask
|
|
20
|
+
from .models import VectorType
|
|
21
|
+
from .utils import is_image_url
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PluginParams(BaseModel):
|
|
25
|
+
"""
|
|
26
|
+
@greyscale: if this is checked, vectors can be recomputed but this time images will be converted to greyscale before
|
|
27
|
+
@model: the name of the transformer to user, values: clip | mobilenet | siglip | auto, default to clip, if using auto, need to specify a huggingface model name, for instance: google/siglip2-base-patch16-224
|
|
28
|
+
"""
|
|
29
|
+
greyscale: bool = False
|
|
30
|
+
model: str = "clip"
|
|
31
|
+
hugging_face_model: str = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PanopticML(APlugin):
|
|
35
|
+
"""
|
|
36
|
+
Default Machine Learning plugin for Panoptic
|
|
37
|
+
Uses CLIP to generate vectors and FAISS for clustering / similarity functions
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, project: PluginProjectInterface, plugin_path: str, name: str):
|
|
41
|
+
super().__init__(name=name, project=project, plugin_path=plugin_path)
|
|
42
|
+
self.params: PluginParams = PluginParams()
|
|
43
|
+
|
|
44
|
+
self.project.on_instance_import(self.compute_image_vectors_on_import)
|
|
45
|
+
self.project.on_folder_delete(self.rebuild_trees)
|
|
46
|
+
self.add_action_easy(self.find_images, ['similar'])
|
|
47
|
+
self.add_action_easy(self.compute_clusters, ['group'])
|
|
48
|
+
self.add_action_easy(self.cluster_by_tags, ['group'])
|
|
49
|
+
self.add_action_easy(self.find_duplicates, ['group'])
|
|
50
|
+
self.add_action_easy(self.search_by_text, ['execute'])
|
|
51
|
+
self._comp_vec_desc = self.add_action_easy(self.compute_vectors, ['execute'])
|
|
52
|
+
self._comp_all_vec_desc = self.add_action_easy(self.compute_all_vectors, ['execute'])
|
|
53
|
+
|
|
54
|
+
self.trees: Dict[VectorType, FaissTree] = {}
|
|
55
|
+
self._transformer = None
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def transformer(self):
|
|
59
|
+
if self._transformer is None:
|
|
60
|
+
self._transformer = self._load_transformer()
|
|
61
|
+
return self._transformer
|
|
62
|
+
|
|
63
|
+
async def start(self):
|
|
64
|
+
await super().start()
|
|
65
|
+
|
|
66
|
+
[await self._get_tree(t) for t in VectorType]
|
|
67
|
+
|
|
68
|
+
def _get_vector_func_notifs(self, vec_type: VectorType):
|
|
69
|
+
res = [
|
|
70
|
+
NotifFunction(self._comp_vec_desc.id,
|
|
71
|
+
ActionContext(ui_inputs={"vec_type": vec_type.value}),
|
|
72
|
+
message=f"Compute all vectors of type {vec_type.value}"),
|
|
73
|
+
NotifFunction(self._comp_all_vec_desc.id,
|
|
74
|
+
ActionContext(),
|
|
75
|
+
message="Compute vectors off all types")
|
|
76
|
+
]
|
|
77
|
+
return res
|
|
78
|
+
|
|
79
|
+
async def compute_vectors(self, context: ActionContext, vec_type: VectorType):
|
|
80
|
+
"""
|
|
81
|
+
Compute image vectors of selected vector type
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
instances = await self.project.get_instances(ids=context.instance_ids)
|
|
85
|
+
for i in instances:
|
|
86
|
+
await self._compute_image_vector(i, vec_type)
|
|
87
|
+
|
|
88
|
+
notif = Notif(type=NotifType.INFO,
|
|
89
|
+
name="ComputeVector",
|
|
90
|
+
message=f"Successfuly started compute of vectors of type {vec_type.value}")
|
|
91
|
+
return ActionResult(notifs=[notif])
|
|
92
|
+
|
|
93
|
+
async def compute_image_vectors_on_import(self, instance: Instance):
|
|
94
|
+
await self._compute_image_vector(instance, VectorType.rgb)
|
|
95
|
+
if self.params.greyscale:
|
|
96
|
+
await self._compute_image_vector(instance, VectorType.greyscale)
|
|
97
|
+
|
|
98
|
+
async def compute_all_vectors(self, context: ActionContext):
|
|
99
|
+
"""
|
|
100
|
+
Compute image vectors of all supported types
|
|
101
|
+
"""
|
|
102
|
+
res = [await self.compute_vectors(context, t) for t in VectorType]
|
|
103
|
+
return ActionResult(notifs=[n for r in res for n in r.notifs])
|
|
104
|
+
|
|
105
|
+
async def _compute_image_vector(self, instance: Instance, vector: VectorType):
|
|
106
|
+
task = ComputeVectorTask(self, self.name, vector, instance, self.data_path)
|
|
107
|
+
self.project.add_task(task)
|
|
108
|
+
|
|
109
|
+
async def compute_clusters(self, context: ActionContext, vec_type: VectorType = VectorType.rgb,
|
|
110
|
+
nb_clusters: int = 10, label_clusters: bool = False):
|
|
111
|
+
"""
|
|
112
|
+
Computes images clusters with Faiss Kmeans
|
|
113
|
+
@nb_clusters: requested number of clusters
|
|
114
|
+
"""
|
|
115
|
+
instances = await self.project.get_instances(context.instance_ids)
|
|
116
|
+
sha1_to_instance = group_by_sha1(instances)
|
|
117
|
+
sha1_to_ahash = {i.sha1: i.ahash for i in instances}
|
|
118
|
+
sha1s = list(sha1_to_instance.keys())
|
|
119
|
+
|
|
120
|
+
if not sha1s:
|
|
121
|
+
empty_notif = Notif(NotifType.ERROR, name="NoData", message="No instance found")
|
|
122
|
+
return ActionResult(notifs=[empty_notif])
|
|
123
|
+
|
|
124
|
+
vectors = await self.project.get_vectors(source=self.name, vector_type=vec_type.value, sha1s=sha1s)
|
|
125
|
+
|
|
126
|
+
if not vectors:
|
|
127
|
+
empty_notif = Notif(NotifType.ERROR,
|
|
128
|
+
name="NoData",
|
|
129
|
+
message=f"""For the clustering function image vectors are needed.
|
|
130
|
+
No such vectors ({vec_type.value}) could be found.
|
|
131
|
+
Compute the vectors and try again.) """,
|
|
132
|
+
functions=self._get_vector_func_notifs(vec_type))
|
|
133
|
+
return ActionResult(notifs=[empty_notif])
|
|
134
|
+
clusters, distances = make_clusters(vectors, method="kmeans", nb_clusters=nb_clusters)
|
|
135
|
+
groups = []
|
|
136
|
+
groups_images = []
|
|
137
|
+
labels = []
|
|
138
|
+
i = 0
|
|
139
|
+
# TODO: put back mistral when it's working properly
|
|
140
|
+
if label_clusters:
|
|
141
|
+
from .mistral_test import create_labels_from_group, generate_group_image
|
|
142
|
+
for cluster, distance in zip(clusters, distances):
|
|
143
|
+
group = Group(score=Score(min=0, max=100, max_is_best=False, value=distance))
|
|
144
|
+
if label_clusters:
|
|
145
|
+
images = [sha1_to_instance[sha1][0].url for sha1 in cluster[:20]]
|
|
146
|
+
groups_images.append(generate_group_image(images, i))
|
|
147
|
+
i += 1
|
|
148
|
+
group.sha1s = sorted(cluster, key=lambda sha1: sha1_to_ahash[sha1])
|
|
149
|
+
groups.append(group)
|
|
150
|
+
if len(groups_images) > 0:
|
|
151
|
+
labels = create_labels_from_group(groups_images)
|
|
152
|
+
for i, g in enumerate(groups):
|
|
153
|
+
g.name = f"Cluster {i}" if not len(labels) > 0 else "-".join(labels[i])
|
|
154
|
+
|
|
155
|
+
return ActionResult(groups=groups)
|
|
156
|
+
|
|
157
|
+
async def find_images(self, context: ActionContext, vec_type: VectorType = VectorType.rgb):
|
|
158
|
+
"""
|
|
159
|
+
Find Similar images using Cosine distances.
|
|
160
|
+
dist: 0 -> images are considered highly dissimilar
|
|
161
|
+
dist: 1 -> images are considered identical
|
|
162
|
+
See: https://en.wikipedia.org/wiki/Cosine_similarity for more.
|
|
163
|
+
"""
|
|
164
|
+
instances = await self.project.get_instances(context.instance_ids)
|
|
165
|
+
sha1s = [i.sha1 for i in instances]
|
|
166
|
+
ignore_sha1s = set(sha1s)
|
|
167
|
+
vectors = await self.project.get_vectors(source=self.name, vector_type=vec_type.value, sha1s=sha1s)
|
|
168
|
+
|
|
169
|
+
if not vectors:
|
|
170
|
+
return ActionResult(notifs=[Notif(
|
|
171
|
+
NotifType.ERROR,
|
|
172
|
+
name="NoData",
|
|
173
|
+
message=f"""For the similarity function image vectors are needed.
|
|
174
|
+
No such vectors ({vec_type.value}) could be found.
|
|
175
|
+
Compute the vectors and try again.) """,
|
|
176
|
+
functions=self._get_vector_func_notifs(vec_type))])
|
|
177
|
+
|
|
178
|
+
vector_datas = [x.data for x in vectors]
|
|
179
|
+
|
|
180
|
+
tree = await self._get_tree(vec_type)
|
|
181
|
+
if not tree:
|
|
182
|
+
notif = Notif(type=NotifType.ERROR, name="NoFaissTree",
|
|
183
|
+
message=f"No Faiss tree could be loaded for vec_type {vec_type.value}")
|
|
184
|
+
return ActionResult(notifs=[notif])
|
|
185
|
+
|
|
186
|
+
res = tree.query(vector_datas)
|
|
187
|
+
index = {r['sha1']: r['dist'] for r in res if r['sha1'] not in ignore_sha1s}
|
|
188
|
+
|
|
189
|
+
res_sha1s = list(index.keys())
|
|
190
|
+
res_scores = ScoreList(min=0, max=1, values=[index[sha1] for sha1 in res_sha1s],
|
|
191
|
+
max_is_best=True,
|
|
192
|
+
description="Similarity between 0 and 1. 1 is best")
|
|
193
|
+
|
|
194
|
+
res = Group(sha1s=res_sha1s, scores=res_scores)
|
|
195
|
+
return ActionResult(groups=[res])
|
|
196
|
+
|
|
197
|
+
async def search_by_text(self, context: ActionContext, vec_type: VectorType = VectorType.rgb, text: str = '', min_similarity: float = 0.5):
|
|
198
|
+
"""Search image using text similarity"""
|
|
199
|
+
if text == '':
|
|
200
|
+
notif = Notif(type=NotifType.ERROR, name="EmptySearchText",
|
|
201
|
+
message="Please give a valid and not empty text search argument")
|
|
202
|
+
return ActionResult(notifs=[notif])
|
|
203
|
+
|
|
204
|
+
context_instances = await self.project.get_instances(context.instance_ids)
|
|
205
|
+
context_sha1s = [i.sha1 for i in context_instances]
|
|
206
|
+
|
|
207
|
+
tree = await self._get_tree(vec_type)
|
|
208
|
+
if not tree:
|
|
209
|
+
notif = Notif(type=NotifType.ERROR, name="NoFaissTree",
|
|
210
|
+
message=f"No Faiss tree could be loaded for vec_type {vec_type.value}")
|
|
211
|
+
return ActionResult(notifs=[notif])
|
|
212
|
+
|
|
213
|
+
if is_image_url(text):
|
|
214
|
+
im = Image.open(requests.get(text, stream=True).raw)
|
|
215
|
+
vec = self.transformer.to_vector(im)
|
|
216
|
+
resulting_images = tree.query([vec])
|
|
217
|
+
else:
|
|
218
|
+
try:
|
|
219
|
+
resulting_images = tree.query_texts([text], self.transformer)
|
|
220
|
+
except ValueError as e:
|
|
221
|
+
return ActionResult(notifs=[Notif(type=NotifType.ERROR, name="TextSimilarityError", message=str(e))])
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# filter out images if they are not in the current context
|
|
225
|
+
filtered_instances = [inst for inst in resulting_images if inst['sha1'] in context_sha1s]
|
|
226
|
+
|
|
227
|
+
index = {r['sha1']: r['dist'] for r in filtered_instances}
|
|
228
|
+
res_sha1s = np.asarray(list(index.keys()))
|
|
229
|
+
res_scores = np.asarray([index[sha1] for sha1 in res_sha1s])
|
|
230
|
+
|
|
231
|
+
# remap score since text to image similary tends to be between 0.1 and 0.4 and filter by similarity
|
|
232
|
+
remaped_scores = np.around(np.interp(res_scores, [0, 0.375], [0, 1]), decimals=2)
|
|
233
|
+
final_scores = remaped_scores[remaped_scores >= min_similarity].tolist()
|
|
234
|
+
final_sha1s = res_sha1s[remaped_scores >= min_similarity].tolist()
|
|
235
|
+
|
|
236
|
+
scores = ScoreList(min=0, max=1, values=final_scores, description="Similarity between image and text never give less than 0.1 and more than 0.4, hence here the values, remapped between 0 and 1")
|
|
237
|
+
res = Group(sha1s=final_sha1s, scores=scores)
|
|
238
|
+
res.name = "Text Search: " + text
|
|
239
|
+
return ActionResult(groups=[res])
|
|
240
|
+
|
|
241
|
+
async def cluster_by_tags(self, context: ActionContext, tags: PropertyId, vec_type: VectorType = VectorType.rgb):
|
|
242
|
+
"""Cluster images using a Tag/MultiTag property to guide the result"""
|
|
243
|
+
props = await self.project.get_properties(ids=[tags])
|
|
244
|
+
tag_prop = props[0]
|
|
245
|
+
|
|
246
|
+
if tag_prop.type != PropertyType.tag and tag_prop.type != PropertyType.multi_tags:
|
|
247
|
+
notif = Notif(type=NotifType.ERROR,
|
|
248
|
+
name="WrongPropertyType",
|
|
249
|
+
message=f"""Property: <{tag_prop.name}> is not of type Tag or MultiTags. This function only
|
|
250
|
+
accepts tag types properties. Please choose another property""")
|
|
251
|
+
return ActionResult(notifs=[notif])
|
|
252
|
+
|
|
253
|
+
instances = await self.project.get_instances(context.instance_ids)
|
|
254
|
+
sha1_to_instance = group_by_sha1(instances)
|
|
255
|
+
sha1s = list(sha1_to_instance.keys())
|
|
256
|
+
if not sha1s:
|
|
257
|
+
return None
|
|
258
|
+
# TODO: get tags text from the PropertyId
|
|
259
|
+
tags_text = [t.value for t in await self.project.get_tags(property_ids=[tags])]
|
|
260
|
+
text_vectors = get_text_vectors(tags_text, self.transformer)
|
|
261
|
+
pano_vectors = await self.project.get_vectors(source=self.name, vector_type=vec_type.value, sha1s=sha1s)
|
|
262
|
+
|
|
263
|
+
if not pano_vectors:
|
|
264
|
+
return ActionResult(notifs=[Notif(
|
|
265
|
+
NotifType.ERROR,
|
|
266
|
+
name="NoData",
|
|
267
|
+
message=f"""The Cluster_By_Tags function needs image vectors.
|
|
268
|
+
No such vectors ({vec_type.value}) could be found.
|
|
269
|
+
Compute the vectors and try again.) """,
|
|
270
|
+
functions=self._get_vector_func_notifs(vec_type))])
|
|
271
|
+
|
|
272
|
+
groups = cluster_by_text(pano_vectors, text_vectors, tags_text)
|
|
273
|
+
|
|
274
|
+
return ActionResult(groups=groups)
|
|
275
|
+
|
|
276
|
+
async def find_duplicates(self, context: ActionContext, min_similarity: float = 0.95):
|
|
277
|
+
"""
|
|
278
|
+
Create clusters with at least `min_similarity` between the images of the cluster
|
|
279
|
+
@min_similarity: the minimal similarity value between images of the cluster
|
|
280
|
+
"""
|
|
281
|
+
# on récupère les vecteurs
|
|
282
|
+
# pour chaque vecteur on récupère ses plus similaires (150 pour test) puis on filtre tout ce qui est < min_similarity
|
|
283
|
+
# on marque tous les images dans le cluster pour ne pas les requêter à nouveau
|
|
284
|
+
instances = await self.project.get_instances(context.instance_ids)
|
|
285
|
+
sha1_to_instance = group_by_sha1(instances)
|
|
286
|
+
sha1s = list(sha1_to_instance.keys())
|
|
287
|
+
if not sha1s:
|
|
288
|
+
return None
|
|
289
|
+
# TODO: get tags text from the PropertyId
|
|
290
|
+
pano_vectors = await self.project.get_vectors(source=self.name, vector_type=VectorType.rgb.value, sha1s=sha1s)
|
|
291
|
+
vectors, sha1s = zip(*[(i.data, i.sha1) for i in pano_vectors])
|
|
292
|
+
already_in_clusters = set()
|
|
293
|
+
groups = []
|
|
294
|
+
for vector, sha1 in zip(vectors, sha1s):
|
|
295
|
+
if sha1 in already_in_clusters:
|
|
296
|
+
continue
|
|
297
|
+
tree = await self._get_tree(VectorType.rgb)
|
|
298
|
+
res = tree.query([vector.data], 150)
|
|
299
|
+
filtered = [r for r in res if r['dist'] >= min_similarity and r['sha1'] in sha1s]
|
|
300
|
+
res_sha1s = [r['sha1'] for r in filtered]
|
|
301
|
+
res_scores = [r['dist'] for r in filtered]
|
|
302
|
+
score_list = ScoreList(min=0, max=1, max_is_best=True, values=res_scores)
|
|
303
|
+
if len(res_sha1s) == 1:
|
|
304
|
+
continue
|
|
305
|
+
already_in_clusters.update(res_sha1s)
|
|
306
|
+
groups.append(Group(sha1s=res_sha1s, scores=score_list))
|
|
307
|
+
return ActionResult(groups=groups)
|
|
308
|
+
|
|
309
|
+
async def rebuild_trees(self, deleted):
|
|
310
|
+
for type_ in VectorType:
|
|
311
|
+
await self._update_tree(type_)
|
|
312
|
+
|
|
313
|
+
async def _get_tree(self, vec_type: VectorType):
|
|
314
|
+
tree = self.trees.get(vec_type)
|
|
315
|
+
if tree:
|
|
316
|
+
return tree
|
|
317
|
+
tree = load_faiss_tree(self, vec_type)
|
|
318
|
+
if tree:
|
|
319
|
+
self.trees[vec_type] = tree
|
|
320
|
+
return tree
|
|
321
|
+
tree = await create_faiss_tree(self, vec_type)
|
|
322
|
+
if tree:
|
|
323
|
+
self.trees[vec_type] = tree
|
|
324
|
+
return tree
|
|
325
|
+
|
|
326
|
+
async def _update_tree(self, vec_type: VectorType):
|
|
327
|
+
tree = await create_faiss_tree(self, vec_type)
|
|
328
|
+
self.trees[vec_type] = tree
|
|
329
|
+
print(f"updated {vec_type.value} faiss tree")
|
|
330
|
+
return tree
|
|
331
|
+
|
|
332
|
+
def _load_transformer(self):
|
|
333
|
+
if TransformerName[self.params.model] == TransformerName.auto and not self.params.hugging_face_model:
|
|
334
|
+
return ActionResult(notifs=[Notif(
|
|
335
|
+
NotifType.ERROR,
|
|
336
|
+
name="No hugging face model specified",
|
|
337
|
+
message=f"""
|
|
338
|
+
Automodel selected but no hugging face model provided
|
|
339
|
+
""")])
|
|
340
|
+
return get_transformer(TransformerName[self.params.model], self.params.hugging_face_model)
|
panopticml/utils.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import io
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import torch
|
|
5
|
+
import re
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
from .models import VectorType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def preprocess_image(image_data: bytes, vector_type: VectorType = VectorType.rgb):
|
|
12
|
+
image = Image.open(io.BytesIO(image_data))
|
|
13
|
+
if vector_type == VectorType.rgb:
|
|
14
|
+
image = image.convert('RGB')
|
|
15
|
+
else:
|
|
16
|
+
image = image.convert('L').convert('RGB')
|
|
17
|
+
return image
|
|
18
|
+
|
|
19
|
+
def cosine_similarity(embedding1: np.array, embedding2: np.array):
|
|
20
|
+
"""
|
|
21
|
+
Calcule la similarité cosinus entre deux embeddings normalisés
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
embedding1: premier embedding (normalisé)
|
|
25
|
+
embedding2: deuxième embedding (normalisé)
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
float: similarité cosinus (entre -1 et 1)
|
|
29
|
+
"""
|
|
30
|
+
# Pour des embeddings normalisés, similarité cosinus = produit scalaire
|
|
31
|
+
embedding1_torch = torch.from_numpy(embedding1).squeeze()
|
|
32
|
+
embedding2_torch = torch.from_numpy(embedding2).squeeze()
|
|
33
|
+
|
|
34
|
+
return torch.dot(embedding1_torch, embedding2_torch).item()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def similarity_matrix(vectors1: list[np.array], vectors2: list[np.array]) -> (torch.Tensor, torch.Tensor):
|
|
38
|
+
"""
|
|
39
|
+
Create a similarity matrix from two list of normalized np vectors
|
|
40
|
+
Returns two tensors, one for best scores and one for best_indices of vectors1 into vectors2
|
|
41
|
+
i.e for each vector1 we'll know the closest vector of vectors2 and the similarity between them
|
|
42
|
+
"""
|
|
43
|
+
astorch1 = torch.from_numpy(np.vstack(vectors1))
|
|
44
|
+
astorch2 = torch.from_numpy(np.vstack(vectors2))
|
|
45
|
+
matrix = torch.mm(astorch1, astorch2.T)
|
|
46
|
+
best_scores, best_indices = torch.max(matrix, dim=1)
|
|
47
|
+
return best_scores, best_indices
|
|
48
|
+
|
|
49
|
+
def resolve_device():
|
|
50
|
+
device = 'cpu'
|
|
51
|
+
if torch.cuda.is_available():
|
|
52
|
+
device = 'cuda'
|
|
53
|
+
# TODO: when silicon bugs are working again put it back
|
|
54
|
+
# elif torch.backends.mps.is_available():
|
|
55
|
+
# device = 'mps'
|
|
56
|
+
return device
|
|
57
|
+
|
|
58
|
+
def is_image_url(url):
|
|
59
|
+
pattern = re.compile(
|
|
60
|
+
r'''(?xi)
|
|
61
|
+
\b
|
|
62
|
+
https?:// # protocole http ou https
|
|
63
|
+
[\w.-]+(?:\.[\w.-]+)+ # domaine
|
|
64
|
+
(?:/[^\s?#]*)* # chemin éventuel
|
|
65
|
+
/? # éventuellement un / final
|
|
66
|
+
[^\s?#]* # éventuellement un nom de fichier
|
|
67
|
+
\.(?:jpg|jpeg|png|gif|webp|svg|bmp|tiff|ico) # extension image
|
|
68
|
+
(?:\?[^\s#]*)? # paramètres après ?
|
|
69
|
+
\b
|
|
70
|
+
'''
|
|
71
|
+
)
|
|
72
|
+
return re.match(pattern, url)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: panopticml
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: A package for Panoptic ML
|
|
5
|
+
Project-URL: Homepage, https://github.com/CERES-Sorbonne/PanopticML
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/CERES-Sorbonne/PanopticML/issues
|
|
7
|
+
Author-email: Félix Alié <felix.alie@sorbonne-universite.fr>
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: faiss-cpu<1.11.0
|
|
21
|
+
Requires-Dist: hdbscan
|
|
22
|
+
Requires-Dist: mistralai
|
|
23
|
+
Requires-Dist: numpy; platform_system != 'Darwin'
|
|
24
|
+
Requires-Dist: numpy<=1.26.0; platform_system == 'Darwin'
|
|
25
|
+
Requires-Dist: scikit-learn; platform_system != 'Darwin'
|
|
26
|
+
Requires-Dist: scikit-learn==1.5.0; platform_system == 'Darwin'
|
|
27
|
+
Requires-Dist: scipy
|
|
28
|
+
Requires-Dist: torch; platform_system != 'Darwin'
|
|
29
|
+
Requires-Dist: torch==2.1.0; platform_system == 'Darwin'
|
|
30
|
+
Requires-Dist: torchvision
|
|
31
|
+
Requires-Dist: transformers<4.52.0
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# PanopticML
|
|
35
|
+
|
|
36
|
+
Default plugin for panoptic, provides all machine learning functionalities such as:
|
|
37
|
+
- image embeddings using OpenAI CLIP
|
|
38
|
+
- images clustering using FAISS Kmeans or HDBScan
|
|
39
|
+
- images to images similarity using FAISS Index L2
|
|
40
|
+
- text to images similarity using FAISS Index L2 + CLIP
|
|
41
|
+
|
|
42
|
+
## Available models
|
|
43
|
+
|
|
44
|
+
Several models are available in this plugin to compute the embeddings:
|
|
45
|
+
- [google mobilenet](https://huggingface.co/google/mobilenet_v2_1.4_224): a really light model with lower performances but made to run on bad devices, no support for text similarity
|
|
46
|
+
- [openAI CLIP](https://huggingface.co/openai/clip-vit-large-patch14): default model for panoptic, good performances, runs well on most computers with a decent CPU, support text similarity and has a good understanding of semantics
|
|
47
|
+
- [meta dinosV2](https://huggingface.co/docs/transformers/en/model_doc/dinov2): model that will have better performances than CLIP on pure visual similarities but less understanding of themes and semantics, no support for text similarity, runs well on CPU
|
|
48
|
+
- [google SIGLIP2](https://huggingface.co/docs/transformers/main/model_doc/siglip2): computing heavy model, NVDIA GPU recommended but works also on CPU (it will take a long time to compute embeddings), way better results than CLIP on text similarity and semantics, and pretty good visual features
|
|
49
|
+
- auto transformers: want to tryout any huggingface multimodal model ? you can just provide its id to panopticML and should be able to use it directly
|
|
50
|
+
|
|
51
|
+
## Clustering functions
|
|
52
|
+
- Kmeans: specify a number of clusters, really fast
|
|
53
|
+
- HDBScan: automatic number of clusters, a bit slower and excludes a lot of images
|
|
54
|
+
- Images to text: considering several texts, create a cluster per text and put each image to the text it is the closest to
|
|
55
|
+
- Find duplicates: create clusters with only images sharing a lot of similarity together, good to find duplicates or near duplicates
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
panopticml/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
panopticml/compute_vector_task.py,sha256=Fe2CRez6tXReJ46v0xREJRKY3RYm4iaIx-maUpr_FQE,1957
|
|
3
|
+
panopticml/models.py,sha256=JSex5wzu1S0iw1xnGJN1zdHo7ARnxXy2jLhHZ-mCQVs,92
|
|
4
|
+
panopticml/panoptic_ml.py,sha256=XH8mSLfcSBvpnBNtqrDenPBl-6FSomke-rTHBL4zyT0,16047
|
|
5
|
+
panopticml/utils.py,sha256=28qUMoBh-qaI8-Y8fIy0rFr3evTV6J1kr5dazehedcI,2470
|
|
6
|
+
panopticml/compute/__init__.py,sha256=Y5okbGIQHPYCal9curiu1-WFES29Qd2Lc9RY2V-BKBI,37
|
|
7
|
+
panopticml/compute/clustering.py,sha256=O1Yshea9D7PCqDE_bAMwKyBkAyPei5Wz_pS2xhayD-E,4937
|
|
8
|
+
panopticml/compute/faiss_tree.py,sha256=WY2oZJCiGMWbyVWIqaNEgsW16VXH5tt17Zf9P5vrpZ4,2408
|
|
9
|
+
panopticml/compute/similarity.py,sha256=u4GmoaWQqq-fBqChiHl6HsLfK8QqVZasIscbbzB9I7c,364
|
|
10
|
+
panopticml/compute/transformers/__init__.py,sha256=zCVjpY1E_afMbmB2bWLT1grPbOlqj7MsxDcDR1sOHec,4381
|
|
11
|
+
panopticml-0.0.2.dist-info/METADATA,sha256=XClZCxAWyjGZerkz3iyoeeCRPLiH9lrHZQj8cWqlGuc,3381
|
|
12
|
+
panopticml-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
13
|
+
panopticml-0.0.2.dist-info/RECORD,,
|