LLM Guides

How to Generate Image Embeddings Using Python

Jul 01, 2026

Image embeddings are one of those things that sound more complicated than they feel once you build the first version.

The basic idea is simple: you give a model an image, and the model turns that image into a list of numbers.

That list of numbers is the embedding.

It may look like this:

[0.012, -0.443, 0.087, 0.991, …]

Cute? Not really.

Useful? Very.

Because once images become vectors, you can compare them, search them, cluster them, recommend similar items, detect duplicates, build visual search, organize media libraries, or connect images to text prompts.

In this guide, we’ll build image embeddings with Python using CLIP and OpenCLIP, then use those embeddings for similarity search with FAISS. We’ll also talk about storage, batching, evaluation, and where LLMAPI fits when image embeddings become part of a larger AI workflow.

Why we can write this guide

We’ve spent around 6 years working with AI APIs, image workflows, embeddings, search systems, and developer automation. We also researched current image embedding tools, CLIP-style models, vector search libraries, and newer multimodal embedding research for this article.

The practical lesson is pretty simple: generating image embeddings is easy. Building a good image search or recommendation workflow around those embeddings takes more planning.

You need the right model, consistent preprocessing, vector storage, metadata, similarity search, and a way to test whether the results actually make sense for your users.

What are image embeddings used for?

Before we write code, let’s make the use case clear.

Image embeddings help when you need to compare images by meaning, not by filename or pixels.

For example:

Use caseWhat embeddings help you do
Visual searchFind images similar to a query image
Text-to-image searchSearch images using a text prompt
Duplicate detectionFind near-identical or similar images
Product recommendationsRecommend visually similar products
Content moderationGroup risky or similar visual content
Asset managementOrganize large image libraries
E-commerceSearch by style, shape, color, or product type
RAG with imagesRetrieve relevant visuals for a multimodal app
AI workflow routingDecide which model or tool should handle an image

The magic is that embeddings place similar images close together in vector space.

So a photo of a red sneaker should be closer to another red sneaker than to a picture of a soup bowl. Very fair, honestly.

Which Python approach should you use?

There are a few ways to generate image embeddings.

ApproachBest for
CLIPSimple image and text embeddings
OpenCLIPMore model choices and open-source flexibility
Sentence Transformers CLIP modelsFriendly embedding workflow
Hosted embedding APIsApps that do not want to manage models
Custom vision modelDomain-specific image similarity
Vector database + embeddingsProduction search and retrieval

For most developers, the easiest first choice is CLIP or OpenCLIP.

CLIP is important because it learns image and text representations in the same space. OpenAI’s CLIP article explains that CLIP was trained to match images with natural language descriptions, which is why it can support zero-shot classification and image-text similarity. That fits image embeddings perfectly because we often want to compare an image to another image or to a text query. The original CLIP work also showed how natural language supervision can make visual models more flexible across tasks.

Path 1: Generate image embeddings with CLIP

Let’s start with the classic CLIP setup.

Install the package:

pip install torch torchvision pillow
pip install git+https://github.com/openai/CLIP.git

Now create a Python file:

import torch
import clip
from PIL import Image

device = "cuda" if torch.cuda.is_available() else "cpu"

model, preprocess = clip.load("ViT-B/32", device=device)

image = preprocess(Image.open("image.jpg")).unsqueeze(0).to(device)

with torch.no_grad():
    image_embedding = model.encode_image(image)

print(image_embedding.shape)
print(image_embedding[0][:10])

You should see something like:

torch.Size([1, 512])

tensor([ 0.0342, -0.1181, 0.2773, …])

That means the image became a 512-dimensional vector.

The official OpenAI CLIP GitHub repo shows this same basic pattern: load the model, preprocess the image, run model.encode_image(), and use the output as the image representation.

Normalize the embedding before search

For similarity search, it is common to normalize embeddings.

image_embedding = image_embedding / image_embedding.norm(dim=-1, keepdim=True)

Why? Because normalized vectors work cleanly with cosine similarity. When vectors are normalized, inner product search behaves like cosine similarity.

That matters when you want to compare one image to another.

def get_image_embedding(image_path):
    image = preprocess(Image.open(image_path)).unsqueeze(0).to(device)

    with torch.no_grad():
        embedding = model.encode_image(image)

    embedding = embedding / embedding.norm(dim=-1, keepdim=True)

    return embedding.cpu().numpy()[0]

Now you have a reusable function.

Path 2: Generate embeddings with OpenCLIP

OpenCLIP is a strong choice if you want more model options.

The OpenCLIP GitHub repo describes it as an open-source implementation of CLIP with many pretrained models. That makes it useful if you want to compare different model sizes, datasets, and checkpoints.

Install it:

pip install open_clip_torch pillow torch

Generate an embedding:

import torch
import open_clip
from PIL import Image

device = "cuda" if torch.cuda.is_available() else "cpu"

model, _, preprocess = open_clip.create_model_and_transforms(
    "ViT-B-32",
    pretrained="laion2b_s34b_b79k"
)

model = model.to(device)
model.eval()

image = preprocess(Image.open("image.jpg")).unsqueeze(0).to(device)

with torch.no_grad():
    image_embedding = model.encode_image(image)

image_embedding = image_embedding / image_embedding.norm(dim=-1, keepdim=True)

print(image_embedding.shape)

OpenCLIP is especially useful when you want to test stronger or more recent CLIP-style checkpoints.

A newer research direction also explains why CLIP-style embeddings keep evolving. The 2025 paper Contrastive Localized Language-Image Pre-Training argues that image-level CLIP training can be weaker for tasks that need fine-grained region understanding, so it adds region-text contrastive learning. This matters for developers because basic image embeddings may work well for “find similar image,” while region-level or localized tasks may need more specialized models.

Path 3: Generate both image and text embeddings

One of the best parts of CLIP-style models is that image and text live in the same embedding space.

That means you can search images with text.

Example:

red running shoes

And compare that text embedding against image embeddings.

text = clip.tokenize(["red running shoes"]).to(device)

with torch.no_grad():
    text_embedding = model.encode_text(text)

text_embedding = text_embedding / text_embedding.norm(dim=-1, keepdim=True)

print(text_embedding.shape)

Now compare image and text:

similarity = (image_embedding @ text_embedding.T).item()

print(similarity)

Higher score means the image and text are more similar.

This is the whole reason CLIP-style models are so useful for visual search. You can search images by image, or search images by text.

The original CLIP paper and project showed how this image-text alignment supports zero-shot classification: instead of training a new classifier for every category, you compare an image embedding with text label embeddings.

Build a small image search engine

Now let’s make this useful.

Imagine you have a folder of images:

images/

  red-shoe.jpg

  blue-shirt.jpg

  black-bag.jpg

  white-sneaker.jpg

We’ll generate embeddings for all images.

from pathlib import Path
import numpy as np

def build_image_index(image_folder):
    image_folder = Path(image_folder)

    image_paths = list(image_folder.glob("*.jpg")) + list(image_folder.glob("*.png"))

    embeddings = []
    metadata = []

    for image_path in image_paths:
        embedding = get_image_embedding(str(image_path))

        embeddings.append(embedding)
        metadata.append({
            "path": str(image_path),
            "filename": image_path.name
        })

        print(f"Embedded {image_path.name}")

    return np.array(embeddings).astype("float32"), metadata

Use it:

embeddings, metadata = build_image_index("images")

print(embeddings.shape)
print(metadata[:2])

Now you have:

  1. A matrix of vectors.
  2. Metadata that tells you which vector belongs to which image.

Search images with FAISS

For small experiments, you can compare every vector manually.

For real search, use a vector index.

FAISS is a library for efficient similarity search and clustering of dense vectors. Its docs explain that FAISS is written in C++ and has Python wrappers, which makes it a common choice for local vector search in Python. The FAISS getting-started guide also shows that vectors are usually represented as NumPy arrays in Python. (faiss.ai)

Install:

pip install faiss-cpu

Create an index:

import faiss

dimension = embeddings.shape[1]

index = faiss.IndexFlatIP(dimension)
index.add(embeddings)

print(index.ntotal)

Because we normalized the embeddings, IndexFlatIP works well for cosine-style similarity search.

Now search with another image:

query_embedding = get_image_embedding("query.jpg").astype("float32")
query_embedding = np.expand_dims(query_embedding, axis=0)

scores, indices = index.search(query_embedding, k=5)

for score, idx in zip(scores[0], indices[0]):
    print(score, metadata[idx])

This returns the 5 most similar images.

Search images with a text query

Now let’s search images using text.

def get_text_embedding(text):
    tokens = clip.tokenize([text]).to(device)

    with torch.no_grad():
        embedding = model.encode_text(tokens)

    embedding = embedding / embedding.norm(dim=-1, keepdim=True)

    return embedding.cpu().numpy()[0]

Use it:

query_embedding = get_text_embedding("a black leather handbag").astype("float32")
query_embedding = np.expand_dims(query_embedding, axis=0)

scores, indices = index.search(query_embedding, k=5)

for score, idx in zip(scores[0], indices[0]):
    print(score, metadata[idx])

This is the base of text-to-image search.

For example, an e-commerce app can let users type:

minimal white sneakers

And return product images that visually match.

Save embeddings so you do not recompute them every time

Image embedding can be slow. Do not regenerate embeddings every time your app starts.

Save them.

import pickle

def save_embeddings(embeddings, metadata, path="image_embeddings.pkl"):
    with open(path, "wb") as file:
        pickle.dump({
            "embeddings": embeddings,
            "metadata": metadata
        }, file)

def load_embeddings(path="image_embeddings.pkl"):
    with open(path, "rb") as file:
        data = pickle.load(file)

    return data["embeddings"], data["metadata"]

Use it:

save_embeddings(embeddings, metadata)

embeddings, metadata = load_embeddings()

For production, you may store embeddings in:

Storage optionBest for
Pickle or NumPy filesLocal prototypes
SQLite/PostgresSmall apps with metadata
FAISS index filesLocal vector search
Pinecone, Qdrant, Weaviate, MilvusProduction vector search
Object storage + databaseLarge image libraries

If you use a vector database, store metadata too: file ID, image URL, user ID, product ID, category, timestamp, and permissions.

Batch embeddings for better performance

If you have many images, process them in batches.

def get_image_embeddings_batch(image_paths, batch_size=32):
    all_embeddings = []

    for i in range(0, len(image_paths), batch_size):
        batch_paths = image_paths[i:i + batch_size]

        images = [
            preprocess(Image.open(path)).unsqueeze(0)
            for path in batch_paths
        ]

        batch = torch.cat(images).to(device)

        with torch.no_grad():
            embeddings = model.encode_image(batch)

        embeddings = embeddings / embeddings.norm(dim=-1, keepdim=True)
        all_embeddings.append(embeddings.cpu().numpy())

        print(f"Processed batch {i // batch_size + 1}")

    return np.vstack(all_embeddings).astype("float32")

Batching is much faster on GPU because the model processes multiple images at once.

Just be careful with memory. If your GPU runs out of memory, reduce batch_size.

Add metadata filters

Vector search finds visually similar images. Your app may also need filters.

Example:

Find similar shoes, but only from the women’s category and only in stock.

That means your search needs embeddings plus metadata.

Example metadata:

metadata = [
    {
        "path": "images/red-shoe.jpg",
        "category": "shoes",
        "gender": "women",
        "in_stock": True,
        "product_id": "sku_1001"
    }
]

For a small local prototype, filter after search:

def search_with_filter(query_embedding, index, metadata, k=20, category=None):
    query_embedding = np.expand_dims(query_embedding.astype("float32"), axis=0)

    scores, indices = index.search(query_embedding, k=k)

    results = []

    for score, idx in zip(scores[0], indices[0]):
        item = metadata[idx]

        if category and item.get("category") != category:
            continue

        results.append({
            "score": float(score),
            "metadata": item
        })

    return results

For production, use a vector database that supports metadata filters directly.

What makes image embeddings good or bad?

An image embedding is useful if similar images land near each other for your use case.

That last part matters.

A fashion app may care about color, cut, fabric, and style. A medical app may care about tiny visual differences. A real estate app may care about room type, layout, lighting, and condition. A moderation app may care about unsafe content.

The same embedding model may behave differently across those tasks.

Test with examples like:

TestWhy it matters
Same object, different angleChecks viewpoint robustness
Same category, different styleChecks semantic grouping
Different objects, similar colorsChecks whether color dominates
Similar layout, different meaningChecks false matches
Text query searchChecks image-text alignment
Duplicate imagesChecks near-duplicate detection
Domain-specific imagesChecks whether CLIP understands your niche

CLIP-style embeddings are strong, but they have limits. The 2024 paper Contrastive Localized Language-Image Pre-Training explains that image-level CLIP training can be insufficient when downstream tasks need fine-grained region understanding. This fits real apps because searching “red dress” may work well, while searching “small logo on the left sleeve” may need more localized or specialized models.

How do you evaluate image search?

Please do not judge it by vibes only.

Make a small test set.

Example:

QueryExpected result type
“white running shoes”White sneaker/running shoe images
“black leather handbag”Black bag images
“blue office chair”Blue chair images
“invoice screenshot”Invoice-like screenshots
Query image: red sneakerSimilar red sneakers

Track:

MetricWhat it tells you
Top-1 accuracyIs the best result correct?
Top-5 accuracyIs a good result near the top?
Mean reciprocal rankHow high the first good result appears
Precision@kHow many top results are relevant
False matchesWhich bad images look too similar
LatencyHow fast search feels
Cost per imageWhether embedding generation scales

For product search, review results with a human. Similarity can be subjective. A user may care more about style than category, or more about category than color.

Use image embeddings for duplicate detection

Duplicate detection is one of the easiest wins.

Generate embeddings for every image, then search each image against the index.

def find_duplicates(index, embeddings, metadata, threshold=0.98):
    duplicates = []

    scores, indices = index.search(embeddings, k=2)

    for i in range(len(embeddings)):
        # First result is usually the image itself
        neighbor_score = scores[i][1]
        neighbor_idx = indices[i][1]

        if neighbor_score >= threshold:
            duplicates.append({
                "image": metadata[i],
                "duplicate_candidate": metadata[neighbor_idx],
                "score": float(neighbor_score)
            })

    return duplicates

Use it:

duplicates = find_duplicates(index, embeddings, metadata)

for item in duplicates:
    print(item)

A high threshold like 0.98 catches near-identical images. Lower thresholds catch visually similar images, which may include false positives.

Use image embeddings for clustering

If you have a large image folder and want to organize it, clustering helps.

You can cluster embeddings with scikit-learn.

pip install scikit-learn
from sklearn.cluster import KMeans

num_clusters = 5

kmeans = KMeans(n_clusters=num_clusters, random_state=42)
labels = kmeans.fit_predict(embeddings)

for item, label in zip(metadata, labels):
    item["cluster"] = int(label)

print(metadata[:5])

This can help group images by visual similarity.

Useful for:

Use caseHow clustering helps
Media librariesGroup similar assets
Product catalogsFind style clusters
Moderation queuesGroup similar content
Dataset cleanupFind repeated or near-duplicate images
Creative toolsOrganize moodboard images

Clustering is not perfect, but it is a great way to explore a dataset.

Where LLMAPI fits after image embeddings

LLMAPI fits when image embeddings become part of a larger AI workflow.

For example:

  1. Python generates image embeddings.
  2. FAISS or a vector database retrieves similar images.
  3. Your app sends image metadata, captions, or retrieved results to an LLM.
  4. LLMAPI routes the task to the best model.
  5. The model summarizes, labels, compares, classifies, or explains the result.

Useful follow-up tasks:

TaskExample
Image search explanation“Why are these images similar?”
Product taggingGenerate product tags from retrieved matches
Content moderationRoute suspicious clusters for review
RAG over imagesRetrieve visuals and answer user questions
Caption enrichmentGenerate captions for unlabeled images
Recommendation textExplain why a product is recommended
Workflow routingSend easy tasks to cheaper models and hard tasks to stronger ones

Image embeddings are the retrieval layer. LLMAPI can help with the reasoning, text generation, routing, and fallback layer around that retrieval.

Common mistakes when generating image embeddings

MistakeBetter approach
Recomputing embeddings every timeSave embeddings and metadata
Mixing models in one indexUse one embedding model per index
Forgetting normalizationNormalize vectors for cosine similarity
Ignoring preprocessingUse the model’s official preprocess function
Testing only 5 imagesBuild a real test set
No metadataStore image path, ID, category, and permissions
No filtersAdd category/user/security filters
Using image embeddings for tiny detailsTest localized or domain-specific models
No human reviewReview search quality manually
No evaluation metricsTrack top-k accuracy and false matches

The “same model per index” point is important. Embeddings from different models usually do not live in the same vector space. If you switch models, rebuild the index.

Full starter script

Here is a complete starter version using CLIP and FAISS.

from pathlib import Path

import clip
import faiss
import numpy as np
import torch
from PIL import Image


device = "cuda" if torch.cuda.is_available() else "cpu"

model, preprocess = clip.load("ViT-B/32", device=device)
model.eval()


def get_image_embedding(image_path):
    image = preprocess(Image.open(image_path).convert("RGB")).unsqueeze(0).to(device)

    with torch.no_grad():
        embedding = model.encode_image(image)

    embedding = embedding / embedding.norm(dim=-1, keepdim=True)

    return embedding.cpu().numpy()[0].astype("float32")


def get_text_embedding(text):
    tokens = clip.tokenize([text]).to(device)

    with torch.no_grad():
        embedding = model.encode_text(tokens)

    embedding = embedding / embedding.norm(dim=-1, keepdim=True)

    return embedding.cpu().numpy()[0].astype("float32")


def build_index(image_folder):
    image_folder = Path(image_folder)
    image_paths = (
        list(image_folder.glob("*.jpg"))
        + list(image_folder.glob("*.jpeg"))
        + list(image_folder.glob("*.png"))
    )

    embeddings = []
    metadata = []

    for image_path in image_paths:
        embedding = get_image_embedding(str(image_path))

        embeddings.append(embedding)
        metadata.append({
            "path": str(image_path),
            "filename": image_path.name
        })

        print(f"Embedded {image_path.name}")

    embeddings = np.vstack(embeddings).astype("float32")

    index = faiss.IndexFlatIP(embeddings.shape[1])
    index.add(embeddings)

    return index, embeddings, metadata


def search_by_text(query, index, metadata, k=5):
    query_embedding = get_text_embedding(query)
    query_embedding = np.expand_dims(query_embedding, axis=0)

    scores, indices = index.search(query_embedding, k)

    results = []

    for score, idx in zip(scores[0], indices[0]):
        results.append({
            "score": float(score),
            "image": metadata[idx]
        })

    return results


def search_by_image(image_path, index, metadata, k=5):
    query_embedding = get_image_embedding(image_path)
    query_embedding = np.expand_dims(query_embedding, axis=0)

    scores, indices = index.search(query_embedding, k)

    results = []

    for score, idx in zip(scores[0], indices[0]):
        results.append({
            "score": float(score),
            "image": metadata[idx]
        })

    return results


if __name__ == "__main__":
    index, embeddings, metadata = build_index("images")

    print("\nSearch by text:")
    text_results = search_by_text("a red sneaker", index, metadata)

    for result in text_results:
        print(result)

    print("\nSearch by image:")
    image_results = search_by_image("query.jpg", index, metadata)

    for result in image_results:
        print(result)

This gives you:

  1. Image embeddings.
  2. A FAISS similarity index.
  3. Text-to-image search.
  4. Image-to-image search.

That is a solid base for visual search.

Before you ship it

Here is the practical checklist.

  1. Pick one embedding model and stick to it for the index.
  2. Use the official preprocessing function.
  3. Normalize embeddings.
  4. Store image IDs and metadata.
  5. Add metadata filters for users, categories, and permissions.
  6. Save embeddings so you do not recompute them.
  7. Test text search and image search separately.
  8. Review false matches manually.
  9. Track top-k accuracy.
  10. Rebuild the index if you change models.

If you’re just learning, CLIP plus FAISS is enough.

If you’re building a real app, add metadata, filters, vector storage, evaluation, and a review loop.

And if image embeddings are only one step in a larger workflow, connect the retrieval results to LLMAPI so your app can route follow-up tasks like tagging, summarization, moderation, explanation, and recommendation copy across different models.

Deploy in minutes