BlitzID
Read identity documents from images: find and compare faces, read printed text, parse the machine-readable zone, and decode the PDF417 barcode on US and Canadian driver's licenses, in one typed Python library that runs on an ordinary CPU.
No GPU and no CUDA stack: inference runs on onnxruntime's CPU provider, face detection takes about 4 ms per image on a laptop, and the weights (SCRFD-2.5G for faces, ArcFace for face verification, PP-OCR for text) download on first use. Faces, ID photos, and document scans are biometric PII, so the pipeline never sends them anywhere. Everything runs in your process, and the optional HTTP API keeps uploads and results in RAM only.
Why blitzid
- One install, one API. Face detection, OCR, MRZ parsing, PDF417 barcode reading, and document crop/QC behind a single typed surface, instead of stitching a detector, an OCR engine, and your own MRZ code together.
- Validation over guessing. Every MRZ field is gated by ICAO 9303
check digits. Obvious OCR confusions (0/O, 1/I, …) are undone by
each field's alphabet before validation, and a zone that still does
not validate raises
MRZErrornaming the failing field, so a wrong-but-plausible document number never comes back from a fuzzy "correction". - CPU is the target, not a fallback. The provider is pinned to
CPUExecutionProvider; there is nothing to install for accelerators and nothing to fall back from. What runs on your laptop runs the same in a container. - Ships as a service. The same analyses come up as an HTTP job
queue with
docker compose up, with a synchronous document-QC endpoint in front of them. - Typed and tested.
py.typed, mypy strict, and a pytest suite: pure-function tests run without models or network, behavioural tests use the committed specimen fixtures.
Installation
pip install blitzid # or: uv add blitzid
pip install blitzid[ocr] # with OCR text reading (RapidOCR)
pip install blitzid[barcode] # with PDF417/AAMVA barcode reading (zxing-cpp)
pip install blitzid[api] # with the HTTP API server
Note: under plain pip, RapidOCR pulls in the GUI opencv-python
alongside this package's opencv-python-headless (both provide cv2).
Prefer uv, which overrides the duplicate away. With pip,
uninstalling opencv-python afterwards keeps a working headless cv2.
Quick start
Detect faces. Output is (x, y, w, h, confidence) tuples, or Face
records with 5-point landmarks:
from blitzid import FaceDetectorDNN
detector = FaceDetectorDNN(confidence_threshold=0.5)
faces = detector.detect_face("images/nl_td1_id_specimen.jpg")
# [(49, 91, 84, 116, 0.82)]
records = detector.detect_face_landmarks("images/nl_td1_id_specimen.jpg")
# [Face(bbox=(49, 91, 84, 116), confidence=0.82,
# landmarks=((71.4, 138.2), (111.8, 138.2), (91.2, 164.7),
# (74.1, 179.9), (108.6, 180.2)))]
Read the machine-readable zone of a document (blitzid[ocr]). Every
check digit is validated before you see a field:
from blitzid import MRZReader
record = MRZReader().read("images/nl_td1_id_specimen.jpg")
print(record.mrz_type, record.document_number, record.surname)
# TD1 SPECI2014 DE BRUIJN
The face runs in single-digit milliseconds on CPU; the OCR pass behind the MRZ takes about a second. Full timings under Performance.
Compare two faces — the document portrait against a fresh selfie, for example (1:1 verification, same pipeline DeepFace-style tools use):
from blitzid import FaceVerifier
verifier = FaceVerifier(threshold=0.4)
result = verifier.verify("id_portrait.jpg", "selfie.jpg")
print(result.verified, f"{result.similarity:.3f}")
# True 0.612
embeddings = verifier.embed("id_portrait.jpg"), verifier.embed("selfie.jpg")
FaceVerifier.similarity(*embeddings) # cosine of the two vectors
What it does
Face detection. SCRFD-2.5G (InsightFace buffalo_m ONNX weights)
through onnxruntime CPU: boxes with confidence, 5-point landmarks
(right eye, left eye, nose tip, mouth corners), padded face crops, NMS
and size filtering, an optional multi-scale pass for small faces, a
content-hash LRU cache, batch processing, and bbox visualization.
Presets: create_fast_detector, create_accurate_detector,
create_balanced_detector.
Face verification. FaceVerifier (1:1 comparison) embeds faces
with ArcFace (InsightFace buffalo_m recognition weights) after
aligning them to the canonical template by their five SCRFD landmarks,
then compares the L2-normalized vectors by cosine similarity against a
decision threshold (default 0.4; 0.35–0.5 is the sensible operating
range, higher is stricter). verify() detects and uses the
highest-confidence face of each image, verify_faces() reuses
already-detected Face records, embed() returns a reusable vector,
and similarity() is the cosine of two embeddings. Same photo, or
re-encoded/rescaled copy? Verification of an image against itself
scores ~1.0; different people land near 0.
Text reading. RapidOCRReader (the ocr extra) returns text lines
as OCRText records: bbox, text, confidence, sorted by confidence.
MRZ parsing. MRZReader (same extra) selects the ICAO 9303 zone
from OCR output, reassembles the lines by position, validates every
check digit plus the letter-only fields (document code, issuer,
nationality), and parses TD1 (3x30), TD2 (2x36), and TD3 (2x44) into an
MRZRecord. Document numbers longer than their field follow the ICAO
convention (overflow at the start of optional_data1, no printed check
digit).
PDF417 barcode reading. BarcodeReader (the barcode extra,
zxing-cpp) decodes the PDF417 code on the back of US and Canadian
driver's licenses and parses the AAMVA DL/ID payload into a typed
BarcodeRecord — jurisdiction, AAMVA version, names, dates, sex, and
license number, plus the raw jurisdiction-specific extras. The header's
subfile offsets, lengths, and number-of-entries count are validated and
the mandatory fields are gated, so a malformed payload raises
BarcodeError naming the offending field.
Structured extraction. StructuredOCRReader (same extra) feeds the
OCR lines to an OpenAI-compatible chat endpoint (e.g. a local vLLM
server) and returns a typed StructuredOCR record; fields the model
cannot recover are omitted instead of hallucinated, date fields come
back as datetime.date, text values are upper-cased by the reader, and
the document_type is a closed vocabulary (id, plate, mrz,
unknown). Endpoint via BLITZID_LLM_BASE_URL / BLITZID_LLM_API_KEY
/ BLITZID_LLM_MODEL. Tracing is opt-in via the standard Langfuse SDK
variables (LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY, optionally
LANGFUSE_BASE_URL): every LLM call is exported as an OpenTelemetry
trace. Note the spans carry the OCR text (names, document numbers,
dates) — point LANGFUSE_BASE_URL at an instance you control.
Document crop and QC. DocumentCropper (classical CV, no model)
finds the document quadrilateral, warps it flat at its own aspect, and
quality-checks the crop: document found, aspect ratio near ID-1/ID-3,
resolution, sharpness, brightness, and a side-aware face-presence check
(front without a face warns; a back side never fails for missing
face). Returns the crop plus a QualityReport verdict.
HTTP API and Docker. The api extra adds a FastAPI service: an
async job queue (Redis, persistence disabled) for
face/attributes/antispoof/OCR/MRZ/barcode/structured/consistency,
synchronous /crop (document QC), /verify (1:1 face comparison), and
/liveness (challenge-response liveness), and /health for
orchestration.
docker compose up runs it with all weights baked in.
Inputs. File paths, NumPy arrays, and PIL Images.
HTTP API
The api extra (pip install blitzid[api]) adds an HTTP service over
the library: async jobs for the analyses, synchronous document-QC and
face-verification endpoints, and a health check. Docker is the primary
deployment: the image bakes all weights for instant cold start and
talks to a RAM-only Redis. docker-compose.yml
is deploy-ready for Coolify (set the BLITZID_LLM_* variables in the
service environment). A step-by-step identity-onboarding guide
lives in docs/integration.md.
docker compose up
curl http://localhost:8000/api/health
POST /analyze submits a job (multipart/form-data): an image
file (any OpenCV-decodable format; PDFs get a precise 415) and
types (repeated and/or comma-separated: face, attributes,
antispoof, ocr,
mrz, barcode, structured, consistency — attributes predicts
age group, gender, and race per face, antispoof scores live vs
printed vs screen photos, barcode decodes the PDF417/AAMVA payload
from a driver's-license back, structured rebuilds the OCR text into a
typed record via the LLM, and consistency cross-checks that printed
record against the machine-readable zone and the portrait's age band
and gender against the birth date and sex; the photo rows need the
attributes engine and the last two the
BLITZID_LLM_* config).
Validation is eager, before the job exists: 422 unknown type or
missing fields, 400 undecodable bytes or an analysis whose engine is
unavailable (ocr/mrz without the ocr extra, barcode without the
barcode extra, structured/consistency without
the extra or the LLM config), 413 above the
upload cap, 503 + Retry-After when the queue is full or Redis is
down. Accepted jobs return 202:
curl -F image=@id.jpg -F types=face,ocr localhost:8000/api/analyze
# {"job_id": "<uuid>", "status": "queued", "status_url": "/jobs/<uuid>"}
GET /jobs/{job_id} polls: {"status": "queued"} / running; done
returns 200 with one section per requested type (face,
attributes, antispoof, ocr,
mrz, barcode, structured, consistency; a failing section carries
{"error": ...} while
the others still return). The read claims the result, so every later
read gets 410 Gone; unknown ids get 404 (an expired job reads 410 until
twice the TTL past submission, then 404); results expire after
BLITZID_API_JOB_TTL_SECONDS (default 900).
What a done result looks like on the specimen ID (abbreviated):
{
"status": "done",
"face": {
"faces": [
{
"bbox": [49, 91, 84, 116],
"confidence": 0.82,
"landmarks": [[71.4, 138.2], [111.8, 138.2], [91.2, 164.7], [74.1, 179.9], [108.6, 180.2]],
"crop_base64": "/9j/4AAQSkZJRgABAQAAAQAB…"
}
],
"processing_time_ms": 39.1
},
"attributes": {
"faces": [{"bbox": [49, 91, 84, 116], "confidence": 0.82,
"age": "20-29", "gender": "Female",
"race": "White", "age_confidence": 0.6,
"gender_confidence": 0.95, "race_confidence": 0.7}],
"processing_time_ms": 96.4
},
"ocr": {
"lines": [{"bbox": [20, 359, 70, 17], "text": "Specimen", "confidence": 1.0}, …],
"processing_time_ms": 1126.9
},
"mrz": {
"record": {
"mrz_type": "TD1", "document_code": "I<", "issuer": "NLD",
"document_number": "SPECI2014", "birth_date": "650310", "sex": "F",
"expiry_date": "240309", "nationality": "NLD",
"surname": "DE BRUIJN", "given_names": "WILLEKE LISELOTTE",
"optional_data1": "999999…", "optional_data2": ""
},
"processing_time_ms": 1059.4
},
"structured": {
"record": {
"document_type": "id",
"fields": [
{"name": "surname", "value": "DE BRUIJN", "confidence": 0.95},
{"name": "date_of_birth", "value": "1965-03-10", "confidence": 0.9}
],
"raw_text": "Specimen\nDE BRUIJN\n…"
},
"processing_time_ms": 2450.3
},
"consistency": {
"comparisons": [
{"field": "document_number", "verdict": "match",
"mrz_value": "SPECI2014", "printed_value": "SPECI2014",
"printed_confidence": 0.95},
{"field": "date_of_birth", "verdict": "match",
"mrz_value": "650310", "printed_value": "1965-03-10",
"printed_confidence": 0.9}
],
"photo_comparisons": [
{"field": "age", "verdict": "match", "document_value": "61",
"photo_value": "60-69", "photo_confidence": 0.55},
{"field": "sex", "verdict": "match", "document_value": "F",
"photo_value": "Female", "photo_confidence": 0.97}
],
"consistent": true,
"processing_time_ms": 4871.0
}
}
On a US driver's-license back, a barcode section looks like this:
{
"record": {
"jurisdiction": "636014",
"aamva_version": "08",
"jurisdiction_version": "00",
"family_name": "PUBLIC",
"given_name": "JOHN",
"middle_name": "QUINCY",
"birth_date": "1989-01-24",
"issue_date": "2013-06-04",
"expiry_date": "2035-01-31",
"sex": "M",
"license_number": "D12345678",
"height": "069 in",
"extra_tags": {"ZCB": "…"},
"symbology": "PDF417"
},
"processing_time_ms": 87.2
}
POST /crop is synchronous document QC (contour detection plus one
SCRFD pass runs in tens of ms): the perspective-corrected canonical
crop plus quality checks (document_found, aspect_ratio,
resolution, sharpness, brightness, and the side-aware
face_present, which never fails a document). Optional side field
(front / back / unknown):
curl -F image=@id.jpg localhost:8000/api/crop
# {"quad": [...], "crop_base64": "...", "width": 856, "height": 540,
# "side": "unknown", "face_found": true, "checks": {...},
# "verdict": "pass"}
The crop is the canonical image to keep and to re-submit to /analyze
for cleaner OCR/MRZ.
POST /verify is synchronous 1:1 face comparison: multipart fields
image1 and image2 (same upload rules as /analyze) and two
optional knobs — threshold (-1.0–1.0, overriding the service
default 0.4) and face1_bbox / face2_bbox (x,y,w,h pinning
which face to compare, e.g. a box from a prior /analyze face result;
handy when a document carries a ghost portrait). Unpinned images use
the highest-confidence face. The chosen faces are aligned to the
ArcFace template and compared by cosine similarity; an image with no
usable face is 422. The response is the verdict plus the evidence
used: each compared face with its bbox, confidence, a
crop_base64 thumbnail, and alternatives listing the candidates
that were not picked. The verifier (SCRFD + ArcFace weights) loads at
service startup alongside the other engines and shows as verify in
/health, so the first call carries no warm-up at the cost of the
extra weights in every deployment's memory:
curl -F image1=@id.jpg -F image2=@selfie.jpg localhost:8000/api/verify
# {"verified": true, "similarity": 0.71, "threshold": 0.4,
# "face1": {"bbox": [49, 91, 84, 116], "confidence": 0.82,
# "crop_base64": "/9j/4AAQ…", "alternatives": []},
# "face2": {"bbox": [402, 90, 470, 560], "confidence": 0.97,
# "crop_base64": "/9j/4AAQ…", "alternatives": []},
# "processing_time_ms": 241.5, "backend": "ONNXRuntime"}
GET /health reports engine, Redis, and job availability for
container orchestration. It returns 503 when the job store is
unreachable (the container is unhealthy: it cannot accept or process
jobs); a missing engine stays 200 with its models flag false, since
submitting that analysis yields a precise 400.
It also returns version from the installed blitzid package and revision
from the image's BLITZID_BUILD_REVISION build argument (the Git commit SHA in
release images, null when unspecified). OpenAPI reports the same package
version. Engine flags indicate initialization, not an inference or LLM
connectivity check.
Privacy: uploads and results live in RAM end to end. Job payloads are
stored as raw image bytes (never base64-inflated), Redis runs with
persistence disabled, results are claim-once and TTL-bounded, and the
service itself never writes to disk. Worst-case Redis memory is bounded
by the upload cap times the queue and TTL windows. Knobs live in
.env.example (BLITZID_API_REDIS_URL,
BLITZID_API_MAX_UPLOAD_MB, BLITZID_API_JOB_TTL_SECONDS,
BLITZID_API_MAX_QUEUED_JOBS, BLITZID_API_MAX_CONCURRENT_JOBS,
BLITZID_API_JOB_LEASE_SECONDS). Every v* tag push publishes
ghcr.io/malvavisc0/blitzid (amd64), gated on the full CI gate plus a
compose smoke test; new GHCR packages are private by default, so make
the package public or docker login ghcr.io to pull.
API reference
All public symbols are exported from blitzid.
FaceDetectorDNN
Defined in face/detector.py. The primary SCRFD-based detector (CPU inference via onnxruntime).
| Method | Description |
|---|---|
detect_face(image_input) |
Returns list[(x, y, w, h, confidence)] |
detect_face_landmarks(image_input) |
Returns list[Face] with 5-point landmarks |
detect_face_with_metrics(image_input) |
Returns a DetectionMetrics dataclass |
detect_from_array(img) |
Detect from an already-loaded array; returns (faces, processing_time, cache_hit) |
extract_faces(image_input, padding=0.2) |
Cropped face arrays with bounding boxes |
visualize_detections(image_input, ...) |
Draw boxes on image; optionally save to disk |
detect_faces_batch(image_paths) |
Process multiple images |
clear_cache() / get_cache_size() |
Manage the optional LRU cache |
Factory presets (classmethods):
create_fast_detector()— high confidence threshold, 480x480 inference, caching enabledcreate_accurate_detector()— low threshold, small min face sizecreate_balanced_detector()— middle ground with caching
Face
Frozen dataclass defined in face/_face.py, returned by detect_face_landmarks.
| Field | Type | Description |
|---|---|---|
bbox |
(x, y, w, h) |
Bounding box in image pixels |
confidence |
float |
Detection score in [0, 1] |
landmarks |
tuple[(x, y), ...] |
Right eye, left eye, nose tip, right mouth corner, left mouth corner; subpixel floats |
DetectionMetrics
Dataclass defined in face/_face.py.
| Field | Type | Description |
|---|---|---|
faces |
list[Face] |
Detected face records |
processing_time |
float |
Inference time in seconds |
image_size |
(w, h) |
Input image dimensions |
backend |
str |
"ONNXRuntime" |
num_faces |
int |
Number of faces detected |
cache_hit |
bool |
Whether the result came from cache |
FaceVerifier
Defined in face/verifier.py. 1:1 face
verification: ArcFace embeddings (InsightFace buffalo_m recognition
weights, CPU inference via onnxruntime) over landmark-aligned faces,
compared by cosine similarity.
| Method | Description |
|---|---|
verify(image_input1, image_input2) |
Detects the best face in each image; returns a VerificationResult |
verify_faces(image_input1, face1, image_input2, face2) |
Verifies two already-detected Face records (no re-detection) |
embed(image_input, face=None) |
L2-normalized 512-d ArcFace embedding; detects the best face when face is omitted |
similarity(embedding1, embedding2) |
Cosine similarity of two embeddings (static) |
The constructor takes a threshold (default 0.4; verified when
similarity >= threshold), an optional detector (a FaceDetectorDNN
to reuse instead of the internal default), model_dir, and
allow_downloads. When model_dir is omitted, the ArcFace weights are
looked up in the supplied detector's models directory, else the default
models dir. Raises FaceVerificationError when an image holds no
detectable face.
VerificationResult — frozen dataclass: verified (bool),
similarity (cosine in [-1, 1]), threshold, processing_time
(end-to-end seconds), backend ("ONNXRuntime").
from blitzid import FaceDetectorDNN, FaceVerifier
detector = FaceDetectorDNN()
verifier = FaceVerifier(detector=detector, threshold=0.4)
faces = detector.detect_face_landmarks("id_card.jpg")
selfie = detector.detect_face_landmarks("selfie.jpg")
result = verifier.verify_faces("id_card.jpg", faces[0], "selfie.jpg", selfie[0])
RapidOCRReader
Defined in reading/ocr.py; requires the ocr extra
(pip install blitzid[ocr]). Reads text lines from document images via
RapidOCR (PP-OCR ONNX models on onnxruntime CPU).
| Method | Description |
|---|---|
read(image_input) |
Returns list[OCRText] sorted by confidence |
OCRText — frozen dataclass: bbox (x, y, w, h) in image pixels,
text (recognized string), confidence in [0, 1].
from blitzid import RapidOCRReader
reader = RapidOCRReader()
for line in reader.read("id_card.jpg"):
print(line.text, line.confidence)
cross_check
Defined in reading/consistency.py.
Compares one document's two data copies and its portrait photo.
| Method | Description |
|---|---|
cross_check(structured, mrz, photo=None, today=None) |
Returns a ConsistencyReport of per-field verdicts |
ConsistencyReport — frozen dataclass: comparisons (printed
text vs code strip, one FieldComparison per field), photo_comparisons
(two PhotoComparison rows: age and sex), and consistent (false
on any outright mismatch). Every row keeps both values and the model's
confidence.
AntiSpoofReader
Defined in face/_antispoof.py.
Scores every detected face as a live person, a printed photo, or a
screen photo with the MiniFASNetV2 model (weights download on first
use, pinned like the rest).
| Method | Description |
|---|---|
read(image_input) |
Returns list[AntiSpoofResult], one per detected face |
read_faces(img, faces) |
Scores already-detected Face records of a loaded BGR array (no re-detection) |
AntiSpoofResult — frozen dataclass: bbox, confidence, and
the three model scores live_score, paper_score, screen_score.
Treat it as an advisory layer: it catches printed and screen photos of
still images, not video replay of a moving person and not generated
faces. The /liveness challenge is the gate; this is the second
opinion.
FaceAttributeReader
Defined in face/_attributes.py.
Predicts age group, gender, and race for every detected face with the
FairFace model (weights download on first use, pinned like the rest).
| Method | Description |
|---|---|
read(image_input) |
Returns list[FaceAttributes], one per detected face |
read_faces(img, faces) |
Predicts for already-detected Face records of a loaded BGR array (no re-detection) |
FaceAttributes — frozen dataclass: bbox, confidence (the
detector's score), age (one of
AGE_GROUPS, e.g. "20-29"), gender ("Male"/"Female"), race
(one of RACES, the model's seven coarse groups), and one softmax
*_confidence per head. Treat these as descriptive metadata: the
labels are model opinions over coarse groups, and the race label in
particular must not drive decisions about people.
MRZReader
Defined in reading/mrz.py; requires the ocr extra.
Selects MRZ lines from OCR text, validates them via ICAO 9303 check
digits, and parses TD1 (3x30, ID cards), TD2 (2x36), and TD3 (2x44,
passports) zones. Obvious OCR confusions (0/O, 1/I, …) are resolved by
each field's alphabet first; the check digits then gate acceptance,
and a zone that fails raises MRZError.
| Method | Description |
|---|---|
read(image_input) |
Returns an MRZRecord |
parse(lines) |
Parses an MRZRecord from already-read OCRText lines (no second OCR pass) |
MRZRecord — frozen dataclass: mrz_type, document_code,
issuer, document_number, birth_date (YYMMDD), sex (M/F/X),
expiry_date (YYMMDD), nationality, surname, given_names,
optional_data1, optional_data2.
from blitzid import MRZReader
record = MRZReader().read("id_card.jpg")
print(record.mrz_type, record.document_number, record.surname)
BarcodeReader
Defined in reading/barcode.py;
requires the barcode extra (pip install blitzid[barcode]). Decodes
the PDF417 code on the back of US and Canadian driver's licenses with
zxing-cpp and parses the AAMVA DL/ID payload (versions 02–14). The
header's subfile offsets, lengths, and number-of-entries count are
validated, the mandatory fields are gated, and a failing payload raises
BarcodeError naming the offending field — nothing is invented or
repaired.
| Method | Description |
|---|---|
read(image_input) |
Decodes and returns a BarcodeRecord |
BarcodeRecord — frozen dataclass: jurisdiction (the issuer's
identification number), aamva_version (AAMVA version, "02"–"14"),
jurisdiction_version, family_name, given_name, middle_name,
birth_date / issue_date / expiry_date (datetime.date), sex
("M"/"F"/"X"), license_number, height (with its unit, or
None), extra_tags (every un-mapped element kept raw, jurisdiction
Z.. extras included), and symbology (the decoded barcode symbology,
stamped by the reader).
from blitzid import BarcodeReader
record = BarcodeReader().read("license_back.jpg")
print(record.aamva_version, record.family_name, record.expiry_date)
StructuredOCRReader
Defined in reading/structurize.py;
requires the ocr extra (pip install blitzid[ocr]). Reconstructs a
typed record from OCR text lines via an LLM on an OpenAI-compatible
endpoint (BLITZID_LLM_BASE_URL, BLITZID_LLM_API_KEY, and
BLITZID_LLM_MODEL environment variables or the matching constructor
arguments — all required). The model is instructed to omit unknown
fields rather than invent them (prompt-enforced; treat low-confidence
fields as unreliable), and a timeout (default 180 s) bounds the
endpoint call.
| Method | Description |
|---|---|
read(texts, kind="auto") |
Returns a StructuredOCR from OCRText lines; kind="id"/"plate"/"mrz" picks a specialized prompt and stamps document_type, "auto" lets the model classify |
StructuredOCR — pydantic model: document_type ("id" —
identity document with person data, "plate" — license plate, "mrz"
— machine readable zone, "unknown", or None), fields (list of
ExtractedField; one entry per name — duplicates collapse to the
highest-confidence value), raw_text (the joined OCR lines fed to the
model, stamped by the reader).
ExtractedField — pydantic model: name (snake_case label),
value (datetime.date for date-named fields like date_of_birth /
birth_date — ISO 8601 or DD.MM.YYYY input — upper-case text
otherwise), confidence in [0, 1] (default 0.0 when the model
omits it — treat such fields as unreliable).
from blitzid import RapidOCRReader, StructuredOCRReader
lines = RapidOCRReader().read("id_card.jpg")
record = StructuredOCRReader().read(lines)
print(record.document_type, [(f.name, f.value) for f in record.fields])
DocumentCropper
Defined in reading/document.py. Pure-CV document localization and QC: finds the largest plausible document quad (Canny edges + contour approximation), warps it into an axis-aligned canonical crop at the quad's own aspect, and quality-checks the crop (thresholds are documented module constants).
| Method | Description |
|---|---|
crop(image_input, side="unknown") |
Returns (crop, QualityReport); crop is None when no document was found |
QualityReport — frozen dataclass: quad (corners clockwise from
top-left, or None), width, height, side, face_found, checks
(each pass / warn / fail / n/a), verdict (pass — no fail,
warn — warns only, reject — any fail). The optional detector
constructor argument (a FaceDetectorDNN) enables the side-aware
face_present check: front without a face warns, back is not
expected (TD1 MRZ lives there), without a detector the check is n/a.
The optional detector_lock argument serializes that check when the
detector instance is shared across threads (the API passes its
per-engine lock).
Exceptions
Defined in exceptions.py.
| Class | Purpose |
|---|---|
BlitzIDError |
Base exception for all blitzid errors |
ModelError |
Model download or load failure |
ImageError |
Image loading, validation, or processing failure |
MRZError |
MRZ not found, malformed, or failed check-digit validation |
BarcodeError |
PDF417 barcode not found, malformed, or failed AAMVA validation |
FaceVerificationError |
Face verification failure (no detectable face, missing landmarks) |
Backward-compatibility aliases (FaceDetectorError, ModelDownloadError, ImageLoadError, etc.) are re-exported from __init__.py.
Architecture
graph TD
A[blitzid] --> B[face/detector.py<br/>FaceDetectorDNN]
A --> N[face/verifier.py<br/>FaceVerifier]
A --> I[reading/mrz.py<br/>MRZReader]
A --> C[reading/ocr.py<br/>RapidOCRReader]
A --> M[reading/document.py<br/>DocumentCropper · QualityReport]
A --> D[exceptions.py<br/>BlitzIDError · ModelError · ImageError · MRZError]
I --> C
B --> E[_models.py<br/>ModelManager · models dir]
B --> F[_image.py<br/>load_image · ImageInput]
B --> G[face/_face.py<br/>Face · DetectionMetrics · cache]
B --> H[face/_scrfd.py<br/>decode · letterbox]
N --> B
N --> E
N --> F
N --> G
N --> J[face/_arcface.py<br/>align · embed]
C --> E
C --> F
M --> F
H --> G
The optional HTTP API lives in src/blitzid/api/ (api extra) and is
not part of the library import graph above.
Models
FaceDetectorDNN runs the SCRFD-2.5G face detector (InsightFace
buffalo_m detection weights) through an onnxruntime CPU session.
The ONNX file is downloaded automatically on first use and reused
afterwards. There is no fallback to other models or providers: a
missing or unloadable model raises ModelError. Inference input size is
configurable via det_size (default 640x640, multiples of 32); images
are letterboxed to preserve aspect ratio and boxes are mapped back to
original image coordinates.
FaceVerifier runs the ArcFace face recognizer (InsightFace
buffalo_m recognition weights, ~174 MB, 512-d embeddings) through the
same onnxruntime CPU profile: faces are aligned to the canonical
112×112 ArcFace template by their five SCRFD landmarks, embedded, and
compared by cosine similarity against the decision threshold. The same
no-fallback rule applies.
RapidOCRReader runs RapidOCR's PP-OCR ONNX models (detection,
classification, recognition) through the same onnxruntime CPU profile.
Weights live in the default models dir: BLITZID_MODELS_DIR when set,
else user_cache_dir("blitzid")/models/. Pre-fetch everything for a
Docker image with python scripts/download_models.py (default target:
models/); see models/README.md.
Performance
Rough single-image timings from scripts/benchmark.py
(11th Gen Intel Core i7-11850H; python 3.13, onnxruntime 1.30, x86_64;
median of 20 warm runs; init is the median of 5 constructions):
| Pipeline | Benchmark | Init (ms) | Median (ms) |
|---|---|---|---|
| fast | detect_face (specimen ID card) |
14 | 4 |
| balanced | detect_face (specimen ID card) |
15 | 4 |
| accurate | detect_face (specimen ID card) |
14 | 28 |
| balanced | detect_face_with_metrics (cache hit) |
— | 5 |
| FaceVerifier | verify (specimen ID card, same image twice) |
200 | 173 |
| RapidOCRReader | read (specimen ID card) |
798 | 903 |
| MRZReader | read (specimen ID card) |
913 | 910 |
The benchmark reports the CPU model automatically, so results files in
results/ carry the same context. Re-run locally with
uv run python scripts/benchmark.py --markdown; the ocr benchmarks
are skipped without the ocr extra. Numbers vary by machine: treat
them as ballpark figures, not guarantees. Inside one process the
onnxruntime sessions are created without intra-op spin-waiting
(session.intra_op.allow_spinning=0), so engines that run back to
back (detector, then recognizer) do not fight each other for cores;
outputs are bit-identical to the default session profile.
Demo
A CLI demo script is included at scripts/face_detector_demo.py:
# Balanced preset on a single image
python scripts/face_detector_demo.py --preset balanced --run all \
--image images/bub_der_personalausweis_kopie.jpg
An OCR demo is available at scripts/ocr_demo.py
(requires blitzid[ocr]):
python scripts/ocr_demo.py --image images/bub_der_personalausweis_kopie.jpg
Development
# Install dev dependencies
uv sync --extra dev
# Run tests
uv run pytest tests/ -v
# Lint & type-check (mirrors CI)
uv run ruff check src/ tests/ scripts/
uv run ruff format --check src/ tests/ scripts/
uv run mypy src/ scripts/
See CONTRIBUTING.md for full guidelines and CHANGELOG.md for release history.
Credits
- InsightFace for the
SCRFD-2.5G detector and the
buffalo_mdetection and recognition weights - RapidOCR for the PP-OCR text recognition pipeline
- ONNX Runtime for CPU inference
- ICAO Doc 9303 for the machine-readable travel document specifications behind the MRZ parser
License
Metadata
Release files for blitzid 0.3.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| blitzid-0.3.6.tar.gz | 2.4 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| blitzid-0.3.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.5 MB
Release files / blitzid-0.3.6.tar.gz
| Download URL | blitzid-0.3.6.tar.gz |
|---|---|
| Size | 2.4 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
516947d6cf0d3b0ee4087d4346fd4b3a939f7514a220cbff52c7bf5450d74277
|
|
BLAKE2b-256 checksum How to use checksums |
021d034691670e9c0c383b1cf6c9f8cf7dcb790e8b4cd746e0f282430dd86587
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 7, 2026.
Transparency logRelease files / blitzid-0.3.6-py3-none-any.whl
| Download URL | blitzid-0.3.6-py3-none-any.whl |
|---|---|
| Size | 92.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f45127ebc04cf269f698c514543c31061fa772854bf8368dfbfdf3fe6aeea2e5
|
|
BLAKE2b-256 checksum How to use checksums |
16d34058f5f82900af2cb76eee0d0c0b248aa61e9eb81a672862a385659ab261
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 7, 2026.
Transparency log