dgc-image-embedding 1.0.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.
- dgc_image_embedding-1.0.0/PKG-INFO +17 -0
- dgc_image_embedding-1.0.0/dgc_image_embedding/__init__.py +13 -0
- dgc_image_embedding-1.0.0/dgc_image_embedding/config.py +13 -0
- dgc_image_embedding-1.0.0/dgc_image_embedding/embedding.py +89 -0
- dgc_image_embedding-1.0.0/dgc_image_embedding/image.py +73 -0
- dgc_image_embedding-1.0.0/dgc_image_embedding.egg-info/PKG-INFO +17 -0
- dgc_image_embedding-1.0.0/dgc_image_embedding.egg-info/SOURCES.txt +10 -0
- dgc_image_embedding-1.0.0/dgc_image_embedding.egg-info/dependency_links.txt +1 -0
- dgc_image_embedding-1.0.0/dgc_image_embedding.egg-info/requires.txt +12 -0
- dgc_image_embedding-1.0.0/dgc_image_embedding.egg-info/top_level.txt +1 -0
- dgc_image_embedding-1.0.0/pyproject.toml +37 -0
- dgc_image_embedding-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dgc_image_embedding
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Author-email: velopace <velopace7@gmail.com>
|
|
5
|
+
License: MIT
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pillow==12.2.0
|
|
8
|
+
Requires-Dist: torch==2.12.0
|
|
9
|
+
Requires-Dist: torchvision==0.27.0
|
|
10
|
+
Requires-Dist: open-clip-torch==3.3.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: build>=1.3.0; extra == "dev"
|
|
13
|
+
Requires-Dist: coverage>=7.10.2; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest>=8.4.1; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-cov>=6.2.1; extra == "dev"
|
|
16
|
+
Requires-Dist: twine>=6.1.0; extra == "dev"
|
|
17
|
+
Requires-Dist: toml>=0.10.2; extra == "dev"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .embedding import embedding_metadata, generate_embedding, generate_embeddings_batch, get_device, get_model
|
|
2
|
+
from .image import load_image_from_bytes, prepare_image_from_bytes, resize_and_pad
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"embedding_metadata",
|
|
6
|
+
"generate_embedding",
|
|
7
|
+
"generate_embeddings_batch",
|
|
8
|
+
"get_device",
|
|
9
|
+
"get_model",
|
|
10
|
+
"load_image_from_bytes",
|
|
11
|
+
"prepare_image_from_bytes",
|
|
12
|
+
"resize_and_pad",
|
|
13
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
TARGET_IMAGE_SIZE = 224
|
|
2
|
+
|
|
3
|
+
MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024
|
|
4
|
+
MIN_SOURCE_SIDE = 100
|
|
5
|
+
MAX_SOURCE_SIDE = 3000
|
|
6
|
+
|
|
7
|
+
PAD_COLOR = (255, 255, 255)
|
|
8
|
+
|
|
9
|
+
MODEL_NAME = "ViT-B-32"
|
|
10
|
+
PRETRAINED = "laion2b_s34b_b79k"
|
|
11
|
+
|
|
12
|
+
CLIP_MEAN = (0.48145466, 0.4578275, 0.40821073)
|
|
13
|
+
CLIP_STD = (0.26862954, 0.26130258, 0.27577711)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
import open_clip
|
|
4
|
+
import torch
|
|
5
|
+
from PIL import Image
|
|
6
|
+
from torchvision import transforms
|
|
7
|
+
|
|
8
|
+
from .config import (
|
|
9
|
+
CLIP_MEAN,
|
|
10
|
+
CLIP_STD,
|
|
11
|
+
MODEL_NAME,
|
|
12
|
+
PRETRAINED,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_model = None
|
|
17
|
+
_device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
18
|
+
_model_lock = threading.Lock()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_device() -> str:
|
|
22
|
+
return _device
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_model():
|
|
26
|
+
global _model
|
|
27
|
+
|
|
28
|
+
if _model is not None:
|
|
29
|
+
return _model
|
|
30
|
+
|
|
31
|
+
with _model_lock:
|
|
32
|
+
if _model is None:
|
|
33
|
+
model, _, _ = open_clip.create_model_and_transforms(
|
|
34
|
+
MODEL_NAME,
|
|
35
|
+
pretrained=PRETRAINED,
|
|
36
|
+
device=_device,
|
|
37
|
+
)
|
|
38
|
+
model.eval()
|
|
39
|
+
_model = model
|
|
40
|
+
|
|
41
|
+
return _model
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def image_to_tensor(image: Image.Image) -> torch.Tensor:
|
|
45
|
+
transform = transforms.Compose(
|
|
46
|
+
[
|
|
47
|
+
transforms.ToTensor(),
|
|
48
|
+
transforms.Normalize(mean=CLIP_MEAN, std=CLIP_STD),
|
|
49
|
+
]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return transform(image).unsqueeze(0)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def generate_embedding(processed_image: Image.Image) -> list[float]:
|
|
56
|
+
model = get_model()
|
|
57
|
+
tensor = image_to_tensor(processed_image).to(_device)
|
|
58
|
+
|
|
59
|
+
with torch.no_grad():
|
|
60
|
+
embedding = model.encode_image(tensor)
|
|
61
|
+
embedding = embedding / embedding.norm(dim=-1, keepdim=True)
|
|
62
|
+
|
|
63
|
+
return embedding.squeeze(0).cpu().tolist()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def generate_embeddings_batch(processed_images: list[Image.Image]) -> list[list[float]]:
|
|
67
|
+
if not processed_images:
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
model = get_model()
|
|
71
|
+
|
|
72
|
+
tensors = torch.cat(
|
|
73
|
+
[image_to_tensor(image) for image in processed_images],
|
|
74
|
+
dim=0,
|
|
75
|
+
).to(_device)
|
|
76
|
+
|
|
77
|
+
with torch.no_grad():
|
|
78
|
+
embeddings = model.encode_image(tensors)
|
|
79
|
+
embeddings = embeddings / embeddings.norm(dim=-1, keepdim=True)
|
|
80
|
+
|
|
81
|
+
return embeddings.cpu().tolist()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def embedding_metadata() -> dict:
|
|
85
|
+
return {
|
|
86
|
+
"modelName": MODEL_NAME,
|
|
87
|
+
"pretrained": PRETRAINED,
|
|
88
|
+
"device": _device,
|
|
89
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
|
|
3
|
+
from PIL import Image, ImageOps, UnidentifiedImageError
|
|
4
|
+
|
|
5
|
+
from .config import (
|
|
6
|
+
MAX_FILE_SIZE_BYTES,
|
|
7
|
+
MAX_SOURCE_SIDE,
|
|
8
|
+
MIN_SOURCE_SIDE,
|
|
9
|
+
PAD_COLOR,
|
|
10
|
+
TARGET_IMAGE_SIZE,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_image_from_bytes(image_bytes: bytes) -> Image.Image:
|
|
15
|
+
if not image_bytes:
|
|
16
|
+
raise ValueError("Image file is empty")
|
|
17
|
+
|
|
18
|
+
if len(image_bytes) > MAX_FILE_SIZE_BYTES:
|
|
19
|
+
raise ValueError("Image file is too large")
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
image = Image.open(BytesIO(image_bytes))
|
|
23
|
+
image = ImageOps.exif_transpose(image)
|
|
24
|
+
except UnidentifiedImageError as err:
|
|
25
|
+
raise ValueError("Invalid image file") from err
|
|
26
|
+
|
|
27
|
+
validate_image_size(image)
|
|
28
|
+
|
|
29
|
+
return image
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def validate_image_size(image: Image.Image) -> None:
|
|
33
|
+
width, height = image.size
|
|
34
|
+
|
|
35
|
+
if width < MIN_SOURCE_SIDE or height < MIN_SOURCE_SIDE:
|
|
36
|
+
raise ValueError(f"Image is too small: {width}x{height}")
|
|
37
|
+
|
|
38
|
+
if width > MAX_SOURCE_SIDE or height > MAX_SOURCE_SIDE:
|
|
39
|
+
raise ValueError(f"Image is too large: {width}x{height}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resize_and_pad(
|
|
43
|
+
image: Image.Image,
|
|
44
|
+
target_size: int = TARGET_IMAGE_SIZE,
|
|
45
|
+
pad_color: tuple[int, int, int] = PAD_COLOR,
|
|
46
|
+
) -> Image.Image:
|
|
47
|
+
image = ImageOps.exif_transpose(image)
|
|
48
|
+
image = image.convert("RGB")
|
|
49
|
+
|
|
50
|
+
width, height = image.size
|
|
51
|
+
scale = target_size / max(width, height)
|
|
52
|
+
|
|
53
|
+
new_width = max(1, round(width * scale))
|
|
54
|
+
new_height = max(1, round(height * scale))
|
|
55
|
+
|
|
56
|
+
resized = image.resize(
|
|
57
|
+
(new_width, new_height),
|
|
58
|
+
Image.Resampling.LANCZOS,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
padded = Image.new("RGB", (target_size, target_size), pad_color)
|
|
62
|
+
|
|
63
|
+
paste_x = (target_size - new_width) // 2
|
|
64
|
+
paste_y = (target_size - new_height) // 2
|
|
65
|
+
|
|
66
|
+
padded.paste(resized, (paste_x, paste_y))
|
|
67
|
+
|
|
68
|
+
return padded
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def prepare_image_from_bytes(image_bytes: bytes) -> Image.Image:
|
|
72
|
+
image = load_image_from_bytes(image_bytes)
|
|
73
|
+
return resize_and_pad(image)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dgc_image_embedding
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Author-email: velopace <velopace7@gmail.com>
|
|
5
|
+
License: MIT
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pillow==12.2.0
|
|
8
|
+
Requires-Dist: torch==2.12.0
|
|
9
|
+
Requires-Dist: torchvision==0.27.0
|
|
10
|
+
Requires-Dist: open-clip-torch==3.3.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: build>=1.3.0; extra == "dev"
|
|
13
|
+
Requires-Dist: coverage>=7.10.2; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest>=8.4.1; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-cov>=6.2.1; extra == "dev"
|
|
16
|
+
Requires-Dist: twine>=6.1.0; extra == "dev"
|
|
17
|
+
Requires-Dist: toml>=0.10.2; extra == "dev"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
dgc_image_embedding/__init__.py
|
|
3
|
+
dgc_image_embedding/config.py
|
|
4
|
+
dgc_image_embedding/embedding.py
|
|
5
|
+
dgc_image_embedding/image.py
|
|
6
|
+
dgc_image_embedding.egg-info/PKG-INFO
|
|
7
|
+
dgc_image_embedding.egg-info/SOURCES.txt
|
|
8
|
+
dgc_image_embedding.egg-info/dependency_links.txt
|
|
9
|
+
dgc_image_embedding.egg-info/requires.txt
|
|
10
|
+
dgc_image_embedding.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dgc_image_embedding
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [ "setuptools", "wheel",]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dgc_image_embedding"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = ""
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
dependencies = [ "pillow==12.2.0", "torch==2.12.0", "torchvision==0.27.0", "open-clip-torch==3.3.0",]
|
|
11
|
+
[[project.authors]]
|
|
12
|
+
name = "velopace"
|
|
13
|
+
email = "velopace7@gmail.com"
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = [ "build>=1.3.0", "coverage>=7.10.2", "pytest>=8.4.1", "pytest-cov>=6.2.1", "twine>=6.1.0", "toml>=0.10.2",]
|
|
17
|
+
|
|
18
|
+
[project.license]
|
|
19
|
+
text = "MIT"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.package-data]
|
|
22
|
+
dgc_image_embedding = [ "py.typed",]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
testpaths = [ "tests",]
|
|
26
|
+
|
|
27
|
+
[tool.coverage.run]
|
|
28
|
+
branch = true
|
|
29
|
+
source = [ "dgc_image_embedding",]
|
|
30
|
+
|
|
31
|
+
[tool.coverage.report]
|
|
32
|
+
show_missing = true
|
|
33
|
+
skip_covered = true
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = [ ".",]
|
|
37
|
+
include = [ "dgc_image_embedding*",]
|