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.
- panopticml-0.0.2/.github/workflows/hatch-build-and-publish.yml +140 -0
- panopticml-0.0.2/.github/workflows/workflow.yaml +38 -0
- panopticml-0.0.2/.gitignore +201 -0
- panopticml-0.0.2/PKG-INFO +55 -0
- panopticml-0.0.2/README.md +22 -0
- panopticml-0.0.2/__init__.py +3 -0
- panopticml-0.0.2/mistral_test.py +104 -0
- panopticml-0.0.2/panopticml/__init__.py +0 -0
- panopticml-0.0.2/panopticml/compute/__init__.py +1 -0
- panopticml-0.0.2/panopticml/compute/clustering.py +122 -0
- panopticml-0.0.2/panopticml/compute/faiss_tree.py +72 -0
- panopticml-0.0.2/panopticml/compute/similarity.py +11 -0
- panopticml-0.0.2/panopticml/compute/transformers/__init__.py +131 -0
- panopticml-0.0.2/panopticml/compute_vector_task.py +64 -0
- panopticml-0.0.2/panopticml/models.py +6 -0
- panopticml-0.0.2/panopticml/panoptic_ml.py +340 -0
- panopticml-0.0.2/panopticml/utils.py +72 -0
- panopticml-0.0.2/pyproject.toml +54 -0
- panopticml-0.0.2/requirements.txt +12 -0
- panopticml-0.0.2/tests/__init__.py +0 -0
- panopticml-0.0.2/tests/resources/bird.gif +0 -0
- panopticml-0.0.2/tests/resources/chat.png +0 -0
- panopticml-0.0.2/tests/resources/cropped_chat.png +0 -0
- panopticml-0.0.2/tests/resources/dog.jpg +0 -0
- panopticml-0.0.2/tests/resources/spider.jpg +0 -0
- panopticml-0.0.2/tests/siglip_benchmark.py +114 -0
- panopticml-0.0.2/tests/siglip_benchmark_dataloader.py +113 -0
- panopticml-0.0.2/tests/test_transformers.py +155 -0
- panopticml-0.0.2/version.py +1 -0
|
@@ -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,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
|