Skip to main content

geofit

Rotation- and scale-invariant shape matching for machine vision — find a part at any angle, under changing illumination, with pieces of it missing.

PyPI Python Platform License

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.

driver bits

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:

retaining clips

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):

bundled sample


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 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

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

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

geofit-0.1.1-py3-none-win_amd64.whl (381.9 kB view details)

Uploaded Python 3Windows x86-64

geofit-0.1.1-py3-none-win32.whl (369.0 kB view details)

Uploaded Python 3Windows x86

geofit-0.1.1-py3-none-musllinux_1_2_x86_64.whl (1.4 MB view details)

Uploaded Python 3musllinux: musl 1.2+ x86-64

geofit-0.1.1-py3-none-musllinux_1_2_aarch64.whl (1.3 MB view details)

Uploaded Python 3musllinux: musl 1.2+ ARM64

geofit-0.1.1-py3-none-manylinux_2_28_x86_64.whl (490.1 kB view details)

Uploaded Python 3manylinux: glibc 2.28+ x86-64

geofit-0.1.1-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl (379.5 kB view details)

Uploaded Python 3manylinux: glibc 2.24+ ARM64manylinux: glibc 2.28+ ARM64

geofit-0.1.1-py3-none-macosx_11_0_arm64.whl (379.6 kB view details)

Uploaded Python 3macOS 11.0+ ARM64

File details

Details for the file geofit-0.1.1-py3-none-win_amd64.whl.

File metadata

  • Download URL: geofit-0.1.1-py3-none-win_amd64.whl
  • Upload date:
  • Size: 381.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

Hashes for geofit-0.1.1-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 104e943cb33b50162f8c3182eebede60ac42aa7cf619f4099777b8c0e1d33a48
MD5 662e484aa6d9a0ebe5491967eb1b0bbb
BLAKE2b-256 2ebbe0b390c9676064c120ec25cf9b67348e4729d9f414b24d6a0e2d5fed30a2

See more details on using hashes here.

Provenance

The following attestation bundles were made for geofit-0.1.1-py3-none-win_amd64.whl:

Publisher: wheels.yml on pashidl-lab/geofit-src

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file geofit-0.1.1-py3-none-win32.whl.

File metadata

  • Download URL: geofit-0.1.1-py3-none-win32.whl
  • Upload date:
  • Size: 369.0 kB
  • Tags: Python 3, Windows x86
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for geofit-0.1.1-py3-none-win32.whl
Algorithm Hash digest
SHA256 13f11b8e5008cddb1d5d1d6e0fa00a1597f37e6b3b96f24fd663b1637f9b7503
MD5 e0955908a9821941a3893d0ecdfc3659
BLAKE2b-256 5ace2c7d24cd72fd0ed09053068930f2762362eb8eaddca717dd8e0c18c3e3c5

See more details on using hashes here.

Provenance

The following attestation bundles were made for geofit-0.1.1-py3-none-win32.whl:

Publisher: wheels.yml on pashidl-lab/geofit-src

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file geofit-0.1.1-py3-none-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for geofit-0.1.1-py3-none-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 db8b3e61aac26eedfb1b59b826424bf7e31c74b8b407cf806611496b55c93af2
MD5 d2ff0a61c2b24c2b6fc86694f4ec2761
BLAKE2b-256 d939e054856bcb224350f4c1694abb195e2e4b6a74fd3f8b440dee5d98cfdc72

See more details on using hashes here.

Provenance

The following attestation bundles were made for geofit-0.1.1-py3-none-musllinux_1_2_x86_64.whl:

Publisher: wheels.yml on pashidl-lab/geofit-src

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file geofit-0.1.1-py3-none-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for geofit-0.1.1-py3-none-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 f1e2cb23054e6b622113e8e3a60b1577ff48701a3e57140596098f52c09c8562
MD5 3ad27c2396cb429707a412fc68e07dc8
BLAKE2b-256 1c4c036c56496804cbef51b759862a54e83243ac83a3c4b1e73c1d9c2937293f

See more details on using hashes here.

Provenance

The following attestation bundles were made for geofit-0.1.1-py3-none-musllinux_1_2_aarch64.whl:

Publisher: wheels.yml on pashidl-lab/geofit-src

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file geofit-0.1.1-py3-none-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for geofit-0.1.1-py3-none-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 4262fc5de07fd01217f9c96f64f9d0be59147aa6528d9e257c36632487236b02
MD5 c9b563d4b195848dc1b958d10faeca40
BLAKE2b-256 958ef9f1f084b11ec7c1c3fc3ada06c032b7fdcef1537121a1e16a031e363acf

See more details on using hashes here.

Provenance

The following attestation bundles were made for geofit-0.1.1-py3-none-manylinux_2_28_x86_64.whl:

Publisher: wheels.yml on pashidl-lab/geofit-src

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file geofit-0.1.1-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for geofit-0.1.1-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 8d327317a2f31d3699e0a80a8c21b0647be20b4a3c7444b746f3247a2f74b721
MD5 bc4495cc5d323641ba98104c66a05045
BLAKE2b-256 e6552e3f39c185799e95d0cafa9d6e147fec09dcb952ba87c93d534f1310ccce

See more details on using hashes here.

Provenance

The following attestation bundles were made for geofit-0.1.1-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl:

Publisher: wheels.yml on pashidl-lab/geofit-src

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file geofit-0.1.1-py3-none-macosx_11_0_arm64.whl.

File metadata

  • Download URL: geofit-0.1.1-py3-none-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 379.6 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

Hashes for geofit-0.1.1-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 53dad78235887b7eaaafe569025298ef0b86af82b8271b603f3954a23ffccac4
MD5 6cc62d1ddbeba6fac041562a7eefb4a5
BLAKE2b-256 34d059cb826cd2afb611ecd9211d70ede85eaafa99a093e1212b22e8c23b80a6

See more details on using hashes here.

Provenance

The following attestation bundles were made for geofit-0.1.1-py3-none-macosx_11_0_arm64.whl:

Publisher: wheels.yml on pashidl-lab/geofit-src

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.2.0

7 files

This release

0.1.1 This release

7 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page