geofit
Rotation- and scale-invariant shape matching for machine vision — find a part at any angle, under changing illumination, with pieces of it missing.
Give it a picture of the part you are looking for. It tells you where that part is in the next image, how far it is rotated, and how sure it is — to a tenth of a pixel and a tenth of a degree, for every instance in the frame, in milliseconds.
It keeps working when the lighting drifts, when the part is darker or lighter than the one you taught it, and when something is sitting on top of it. Those are the conditions that break a plain correlation template match, and they are the everyday conditions on a line.
A C++ core behind a Python API that needs only numpy. No OpenCV, no PyTorch, no model to train, nothing to configure. A 2592x1944 image searched over the full 360 degrees takes about 20 ms.
pip install geofit
geofit demo --save result.png # runs on a bundled sample, nothing else needed
What it looks like
Seven screwdriver bits on a dark background, every one at a different angle. The red outline is the model drawn back onto the pose that was found, the green box is the template footprint, and the blue line shows which way it is facing.
Four retaining clips, two of them overlapping and one lying on top of a gear. All four are found and told apart, in 4 ms:
The bundled sample — four rotated instances, an illumination gradient across the frame, noise, and one instance about 30% occluded (bottom left, still scoring 0.83):
Install
pip install geofit
Python 3.10 – 3.14 on Windows x86-64, Linux x86-64 / aarch64, macOS arm64 / x86-64. The only runtime dependency is numpy.
Reading and writing PNG and PGM needs nothing else. Other formats (JPEG, TIFF, …) go through Pillow if it is installed:
pip install "geofit[image]"
The wheel is tagged py3-none-<platform>: it does not link against the CPython ABI, so one
file covers every supported Python and keeps working when a new one is released.
Quick start
import geofit as gf
template = gf.imread("template.png") # uint8 grayscale numpy array
image = gf.imread("scene.png")
model = gf.ShapeModel.create(template)
matches = model.find(image, min_score=0.6, num_matches=0) # 0 = find them all
for m in matches:
print(m.x, m.y, m.angle, m.scale, m.score)
gf.imwrite("result.png", gf.overlay(image, model, matches))
m.x, m.y is where the centre of the template landed, in image pixels. m.angle is
degrees, positive counter-clockwise on screen. To map a template coordinate p into the
image: R(angle) @ (scale * (p - center)) + (x, y) — or call model.transform(m).
Any uint8 grayscale numpy array works, so gf.imread is a convenience rather than a
requirement: OpenCV, Pillow, scikit-image or a frame grabber SDK all feed it directly.
import cv2
image = cv2.imread("scene.png", cv2.IMREAD_GRAYSCALE)
matches = model.find(image, min_score=0.6)
Command line
geofit serve # dashboard: crop a template, try parameters
geofit demo # bundled sample, prints the matches
geofit demo --save result.png # ... and writes an overlay image
geofit find -t tpl.png -i scene.png --min-score 0.6 --save out.png
geofit find -t tpl.png -i scene.png --json # machine-readable
geofit info # version, SIMD path, threads, licence
geofit bench # quick timing on the bundled sample
geofit check # licence key and evaluation status
The dashboard
geofit serve # opens http://localhost:8020
geofit serve --port 9000
Drag a rectangle over the part you want to find, press Build model, and the model points appear on top of your crop so you can see what it latched on to. Load a search image, press Search, and the matches are drawn over it with a table and timings.
min_score and the match count are sliders that filter instantly - the search is not
re-run - and the score histogram shows where the gap between real matches and noise
sits, which is a better way to pick a threshold than guessing. Copy as Python gives
you the call with whatever parameters you have arrived at.
It binds to localhost and has no authentication; keep it that way unless you have a reason not to.
geofit demo needs no files of your own — a synthetic template and scene ship inside the
package, and geofit.sample_paths() returns where they are.
$ geofit demo
template 100x84 image 720x480
model 3 levels, points [131, 127, 93], built in 6.8 ms
search 8.8 ms (pyramid 5.1 / top level 3.3 / refine 0.4)
early termination: 43.5% of the work done (2.3x saved)
4 matches
# x y angle scale score
0 399.21 110.05 +37.22 1.000 0.998
1 139.39 118.91 -0.05 1.000 0.998
2 600.81 300.02 -117.76 1.000 0.995
3 169.52 349.99 +164.53 1.000 0.828
--json prints the same thing as a JSON document on stdout and nothing else, so it drops
straight into a pipeline.
Python API
ShapeModel.create(template, ...)
| default | ||
|---|---|---|
num_levels |
0 (auto) | pyramid levels |
min_dist |
3.0 | minimum spacing between model points, px. Larger is faster |
max_points |
400 | maximum points per level |
min_contrast |
15.0 | gradients below this are treated as noise |
canny |
(0, 0) = auto | explicit Canny thresholds when you want them |
blur |
1.0 | Gaussian sigma before computing gradients |
model.find(image, ...)
| default | ||
|---|---|---|
min_score |
0.5 | minimum similarity, 0–1 |
num_matches |
1 | 0 returns every match above min_score |
max_overlap |
0.5 | rotated-rectangle IoU above which a match is suppressed |
greediness |
0.7 | 0 never misses a match, 1 is fastest |
angle_start / angle_extent |
−180 / 360 | narrowing this speeds the search proportionally |
scale_min / scale_max / scale_step |
1 / 1 / 0.05 | isotropic scale search |
metric |
use_polarity |
ignore_global_polarity when background brightness flips |
subpixel |
True | parabola fit on position and angle |
num_threads |
0 | 0 uses one thread per core |
with_timing |
False | also return per-stage timings |
Calls release the GIL, so several find() calls from Python threads genuinely run in
parallel.
matches, t = model.find(image, min_score=0.6, num_matches=0, with_timing=True)
print(t.total_ms, t.toplevel_ms, t.early_termination_ratio)
Other functions
gf.imread(path) / gf.imwrite(path, img) |
PGM and PNG with numpy alone; other formats via Pillow |
gf.overlay(image, model, matches) |
the RGB overlay used in the images above |
gf.sample_paths() |
paths to the bundled template and scene |
gf.build_info() / gf.simd_name() / gf.num_threads() |
what the runtime picked |
gf.license_status() |
licence and evaluation state |
model.transform(match) |
model points in image coordinates |
model.points(), model.num_points(), model.template_size |
the model itself |
Getting good results
Four things account for most of the difference between a model that works and one that does not.
Crop the template tightly. Background that varies from one instance to the next costs
more score than any parameter you can tune. On a board where the same component sits next to
different neighbours, trimming 15% off the template border took detections from 14 to 36 —
a bigger change than anything min_score could do.
Elongated, self-similar shapes give duplicate matches slid along their own axis. Raise
min_score rather than max_overlap: a duplicate offset along the axis does not overlap
enough for IoU to catch it, but its score is clearly lower.
A rotationally symmetric part returns its angle modulo the symmetry. A brake rotor with six arms whose shapes alternate has a period of 120°, not 60°, and the search will tell you so: at the 60° offsets the score drops to 0.31.
Narrow the angle range when you know it. angle_start / angle_extent cut the top-level
search proportionally. If parts arrive within ±15°, say so and the search gets an order of
magnitude cheaper.
Accuracy
Measured against exact ground truth by rotating a real 2592×1944 image by known angles:
position error mean 0.13 px max 0.28
angle error mean 0.080° max 0.218 θ = -150° … +150°, 9/9 found
Against a public benchmark set (DennisLiu1993/Fastest_Image_Pattern_Matching), on ten
scenes covering scattered parts at arbitrary angles, repeated grids, overlapping parts and
fine particles, the detection count matched the expected count in every case where an
expected count is well defined.
Performance
AMD Zen 4, 16 cores, full 360° search:
| Image | Template | Matches | Time |
|---|---|---|---|
| 2592×1944 | 466×135 | 7 | 20 ms |
| 4096×3000 | 848×446 | 16 | 33 ms |
| 4024×3036 | 762×521 | 3 | 31 ms |
| 3648×3648 | 54×54 | 161 | 51 ms |
| 640×480 | 200×200 | 3 | 3.3 ms |
The hot loop is compiled three ways — a portable baseline, AVX2+FMA on x86-64, and NEON on
ARM64 — and the x86 path is chosen at runtime by CPUID. The same wheel therefore runs on
CPUs without AVX2, falling back to the portable path instead of crashing. geofit info
reports which one it picked.
The search spends its time only where a match is still possible, which on real images is a
small fraction of the frame. geofit demo reports how much of the work that saved on the
run you just did — typically three- to eightfold. greediness controls how aggressively it
does this: leave it at the default, raise it when you need speed, drop it to 0 when you
would rather not miss a weak match.
Licensing
pip install geofit gives you a 90-day evaluation, counted from the first run on each
machine. No sign-up, no key, no network call — nothing is transmitted anywhere at any point,
during the evaluation or afterwards.
geofit check # days remaining, and where the settings live
When the 90 days are up geofit stops running: calls raise geofit.TrialExpired and the CLI
exits with status 3.
After the evaluation
Write to pashidl.lab@gmail.com and we will send a licence key — one line of text. There is no account to create and no licence server to reach.
A key is a signed statement, verified offline against a public key inside the package. Install it either way:
# environment variable
setx GEOFIT_LICENSE "<key>" # Windows
export GEOFIT_LICENSE='<key>' # macOS / Linux
# or save the key to a file
~/.geofit/license
geofit check then shows the licensee name and the expiry date. Keys carry their own
expiry, and geofit check starts warning 30 days ahead of it.
License
Proprietary. This package is distributed for evaluation; see the LICENSE file inside the wheel for the full terms. Commercial licensing: pashidl.lab@gmail.com
The source code is not distributed.
Documentation and screenshots: https://github.com/pashidl-lab/geofit
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
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 geofit-0.2.0-py3-none-win_amd64.whl.
File metadata
- Download URL: geofit-0.2.0-py3-none-win_amd64.whl
- Upload date:
- Size: 395.9 kB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
63f245e01a13c96a1729ee1e1e0377e8cc09f44375f71a1a11c00bb0066d779b
|
|
| MD5 |
018ffdb0727794546acc190ee9b511c5
|
|
| BLAKE2b-256 |
1aa9083eacace4a4af26451a90224fe0278982ed1b710e9b543415f204a1cd2b
|
Provenance
The following attestation bundles were made for geofit-0.2.0-py3-none-win_amd64.whl:
Publisher:
wheels.yml on pashidl-lab/geofit-src
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
geofit-0.2.0-py3-none-win_amd64.whl -
Subject digest:
63f245e01a13c96a1729ee1e1e0377e8cc09f44375f71a1a11c00bb0066d779b - Sigstore transparency entry: 2624086029
- Sigstore integration time:
-
Permalink:
pashidl-lab/geofit-src@aa5be1fd14a400b56dea0aad56a26a02c1328eb4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/pashidl-lab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
wheels.yml@aa5be1fd14a400b56dea0aad56a26a02c1328eb4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file geofit-0.2.0-py3-none-win32.whl.
File metadata
- Download URL: geofit-0.2.0-py3-none-win32.whl
- Upload date:
- Size: 383.0 kB
- Tags: Python 3, Windows x86
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
57425a826bd2d13ff47170e267549b8060af34c5752db30ad04f91d26aa728d9
|
|
| MD5 |
015cb1487d6dc3f2baaeca76a94ffb70
|
|
| BLAKE2b-256 |
797efb5e0233dc2885bea65522b823767610c04b57493c2d9c2a82804c6a3957
|
Provenance
The following attestation bundles were made for geofit-0.2.0-py3-none-win32.whl:
Publisher:
wheels.yml on pashidl-lab/geofit-src
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
geofit-0.2.0-py3-none-win32.whl -
Subject digest:
57425a826bd2d13ff47170e267549b8060af34c5752db30ad04f91d26aa728d9 - Sigstore transparency entry: 2624085848
- Sigstore integration time:
-
Permalink:
pashidl-lab/geofit-src@aa5be1fd14a400b56dea0aad56a26a02c1328eb4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/pashidl-lab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
wheels.yml@aa5be1fd14a400b56dea0aad56a26a02c1328eb4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file geofit-0.2.0-py3-none-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: geofit-0.2.0-py3-none-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 1.4 MB
- Tags: Python 3, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a4de057031eb60a540b5ac41dca2e1c67c76f3c3e691a8ac72feda93d28b1819
|
|
| MD5 |
c19d0ba68ee473f740340e541c236610
|
|
| BLAKE2b-256 |
c3e52f073ac2c8dee57c84dcbc74a756d6c4caff7012d7d42f9007c093f0f52d
|
Provenance
The following attestation bundles were made for geofit-0.2.0-py3-none-musllinux_1_2_x86_64.whl:
Publisher:
wheels.yml on pashidl-lab/geofit-src
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
geofit-0.2.0-py3-none-musllinux_1_2_x86_64.whl -
Subject digest:
a4de057031eb60a540b5ac41dca2e1c67c76f3c3e691a8ac72feda93d28b1819 - Sigstore transparency entry: 2624085314
- Sigstore integration time:
-
Permalink:
pashidl-lab/geofit-src@aa5be1fd14a400b56dea0aad56a26a02c1328eb4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/pashidl-lab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
wheels.yml@aa5be1fd14a400b56dea0aad56a26a02c1328eb4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file geofit-0.2.0-py3-none-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: geofit-0.2.0-py3-none-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 1.3 MB
- Tags: Python 3, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
271190c9fc287c6c669cb07e78383f13689e82aa9d8a73e238af2ce3f1ff63af
|
|
| MD5 |
0914f2b63bb5acc0cd0b12385ca561ef
|
|
| BLAKE2b-256 |
793f051ccd7654e8d943c504bdf0b41f0f3dbabc9e2db9510983bbc5ac7a1fea
|
Provenance
The following attestation bundles were made for geofit-0.2.0-py3-none-musllinux_1_2_aarch64.whl:
Publisher:
wheels.yml on pashidl-lab/geofit-src
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
geofit-0.2.0-py3-none-musllinux_1_2_aarch64.whl -
Subject digest:
271190c9fc287c6c669cb07e78383f13689e82aa9d8a73e238af2ce3f1ff63af - Sigstore transparency entry: 2624085389
- Sigstore integration time:
-
Permalink:
pashidl-lab/geofit-src@aa5be1fd14a400b56dea0aad56a26a02c1328eb4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/pashidl-lab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
wheels.yml@aa5be1fd14a400b56dea0aad56a26a02c1328eb4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file geofit-0.2.0-py3-none-manylinux_2_28_x86_64.whl.
File metadata
- Download URL: geofit-0.2.0-py3-none-manylinux_2_28_x86_64.whl
- Upload date:
- Size: 504.2 kB
- Tags: Python 3, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
df6ca1ff6bc94ded389d8da03741bd6e10723255b20341a0a94a0df068733dd1
|
|
| MD5 |
cfb4eb3cd5ee2772be354ccd5c08d552
|
|
| BLAKE2b-256 |
904e4272c6e061dd64b6494a8b28a52cbea3469c5aa482b28dfc34286946ff46
|
Provenance
The following attestation bundles were made for geofit-0.2.0-py3-none-manylinux_2_28_x86_64.whl:
Publisher:
wheels.yml on pashidl-lab/geofit-src
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
geofit-0.2.0-py3-none-manylinux_2_28_x86_64.whl -
Subject digest:
df6ca1ff6bc94ded389d8da03741bd6e10723255b20341a0a94a0df068733dd1 - Sigstore transparency entry: 2624085704
- Sigstore integration time:
-
Permalink:
pashidl-lab/geofit-src@aa5be1fd14a400b56dea0aad56a26a02c1328eb4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/pashidl-lab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
wheels.yml@aa5be1fd14a400b56dea0aad56a26a02c1328eb4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file geofit-0.2.0-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl.
File metadata
- Download URL: geofit-0.2.0-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl
- Upload date:
- Size: 393.6 kB
- Tags: Python 3, manylinux: glibc 2.24+ ARM64, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
453628b9df374a0306ec0207fb4d508e5c272b044ba1195b245b2f03285a6bbe
|
|
| MD5 |
14feec56a49cb8aa132498f8f8e79b6d
|
|
| BLAKE2b-256 |
74aba1bb5ea18a747d88f221be63f304c7c757a3c839590c83a881e72aceb7ec
|
Provenance
The following attestation bundles were made for geofit-0.2.0-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl:
Publisher:
wheels.yml on pashidl-lab/geofit-src
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
geofit-0.2.0-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl -
Subject digest:
453628b9df374a0306ec0207fb4d508e5c272b044ba1195b245b2f03285a6bbe - Sigstore transparency entry: 2624085565
- Sigstore integration time:
-
Permalink:
pashidl-lab/geofit-src@aa5be1fd14a400b56dea0aad56a26a02c1328eb4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/pashidl-lab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
wheels.yml@aa5be1fd14a400b56dea0aad56a26a02c1328eb4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file geofit-0.2.0-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: geofit-0.2.0-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 393.7 kB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cb3366cd766f246b6bdb72c7ffc0b18705d0d35d4bc9eeb6180e5a75055be5f2
|
|
| MD5 |
06adfbdcd522bdccc66b30d38eb03318
|
|
| BLAKE2b-256 |
ad14c030686a814b12d927b22dad9e4c19c85af3dda367361fa6a039ad0ef567
|
Provenance
The following attestation bundles were made for geofit-0.2.0-py3-none-macosx_11_0_arm64.whl:
Publisher:
wheels.yml on pashidl-lab/geofit-src
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
geofit-0.2.0-py3-none-macosx_11_0_arm64.whl -
Subject digest:
cb3366cd766f246b6bdb72c7ffc0b18705d0d35d4bc9eeb6180e5a75055be5f2 - Sigstore transparency entry: 2624085464
- Sigstore integration time:
-
Permalink:
pashidl-lab/geofit-src@aa5be1fd14a400b56dea0aad56a26a02c1328eb4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/pashidl-lab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
wheels.yml@aa5be1fd14a400b56dea0aad56a26a02c1328eb4 -
Trigger Event:
push
-
Statement type: