Source code for pyvisim.eval
"""
This module contains functions to evaluate the performance of a retrieval system.
"""
from collections import Counter
from collections.abc import Iterable
import numpy as np
from .distance import cosine_similarity
from .typing import EmbeddingStore, MatLike
__all__ = ["top_k_map", "top_k_accuracy"]
[docs]
def top_k_map(
images: Iterable[MatLike],
image_labels: Iterable[int],
store: EmbeddingStore,
path_labels_dict: dict[str, int],
k: int | None = None,
) -> float:
"""
Computes mean Average Precision over the queries,
based on whether retrieved images have matching labels.
:param images: Query images.
:param image_labels: Corresponding labels for the query images.
:param store: An :class:`~pyvisim.retrieval.image_store.InMemoryImageEmbeddingStore`
(or any :class:`~pyvisim.typing.EmbeddingStore`) holding the gallery
embeddings and the embedder.
:param path_labels_dict: dict {img_path: label}, covering every path of
the store.
:param k: Number of top results to consider. Each average precision is
divided by the number of gallery images sharing the query label,
capped at ``k``.
:return: mAP
"""
all_vectors = np.asarray(store.embeddings)
all_paths = store.paths
embedder = store.embedder
gallery_label_counts = Counter(path_labels_dict[path] for path in all_paths)
APs = []
for query_img, true_label in zip(images, image_labels, strict=True):
query_vec = embedder.embed(query_img)
if query_vec.ndim == 1:
query_vec = query_vec.reshape(1, -1)
sims = cosine_similarity(query_vec, all_vectors)[0]
# Sort by descending similarity
sorted_idx = np.argsort(-sims)
if k is not None:
sorted_idx = sorted_idx[:k]
sorted_paths = [all_paths[i] for i in sorted_idx]
# compute average precision by counting relevant images at each rank
relevant_count = 0
precision_sum = 0.0
for rank, path in enumerate(sorted_paths, start=1):
if path_labels_dict[path] == true_label:
relevant_count += 1
precision_sum += relevant_count / rank
# With R relevant images in the gallery, the ranking can hold at most
# min(R, k) of them, so that is what the precision sum is divided by.
n_relevant = gallery_label_counts[true_label]
if k is not None:
n_relevant = min(n_relevant, k)
AP = precision_sum / n_relevant if n_relevant > 0 else 0.0
APs.append(AP)
return float(np.mean(APs))
[docs]
def top_k_accuracy(
images: Iterable[MatLike],
image_labels: Iterable[int],
store: EmbeddingStore,
path_labels_dict: dict[str, int],
k: int,
) -> float:
"""
Computes top-k accuracy. For each query, we look at the top-k
most similar results in the dataset. If any of them match the
query's label, that query is considered correct.
:param images: Query images.
:param image_labels: List of true labels for each query image.
:param store: An :class:`~pyvisim.retrieval.image_store.InMemoryImageEmbeddingStore`
(or any :class:`~pyvisim.typing.EmbeddingStore`) holding the gallery
embeddings and the embedder.
:param path_labels_dict: dict {path: label}.
:param k: Number of top results to check for a correct match.
:return: Top-k accuracy (float) in the range [0, 1].
"""
all_paths = store.paths
all_vectors = np.asarray(store.embeddings)
embedder = store.embedder
correct_count = 0
num_images = 0
for query_img, true_label in zip(images, image_labels, strict=True):
num_images += 1
q_vec = embedder.embed(query_img)
if q_vec.ndim == 1:
q_vec = q_vec.reshape(1, -1)
sims = cosine_similarity(q_vec, all_vectors)[0]
sorted_idx = np.argsort(-sims)[:k] # top-k
# Check if any of the top-k share the query's label
found_match = False
for idx in sorted_idx:
if path_labels_dict[all_paths[idx]] == true_label:
found_match = True
break
if found_match:
correct_count += 1
if num_images == 0:
return 0.0
return float(correct_count / num_images)