Unsupervised image clustering with modern deep embeddings.
tasnif turns a folder of images into clusters you can browse on disk — no
labels required. It uses modern pretrained vision backbones (timm by default,
optionally CLIP via open_clip), reduces the embedding with PCA, and runs
K-Means on top.
Highlights
- Modern backbones — any
timmmodel (ResNet, ConvNeXt, ViT, DINOv2, ...) or CLIP via the[clip]extra. - GPU / MPS / CPU with automatic device detection.
- scikit-learn-style API:
fit,predict,fit_predict,transform. - Rich export: per-cluster folders, CSV manifest, JSON summary, preview grids, raw embeddings.
- Multiple materialization modes:
copy,symlink,move, ornone(metadata only). - First-class CLI powered by Typer.
- Pluggable embedders — register your own backend.
- Deterministic with a
random_stateseed.
Installation
tasnif is built and packaged with uv, but plain pip works too.
pip install tasnif # core
pip install "tasnif[cli]" # core + CLI
pip install "tasnif[clip]" # core + CLIP backend
pip install "tasnif[all]" # everything
Development setup:
git clone https://github.com/cobanov/tasnif
cd tasnif
uv sync --extra dev
uv run pytest
Quickstart — Python API
from tasnif import TasnifClusterer
clf = TasnifClusterer(n_clusters=5, embedder="timm", pca_dim=16)
clf.fit("photos/")
result = clf.result_
# Inspect without exporting
print(result.counts) # e.g. [42, 31, 28, 19, 7]
print(result.silhouette) # None unless compute_silhouette=True
mapping = result.as_dict() # {Path('photos/a.jpg'): 2, ...}
buckets = result.by_cluster() # {0: [Path(...), ...], 1: [...]}
# Export to disk
clf.export("output/", mode="copy")
One-shot helper
from tasnif import cluster_directory
cluster_directory("photos/", "output/", n_clusters=5, mode="symlink")
CLI
# Cluster a directory with the default ResNet-50 (timm) backbone
tasnif cluster photos/ -k 5 -o output/
# Use CLIP and copy via symlink (fast, non-destructive)
tasnif cluster photos/ -k 8 --embedder clip --model ViT-B-32 --mode symlink
# Just compute embeddings, save .npy + .json
tasnif embed photos/ --embedder timm --model convnext_base -o embeddings.npy
# List available backends
tasnif backends
Run tasnif --help to see all options.
Use a different model
Default is timm:resnet50. Pass any timm model name:
clf = TasnifClusterer(
n_clusters=8,
embedder="timm",
embedder_kwargs={"model": "vit_base_patch14_dinov2.lvd142m", "device": "cuda"},
)
CLIP:
clf = TasnifClusterer(
n_clusters=8,
embedder="clip",
embedder_kwargs={"model": "ViT-L-14", "pretrained": "laion2b_s32b_b82k"},
)
Custom backend
Anything implementing the Embedder protocol works:
import numpy as np
from tasnif import TasnifClusterer, register_embedder
class MyEncoder:
name = "my-encoder"
@property
def dim(self): return 128
@property
def device(self): return "cpu"
def embed(self, images, *, batch_size=32, show_progress=True):
return np.stack([extract(img) for img in images])
register_embedder("my-encoder", lambda: MyEncoder())
clf = TasnifClusterer(n_clusters=5, embedder="my-encoder")
Building blocks
All pieces are independently usable:
from tasnif import (
discover_images, create_embedder,
reduce_pca, fit_kmeans, export_clusters, ClusterResult,
)
paths = discover_images("photos/")
embedder = create_embedder("timm", model="resnet50")
embeddings = embedder.embed([open_pil(p) for p in paths])
reduced = reduce_pca(embeddings, n_components=16)
fit = fit_kmeans(reduced, n_clusters=5, compute_silhouette=True)
result = ClusterResult(
labels=fit.labels, centroids=fit.centroids, counts=fit.counts,
paths=tuple(paths), n_clusters=5, inertia=fit.inertia, silhouette=fit.silhouette,
embedder=embedder.name, pca_dim=16,
)
export_clusters(result, "output/")
Migrating from 0.1.x
The 0.1.x API (Tasnif().read().calculate().export()) was removed in 0.2.0.
Replace with:
- from tasnif import Tasnif
- c = Tasnif(num_classes=5, pca_dim=16, use_gpu=False)
- c.read("photos/")
- c.calculate()
- c.export("output/")
+ from tasnif import TasnifClusterer
+ clf = TasnifClusterer(n_clusters=5, pca_dim=16, embedder_kwargs={"device": "auto"})
+ clf.fit("photos/")
+ clf.export("output/")
See CHANGELOG.md for the full list of breaking changes.
Contributing
Issues and PRs welcome. Before submitting, run:
uv run ruff check . && uv run ruff format --check .
uv run mypy
uv run pytest
Pre-commit is configured — install hooks with uv run pre-commit install.
License
MIT — see LICENSE.
Release files for tasnif 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tasnif-0.2.0.tar.gz | 18.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tasnif-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 42.6 kB
Release files / tasnif-0.2.0.tar.gz
| Download URL | tasnif-0.2.0.tar.gz |
|---|---|
| Size | 18.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ebd0a63eea60278c677a38885fa2289ccfdc89cca2df5dc01626e99f228824ac
|
|
BLAKE2b-256 checksum How to use checksums |
68141b47417010f682f3adec62c905d16a33a4ae0ab8036c55f0ccc75d76b14e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 14, 2026.
Transparency logRelease files / tasnif-0.2.0-py3-none-any.whl
| Download URL | tasnif-0.2.0-py3-none-any.whl |
|---|---|
| Size | 23.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8d91f5952a926060de059798bbdb92f44b76d9d3978b6dc2ca4997f620d7443b
|
|
BLAKE2b-256 checksum How to use checksums |
a39f923ce25afd19c8aa9d5d53bdd87b674c8e314d98d06a2eb485d7c99e45b4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 14, 2026.
Transparency log