panopticml 0.0.2__tar.gz

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.
@@ -0,0 +1,140 @@
1
+ name: Hatch Build and Publish
2
+ run-name: HATCH to PyPI by @${{ github.actor }}
3
+
4
+ on:
5
+ workflow_dispatch:
6
+ push:
7
+ tags:
8
+ - 'v*.*.*'
9
+
10
+ permissions:
11
+ contents: write
12
+ packages: write
13
+ id-token: write
14
+
15
+ jobs:
16
+ get-ref:
17
+ runs-on: ubuntu-latest
18
+
19
+ steps:
20
+ - name: Checkout
21
+ uses: actions/checkout@v4
22
+ with:
23
+ fetch-depth: 0
24
+
25
+ - name: Get current branch
26
+ id: check_step
27
+ run: |
28
+ raw=$(git branch -r --contains ${{ github.ref }})
29
+ if [[ -z "$raw" ]]; then
30
+ raw=$(git branch -r --contains ${{ github.sha }})
31
+ fi
32
+ if [[ -z "$raw" ]]; then
33
+ echo "No branch found for ${{ github.ref }} or ${{ github.sha }}."
34
+ echo "This is probably a tag push without a branch."
35
+ exit 1
36
+ fi
37
+ BRANCH=${raw##*/}
38
+ BRANCH=$(echo $BRANCH | base64 -w 0)
39
+ echo "BRANCH=$BRANCH" >> $GITHUB_OUTPUT
40
+ echo "Branch is $BRANCH."
41
+
42
+ outputs:
43
+ branch-name: ${{ steps.check_step.outputs.BRANCH }}
44
+
45
+ get-tag:
46
+ runs-on: ubuntu-latest
47
+ needs: get-ref
48
+ steps:
49
+ - name: Decode ref
50
+ id: decode_ref
51
+ run: |
52
+ name=${{ needs.get-ref.outputs.branch-name }}
53
+ name=$(echo $name | base64 -d)
54
+ echo "NAME=$name" >> $GITHUB_ENV
55
+
56
+ - name: Checkout repository
57
+ uses: actions/checkout@v4
58
+ with:
59
+ ref: ${{ env.NAME }}
60
+ fetch-depth: 0
61
+
62
+ - name: Set env
63
+ id: set-env
64
+ run: |
65
+ if [[ "${{ github.event_name }}" == "push" ]]; then
66
+ TAG=$(echo $GITHUB_REF | cut -d / -f 3)
67
+ echo "TAG=$TAG" >> $GITHUB_ENV
68
+ fi
69
+
70
+ outputs:
71
+ tag: ${{ env.TAG }}
72
+
73
+ deploy:
74
+ runs-on: ubuntu-latest
75
+ environment: release
76
+ needs:
77
+ - get-ref
78
+ - get-tag
79
+ steps:
80
+ - name: Decode ref
81
+ id: decode_ref
82
+ run: |
83
+ name=${{ needs.get-ref.outputs.branch-name }}
84
+ echo "Encoded ref is $name"
85
+ name=$(echo $name | base64 -d)
86
+ echo "Decoded ref is $name"
87
+ echo "REF=$name" >> $GITHUB_ENV
88
+
89
+ - name: Checkout repository
90
+ uses: actions/checkout@v4
91
+ with:
92
+ ref: ${{ env.REF }}
93
+
94
+ - name: Set up Python
95
+ uses: actions/setup-python@v4
96
+ with:
97
+ python-version: '3.12'
98
+ cache: 'pip'
99
+
100
+ - name: Install dependencies
101
+ run: |
102
+ python -m pip install --upgrade pip
103
+ pip install hatch
104
+
105
+ - name: Version bump
106
+ if : ${{ github.event_name != 'push' }}
107
+ run: |
108
+ hatch version fix
109
+ echo "Version bumped"
110
+
111
+ - name: Version from tag
112
+ if : ${{ github.event_name == 'push' }}
113
+ run: |
114
+ TAG=${{ needs.get-tag.outputs.tag }}
115
+ echo "TAG=$TAG"
116
+ hatch version $TAG
117
+ # && echo "Version set to $TAG" || echo "Version already exists" && exit 1
118
+
119
+ - name: Build package
120
+ run: hatch build
121
+
122
+ - name: Publish package
123
+ uses: pypa/gh-action-pypi-publish@release/v1
124
+ with:
125
+ user: __token__
126
+ password: ${{ secrets.PYPI_API_TOKEN }} # PYPI_API_TOKEN
127
+ # repository-url: https://test.pypi.org/legacy/ # uncomment for test.pypi.org
128
+
129
+ - name: Update the about.py file
130
+ uses: stefanzweifel/git-auto-commit-action@v5
131
+ with:
132
+ commit_message: "Update about.py file"
133
+ file_pattern: "version.py"
134
+ status_options: '--untracked-files=no'
135
+
136
+ - name: Push changes
137
+ uses: ad-m/github-push-action@master
138
+ with:
139
+ github_token: ${{ secrets.GITHUB_TOKEN }}
140
+ branch: ${{ env.REF }}
@@ -0,0 +1,38 @@
1
+ name: Tests Multi-Plateformes
2
+
3
+ # Déclenchement manuel uniquement
4
+ on:
5
+ workflow_dispatch:
6
+
7
+ jobs:
8
+ test:
9
+ name: "Tests sur ${{ matrix.os }} : ${{ matrix.python-version }}"
10
+ runs-on: ${{ matrix.os }}
11
+
12
+ strategy:
13
+ # Continue même si un OS échoue
14
+ fail-fast: false
15
+ matrix:
16
+ os: [ubuntu-latest, windows-latest, macos-latest]
17
+ python-version: ['3.10', '3.11', '3.12']
18
+
19
+ steps:
20
+ - name: Checkout du code
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Configuration de Python ${{ matrix.python-version }}
24
+ uses: actions/setup-python@v4
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+
28
+ - name: Mise à jour de pip
29
+ run: python -m pip install --upgrade pip
30
+
31
+ - name: Installation des dépendances
32
+ run: |
33
+ pip install pytest
34
+ pip install panoptic
35
+ pip install -r requirements.txt
36
+
37
+ - name: Lancement des tests avec pytest
38
+ run: pytest -v
@@ -0,0 +1,201 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ .DS_Store
12
+ dist
13
+ dist-ssr
14
+ coverage
15
+ *.local
16
+
17
+ /cypress/videos/
18
+ /cypress/screenshots/
19
+
20
+ # Editor directories and files
21
+ .vscode/*
22
+ !.vscode/extensions.json
23
+ .idea
24
+ *.suo
25
+ *.ntvs*
26
+ *.njsproj
27
+ *.sln
28
+ *.sw?
29
+
30
+ */__pycache__/
31
+
32
+ /panoptic_back/test.json
33
+ # /panoptic_back/*.db
34
+
35
+ # Byte-compiled / optimized / DLL files
36
+ __pycache__/
37
+ *.py[cod]
38
+ *$py.class
39
+
40
+ # C extensions
41
+ *.so
42
+
43
+ # Distribution / packaging
44
+ .Python
45
+ build/
46
+ develop-eggs/
47
+ dist/
48
+ downloads/
49
+ eggs/
50
+ .eggs/
51
+ lib/
52
+ lib64/
53
+ parts/
54
+ sdist/
55
+ var/
56
+ wheels/
57
+ share/python-wheels/
58
+ *.egg-info/
59
+ .installed.cfg
60
+ *.egg
61
+ MANIFEST
62
+
63
+ # PyInstaller
64
+ # Usually these files are written by a python script from a template
65
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
66
+ *.manifest
67
+ *.spec
68
+
69
+ # Installer logs
70
+ pip-log.txt
71
+ pip-delete-this-directory.txt
72
+
73
+ # Unit test / coverage reports
74
+ htmlcov/
75
+ .tox/
76
+ .nox/
77
+ .coverage
78
+ .coverage.*
79
+ .cache
80
+ nosetests.xml
81
+ coverage.xml
82
+ *.cover
83
+ *.py,cover
84
+ .hypothesis/
85
+ .pytest_cache/
86
+ cover/
87
+
88
+ # Translations
89
+ *.mo
90
+ *.pot
91
+
92
+ # Django stuff:
93
+ *.log
94
+ local_settings.py
95
+ db.sqlite3
96
+ db.sqlite3-journal
97
+
98
+ # Flask stuff:
99
+ instance/
100
+ .webassets-cache
101
+
102
+ # Scrapy stuff:
103
+ .scrapy
104
+
105
+ # Sphinx documentation
106
+ docs/_build/
107
+
108
+ # PyBuilder
109
+ .pybuilder/
110
+ target/
111
+
112
+ # Jupyter Notebook
113
+ .ipynb_checkpoints
114
+
115
+ # IPython
116
+ profile_default/
117
+ ipython_config.py
118
+
119
+ # pyenv
120
+ # For a library or package, you might want to ignore these files since the code is
121
+ # intended to run in multiple environments; otherwise, check them in:
122
+ # .python-version
123
+
124
+ # pipenv
125
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
126
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
127
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
128
+ # install all needed dependencies.
129
+ #Pipfile.lock
130
+
131
+ # poetry
132
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
133
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
134
+ # commonly ignored for libraries.
135
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
136
+ #poetry.lock
137
+
138
+ # pdm
139
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
140
+ #pdm.lock
141
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
142
+ # in version control.
143
+ # https://pdm.fming.dev/#use-with-ide
144
+ .pdm.toml
145
+
146
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
147
+ __pypackages__/
148
+
149
+ # Celery stuff
150
+ celerybeat-schedule
151
+ celerybeat.pid
152
+
153
+ # SageMath parsed files
154
+ *.sage.py
155
+
156
+ # Environments
157
+ .env
158
+ .venv
159
+ env/
160
+ venv/
161
+ ENV/
162
+ env.bak/
163
+ venv.bak/
164
+
165
+ # Spyder project settings
166
+ .spyderproject
167
+ .spyproject
168
+
169
+ # Rope project settings
170
+ .ropeproject
171
+
172
+ # mkdocs documentation
173
+ /site
174
+
175
+ # mypy
176
+ .mypy_cache/
177
+ .dmypy.json
178
+ dmypy.json
179
+
180
+ # Pyre type checker
181
+ .pyre/
182
+
183
+ # pytype static type analyzer
184
+ .pytype/
185
+
186
+ # Cython debug symbols
187
+ cython_debug/
188
+
189
+ # PyCharm
190
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
191
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
192
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
193
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
194
+ #.idea/
195
+
196
+ .vscode
197
+
198
+ mini/
199
+
200
+ *.pkl
201
+
@@ -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,22 @@
1
+ # PanopticML
2
+
3
+ Default plugin for panoptic, provides all machine learning functionalities such as:
4
+ - image embeddings using OpenAI CLIP
5
+ - images clustering using FAISS Kmeans or HDBScan
6
+ - images to images similarity using FAISS Index L2
7
+ - text to images similarity using FAISS Index L2 + CLIP
8
+
9
+ ## Available models
10
+
11
+ Several models are available in this plugin to compute the embeddings:
12
+ - [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
13
+ - [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
14
+ - [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
15
+ - [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
16
+ - 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
17
+
18
+ ## Clustering functions
19
+ - Kmeans: specify a number of clusters, really fast
20
+ - HDBScan: automatic number of clusters, a bit slower and excludes a lot of images
21
+ - Images to text: considering several texts, create a cluster per text and put each image to the text it is the closest to
22
+ - Find duplicates: create clusters with only images sharing a lot of similarity together, good to find duplicates or near duplicates
@@ -0,0 +1,3 @@
1
+ from .panopticml.panoptic_ml import PanopticML
2
+
3
+ plugin_class = PanopticML
@@ -0,0 +1,104 @@
1
+ import json
2
+
3
+ from PIL import Image
4
+ import base64
5
+ from io import BytesIO
6
+ from mistralai import Mistral
7
+ import os
8
+
9
+ model = "pixtral-12b-2409"
10
+ api_key = os.getenv("MISTRAL_KEY")
11
+ client = Mistral(api_key=api_key)
12
+
13
+
14
+ def create_batch(liste, batch_size):
15
+ return [liste[i:i + batch_size] for i in range(0, len(liste), batch_size)]
16
+
17
+
18
+ def create_labels_from_group(groups_images):
19
+ all_labels = []
20
+ for batch in create_batch(groups_images, 8):
21
+ invalid = True
22
+ while invalid:
23
+ labels = create_labels_from_batch(batch)
24
+ try:
25
+ res = json.loads(labels)
26
+ invalid = False
27
+ except json.JSONDecodeError:
28
+ print("Invalid JSON")
29
+ all_labels.extend(res)
30
+ return all_labels
31
+
32
+
33
+ def create_labels_from_batch(groups_images):
34
+ messages = [
35
+ {
36
+ "role": "user",
37
+ "content": []
38
+ },
39
+ {
40
+ "role": "assistant",
41
+ "content": [{
42
+ "type": "text",
43
+ "text": """[[
44
+ """
45
+ }],
46
+ "prefix": "true"
47
+ }
48
+ ]
49
+
50
+ for base64_image in groups_images:
51
+ messages[0]['content'].append({
52
+ "type": "image_url",
53
+ "image_url": f"data:image/jpeg;base64,{base64_image}"
54
+ })
55
+ messages[0]['content'].append({
56
+ "type": "text",
57
+ "text": """\
58
+ Chaque image est une mosaïque du contenu d'un cluster d'images.
59
+ Donne CINQ (5) mots-clés (par mosaïque) et UNIQUEMENT ces mots-clés, sans aucun autre texte
60
+ sous forme de liste de listes qui devra être un JSON valide, exemple [[mot_cle1, mot_cle2, mot_cle3, mot_cle4, mot_cle5],[...]].""" # , séparez ces mots par des virgules.
61
+ # Essayez d'inclure des mots-clés généraux qui pourraient décrire le contenu de nombreuses images à la fois.
62
+ # Veuillez séparer les mots-clés pour chaque mosaïque avec un point-virgule et NE PAS INCLURE de texte supplémentaire.
63
+ f"""Assure toi de ne répondre qu'en français tu me donneras EXACTEMENT {len(groups_images)} LISTES (soit une par image) et fais bien attention à respecter L'ORDRE des images !.
64
+ """
65
+ })
66
+ # Get the chat response
67
+ chat_response = client.chat.complete(
68
+ model=model,
69
+ messages=messages
70
+ )
71
+
72
+ # Print the content of the response
73
+ return chat_response.choices[0].message.content
74
+
75
+
76
+ def generate_group_image(images_paths, cluster_n):
77
+ # Définir la taille des images dans la mosaïque
78
+ cols, rows = 5, 4
79
+ thumb_width, thumb_height = 200, 200 # Taille des miniatures
80
+
81
+ # Créer une image blanche pour la mosaïque
82
+ mosaic = Image.new('RGB', (cols * thumb_width, rows * thumb_height), (255, 255, 255))
83
+
84
+ for index, img_path in enumerate(images_paths):
85
+ if index >= cols * rows:
86
+ break # S'assure de ne traiter que 20 images
87
+
88
+ img = Image.open(img_path)
89
+ img = img.resize((thumb_width, thumb_height))
90
+
91
+ # Calcul des coordonnées de placement
92
+ x_offset = (index % cols) * thumb_width
93
+ y_offset = (index // cols) * thumb_height
94
+
95
+ mosaic.paste(img, (x_offset, y_offset))
96
+
97
+ # Convertir en base64
98
+ buffered = BytesIO()
99
+ mosaic.save(buffered, format="JPEG")
100
+ with open(f'clusters/cluster_{cluster_n}.jpg', 'wb') as f:
101
+ mosaic.save(f)
102
+ encoded_image = base64.b64encode(buffered.getvalue()).decode('utf-8')
103
+
104
+ return encoded_image
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