Model-agnostic feature-map visualization: PCA, cosine-similarity, k-means and foreground maps from any vision model and any layer.
Project description
📖 Documentation: https://turhancan97.github.io/FeatLens/ · 🤗 Live demo: https://huggingface.co/spaces/turhancan97/FeatLens-demo
See what any vision model encodes. FeatLens renders feature maps for
any vision model — DINO, DINOv2/v3, CLIP, SigLIP, MAE, DeiT, V-JEPA, CNNs, … — loaded from
any source (timm, HuggingFace transformers, torch.hub, an external repo, or a model you
built yourself), and from any layer, as a clean model × layer grid. Color the features by
robust PCA, cosine-similarity to a seed patch, k-means segmentation, or a foreground
mask — and match patches across two images.
Most "DINO PCA" scripts are welded to one model. FeatLens separates representation access (a small adapter layer over the model zoo) from visualization (PCA / cosine / k-means / foreground), so you can point it at a new model in seconds and compare models/layers side by side.
Gallery
All produced by examples/quickstart.py. The per-image rows below use DINO ViT-S/8 at 768px
— a small patch-8 backbone at high resolution gives a fine 96×96 feature grid, so thin
structures (whiskers, feather barbs, individual fruit) stay crisp. The model × layer, compare
and method figures further down use DINO ViT-B/16 at the default 224px.
visualize(...) — DINO ViT-S/8 @ 768px, feature maps across layers 2 / 5 / 8 / 11:
| Image (original size) | Source | Feature maps |
|---|---|---|
peacock.jpg · 1600×1280 |
||
cat_hires.jpg · 1600×1200 |
||
market.jpg · 1600×1063 |
grid(...) — model × layer, overlaid on the image (DINO vs DINOv2 across layers 2/5/8/11):
compare(...) — models at the final layer | custom_adapter — a ResNet-50 (CNN escape hatch)
Same scene, six ViT-B/16 backbones — market.jpg at 1024px, last-layer features (a 64×64
grid), PCA→RGB per model. Architecture and patch size are held fixed, so the differences are purely
the training objective: DINOv3 and DINO carve the scene into smooth semantic regions, MAE stays
low-frequency, while SigLIP, supervised, and Perception Encoder encode much higher-frequency detail.
Beyond PCA — the same DINOv2 row, recolored by cosine-similarity to a seed patch, k-means segmentation, and a foreground mask (across layers 2 / 5 / 8 / 11):
| Method | Across layers |
|---|---|
cosine (seed on the cat) |
|
kmeans (k=6) |
|
foreground |
correspond(...) — seed a patch in image A, find the matches in image B. Here the seed is on
a real cat's eye; DINOv2 features match the same semantic part on a watercolor cat, across the
photo→illustration domain gap:
Install
pip install -e ".[timm]" # timm backend (DINO, CLIP, SigLIP, DeiT, ...)
# extras: [hf] transformers · [clip] open_clip · [all]
Install PyTorch for your platform first (https://pytorch.org).
Quick start (Python)
import featlens as ll
# One model, scrub layers (shared PCA basis -> colors comparable across the row)
ll.visualize("dinov2_vitb14", "img.jpg", layers=[2, 5, 8, 11], out="row.png")
# Compare models at the final layer (per-tile basis)
ll.compare(["dino_vitb16", "mae_vitb16", "clip_large_openai"], "img.jpg", layer=-1, out="cmp.png")
# Full model x layer grid, overlaid on the image
ll.grid(["dino_vitb16", "dinov2_vitb14"], "img.jpg", layers=[2, 5, 8, 11], overlay=True, out="grid.png")
# Batch a whole folder -> one figure per image (model loads once, reused across images)
ll.batch("dino_vitb16", "photos/", "out/", layers=[2, 5, 8, 11])
Quick start (CLI)
featlens --models dino_vitb16 clip_large_openai --layers 2 5 8 11 \
--images examples/images/cat.jpg --mode grid --out out/grid.png
featlens --config configs/example.yaml --images examples/images/cat.jpg --out out/grid.png
# Batch: point --images at a folder (or glob) and --out-dir at an output folder
featlens --models dino_vitb16 --layers 2 5 8 11 --images photos/ --out-dir out/
Image size & resizing
Images are resized to a square img_size × img_size before the model (default 224).
img_size must be divisible by the model's patch size (multiples of 16 for patch-16 models,
14 for patch-14). Larger sizes give a finer feature grid at more compute:
ll.visualize("dinov2_vitb14", "img.jpg", layers=[2, 5, 8, 11], img_size=448) # 32x32 grid
For non-square images, choose how aspect ratio is handled with resize_mode:
resize_mode |
behavior |
|---|---|
squash (default) |
resize straight to img_size² — may distort |
crop |
resize shortest side to img_size, center-crop — aspect preserved |
pad |
resize longest side to img_size, pad to square — keeps the whole image |
ll.grid([...], "wide.jpg", resize_mode="crop") # Python
featlens --models dino_vitb16 --images wide.jpg --resize-mode pad --img-size 448 --out g.png
(FeatureGrid(interpolation_size=…) is separate — it only upscales the rendered tiles, not the
model input.)
Model sources
| Source | How to pass it | Needs |
|---|---|---|
| timm | friendly name (dinov2_vitb14) or raw id (vit_base_patch16_224) |
[timm] |
| HuggingFace | hf:facebook/dinov2-base |
[hf] |
| torch.hub (V-JEPA) | vjepa2_vitl16 |
network for weights |
| External repo (VGGT/SPA/…) | external_adapter.load(repo_dir, builder, hook_target=…) |
the cloned repo |
| Your own model | custom_adapter.load(model, feature_fn=…) |
— |
Friendly names (see featlens/registry.py) cover DINO, DINOv2/v3, CLIP, SigLIP, MAE, DeiT,
Perception Encoder and V-JEPA; any other timm id works directly.
Layers
layers=[2, 5, 8, 11] selects transformer block indices (0-based, negatives allowed,
-1 = last). The same convention holds across backends — for HuggingFace models FeatLens maps
block i to hidden_states[i+1] (skipping the embedding output) for you.
Visualization methods
Every method consumes the same dense feature stack, so it works on grid / visualize /
compare and across any layer:
cosine mode: the heatmap (right) tracks the seed patch (white dot) — click anywhere on the 🤗 live demo.
method |
shows | extra args |
|---|---|---|
pca (default) |
robust PCA → RGB | basis, remove_first_component |
cosine |
cosine similarity to a seed patch (with a [-1, 1] colorbar) | seed=(x, y), colormap |
kmeans |
unsupervised k-means segmentation (with a cluster legend) | k |
foreground |
fg/bg mask (first PCA component) | — |
fl.visualize("dino_vitb16", "img.jpg", layers=[2, 5, 8, 11], method="cosine", seed=(0.5, 0.5))
fl.compare(["dino_vitb16", "dinov2_vitb14"], "img.jpg", layer=-1, method="kmeans", k=8)
fl.correspond("dino_vitb16", "a.jpg", "b.jpg", seed=(0.4, 0.5), topk=3, out="corr.png") # cross-image
seed is normalized image coords (x, y) ∈ [0, 1] (resolution/model independent). Pass
cache=True to memoize extraction on disk ($FEATLENS_CACHE_DIR, else ~/.cache/featlens) so
re-renders are instant. Try it in the browser on the
🤗 live demo (or run demo/
locally) — in cosine mode, click the image to move the seed. See the
docs.
Bring your own model
Anything that isn't built in works through the escape hatch — give a feature function or a hook target. CNNs work for free (their conv map is already spatial):
import torch.nn as nn, torchvision
from featlens import FeatureExtractor, FeatureGrid
from featlens.adapters import custom_adapter
resnet = torchvision.models.resnet50(weights="DEFAULT")
trunk = nn.Sequential(*list(resnet.children())[:-2]) # -> [B, 2048, h, w]
lm = custom_adapter.load(trunk, patch_size=32, feature_fn=lambda m, x: m(x), name="resnet50")
FeatureGrid([FeatureExtractor(lm)]).render("img.jpg", out_path="resnet50.png")
For a model in its own repo, external_adapter.load(repo_dir, builder, hook_target="blocks")
puts the repo on sys.path, builds the model, and hooks its blocks.
How it works
- Adapters resolve a spec → a
LoadedModeland drive extraction in one of three modes: forward hooks on per-block modules (ViTs/CNNs/V-JEPA), HFoutput_hidden_states, or a user callable. tokens_to_gridnormalizes whatever a layer emits ([B,N,D]tokens with optional CLS/register prefixes, or[B,D,h,w]maps) into a dense[B,D,h,w]grid.- Robust PCA (median-absolute-deviation outlier filtering) projects features to RGB;
FeatureGridlays out the model × layer tiles with a per-tile or shared-per-model basis.
The extraction core adapts the FrozenBackbone pattern; the PCA is adapted from the SpaRRTa
feature-map script.
Releasing
Releases publish to PyPI automatically via
.github/workflows/publish.yml (PyPI Trusted Publishing — no API token stored in the repo).
One-time setup on PyPI: add a trusted publisher for the project (Account → Publishing) with
owner turhancan97, repository FeatLens, workflow publish.yml, environment pypi. PyPI
supports a pending publisher so the very first release can also go through Actions.
Then cut a release by pushing a tag:
# bump the version in pyproject.toml first, then:
git tag v0.1.0 && git push origin v0.1.0
The workflow builds the sdist + wheel, runs twine check, and uploads to PyPI.
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file featlens-0.2.5.tar.gz.
File metadata
- Download URL: featlens-0.2.5.tar.gz
- Upload date:
- Size: 41.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c01d440e8a66216d53b106a6a33ae6fa43c9bfce036dbb4ca5d8daf1657d899
|
|
| MD5 |
a944922e67d9e3ebb9d42556b12dcddf
|
|
| BLAKE2b-256 |
4c5f2379c52c610666794bf3d87b306731a7d4a83314777905d25f7c33cd667b
|
Provenance
The following attestation bundles were made for featlens-0.2.5.tar.gz:
Publisher:
publish.yml on turhancan97/FeatLens
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
featlens-0.2.5.tar.gz -
Subject digest:
0c01d440e8a66216d53b106a6a33ae6fa43c9bfce036dbb4ca5d8daf1657d899 - Sigstore transparency entry: 2012040552
- Sigstore integration time:
-
Permalink:
turhancan97/FeatLens@43425d84781fb2ecbcecbea04144d7ffd8d79e67 -
Branch / Tag:
refs/tags/v0.2.5 - Owner: https://github.com/turhancan97
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@43425d84781fb2ecbcecbea04144d7ffd8d79e67 -
Trigger Event:
push
-
Statement type:
File details
Details for the file featlens-0.2.5-py3-none-any.whl.
File metadata
- Download URL: featlens-0.2.5-py3-none-any.whl
- Upload date:
- Size: 40.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0414252f12c53d38bec9724d86032f91bd3529e12106118b10319f757275c170
|
|
| MD5 |
6db14dddb4273df7790845b291ae98c4
|
|
| BLAKE2b-256 |
088cd06326c4e6d69095e38280c3df8751fc432fe6fbed94abd9aa4a24775049
|
Provenance
The following attestation bundles were made for featlens-0.2.5-py3-none-any.whl:
Publisher:
publish.yml on turhancan97/FeatLens
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
featlens-0.2.5-py3-none-any.whl -
Subject digest:
0414252f12c53d38bec9724d86032f91bd3529e12106118b10319f757275c170 - Sigstore transparency entry: 2012040782
- Sigstore integration time:
-
Permalink:
turhancan97/FeatLens@43425d84781fb2ecbcecbea04144d7ffd8d79e67 -
Branch / Tag:
refs/tags/v0.2.5 - Owner: https://github.com/turhancan97
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@43425d84781fb2ecbcecbea04144d7ffd8d79e67 -
Trigger Event:
push
-
Statement type: