Metadata-Version: 2.5
Name: sigorbit
Version: 0.2.3
Summary: Rotation-robust handwritten-signature embeddings with SO(2) canonicalization
Project-URL: Homepage, https://github.com/jordi-murgo/sigorbit
Project-URL: Repository, https://github.com/jordi-murgo/sigorbit.git
Project-URL: Documentation, https://github.com/jordi-murgo/sigorbit#readme
Project-URL: Issues, https://github.com/jordi-murgo/sigorbit/issues
Project-URL: Changelog, https://github.com/jordi-murgo/sigorbit/blob/main/CHANGELOG.md
Author-email: Jordi Murgó <jordi.murgo@gmail.com>
Maintainer-email: Jordi Murgó <jordi.murgo@gft.com>
License-Expression: MIT
License-File: LICENSE
License-File: NOTICE
Keywords: biometrics,embeddings,equivariant-cnn,rotation-invariance,signature
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Requires-Dist: e2cnn<0.3,>=0.2.3
Requires-Dist: numpy>=1.26
Requires-Dist: pillow>=12.3
Requires-Dist: scipy>=1.10
Requires-Dist: torch>=2.7
Provides-Extra: api
Requires-Dist: fastapi>=0.141.1; extra == 'api'
Requires-Dist: python-multipart>=0.0.32; extra == 'api'
Requires-Dist: uvicorn[standard]>=0.52.1; extra == 'api'
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Description-Content-Type: text/markdown

# SigOrbit

[![CI](https://github.com/jordi-murgo/sigorbit/actions/workflows/ci.yml/badge.svg)](https://github.com/jordi-murgo/sigorbit/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

**Rotation-robust handwritten-signature embeddings with continuous SO(2)
canonicalization and steerable C4/C8 backbones.**
Created and maintained by **Jordi Murgó**
([GitHub](https://github.com/jordi-murgo) ·
[jordi.murgo@gmail.com](mailto:jordi.murgo@gmail.com) ·
[jordi.murgo@gft.com](mailto:jordi.murgo@gft.com)).

SigOrbit turns a cropped signature image into a deterministic, L2-normalized
256-dimensional vector. It is designed for retrieval, clustering and downstream
verification systems that must tolerate arbitrary in-plane rotation without
making mirrored signatures equivalent.

> **Alpha release.** SigOrbit generates embeddings; it is not a complete fraud
> detector, legal signature verifier, document detector, identity database or
> anti-spoofing system. Do not use one model score as the sole basis for legal,
> financial or access-control decisions.

## Why the name?

A group action moves an input through its *orbit*. SigOrbit learns a canonical
SO(2) pose and embeds signatures consistently across that rotation orbit.
`sigorbit` was clear on PyPI and had no exact GitHub repository collision when
checked on 2026-08-07. This is not a trademark opinion.

## Architecture

![Mermaid diagram](https://mermaid.ink/svg/JSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJwcmltYXJ5Q29sb3IiOiIjZThmMGZlIiwicHJpbWFyeVRleHRDb2xvciI6IiMxYTFhMWEiLCJwcmltYXJ5Qm9yZGVyQ29sb3IiOiIjNGE3YWI1IiwibGluZUNvbG9yIjoiIzY2NjY2NiIsImJhY2tncm91bmQiOiIjZmZmZmZmIiwibWFpbkJrZyI6IiNmNWY4ZmMiLCJjbHVzdGVyQmtnIjoiI2ZhZmJmYyIsImNsdXN0ZXJCb3JkZXIiOiIjYzhkM2UwIn19fSUlCmZsb3djaGFydCBUQgogICAgc3ViZ3JhcGggSW5wdXRbIlByZXByb2Nlc3NpbmciXQogICAgICAgIElNR1siQ3JvcHBlZCBzaWduYXR1cmUgaW1hZ2UiXQogICAgICAgIEdSQVlbIkdyYXlzY2FsZSDihpIgMjU3w5cyNTcgYmljdWJpYyDihpIgWy0xLCAxXSJdCiAgICAgICAgSU1HIC0tPiBHUkFZCiAgICBlbmQKCiAgICBzdWJncmFwaCBDYW5vblsiT3JpZW50YXRpb25DYW5vbmljYWxpemVyIChTTygyKSkiXQogICAgICAgIGRpcmVjdGlvbiBUQgogICAgICAgIENDMVsiQ29udjJkIDHihpIxNiBrNSBzMiBwMjxici8-KyBSZUxVICsgQmF0Y2hOb3JtMmQiXQogICAgICAgIENDMlsiQ29udjJkIDE24oaSMzIgazUgczIgcDI8YnIvPisgUmVMVSArIEJhdGNoTm9ybTJkIl0KICAgICAgICBDQzNbIkNvbnYyZCAzMuKGkjY0IGszIHMyIHAxPGJyLz4rIFJlTFUgKyBCYXRjaE5vcm0yZCJdCiAgICAgICAgQ0NQWyJBZGFwdGl2ZUF2Z1Bvb2wyZCgxKSDihpIgRmxhdHRlbiJdCiAgICAgICAgQ0xJTlsiTGluZWFyIDY04oaSMjxici8-4oaSIChjb3MgzrgsIHNpbiDOuCksIEwyLW5vcm1hbGl6ZWQiXQogICAgICAgIENBRkZbImFmZmluZV9ncmlkICsgZ3JpZF9zYW1wbGU8YnIvPihiaWN1YmljLCByb3RhdGlvbiBvbmx5KSJdCiAgICAgICAgQ0MxIC0tPiBDQzIgLS0-IENDMyAtLT4gQ0NQIC0tPiBDTElOIC0tPiBDQUZGCiAgICBlbmQKCiAgICBzdWJncmFwaCBCYWNrYm9uZVsiU3RlZXJhYmxlRW5jb2RlciAoQzQgb3IgQzgtc3RlZXJhYmxlIENOTiwgZTJjbm4pIl0KICAgICAgICBkaXJlY3Rpb24gVEIKICAgICAgICBTVEVNWyJTdGVtPGJyLz5SMkNvbnYgMeKGkjI0wrdOIHJlZ3VsYXIgazcgcDM8YnIvPisgSW5uZXJCYXRjaE5vcm0gKyBSZUxVPGJyLz4rIEJsdXJQb29sIC8yIChOID0gZ3JvdXBfb3JkZXIpIl0KICAgICAgICBMMVsiTGF5ZXIgMSAoLzQpPGJyLz5SMkNvbnYgMjTihpI0OCBrNSBwMiArIElCTiArIFJlTFU8YnIvPlIyQ29udiA0OOKGkjQ4IGs1IHAyICsgSUJOICsgUmVMVTxici8-KyBCbHVyUG9vbCAvMiJdCiAgICAgICAgTDJbIkxheWVyIDIgKC84KTxici8-UjJDb252IDQ44oaSOTYgazUgcDIgKyBJQk4gKyBSZUxVPGJyLz5SMkNvbnYgOTbihpI5NiBrNSBwMiArIElCTiArIFJlTFU8YnIvPisgQmx1clBvb2wgLzIiXQogICAgICAgIEwzWyJMYXllciAzICgvMTYpPGJyLz5SMkNvbnYgOTbihpIxMjggazUgcDIgKyBJQk4gKyBSZUxVPGJyLz5SMkNvbnYgMTI44oaSMTI4IGs1IHAyICsgSUJOICsgUmVMVTxici8-KyBCbHVyUG9vbCAvMiJdCiAgICAgICAgR1BbIkdyb3VwUG9vbGluZzxici8-bWF4IG92ZXIgQzQvQzggZmliZXI8YnIvPuKGkiAxMjggaW52YXJpYW50IGNoYW5uZWxzIl0KICAgICAgICBTVEVNIC0tPiBMMSAtLT4gTDIgLS0-IEwzIC0tPiBHUAogICAgZW5kCgogICAgc3ViZ3JhcGggSGVhZFsiRW1iZWRkaW5nIGhlYWQiXQogICAgICAgIGRpcmVjdGlvbiBUQgogICAgICAgIFBPT0xbIkFkYXB0aXZlQXZnUG9vbDJkKDEpPGJyLz7ihpIgRmxhdHRlbiDihpIgMTI4Il0KICAgICAgICBGQzFbIkxpbmVhciAxMjjihpI1MTI8YnIvPisgQmF0Y2hOb3JtMWQgKyBSZUxVPGJyLz4rIERyb3BvdXQgMC4zIl0KICAgICAgICBGQzJbIkxpbmVhciA1MTLihpIyNTY8YnIvPisgQmF0Y2hOb3JtMWQiXQogICAgICAgIE5PUk1bIkwyLW5vcm1hbGl6ZSJdCiAgICAgICAgUE9PTCAtLT4gRkMxIC0tPiBGQzIgLS0-IE5PUk0KICAgIGVuZAoKICAgIEdSQVkgLS0-IENDMQogICAgQ0FGRiAtLT4gU1RFTQogICAgR1AgLS0-IFBPT0wKICAgIE5PUk0gLS0-IE9VVFsiMjU2LUQgTDItbm9ybWFsaXplZCBlbWJlZGRpbmciXQoKICAgIHN0eWxlIElucHV0IGZpbGw6IzFhMmEzYyxjb2xvcjojZmZmCiAgICBzdHlsZSBDYW5vbiBmaWxsOiMxYTNhNWMsY29sb3I6I2ZmZgogICAgc3R5bGUgQmFja2JvbmUgZmlsbDojMmQ1YTJkLGNvbG9yOiNmZmYKICAgIHN0eWxlIEhlYWQgZmlsbDojNGEzYTFjLGNvbG9yOiNmZmYKICAgIHN0eWxlIE9VVCBmaWxsOiM1YzFhMWEsY29sb3I6I2ZmZgo)
- C4: 2,254,466 trainable parameters (2.2 M); C8: 4,276,354 (4.3 M)
- 257×257 grayscale input
- 256-dimensional float32 output
- continuous SO(2) rotation canonicalization; no scale or reflection canonicalization
- C4 or C8 regular representations and invariant group pooling; the canonicalizer
  handles continuous rotation, so C4 equivariance (90° symmetry) is sufficient for
  signatures and trains 2.7× faster

See [the architecture notes](https://github.com/jordi-murgo/sigorbit/blob/main/docs/ARCHITECTURE.md) and
[model card](https://github.com/jordi-murgo/sigorbit/blob/main/docs/MODEL_CARD.md).

## Training and model lineage

The runtime package owns the exact model and preprocessing classes. The companion
[`sigorbit-trainer`](https://github.com/jordi-murgo/sigorbit-trainer) package
imports those classes directly rather than maintaining a second architecture.
Its current from-scratch protocol has three stages:

1. train the C4 or C8 backbone and a temporary ArcFace classifier;
2. restore the best backbone, freeze it, and pretrain only the SO(2)
   canonicalizer against known synthetic angles;
3. jointly fine-tune canonicalizer, backbone and ArcFace head with identity,
   circular-orientation and embedding-consistency losses.

The ArcFace head exists only during training and is not part of an exported
encoder. The published package still identifies the historically selected
checkpoint as `sigorbit-c8-257-v1`; that checkpoint used an older C8 initializer
whose complete resume history was not archived. The auditable trainer produces
two from-scratch model IDs: `sigorbit-c8-257-retrained-v1` (C8, batch 32) and
`sigorbit-c4-257-b64` (C4, batch 64). Neither claims a byte-for-byte
reproduction of the deployed model. See [training and reproducibility](https://github.com/jordi-murgo/sigorbit/blob/main/docs/TRAINING.md)
for all lineages.

## Install

Python 3.10+ is supported. Install the correct PyTorch build for your CPU, CUDA
or ROCm platform first, then install SigOrbit:

```bash
python -m pip install sigorbit
# Optional FastAPI example:
python -m pip install "sigorbit[api]"
```

For an unreleased commit, installation directly from GitHub is also supported:

```bash
python -m pip install "sigorbit @ git+https://github.com/jordi-murgo/sigorbit.git@main"
```

For NVIDIA/ROCm, follow the official PyTorch selector rather than relying on the
CPU wheel chosen by a generic resolver.

## Python API

```python
from sigorbit import SignatureEncoder

encoder = SignatureEncoder(
    checkpoint="/secure/path/sigorbit-c8-257-v1.pt",
    device="auto",
)
vector = encoder.embed("signature.png")

print(vector.shape)  # (256,)
print(vector.dtype)  # float32
print(float(vector @ vector))  # ~1.0
print(encoder.model_id)  # sigorbit-c8-257-v1
print(encoder.preprocess_version)  # sigorbit-gray-square-257-v1
```

Batch inference preserves order:

```python
vectors = encoder.embed_batch(["a.png", "b.png"], batch_size=16)
assert vectors.shape == (2, 256)
```

Accepted inputs are `PIL.Image`, NumPy arrays, encoded bytes and file paths.
Inputs must already be cropped signatures. **Never compare embeddings generated
by different `model_id` or `preprocess_version` values.**

## FastAPI example

```bash
pip install -e '.[api]'
export SIGORBIT_CHECKPOINT=/secure/path/sigorbit-c8-257-v1.pt
export SIGORBIT_CHECKPOINT_SHA256=ec8d99f887f5a2658d93b14a14911b29a1411e9cf142efa85862a47b30cd233e
export SIGORBIT_API_KEY=replace-with-a-secret-from-your-secret-manager
SIGORBIT_DEVICE=auto sigorbit-api
```

Then, from an authorized client:

```bash
curl http://127.0.0.1:8000/health
curl -X POST http://127.0.0.1:8000/embed \
  -H "Authorization: Bearer $SIGORBIT_API_KEY" \
  -F 'file=@signature.png'
```

The response includes the model/preprocess identity, predicted canonicalization
angle and 256 normalized floats. Interactive docs are at `/docs`.

Configuration:

| Variable | Default | Meaning |
|---|---|---|
| `SIGORBIT_CHECKPOINT` | required in code-only release | Local approved checkpoint path |
| `SIGORBIT_CHECKPOINT_SHA256` | required by HTTP API | Expected approved artifact digest |
| `SIGORBIT_DEVICE` | `auto` | `cpu`, `cuda`, `cuda:0`, etc. |
| `SIGORBIT_API_KEY` | unset on loopback | Bearer token; required by CLI for non-loopback binding |
| `SIGORBIT_HOST` | `127.0.0.1` | Bind address |
| `SIGORBIT_PORT` | `8000` | Bind port |
| `SIGORBIT_MAX_UPLOAD_BYTES` | `10485760` | Encoded upload-file byte limit |
| `SIGORBIT_MAX_REQUEST_BYTES` | upload limit + 65536 | Pre-multipart request-body limit |
| `SIGORBIT_MAX_IMAGE_PIXELS` | `4194304` | Decompression-bomb pixel limit |
| `SIGORBIT_MAX_CONCURRENT_REQUESTS` | `1` | Full concurrent `/embed` request capacity |
| `SIGORBIT_QUEUE_TIMEOUT_SECONDS` | `1.0` | Wait before returning HTTP 503 |

Uploaded filenames are ignored: the API passes bytes to the decoder and never
constructs or writes a filesystem path from client input. Encoded inputs are
restricted to PNG, JPEG and WebP. The in-process Python API deliberately accepts
trusted `str`/`Path` inputs, so applications must not forward a remote filename
to `SignatureEncoder.embed()`.

See the [security review](https://github.com/jordi-murgo/sigorbit/blob/main/docs/SECURITY_REVIEW.md) for the threat model,
adversarial tests and residual risks.

The server binds to loopback by default. Before binding to a non-loopback
address, configure bearer authentication and place it behind a TLS reverse proxy
with request/time/rate limits. The built-in limits reduce accidental and simple
resource exhaustion; they are not a replacement for an edge proxy or process
isolation.

The example intentionally has no signer database and no hard-coded MATCH
threshold. Thresholds are catalogue-, domain- and reference-count-specific.

## Measured behavior

On the held-out 33-signer/792-image **BHSig260 Bengali** test split, the
257px checkpoint achieved:

- clean leave-one-out top-1: **100.0% (792/792)**;
- clean median margin: **+0.3196**;
- model-canvas non-zero-angle mean top-1: **98.04%**;
- square-expand mean top-1: **98.16%**;
- raw-expand mean top-1: **89.87%**;
- real-signature all-triplet mean top-1: **87.64%**.

These numbers describe one dataset/protocol, not a universal error rate. See the
model card for exact protocols, limitations and the 129px comparison.

## Development

```bash
python -m pip install -e '.[api,dev]'
ruff check .
pytest
python -m build
```

Repository layout:

```text
src/sigorbit/model.py          architecture
src/sigorbit/checkpoint.py     safe artifact loading and identity
src/sigorbit/preprocessing.py  versioned image contract
src/sigorbit/encoder.py        public Python API
src/sigorbit/api.py            minimal FastAPI example
docs/                          architecture, model card, training and release notes
CITATION.bib                   project and technical bibliography
```

## Responsible use and release status

Handwritten signatures are biometric personal data. Do not log uploads or
embeddings unnecessarily, and establish retention, consent, access control and
deletion policies before deployment.

The source code is MIT licensed. The repository contains no training images.
Before publishing the trained checkpoint, read the completed
[dataset licence audit](https://github.com/jordi-murgo/sigorbit/blob/main/docs/DATASET_LICENSE_AUDIT.md) and complete
[RELEASING.md](https://github.com/jordi-murgo/sigorbit/blob/main/docs/RELEASING.md). All 7,560 aggregate images were matched
pixel-for-pixel to genuine CEDAR and BHSig260 samples. CEDAR has no explicit
licence grant, while the BHSig260 authors state only “for research purposes”. The Hub uploader's MIT tag therefore
does not establish a sublicence chain. The local checkpoint is present to
validate this extraction; **do not push or publish it** until upstream permission
or counsel approval is recorded.

## License and citation

Source code: [MIT](https://github.com/jordi-murgo/sigorbit/blob/main/LICENSE). Third-party notices: [NOTICE](https://github.com/jordi-murgo/sigorbit/blob/main/NOTICE).
Repository: <https://github.com/jordi-murgo/sigorbit>.

If SigOrbit is useful in research, cite the software as:

```bibtex
@software{murgo2026sigorbit,
  author  = {Jordi Murgó},
  title   = {{SigOrbit}: Rotation-Robust Handwritten-Signature Embeddings},
  year    = {2026},
  version = {0.1.0},
  url     = {https://github.com/jordi-murgo/sigorbit},
  license = {MIT}
}
```

The complete BibTeX bibliography—including the e2cnn, CEDAR and BHSig260
references—is in [`CITATION.bib`](https://github.com/jordi-murgo/sigorbit/blob/main/CITATION.bib); machine-readable citation
metadata is in [`CITATION.cff`](https://github.com/jordi-murgo/sigorbit/blob/main/CITATION.cff). Citing a dataset does not imply
that it has an open licence; consult the
[dataset licence audit](https://github.com/jordi-murgo/sigorbit/blob/main/docs/DATASET_LICENSE_AUDIT.md).
