Skip to content

Releases

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 28 Sep 11:56

Release Notes

v1.0.0

Highlights

🚀 The first official release of pyvisim is out!

Upgrade Notes

  • Renamed set_pca_from_sklearn of VLADEmbedder and FisherVectorEmbedder to load_pca_model_from_sklearn. The arguments stay the same.

Enhancement Notes

  • Truncated an unnecessary markdown cell in notebook vlad_and_fisher_with_resnet18_deep_features.ipynb.

v0.10.2

v0.10.2 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 27 Sep 15:36

Release Notes

v0.10.2

Upgrade Notes

  • ExternalSearchIndex is removed, and with it the support for indexes of libraries other than FAISS. Pass the FAISS index itself as search_index to InMemoryImageEmbeddingStore and to its load_from_disk.
  • A store on a FAISS index is named after the index type, such as faiss.IndexHNSWFlat, and takes its space from the index metric. index_params and FAISS metrics other than METRIC_L2 and METRIC_INNER_PRODUCT raise ValueError.
  • A store on a FAISS inner-product index now scores by 1 - inner_product like the built-in "ip" space, so lower is more similar. 1 - candidate.score gives the inner product back.

New Features

  • KReciprocalReranker accepts a store on a FAISS index.

  • Added to_single_image helper in pyvisim.utils.image_utils, which converts one image into the canonical uint8 array described by dims and value_range. An example is shown below.

    import numpy as np
    
    from pyvisim.utils.image_utils import to_single_image
    
    rng = np.random.default_rng(0)
    
    # A uint8 image in the NumPy layout (height, width, channels), values in [0, 255]
    image_hwc = rng.integers(0, 256, size=(64, 80, 3), dtype=np.uint8)
    
    # The same pixels as a float image in the PyTorch layout
    # (channels, height, width), values in [0, 1]
    image_chw = image_hwc.transpose(2, 0, 1) / 255.0
    
    # dims and value_range describe each input, and both come back as a
    # uint8 array of shape (64, 80, 3) with values in [0, 255]
    canonical_hwc = to_single_image(image_hwc, dims="HWC")
    canonical_chw = to_single_image(image_chw, dims="CHW", value_range=(0.0, 1.0))
    
    print(canonical_chw.shape, canonical_chw.dtype)  # (64, 80, 3) uint8
    print(np.array_equal(canonical_hwc, canonical_chw))  # True
  • Added set_pca_from_sklearn to VLADEmbedder and FisherVectorEmbedder. It replaces the embedder's PCA with one created from a sklearn.decomposition.PCA estimator, fitted or not.

Enhancement Notes

  • The new tutorial Custom Feature Extractor with RootSIFT under the classical methods implements RootSIFT on top of the SIFT of OpenCV, which is the recommended way to use RootSIFT since it is much faster than the built-in RootSIFT.
  • The new tutorial Introduction to VLAD and Fisher Vectors under the classical methods trains both embedders on RootSIFT features, once with learn and once from fitted scikit-learn estimators, which have more training options.

Bug Fixes

  • A store saved on an ExternalSearchIndex named "hnsw" was loaded onto a newly built HNSW graph without a warning. Index names now come from the index type, so they cannot collide with a built-in index.
  • A store saved on a FAISS index and loaded without it falls back to a brute-force scan in the metric of that index, and no longer in "cosine".
  • VLADEmbedder and FisherVectorEmbedder now raise RuntimeError for a fitted PCA whose dimensions do not match the feature extractor or the clustering model, like their other dimension checks.
  • learn(dim_reduction_factor=...) now emits a FutureWarning when it replaces a PCA the embedder already has.

v0.10.1

v0.10.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 25 Sep 22:34

Release Notes

v0.10.1

Upgrade Notes

  • ExternalSearchIndex.from_faiss_index no longer takes vectors, so passing them raises TypeError. For indexes that need the exact vectors for reconstruction, pass them to the constructor instead: ExternalSearchIndex(index, vectors).
  • ExternalSearchIndex.from_faiss_index now raises ValueError for an IVF index that has no direct map. Call faiss.extract_index_ivf(index).make_direct_map() first, or pass the vectors to ExternalSearchIndex(index, vectors).
  • Loading an InMemoryImageEmbeddingStore file saved by an earlier version raises ValueError. Build the store from its images again and save it.
  • InMemoryImageEmbeddingStore.save_to_disk ignores embeddings on a store on an HNSW graph and emits a FutureWarning, since the file holds the graph with the embeddings in it.

Performance

  • Building or updating a BruteForceIndex no longer copies the gallery first, which halves the extra peak memory and saves 14 to 17 % of the time. For 500 000 vectors of dimension 512 the peak went from 1985 MB to 1008 MB, and the build from 726 ms to 605 ms.

    Specs: 16 threads, 30 GB RAM, Python 3.14.3, NumPy 2.5.3.

  • ExternalSearchIndex.from_faiss_index no longer keeps a copy of the gallery and reads the vectors from the FAISS index instead. For 500 000 vectors of dimension 512 the adapter's peak memory went from 1952 MB to 0 MB and its steady memory from 977 MB to 0 MB.

    Specs: 16 threads, 30 GB RAM, Python 3.14.3, NumPy 2.5.3, faiss-cpu 1.15.0.

  • Building or updating an HnswIndex no longer copies the gallery first, which saves one gallery of peak memory. For 500 000 vectors of dimension 512 the extra peak went from 2097 MB to 1120 MB.

    Specs: 16 threads, 30 GB RAM, Python 3.14.3, NumPy 2.5.3.

  • A store on an HNSW graph saves the graph and loads it back without building it again. Loading a store of 100 000 embeddings of dimension 512 went from 22.7 s to 0.2 s, and saving it from 2.4 s to 0.2 s.

    Specs: 16 threads, 30 GB RAM, Python 3.14.3, NumPy 2.4.6, safetensors 0.8.0.

Bug Fixes

  • A loaded HNSW store returns the same neighbors as the store that was saved. Before, the graph was built again on load, which with several threads could return different neighbors.

v0.10.0

v0.10.0 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 25 Sep 11:42

Release Notes

v0.10.0

Upgrade Notes

  • The British spelling neighbour is replaced by neighbor throughout the library. One public name changes with it: InMemoryImageEmbeddingStore.retrieve_top_k_similar now takes expansion_neighbors where it took expansion_neighbours.

  • The British spelling serialise is replaced by serialize throughout the library. Only documentation, docstrings and comments are affected.

  • DeepConvFeature moved to pyvisim.neural_networks.features, so from pyvisim.features import DeepConvFeature now raises ImportError. If your code imports the class, change the import:

    # before
    from pyvisim.features import DeepConvFeature
    
    # after
    from pyvisim.neural_networks.features import DeepConvFeature
  • pyvisim.structural and pyvisim.pixelwise moved under the new pyvisim.dense package. Imports of SSIM, MSSSIM and PSNR from the old paths fail with ModuleNotFoundError, so change them to pyvisim.dense.structural and pyvisim.dense.pixelwise:

    from pyvisim.dense.pixelwise import PSNR
    from pyvisim.dense.structural import MSSSIM, SSIM
  • pyvisim.features.feature_extractor_from_dict was removed. Feature extractors are now rebuilt through FeatureExtractorBase.from_dict, which takes the same mapping and returns the same extractor. You are affected if you import the function by name:

    # Before
    from pyvisim.features import feature_extractor_from_dict
    
    extractor = feature_extractor_from_dict(data)
    
    # After
    from pyvisim.base import FeatureExtractorBase
    
    extractor = FeatureExtractorBase.from_dict(data)

    Saved .embedder files are unaffected, since the serialized format did not change.

  • The first parameter of FeatureExtractorBase.from_dict was renamed from data to state. You are affected if you pass it by keyword, and from_dict(data=...) becomes from_dict(state=...).

  • The mapping returned by a feature extractor's to_dict now also holds a "format_version" key.

  • The _serialization_config hook of FeatureExtractorBase was removed. A custom extractor that overrides it now overrides _state instead and returns {"config": ...}, otherwise its arguments are no longer saved.

  • InMemoryImageEmbeddingStore no longer embeds its gallery in the constructor, which affects every store built through it. Call build_store() right after the constructor, or pass lazy_build=False to have the constructor build the store.

  • Until build_store() has run, search, retrieve_top_k_similar, embeddings, embeddings_of, index, dim, to_dict and save_to_disk raise RuntimeError. A store that adopts an ExternalSearchIndex or is rebuilt by load_from_disk or from_dict comes back built.

  • build_store() now raises for an unreadable image file and for a gallery none of whose images could be embedded, which the constructor used to report. An unknown search_index, a rejected index_params entry, a non-string path and an empty gallery still raise in the constructor.

  • pyvisim.neural_networks no longer re-exports DeepConvFeature, so from pyvisim.neural_networks import DeepConvFeature now raises ImportError. Import the class from pyvisim.neural_networks.features instead:

    # before
    from pyvisim.neural_networks import DeepConvFeature
    
    # after
    from pyvisim.neural_networks.features import DeepConvFeature
  • Importing pyvisim no longer configures logging, and its loggers are named after their modules (e.g. pyvisim.datasets.datasets) instead of Data_Set, Feature_Extractor, Pipeline and Similarity_Metrics. Configure the pyvisim logger to see the library's messages.

  • from pyvisim import * no longer imports datasets, which needs the nn extra. Access it as pyvisim.datasets or import it explicitly.

  • VLADEmbedder and FisherVectorEmbedder default to raise_error_when_pca_incompatible=True, so a clustering model the fitted PCA cannot feed raises a RuntimeError instead of silently dropping the PCA. Pass False to keep the old behavior.

  • pyvisim._base_classes is removed. Import SimilarityMetric, FeatureExtractorBase, ImageEmbedderBase and SerializableImageEmbedder from pyvisim.base instead.

  • VLADEmbedder and FisherVectorEmbedder no longer take raise_error_when_pca_incompatible. A clustering model the fitted PCA cannot feed always raises a RuntimeError, so drop the argument.

  • VLADEmbedder and FisherVectorEmbedder no longer take flatten. embed always returns one flat row per image, so drop the argument.

  • Embedders saved by VLADEmbedder and FisherVectorEmbedder are written under format version 4. Files of version 3 still load, files of version 4 do not load with an older pyvisim.

  • OxfordFlowerDataset no longer accepts a transform argument. Passing one raised NotImplementedError before, so only calls that spelled out transform=None or passed purpose positionally are affected: drop the argument and pass purpose by keyword. Every item stays the raw RGB uint8 array that the embedders and metrics accept, and any preprocessing belongs to a wrapping Dataset.

  • save_to_disk writes to the given path as is, and .embedder or .safetensors is no longer appended to it. Code that saves under a name without a suffix and loads the file with the suffix added passes the same full file name to both calls.

  • The __file_format__ class attribute is removed from SerializerMixin and every serializable class. Subclasses outside the library drop their __file_format__.

  • InMemoryImageEmbeddingStore.search is removed. Code that searched a store by vectors calls store.index.search(query_vectors, k) instead.

  • EmbeddingStore protocol class declares retrieve_top_k_similar(query_images, k) in place of search(query_vectors, k). Custom stores implement the new method.

  • pyvisim.image_store is now pyvisim.retrieval, split into the subpackages image_store (InMemoryImageEmbeddingStore, BruteForceIndex, HnswIndex, ExternalSearchIndex), reranking (KReciprocalReranker) and data (Candidate), so from pyvisim.image_store import ... now raises ModuleNotFoundError. If your code imports from pyvisim.image_store, change the import:

    # before
    from pyvisim.image_store import (
        Candidate,
        InMemoryImageEmbeddingStore,
        KReciprocalReranker,
    )
    
    # after
    from pyvisim.retrieval.data import Candidate
    from pyvisim.retrieval.image_store import InMemoryImageEmbeddingStore
    from pyvisim.retrieval.reranking import KReciprocalReranker
  • The vectors parameter of InMemoryImageEmbeddingStore.save_to_disk is renamed to embeddings. Code that passes vectors=... passes embeddings=... instead.

  • Notebooks are committed without outputs. Run make strip-notebooks before committing one, since the CI rejects notebooks that still carry outputs or execution metadata.

  • Every .embedder file and image store file now names its class under "__class__", and their format versions go up by one. Files saved by 0.9.5 or earlier no longer load, so rebuild the embedder or store and save it again with save_to_disk.

  • The __class_key__ class attribute is removed from SerializerMixin and every serializable class. Subclasses outside the library drop their __class_key__, since the class name is always stored under "__class__".

New Features

  • Feature extractors can be saved on their own with save_to_disk and loaded back with load_from_disk, as .safetensors files.
  • InMemoryImageEmbeddingStore.build_store embeds the gallery and builds the search index. It does nothing on a store that is already built.
  • InMemoryImageEmbeddingStore.is_built reports whether the store has been built.

Enhancement Notes

  • The new tutorial Custom Feature Extractor with ORB under the classical methods shows how to write a feature extractor by inheriting from FeatureExtractorBase, train a VLADEmbedder with it and compare two images.

  • The tutorials dependency group installs opencv-python-headless, which the custom feature extractor tutorial uses for ORB.

  • FeatureExtractorBase now owns both halves of its serialization contract: to_dict and the new from_dict. Subclasses register themselves as they are defined, so a new feature extractor is rebuilt from a serialized description without being added to a hand-maintained table. Extractors that need more than their stored constructor arguments override the _from_config hook, as DeepConvFeature does to rebuild its backbone by name.

  • FeatureExtractorBase.from_dict forwards its keyword arguments to the _from_config hook, so a custom extractor can take objects its state cannot hold.

  • A DeepConvFeature built on a user-supplied model or a custom transform can be rebuilt by passing them back as backbone= and transform=. A FutureWarning is emitted if the transform differs from the saved one.

  • VLADEmbedder and FisherVectorEmbedder take a feature_extractor_params dict in from_dict and load_from_disk, which is forwarded to the feature extractor. An embedder on a DeepConvFeature with a user-supplied model is loaded with feature_extractor_params={"backbone": model}.

  • The tutorial notebooks define their plotting helpers themselves instead of importing them from a shared module, so that a change to a helper no longer executes every notebook again.

  • OxfordFlowerDataset reads its .mat files with scipy.io.loadmat again. The internal MAT-file reader pyvisim.datasets._matloader was removed since SciPy is a runtime dependency.

  • Simplified the [dependency-groups] in pyproject.toml:

    • Merged the release group into docs, since reno and dulwich are only used to build the rele...
Read more

v0.9.5

v0.9.5 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 14 Sep 12:08

Release Notes

v0.9.5

Upgrade Notes

  • Binary files for the docs are now added through a pull request against the assets branch instead of a force-push. Contributors who used the old recipe follow the updated "Adding binary files to the docs" section of CONTRIBUTING.md.
  • The file format of a serialisable class is now declared on the class itself under one set of names, and the module-level constants that held it are gone. _FILE_SUFFIX became __file_format__, _METADATA_KEY became __metadata_key__, _CLASS_KEY became __class_key__, _STATE_KEYS became __state_keys__, and every per-module format version (_CLUSTERING_EMBEDDER_FILE_FORMAT_VERSION, _NEURAL_EMBEDDER_FORMAT_VERSION, _PIPELINE_FORMAT_VERSION, _CLIP_EMBEDDER_FORMAT_VERSION, _STORE_FORMAT_VERSION) became __format_version__ on the class that writes it. Their compatibility table is __compatibility_mapping__. Subclasses outside the library that set any of the old names have to rename them: the old names are no longer read, and SerializerMixin now raises a TypeError at class-definition time when a class that implements to_dict and from_dict leaves one of the required attributes undeclared.
  • pyvisim.serialization.EMBEDDER_METADATA_KEY and pyvisim._base_classes.EMBEDDER_FILE_SUFFIX were removed. Read them from the embedder class instead, for example VLADEmbedder.__metadata_key__ and VLADEmbedder.__file_format__.
  • Subclasses outside the library that implement _embed now receive one batch as a list of canonical uint8 (H, W[, C]) images, without dims and value_range, since ImageEmbedderBase.embed normalizes and batches the input. Drop the two parameters and the batching loop from your _embed.
  • SerializerMixin.to_dict is no longer abstract. It adds the format version and the class name to the mapping returned by the new abstract _state method. Subclasses outside the library rename their to_dict to _state and drop the format_version and class-name entries from it, and __state_keys__ no longer lists the class key. Loading a file whose state lacks the format version or the class name raises a ValueError.
  • pyvisim.serialization.embedder_to_dict and pyvisim.serialization.embedder_from_dict are removed. Code importing them calls embedder.to_dict() instead, and SerializableImageEmbedder.from_dict(state) to rebuild an embedder of any class from its state.

New Features

  • The KMeans, DiagCovarGaussianMixture and PCA models of the classic embedders can be saved on their own with save_to_disk and loaded back with load_from_disk, as .safetensors files.

Enhancement Notes

  • The Benchmarks workflow pushes its barplots to docs/benchmark-barplots for review instead of publishing them directly. The new Squash assets workflow keeps assets at a single commit after each merge.
  • Every embedder rejects an empty input with the same ValueError ("Expected at least one image, got none.") instead of a NumPy concatenation error or an embedder-specific message.
  • Every load_from_disk reports an unreadable file the same way, as "File ... is not a valid <suffix> file" followed by the reason. The image store used to raise the bare reason, while the embedders dropped it.
  • The clustering models of VLADEmbedder and FisherVectorEmbedder record a format_version in their part of the .embedder file. Files written before still load.

v0.9.4

v0.9.4 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 11 Sep 09:17

Release Notes

v0.9.4

Highlights

Release notes are now managed with reno. Every contributor adds a note for the change they are making in the same pull request that carries the code, and the notes of a release are published as the body of its GitHub Release.

Upgrade Notes

  • Contributors need the new release dependency group to add a release note. Run uv sync --group release to install it, then create a note with make release-note NAME=my-change. A pull request touching library code that carries no note fails the Check Release Notes job.
  • VLADEmbedder, FisherVectorEmbedder, Pipeline, ContrastiveSiameseNetwork and TripletNeuralNetwork now L2-normalize what embed returns. Vectors already stored in an InMemoryImageEmbeddingStore built in the "l2" or "ip" space are on a different scale than newly embedded queries, so rebuild such a store or construct the embedder with normalize=False. A "cosine" store is unaffected, since it normalizes its vectors anyway.
  • Subclasses outside the library that implement embed have to rename it to _embed: ImageEmbedderBase.embed is now a concrete method that applies normalize to whatever _embed returns.
  • The .embedder file format carries the normalize setting and is bumped to version 3. Files written by earlier versions lack the key and no longer load, so re-save any embedder saved with pyvisim 0.9.3 or older.

New Features

  • Every image embedder now takes a normalize parameter, which ImageEmbedderBase owns and all embedders inherit. It defaults to True, so embed returns rows of unit L2 length and two embeddings can be compared with a plain dot product. Pass normalize=False to get the raw encoding instead.
  • macOS is now a supported operating system. The package metadata lists it next to Linux and Windows, and the unit and slow test suites run on macos-latest in CI.

Enhancement Notes

  • The documentation now shows the pyvisim symbol above the title in the sidebar, with a separate variant for the dark theme, and a clickable GitHub icon in the page footer.
  • CHANGELOG.md is frozen at 0.9.3 and is no longer updated. It stays in the repository as the record of every release up to that point. Notes for later releases live in releasenotes/notes and are rendered onto the GitHub Releases page.

[0.9.3] - 2026-09-09

[0.9.3] - 2026-09-09 Pre-release
Pre-release

Choose a tag to compare

@MechaCritter MechaCritter released this 09 Sep 06:06

Added

  • Every similarity metric now takes a batch_size argument, exposes it as the
    batch_size attribute and takes a new value through set_batch_size. It
    defaults to 16 everywhere; -1 turns the splitting off and processes the
    whole input as one batch, whatever its size.
  • Serializable embedders store their batch size, so a reloaded embedder runs
    with the batch size it was saved with.
  • VLADEmbedder and FisherVectorEmbedder take a batch_size. The images of
    a batch are extracted, reduced, assigned and normalized as one matrix instead
    of one image at a time, and an iterable input stays a stream.
  • FeatureExtractorBase.extract_batch extracts a batch of images and returns
    one feature array per image. Extractors that can do a whole batch in one go
    override it; the default extracts one image at a time.
  • DeepConvFeature pushes a whole batch through its backbone in one forward
    pass. A custom transform that keeps the input size leaves the images
    unstackable, and they are then extracted one at a time as before.
  • ClipEmbedder, ContrastiveSiameseNetwork, TripletNeuralNetwork and
    BCESiameseNetwork take a batch_size that splits their forward passes and
    bounds the activation memory of each.
  • Pipeline takes a batch_size bounding how many images it hands its
    embedders at a time. Each embedder still applies its own batch size within.
  • InMemoryImageEmbeddingStore embeds its gallery one batch at a time, sized by
    the batch_size of the embedder it is given, instead of one image per call.
  • InMemoryImageEmbeddingStore takes a num_workers (default 4) reading and
    decoding the gallery files on worker threads while the embedder works on the
    previous batch.
  • InMemoryImageEmbeddingStore takes a num_prefetch_batches (default 4)
    controlling how many batches of images the reading threads may run ahead of
    the embedder.
  • InMemoryImageEmbeddingStore.retrieve_top_k_similar takes query_expansion,
    expansion_alpha and expansion_neighbours to refine every query with the
    alpha query expansion of Radenović et al. (2019) before the final search. It
    is off by default, since it costs one extra search per query.
  • KReciprocalReranker (in pyvisim.image_store): re-ranks the candidates of a
    query with the k-reciprocal encoding of Zhong et al. (2017) through
    rerank(candidates, top_k), reading their embeddings back from the store.
  • InMemoryImageEmbeddingStore.embeddings_of(paths) and vectors_at(ids) on
    every index read a few gallery vectors back without decoding the whole
    gallery.
  • Candidate.array reads the matched image as an RGB uint8 array on first
    access and keeps it, and Candidate.clear_buffer drops it again.
  • scripts/benchmark_reranking.py measures plain retrieval, alpha query
    expansion and k-reciprocal re-ranking on the Oxford Flower dataset. The
    results are in the README.

Performance

Building a store over all 6149 train images of the Oxford Flower dataset, before
and after the batched gallery build, measured with
a benchmark script on
the CPU with PYVISIM_NUM_THREADS=4. Only the store constructor is timed.

from pyvisim.datasets import OxfordFlowerDataset
from pyvisim.image_store import InMemoryImageEmbeddingStore
from pyvisim.neural_networks import ClipEmbedder

train_dataset = OxfordFlowerDataset()
train_image_paths = train_dataset.image_paths

embedder = ClipEmbedder()

image_store = InMemoryImageEmbeddingStore(
    image_paths=train_image_paths,
    embedder=embedder,
    search_index="hnsw",
    index_params={"m": 16, "ef_construction": 200},
)
Before After
338 s 249 s
from pyvisim.classic import FisherVectorEmbedder
from pyvisim.datasets import OxfordFlowerDataset
from pyvisim.image_store import InMemoryImageEmbeddingStore

train_dataset = OxfordFlowerDataset()
train_image_paths = train_dataset.image_paths

# Fitted beforehand with learn(images, dim_reduction_factor=2)
embedder = FisherVectorEmbedder(n_components=32)

image_store = InMemoryImageEmbeddingStore(
    image_paths=train_image_paths,
    embedder=embedder,
    search_index="hnsw",
    index_params={"m": 16, "ef_construction": 200},
)
Before After
2936 s 2706 s

Removed

  • ⚠️ DeepConvFeature no longer appends normalized (x, y) coordinates to its
    descriptors: the spatial_embedding argument is gone and output_dim is now
    the channel count of the selected conv layer.

Changed

  • ⚠️ Candidate is a frozen dataclass instead of a named tuple, so it no longer
    unpacks or indexes: read candidate.path and candidate.score. It lives in
    pyvisim.image_store.candidate and is still exported from pyvisim.image_store.
  • Added backbones resnet34, resnet50, resnet101, resnet152 under
    pyvisim.neural_networks.backbones.
  • ⚠️ HnswIndex takes graph_degree, build_candidates and
    search_candidates instead of m, ef_construction and ef_search, and
    exposes them under those names. The old names are gone, in index_params
    too, so a store saved by an earlier release cannot be loaded by this one.
  • Every save_to_disk rejects a destination whose directory does not exist
    with an OSError, and every load_from_disk reports a missing file as a
    FileNotFoundError. The embedders reported both as a safetensors error.
  • ⚠️ SSIM and MSSSIM score 16 image pairs per batch instead of two, and
    the neural embedders now embed 16 images per forward pass instead of the
    whole input at once. Pass batch_size=-1 for the previous behaviour.
  • ⚠️ PSNR takes batch_size=-1 instead of batch_size=None to score the
    whole input as one batch, and defaults to 16 rather than to the whole input.
  • ⚠️ PSNR raises on a batch holding no image instead of returning an empty
    score matrix, which is what every other metric already did.
  • ⚠️ The batch size is a required key of the .embedder format, so a file
    written by an earlier release cannot be loaded by this one.

Removed

  • ⚠️ pyvisim.serialization.save_embedder_state and load_embedder_state.
    They only called save_state/load_state with the embedder metadata key,
    which is now exported as EMBEDDER_METADATA_KEY.

[0.9.2] - 2026-08-26

[0.9.2] - 2026-08-26 Pre-release
Pre-release

Choose a tag to compare

@MechaCritter MechaCritter released this 07 Sep 15:43

Added

  • HnswIndex and BruteForceIndex (in pyvisim.image_store): an approximate
    HNSW graph and an exhaustive scan, both compiled into the package and built
    in cosine space by default.
  • ExternalSearchIndex (in pyvisim.image_store): searches through an index
    built elsewhere. ExternalSearchIndex.from_faiss_index(index, vectors=None)
    adapts any FAISS index without FAISS being a dependency of this library.
  • InMemoryImageEmbeddingStore.retrieve_top_k_similar ranks the gallery against
    query images and returns Candidate matches, both now owned by the store.
  • InMemoryImageEmbeddingStore.save_to_disk takes the gallery vectors to
    write, for an index that hands back an approximation of what it was given.
  • InMemoryImageEmbeddingStore.load_from_disk forwards keyword arguments:
    search_index=... restores a store onto a rebuilt external index, and
    anything else reaches the embedder.

Changed

  • ⚠️ The store's index_type parameter is now search_index, which takes
    "hnsw", None for a brute-force scan, or an ExternalSearchIndex. Its
    quantizer parameter is now space, taking "cosine" (the default), "l2"
    or "ip".
  • ⚠️ The scores of Candidate and search are distances for the built-in
    indexes, so lower is more similar. An ExternalSearchIndex reports whatever
    its own metric produces.
  • The index owns the gallery vectors and the store keeps no second copy, so
    store.embeddings is read-only. Both built-in indexes decode it out of their
    own storage, which makes every access a fresh copy.
  • Store files are written in a new layout; a store saved by an earlier version
    cannot be loaded by this one.

Removed

  • ⚠️ pyvisim.retrieval (ImageRetriever, ImageIndex and the FAISS-backed
    IVF indexes) and pyvisim.functional. The store now covers both.
  • ⚠️ The search extra, along with the faiss-cpu dependency behind it.

[0.9.1] - 2026-08-24

[0.9.1] - 2026-08-24 Pre-release
Pre-release

Choose a tag to compare

@MechaCritter MechaCritter released this 07 Sep 15:42

Added

  • Added Triplet Neural Network under pyvisim.neural_networks with TripletLoss.

[0.9.0] - 2026-08-23

[0.9.0] - 2026-08-23 Pre-release
Pre-release

Choose a tag to compare

@MechaCritter MechaCritter released this 07 Sep 15:42

Fixed

  • PSNR.similarity_score accepts a channel-less grayscale image again: a 2-D
    array passed with the default dims="HWC" raised instead of being read as
    single-channel, unlike SSIM and the rest of the library.
  • make test-types no longer prints a DeprecationWarning: the
    numpy.typing.mypy_plugin entry is removed from the mypy configuration.
  • The development interpreter is pinned to Python 3.10 (.python-version), the
    project's minimum supported version and the one every CI job already uses.
  • The ruff check CI step no longer fails on import sorting (I001) in
    tests/neural_networks/test_oxford_flowers_quick.py and
    test_oxford_flowers_slow.py.

Added

  • load_from_disk now forwards keyword arguments to from_dict, so an
    embedder can be handed the objects its file cannot hold. The Siamese
    networks use it for their transform:
    ContrastiveSiameseNetwork.load_from_disk(path, transform=transform)
    restores the exact embeddings of a network built with a custom one.
  • The embedders of pyvisim.neural_networks (ClipEmbedder,
    ContrastiveSiameseNetwork, BCESiameseNetwork) are now serialisable to a
    safetensors .embedder file via save_to_disk/load_from_disk, weights
    included; a reloaded embedder produces identical embeddings without downloading
    any pretrained weights.
  • NeuralImageEmbedder (in pyvisim.neural_networks): the shared base for the
    neural embedders, both a SerializableImageEmbedder and a torch.nn.Module.
    SiameseNetworkBase now derives from it, so the Siamese networks and the
    classic embedders expose the same embed/similarity_score surface.
  • Clustering models can now be built from a fitted scikit-learn estimator:
    KMeans.from_sklearn, DiagCovarGaussianMixture.from_sklearn and
    PCA.from_sklearn, plus load_clustering_model_from_sklearn on VLADEncoder
    and FisherVectorEncoder to drop one straight into an encoder. Handy for reusing
    a vocabulary you already trained with scikit-learn.

Changed

  • The Siamese networks now store the repr of their transform in the .embedder
    file and warn on load_from_disk when the rebuilt network's transform differs
    from it, instead of warning on every save of a custom transform.
  • tqdm and requests are no longer runtime dependencies of the core
    package; they moved into the nn extra. Only pyvisim.datasets uses them,
    and that module already requires torch from the same extra.
  • CI restores the Oxford Flowers dataset and the pretrained backbone weights from
    the GitHub Actions cache instead of re-downloading them on every run; the new
    Warm asset cache workflow keeps that cache populated on main.
  • The similarity_func registry in pyvisim._utils now maps the metric names
    straight onto pyvisim.distance.
  • ℹ️ Dropped scikit-learn as a runtime dependency.
  • Added PSNR (under pyvisim.pixelwise) and SSIM/MSSSIM (under
    pyvisim.structural) metrics as well as their benchmark scripts against
    existing implementations under docs/pixelwise/benchmarks and
    docs/structural/benchmarks.
  • BCESiameseNetwork (in pyvisim.neural_networks): the pair-classifying
    Siamese variant of Koch, Zemel & Salakhutdinov (2015).
  • The Siamese networks are split along a shared abstract base,
    SiameseNetworkBase.
  • Removed the Siamese Network's train scripts. This is now demonstrated
    in a notebook in the "examples" repository.

Breaking

  • ⚠️ VLADEmbedder, FisherVectorEmbedder and Pipeline moved from
    pyvisim.encoders to pyvisim.classic.

  • ⚠️ The bundled pretrained VLAD and Fisher Vector encoders are removed to make the binary smaller, together
    with from_pretrained, PretrainedVLAD/PretrainedFisher, the deprecated
    weights= argument and KMeansWeights/GMMWeights. Train a vocabulary with
    learn() and persist it with save_to_disk/load_from_disk instead.

  • ⚠️ "Encoder" is now "embedder" throughout: ImageEncoderBase -> ImageEmbedderBase,
    VLADEncoder -> VLADEmbedder, FisherVectorEncoder -> FisherVectorEmbedder,
    the Encoder protocol -> Embedder, encode() -> embed() and store.encoder
    -> store.embedder.

  • ⚠️ Saved models use the .embedder suffix and an embedder_class state key, so
    existing .encoder files no longer load. Re-save them with save_to_disk.

  • ⚠️ SiameseNeuralNetwork is renamed to ContrastiveSiameseNetwork
    (pyvisim.neural_networks.siamese.siamese_neural_network is gone; the base
    class now lives in pyvisim.neural_networks.siamese._base_siamese):

    from pyvisim.neural_networks import ContrastiveSiameseNetwork
    
    model = ContrastiveSiameseNetwork(backbone="resnet18", embedding_dim=128)
    score = model.similarity_score(image1, image2)  # cosine similarity in [-1, 1]
  • ⚠️ The clustering models (KMeans, DiagCovarGaussianMixture, PCA,
    ClusteringModelBase) are now internal to the encoders package and moved from
    pyvisim.clustering to pyvisim.classic._clustering.

  • ⚠️ Encoder clustering parameters changed: pass rng instead of random_state
    inside kmeans_params / gmm_params / pca_params (see
    vlad.md and
    fisher_vector.md for every accepted key).