panoptic 0.0.9.dev9__tar.gz → 0.1.0__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.
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/PKG-INFO +2 -2
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/api.py +70 -28
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/compute/__init__.py +1 -1
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/compute/similarity.py +3 -3
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/compute/transform.py +4 -4
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/compute/utils.py +3 -2
- panoptic-0.1.0/panoptic/core/__init__.py +266 -0
- panoptic-0.1.0/panoptic/core/db.py +376 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/core/db_utils.py +2 -2
- panoptic-0.1.0/panoptic/core/image_importer.py +113 -0
- panoptic-0.1.0/panoptic/core/process_queue.py +145 -0
- panoptic-0.1.0/panoptic/html/assets/index-2b4b1df5.css +6 -0
- panoptic-0.1.0/panoptic/html/assets/index-9aba2054.js +22 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/html/index.html +2 -2
- panoptic-0.1.0/panoptic/main.py +269 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/models/models.py +32 -22
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/models/payloads.py +14 -6
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/scripts/create_db.sql +26 -11
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/scripts/populate_db.sql +5 -5
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/scripts/to_pca.py +9 -8
- panoptic-0.1.0/panoptic/utils.py +24 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic.egg-info/PKG-INFO +2 -2
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic.egg-info/SOURCES.txt +4 -2
- panoptic-0.0.9.dev9/requirements.txt → panoptic-0.1.0/panoptic.egg-info/requires.txt +3 -2
- panoptic-0.0.9.dev9/panoptic.egg-info/requires.txt → panoptic-0.1.0/requirements.txt +2 -1
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/setup.py +2 -2
- panoptic-0.0.9.dev9/panoptic/core/__init__.py +0 -253
- panoptic-0.0.9.dev9/panoptic/core/db.py +0 -301
- panoptic-0.0.9.dev9/panoptic/core/image_importer.py +0 -139
- panoptic-0.0.9.dev9/panoptic/html/assets/index-66fe5260.css +0 -6
- panoptic-0.0.9.dev9/panoptic/html/assets/index-7472937f.js +0 -21
- panoptic-0.0.9.dev9/panoptic/main.py +0 -118
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/build_commans.txt +0 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/description.md +0 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/__init__.py +0 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/compute/ocr.py +0 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/html/assets/bootstrap-icons-966620f9.woff2 +0 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/html/assets/bootstrap-icons-c6569d46.woff +0 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/html/favicon.ico +0 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/models/__init__.py +0 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic/scripts/__init__.py +0 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic.egg-info/dependency_links.txt +0 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic.egg-info/entry_points.txt +0 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/panoptic.egg-info/top_level.txt +0 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/setup.cfg +0 -0
- {panoptic-0.0.9.dev9 → panoptic-0.1.0}/thunder-collection_Panoptic.json +0 -0
|
@@ -1,25 +1,27 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import os
|
|
2
3
|
from sys import platform
|
|
3
4
|
from typing import Optional
|
|
4
5
|
|
|
5
6
|
import aiofiles as aiofiles
|
|
7
|
+
import pandas as pd
|
|
6
8
|
from fastapi import FastAPI, Query, UploadFile
|
|
7
9
|
from fastapi.middleware.cors import CORSMiddleware
|
|
10
|
+
from fastapi.responses import ORJSONResponse
|
|
8
11
|
from pydantic import BaseModel
|
|
9
12
|
from starlette.responses import Response
|
|
10
13
|
from starlette.staticfiles import StaticFiles
|
|
11
|
-
import pandas as pd
|
|
12
14
|
|
|
13
15
|
from panoptic import core
|
|
14
|
-
from panoptic.
|
|
15
|
-
from panoptic.core import create_property, add_property_to_images, get_images, create_tag, \
|
|
16
|
-
delete_image_property, \
|
|
16
|
+
from panoptic.core import create_property, create_tag, \
|
|
17
17
|
update_tag, get_tags, get_properties, delete_property, update_property, delete_tag, delete_tag_parent, add_folder, \
|
|
18
|
-
db_utils, make_clusters, get_similar_images,
|
|
18
|
+
db_utils, make_clusters, get_similar_images, read_properties_file, get_full_images, set_property_values
|
|
19
19
|
from panoptic.core import db
|
|
20
|
-
from panoptic.models import Property,
|
|
21
|
-
|
|
22
|
-
UpdateTagPayload, UpdatePropertyPayload, Tab, MakeClusterPayload
|
|
20
|
+
from panoptic.models import Property, Tag, Tags, Properties, PropertyPayload, \
|
|
21
|
+
SetPropertyValuePayload, AddTagPayload, DeleteImagePropertyPayload, \
|
|
22
|
+
UpdateTagPayload, UpdatePropertyPayload, Tab, MakeClusterPayload, PropertyValue, GetSimilarImagesPayload, \
|
|
23
|
+
ChangeProjectPayload
|
|
24
|
+
from panoptic.scripts.to_pca import compute_all_pca
|
|
23
25
|
|
|
24
26
|
app = FastAPI()
|
|
25
27
|
app.add_middleware(
|
|
@@ -42,7 +44,7 @@ async def startup_event():
|
|
|
42
44
|
# Route pour créer une property et l'insérer dans la table des properties
|
|
43
45
|
@app.post("/property")
|
|
44
46
|
async def create_property_route(payload: PropertyPayload) -> Property:
|
|
45
|
-
return await create_property(payload.name, payload.type)
|
|
47
|
+
return await create_property(payload.name, payload.type, payload.mode)
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
@app.get("/property")
|
|
@@ -54,22 +56,24 @@ async def get_properties_route() -> Properties:
|
|
|
54
56
|
async def update_property_route(payload: UpdatePropertyPayload) -> Property:
|
|
55
57
|
return await update_property(payload)
|
|
56
58
|
|
|
59
|
+
|
|
57
60
|
@app.post('/property/file')
|
|
58
61
|
async def properties_by_file(file: UploadFile):
|
|
59
62
|
data = pd.read_csv(file.file, sep=";")
|
|
60
63
|
return await read_properties_file(data)
|
|
61
64
|
|
|
65
|
+
|
|
62
66
|
@app.delete('/property/{property_id}')
|
|
63
67
|
async def delete_property_route(property_id: str):
|
|
64
68
|
await delete_property(property_id)
|
|
65
|
-
return
|
|
69
|
+
return await get_properties()
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
# Route pour récupérer la liste de toutes les images
|
|
69
|
-
@app.get("/images")
|
|
73
|
+
@app.get("/images", response_class=ORJSONResponse)
|
|
70
74
|
async def get_images_route():
|
|
71
|
-
images = await
|
|
72
|
-
return images
|
|
75
|
+
images = await get_full_images()
|
|
76
|
+
return ORJSONResponse(images)
|
|
73
77
|
|
|
74
78
|
|
|
75
79
|
@app.get('/images/{file_path:path}')
|
|
@@ -90,15 +94,15 @@ async def get_image(file_path: str):
|
|
|
90
94
|
# Route pour ajouter une property à une image dans la table de jointure entre image et property
|
|
91
95
|
# On retourne le payload pour pouvoir valider l'update côté front
|
|
92
96
|
@app.post("/image_property")
|
|
93
|
-
async def add_image_property(payload:
|
|
94
|
-
await
|
|
95
|
-
return
|
|
97
|
+
async def add_image_property(payload: SetPropertyValuePayload):
|
|
98
|
+
updated, value = await set_property_values(property_id=payload.property_id, image_ids=payload.image_ids, sha1s=payload.sha1s, value=payload.value)
|
|
99
|
+
return {'updated_ids': updated, 'value': value}
|
|
96
100
|
|
|
97
101
|
|
|
98
102
|
# Route pour supprimer une property d'une image dans la table de jointure entre image et property
|
|
99
103
|
@app.delete("/image_property")
|
|
100
|
-
async def
|
|
101
|
-
await
|
|
104
|
+
async def delete_property_value_route(payload: DeleteImagePropertyPayload) -> DeleteImagePropertyPayload:
|
|
105
|
+
await db.delete_property_value(payload.property_id, image_ids=[payload.image_id])
|
|
102
106
|
return payload
|
|
103
107
|
|
|
104
108
|
|
|
@@ -109,10 +113,10 @@ async def add_tag(payload: AddTagPayload) -> Tag:
|
|
|
109
113
|
return await create_tag(payload.property_id, payload.value, payload.parent_id, payload.color)
|
|
110
114
|
|
|
111
115
|
|
|
112
|
-
@app.get("/tags")
|
|
113
|
-
async def get_tags_route(property: Optional[str] = None)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
@app.get("/tags", response_class=ORJSONResponse)
|
|
117
|
+
async def get_tags_route(property: Optional[str] = None):
|
|
118
|
+
tags = await get_tags(property)
|
|
119
|
+
return ORJSONResponse(tags)
|
|
116
120
|
|
|
117
121
|
@app.patch("/tags")
|
|
118
122
|
async def update_tag_route(payload: UpdateTagPayload) -> Tag:
|
|
@@ -139,9 +143,18 @@ async def get_folders_route():
|
|
|
139
143
|
@app.get('/import_status')
|
|
140
144
|
async def get_import_status_route():
|
|
141
145
|
image_import = core.importer
|
|
142
|
-
new_image =
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
new_image = image_import.get_new_images()
|
|
147
|
+
|
|
148
|
+
update = []
|
|
149
|
+
if new_image:
|
|
150
|
+
update = await get_full_images(new_image)
|
|
151
|
+
|
|
152
|
+
res = {
|
|
153
|
+
'to_import': image_import.total_import,
|
|
154
|
+
'imported': image_import.current_import,
|
|
155
|
+
'computed': image_import.current_computed,
|
|
156
|
+
'new_images': update
|
|
157
|
+
}
|
|
145
158
|
return res
|
|
146
159
|
|
|
147
160
|
|
|
@@ -181,12 +194,41 @@ async def make_clusters_route(payload: MakeClusterPayload) -> list[list[str]]:
|
|
|
181
194
|
return await make_clusters(payload.nb_groups, payload.image_list)
|
|
182
195
|
|
|
183
196
|
|
|
184
|
-
@app.
|
|
185
|
-
async def get_similar_images_route(
|
|
186
|
-
return await get_similar_images(sha1_list)
|
|
197
|
+
@app.post("/similar")
|
|
198
|
+
async def get_similar_images_route(payload: GetSimilarImagesPayload) -> list:
|
|
199
|
+
return await get_similar_images(payload.sha1_list)
|
|
200
|
+
|
|
187
201
|
|
|
188
202
|
@app.post("/pca")
|
|
189
203
|
async def start_pca_route():
|
|
190
204
|
return await compute_all_pca(force=True)
|
|
191
205
|
|
|
206
|
+
|
|
207
|
+
@app.post("/project")
|
|
208
|
+
async def change_project_route(payload: ChangeProjectPayload):
|
|
209
|
+
os.environ['PANOPTIC_DATA'] = payload.project
|
|
210
|
+
await db_utils.init()
|
|
211
|
+
return f"changed project to {payload.project}"
|
|
212
|
+
|
|
213
|
+
@app.get('/small/images/{file_path:path}')
|
|
214
|
+
async def get_image(file_path: str):
|
|
215
|
+
path = os.path.join(os.environ['PANOPTIC_DATA'], 'mini', file_path)
|
|
216
|
+
async with aiofiles.open(path, 'rb') as f:
|
|
217
|
+
data = await f.read()
|
|
218
|
+
|
|
219
|
+
ext = path.split('.')[-1]
|
|
220
|
+
|
|
221
|
+
# # media_type here sets the media type of the actual response sent to the client.
|
|
222
|
+
return Response(content=data, media_type="image/" + ext)
|
|
223
|
+
|
|
224
|
+
# app.mount("/small/images/", StaticFiles(directory=os.path.join('PANOPTIC_DATA', 'mini')), name="static")
|
|
192
225
|
app.mount("/", StaticFiles(directory=os.path.join(os.path.dirname(__file__), "html"), html=True), name="static")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class EndpointFilter(logging.Filter):
|
|
229
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
230
|
+
return record.getMessage().find("/import_status") == -1
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# Filter out /endpoint
|
|
234
|
+
logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
from .transform import transform_image, transform_directory, to_average_hash, to_sha1, to_vector,
|
|
1
|
+
from .transform import transform_image, transform_directory, to_average_hash, to_sha1, to_vector, can_compute_pca, create_pca, to_pca
|
|
2
2
|
from .similarity import make_clusters, get_similar_images, create_similarity_tree
|
|
@@ -5,12 +5,12 @@ import numpy as np
|
|
|
5
5
|
from sklearn.cluster import DBSCAN, KMeans, estimate_bandwidth, MeanShift
|
|
6
6
|
|
|
7
7
|
from panoptic.compute.utils import load_similarity_tree, SimilarityTreeWithLabel
|
|
8
|
-
from panoptic.models import
|
|
8
|
+
from panoptic.models import ComputedValue
|
|
9
9
|
|
|
10
10
|
SIMILARITY_TREE: SimilarityTreeWithLabel = load_similarity_tree()
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def create_similarity_tree(images: list[
|
|
13
|
+
def create_similarity_tree(images: list[ComputedValue]):
|
|
14
14
|
tree = SimilarityTreeWithLabel(images)
|
|
15
15
|
with open(os.path.join(os.getenv('PANOPTIC_DATA'), 'tree.pkl'), 'wb') as f:
|
|
16
16
|
pickle.dump(tree, f)
|
|
@@ -25,7 +25,7 @@ def get_similar_images(vectors: list[np.ndarray]):
|
|
|
25
25
|
return SIMILARITY_TREE.query(vector)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def make_clusters(images: list[
|
|
28
|
+
def make_clusters(images: list[ComputedValue], *, method='kmeans', **kwargs) -> list[list[str]]:
|
|
29
29
|
res = []
|
|
30
30
|
vectors, sha1, ahashs = zip(*[(i.vector, i.sha1, i.ahash) for i in images])
|
|
31
31
|
sha1 = np.asarray(sha1)
|
|
@@ -13,7 +13,7 @@ import glob
|
|
|
13
13
|
from PIL import Image
|
|
14
14
|
from sklearn.decomposition import PCA
|
|
15
15
|
from sklearn.neighbors import KDTree
|
|
16
|
-
from transformers import AutoImageProcessor, MobileNetV2Model
|
|
16
|
+
from transformers import AutoImageProcessor, MobileNetV2Model
|
|
17
17
|
# from .ocr import full_ocr
|
|
18
18
|
from .utils import load_data
|
|
19
19
|
|
|
@@ -89,7 +89,7 @@ def transform_image(_image_path: str, ocr=False):
|
|
|
89
89
|
|
|
90
90
|
data[sha1] = result
|
|
91
91
|
# can we trigger pca and use it ?
|
|
92
|
-
if pca is None and
|
|
92
|
+
if pca is None and can_compute_pca(len(data.values()), list(data.values())[0]['vector']):
|
|
93
93
|
sha1_list = list(data.keys())
|
|
94
94
|
vector_list = [data[sha1_key]['vector'] for sha1_key in sha1_list]
|
|
95
95
|
create_pca(vector_list)
|
|
@@ -159,7 +159,7 @@ def save_pca():
|
|
|
159
159
|
pickle.dump(pca, f)
|
|
160
160
|
|
|
161
161
|
|
|
162
|
-
def
|
|
162
|
+
def can_compute_pca(nb_vectors: int, vector_sample: np.array) -> bool:
|
|
163
163
|
"""
|
|
164
164
|
Can we start training a PCA to reduce vector size ?
|
|
165
165
|
Basically it's: do we have more vectors than 110% the number of dimensions ?
|
|
@@ -169,7 +169,7 @@ def can_use_pca(nb_vectors: int, vector_sample: np.array) -> bool:
|
|
|
169
169
|
"""
|
|
170
170
|
if not USE_PCA_IF_POSSIBLE:
|
|
171
171
|
return False
|
|
172
|
-
if
|
|
172
|
+
if get_pca() is not None:
|
|
173
173
|
return False
|
|
174
174
|
if nb_vectors > vector_sample.shape[0] + (vector_sample.shape[0] / 10):
|
|
175
175
|
return True
|
|
@@ -4,10 +4,11 @@ import pickle
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
from sklearn.neighbors import KDTree
|
|
6
6
|
|
|
7
|
-
from panoptic.models import
|
|
7
|
+
from panoptic.models import ComputedValue
|
|
8
8
|
|
|
9
9
|
os.environ['PANOPTIC_DATA'] = os.getenv('PANOPTIC_DATA', os.getcwd())
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
def load_data(data_path=os.getenv('PANOPTIC_DATA')):
|
|
12
13
|
if not data_path:
|
|
13
14
|
raise ValueError("PANOPTIC_DATA was not set")
|
|
@@ -42,7 +43,7 @@ def load_similarity_tree():
|
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
class SimilarityTreeWithLabel:
|
|
45
|
-
def __init__(self, images: list[
|
|
46
|
+
def __init__(self, images: list[ComputedValue]):
|
|
46
47
|
vectors, sha1_list = zip(*[(i.vector, i.sha1) for i in images])
|
|
47
48
|
self.image_labels = sha1_list
|
|
48
49
|
self.tree = KDTree(vectors)
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import random
|
|
5
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
6
|
+
import time
|
|
7
|
+
from typing import List, Any
|
|
8
|
+
|
|
9
|
+
import pandas
|
|
10
|
+
from fastapi import HTTPException
|
|
11
|
+
from tqdm import tqdm
|
|
12
|
+
|
|
13
|
+
from panoptic.core import db
|
|
14
|
+
from panoptic import compute
|
|
15
|
+
from panoptic.models import PropertyType, JSON, Tag, Property, Tags, Properties, \
|
|
16
|
+
UpdateTagPayload, UpdatePropertyPayload, Image, PropertyValue
|
|
17
|
+
from .image_importer import ImageImporter
|
|
18
|
+
from .image_importer import ImageImporter
|
|
19
|
+
|
|
20
|
+
executor = ProcessPoolExecutor(max_workers=4)
|
|
21
|
+
atexit.register(executor.shutdown)
|
|
22
|
+
importer = ImageImporter(executor)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def create_property(name: str, property_type: PropertyType, mode='id') -> Property:
|
|
26
|
+
return await db.add_property(name, property_type.value, mode)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def update_property(payload: UpdatePropertyPayload) -> Property:
|
|
30
|
+
existing_property = await db.get_property_by_id(payload.id)
|
|
31
|
+
if not existing_property:
|
|
32
|
+
raise HTTPException(status_code=400, detail="Trying to modify non existent property")
|
|
33
|
+
new_property = existing_property.copy(update=payload.dict(exclude_unset=True))
|
|
34
|
+
await db.update_property(new_property)
|
|
35
|
+
return new_property
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def get_properties() -> Properties:
|
|
39
|
+
properties = await db.get_properties()
|
|
40
|
+
return {prop.id: prop for prop in properties}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def delete_property(property_id: str):
|
|
44
|
+
await db.delete_property(property_id)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def set_property_values(property_id: int, value: Any, image_ids: List[int] = None, sha1s: List[int] = None):
|
|
48
|
+
if image_ids and sha1s:
|
|
49
|
+
raise TypeError('Only image_ids or sha1s should be given as keys. Never both')
|
|
50
|
+
|
|
51
|
+
prop = await db.get_property_by_id(property_id)
|
|
52
|
+
if prop.mode == 'id' and not image_ids:
|
|
53
|
+
raise TypeError(f'Property {property_id}: {prop.name} needs image ids as key [mode: {prop.mode}]')
|
|
54
|
+
if prop.mode == 'sha1' and not sha1s:
|
|
55
|
+
raise TypeError(f'Property {property_id}: {prop.name} needs sha1s as key [mode: {prop.mode}]')
|
|
56
|
+
|
|
57
|
+
if prop.type == PropertyType.tag or prop.type == PropertyType.multi_tags:
|
|
58
|
+
if value and not isinstance(value, list):
|
|
59
|
+
value = [int(value)]
|
|
60
|
+
|
|
61
|
+
return await db.set_property_values(property_id, value, image_ids, sha1s)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def get_full_images(image_ids: List[int] = None) -> List[Image]:
|
|
65
|
+
images = await db.get_images(image_ids)
|
|
66
|
+
sha1s = list({img.sha1 for img in images})
|
|
67
|
+
property_values = await db.get_property_values(image_ids=image_ids)
|
|
68
|
+
ahashs = await db.get_sha1_ahashs(sha1s=sha1s)
|
|
69
|
+
image_index = {img.id: img for img in images}
|
|
70
|
+
|
|
71
|
+
def assign_value(prop):
|
|
72
|
+
image_index[prop.image_id].properties[prop.property_id] = prop
|
|
73
|
+
|
|
74
|
+
[assign_value(prop) for prop in property_values if prop.image_id >= 0]
|
|
75
|
+
|
|
76
|
+
sha1_properties = {}
|
|
77
|
+
|
|
78
|
+
def register_sha1_value(prop: PropertyValue):
|
|
79
|
+
if prop.sha1 not in sha1_properties:
|
|
80
|
+
sha1_properties[prop.sha1] = []
|
|
81
|
+
sha1_properties[prop.sha1].append(prop)
|
|
82
|
+
|
|
83
|
+
[register_sha1_value(prop) for prop in property_values if prop.image_id < 0]
|
|
84
|
+
|
|
85
|
+
def assign_sha1_value(image_id, prop: PropertyValue):
|
|
86
|
+
image_index[image_id].properties[prop.property_id] = prop
|
|
87
|
+
|
|
88
|
+
[assign_sha1_value(img.id, prop) for img in images if img.sha1 in sha1_properties for prop in
|
|
89
|
+
sha1_properties[img.sha1]]
|
|
90
|
+
|
|
91
|
+
[setattr(img, 'ahash', ahashs[img.sha1]) for img in images if img.sha1 in ahashs]
|
|
92
|
+
return images
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def make_clusters(sensibility: float, sha1s: [str]) -> list[list[str]]:
|
|
96
|
+
"""
|
|
97
|
+
Compute clusters and return a list of lists of sha1
|
|
98
|
+
"""
|
|
99
|
+
if not sha1s:
|
|
100
|
+
return []
|
|
101
|
+
values = await db.get_sha1_computed_values(sha1s)
|
|
102
|
+
return compute.make_clusters(values, method="kmeans", nb_clusters=sensibility)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def get_similar_images(sha1s: list[str]):
|
|
106
|
+
vectors = [i.vector for i in await db.get_sha1_computed_values(sha1s)]
|
|
107
|
+
res = compute.get_similar_images(vectors)
|
|
108
|
+
return [img for img in res if img['sha1'] not in sha1s]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def add_property_to_images(property_id: int, sha1_list: list[str], value: JSON) -> str:
|
|
112
|
+
# first check that the property and the image exist:
|
|
113
|
+
images = await db.get_images(sha1s=sha1_list)
|
|
114
|
+
if await db.get_property_by_id(property_id) and images:
|
|
115
|
+
await db.set_property_values(image_ids=[img.id for img in images], property_id=property_id, value=value)
|
|
116
|
+
# check if a value already exists
|
|
117
|
+
# if await db.get_image_property(sha1, property_id):
|
|
118
|
+
# await db.update_image_property(sha1, property_id, value)
|
|
119
|
+
# else:
|
|
120
|
+
# await db.add_image_property(sha1, property_id, value)
|
|
121
|
+
return value
|
|
122
|
+
else:
|
|
123
|
+
raise HTTPException(status_code=400, detail="Trying to set a value on a non existent property or sha1")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def read_properties_file(data: pandas.DataFrame):
|
|
127
|
+
filenames, ids, sha1 = zip(*[(i.name, i.id, i.sha1) for i in await db.get_images()])
|
|
128
|
+
|
|
129
|
+
# if there are no duplicates them lets assume the props are sha1
|
|
130
|
+
if data.duplicated('key').sum() == 0:
|
|
131
|
+
prop_mode = "sha1"
|
|
132
|
+
matcher = dict(zip(filenames, sha1))
|
|
133
|
+
else:
|
|
134
|
+
prop_mode = "id"
|
|
135
|
+
matcher = dict(zip(filenames, ids))
|
|
136
|
+
data = data[data.key.isin(filenames)]
|
|
137
|
+
# data = data.drop_duplicates(subset='key', keep='first')
|
|
138
|
+
# add panoptic id or sha1 to the dataframe
|
|
139
|
+
data.loc[data.index, 'panoptic_id'] = data['key'].map(matcher)
|
|
140
|
+
|
|
141
|
+
# first, create new images where ids are the same
|
|
142
|
+
unique_ids = list(data.panoptic_id.unique())
|
|
143
|
+
clones_created = 0
|
|
144
|
+
# TODO: create clones all at once ? it's tricky with the ids to update
|
|
145
|
+
if prop_mode == "id":
|
|
146
|
+
for id in tqdm(unique_ids):
|
|
147
|
+
sub_data = data[data.panoptic_id == id]
|
|
148
|
+
# create clones only if needed
|
|
149
|
+
if len(list(sub_data.panoptic_id)) == 0:
|
|
150
|
+
continue
|
|
151
|
+
image = await db.get_images([id])
|
|
152
|
+
image = image[0]
|
|
153
|
+
new_ids = await db.create_clones(image, sub_data.shape[0] - 1)
|
|
154
|
+
clones_created += len(new_ids)
|
|
155
|
+
data.loc[data.panoptic_id == id, "panoptic_id"] = [image.id, *new_ids]
|
|
156
|
+
logging.getLogger('panoptic').info(f"created {clones_created} new images")
|
|
157
|
+
properties_to_create = data.columns.tolist()
|
|
158
|
+
|
|
159
|
+
# then for each property to create
|
|
160
|
+
for prop in properties_to_create:
|
|
161
|
+
if prop == "key" or prop == "panoptic_id":
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
# create the property
|
|
165
|
+
prop_name, prop_type = prop.split('[')
|
|
166
|
+
prop_type = PropertyType(prop_type.split(']')[0])
|
|
167
|
+
property = await create_property(prop_name, prop_type, prop_mode)
|
|
168
|
+
# then get all possible values to insert
|
|
169
|
+
prop_values = list(data[prop].unique())
|
|
170
|
+
|
|
171
|
+
# if it's a tag property let's create the tags in the db
|
|
172
|
+
if property.type == PropertyType.tag or property.type == PropertyType.multi_tags:
|
|
173
|
+
tag_matcher = {}
|
|
174
|
+
# TODO: can we optimize to create all tags at once ?
|
|
175
|
+
for value in tqdm(prop_values):
|
|
176
|
+
created_tags = []
|
|
177
|
+
# if it's multi tag, assume tags are separated by a comma and create them separately
|
|
178
|
+
for single_tag in value.split(','):
|
|
179
|
+
colors = ["7c1314", "c31d20", "f94144", "f3722c", "f8961e", "f9c74f", "90be6d", "43aa8b", "577590",
|
|
180
|
+
"9daebe"]
|
|
181
|
+
color = '#' + colors[random.randint(0, len(colors) - 1)]
|
|
182
|
+
tag = await create_tag(property.id, single_tag, 0, color)
|
|
183
|
+
created_tags.append(tag.id)
|
|
184
|
+
tag_matcher[value] = created_tags
|
|
185
|
+
# now change all values in the dataframe with the real value that we are going to insert
|
|
186
|
+
data.loc[data.index, prop] = data[prop].map(tag_matcher)
|
|
187
|
+
await db.set_multiple_property_values(property.id, list(data[prop]), list(data.panoptic_id))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def add_folder(folder):
|
|
191
|
+
found = await importer.import_folder(folder)
|
|
192
|
+
print(f'found {found} images')
|
|
193
|
+
return folder
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
new_images = []
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_new_images():
|
|
200
|
+
copy = [i for i in new_images]
|
|
201
|
+
new_images.clear()
|
|
202
|
+
return copy
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
async def create_tag(property_id, value, parent_id, color: str) -> Tag:
|
|
206
|
+
existing_tag = await db.get_tag(property_id, value)
|
|
207
|
+
if existing_tag is not None:
|
|
208
|
+
if await db.tag_in_ancestors(existing_tag.id, parent_id):
|
|
209
|
+
raise HTTPException(status_code=400, detail="Adding a tag that is an ancestor of himself")
|
|
210
|
+
existing_tag.parents = list({*existing_tag.parents, parent_id})
|
|
211
|
+
await db.update_tag(existing_tag)
|
|
212
|
+
tag_id = existing_tag.id
|
|
213
|
+
else:
|
|
214
|
+
parents = [parent_id]
|
|
215
|
+
tag_id = await db.add_tag(property_id, value, json.dumps(parents), color)
|
|
216
|
+
return await db.get_tag_by_id(tag_id)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def update_tag(payload: UpdateTagPayload) -> Tag:
|
|
220
|
+
existing_tag = await db.get_tag_by_id(payload.id)
|
|
221
|
+
if not existing_tag:
|
|
222
|
+
raise HTTPException(status_code=400, detail="Trying to modify non existent tag")
|
|
223
|
+
if await db.tag_in_ancestors(existing_tag.id, payload.parent_id):
|
|
224
|
+
raise HTTPException(status_code=400, detail="Adding a tag that is an ancestor of himself")
|
|
225
|
+
# change only fields of the tags that are set in the payload
|
|
226
|
+
new_tag = existing_tag.copy(update=payload.dict(exclude_unset=True))
|
|
227
|
+
await db.update_tag(new_tag)
|
|
228
|
+
return new_tag
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
async def delete_tag(tag_id: int) -> List[int]:
|
|
232
|
+
# first delete the tag
|
|
233
|
+
modified_tags = [await db.delete_tag_by_id(tag_id)]
|
|
234
|
+
# when deleting a tag, get all children of this tag
|
|
235
|
+
children = await db.get_tags_by_parent_id(tag_id)
|
|
236
|
+
# and remove parent ref of tag in children
|
|
237
|
+
[modified_tags.extend(await delete_tag_parent(c.id, tag_id)) for c in children]
|
|
238
|
+
# then get all images properties tagged with it
|
|
239
|
+
property_values = await db.get_property_values_with_tag(tag_id)
|
|
240
|
+
# and delete the tag ref from them
|
|
241
|
+
for data in property_values:
|
|
242
|
+
if tag_id in data.value:
|
|
243
|
+
data.value.remove(tag_id)
|
|
244
|
+
await db.set_property_values(data.property_id, data.value, [data.image_id])
|
|
245
|
+
return modified_tags
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
async def delete_tag_parent(tag_id: int, parent_id: int) -> List[int]:
|
|
249
|
+
print(tag_id, parent_id)
|
|
250
|
+
tag = await db.get_tag_by_id(tag_id)
|
|
251
|
+
tag.parents.remove(parent_id)
|
|
252
|
+
if not tag.parents:
|
|
253
|
+
return await delete_tag(tag.id)
|
|
254
|
+
else:
|
|
255
|
+
await db.update_tag(tag)
|
|
256
|
+
return []
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
async def get_tags(prop: str = None) -> Tags:
|
|
260
|
+
res = {}
|
|
261
|
+
tag_list = await db.get_tags(prop)
|
|
262
|
+
for tag in tag_list:
|
|
263
|
+
if tag.property_id not in res:
|
|
264
|
+
res[tag.property_id] = {}
|
|
265
|
+
res[tag.property_id][tag.id] = tag
|
|
266
|
+
return res
|