File size: 12,886 Bytes
50c6ee2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
---
title: ID OCR Engine
emoji: "\U0001F50D"
colorFrom: blue
colorTo: green
sdk: docker
app_port: 7860
---

<p align="center">
  <img src="https://img.shields.io/badge/python-3.12+-3776AB?style=flat&logo=python&logoColor=white" alt="Python 3.12+">
  <img src="https://img.shields.io/badge/flask-3.1-000000?style=flat&logo=flask&logoColor=white" alt="Flask 3.1">
  <img src="https://img.shields.io/badge/docker-ready-2496ED?style=flat&logo=docker&logoColor=white" alt="Docker Ready">
  <img src="https://img.shields.io/badge/ocr-paddleocr-0A7D3B?style=flat&logo=python&logoColor=white" alt="PaddleOCR">
  <img src="https://img.shields.io/badge/mrz-fastmrz-4A5568?style=flat" alt="FastMRZ">
  <img src="https://img.shields.io/badge/barcode-zxing--cpp-6A1B9A?style=flat" alt="zxing-cpp">
</p>
<p align="center">
  <img src="https://img.shields.io/badge/api-rest-0B7285?style=flat" alt="REST API">
  <img src="https://img.shields.io/badge/logging-json-2B8A3E?style=flat" alt="JSON Logging">
  <img src="https://img.shields.io/badge/rate--limit-flask--limiter-364FC7?style=flat" alt="Rate limiting">
  <img src="https://img.shields.io/badge/security-api%20key-5C7CFA?style=flat" alt="API Key Auth">
  <img src="https://img.shields.io/badge/license-proprietary-8D99AE?style=flat" alt="License">
</p>

# ID OCR Engine

A Flask microservice that extracts identity fields from ID cards, passports, and product serial numbers using a multi-engine pipeline: VLM (Vision Language Model) + PaddleOCR + barcode/MRZ reading.

## Highlights

- Multi-engine ID extraction (barcode/MRZ + VLM + OCR) with cross-validation
- Product serial number scanning via VLM with digit-density scoring
- Live camera scanning with auto-detection (barcode + serial number modes)
- Built-in web UI at `/` with file upload and camera support
- JSON logs with request IDs
- Rate limiting and optional API key auth
- No persistent storage of uploaded images (files are deleted immediately after processing)
- Models used:
  - VLM: `Qwen/Qwen3-VL-8B-Instruct` via Hugging Face Inference API (configurable)
  - OCR: PaddleOCR (English)
  - MRZ: FastMRZ (ONNX)

## Supported Document Types

| Type | `doc_type` | Input | Machine-readable |
|------|-----------|-------|-----------------|
| SA Smart Card | `sa_id_card` | Front (required) + Back (optional, PDF417 barcode) | Barcode on back |
| SA Green Book | `sa_id_book` | Front (required) | None |
| Passport (any country) | `passport` | Front (required) | MRZ on front |
| Product Serial Number | `product_serial` | Product image | None |

## Extraction Pipelines

```
sa_id_card:      Barcode (back) β†’ VLM (front) β†’ PaddleOCR (front) β†’ Cross-validate
sa_id_book:      VLM (front) β†’ PaddleOCR (front) β†’ Cross-validate
passport:        MRZ (front) β†’ VLM (front) β†’ PaddleOCR (front) β†’ Cross-validate
product_serial:  VLM (center-cropped frame) β†’ digit-density scoring β†’ validation
```

## Web UI

- Extracts ID number, surname, names, date of birth, sex, country of birth, and citizenship status
- Validates the 13-digit SA ID number using the SA-specific Luhn algorithm
- Cross-validates OCR-extracted fields (DOB, gender, citizenship) against values encoded in the ID number
- Multi-pass OCR with fallback preprocessing (CLAHE, adaptive thresholding, high-contrast) for difficult images
- Automatic front/back card detection
- Built-in web dashboard with drag-and-drop upload, batch processing, history, and CSV export
- API key authentication, rate limiting, and request tracking
- Automatic cleanup of uploaded images (personal data is never persisted)

## Web Dashboard

Navigate to `http://localhost:5001` to access the interactive dashboard:

- Drag-and-drop or browse to upload ID card images
- Batch processing queue for multiple images
- Real-time results display with validation status
- Processing history and statistics
- CSV export of results

## API

### `GET /health`
Health check (no auth).

### `GET /warm`
Warm up VLM + PaddleOCR + MRZ models (no auth). Call before first OCR request.

### `POST /api/ocr`

Extract fields from an ID document image.

**Headers:**

| Header | Required | Description |
|---|---|---|
| `X-API-Key` | Yes (if configured) | API authentication key |
| `X-Request-ID` | No | Client-provided request ID (echoed back) |

**Request:** `multipart/form-data`

| Field | Required | Description |
|---|---|---|
| `front` | Yes | Front image (or `file` for legacy) |
| `back` | No | Back image (only for `sa_id_card`) |
| `doc_type` | Yes | `sa_id_card`, `sa_id_book`, or `passport` |

Accepted formats: `png`, `jpg`, `jpeg`, `bmp`, `webp` (max 10MB).

**Response:**

The response includes:

- `fields` extracted from barcode/MRZ/VLM/OCR
- `checks` for validation and cross-checks
- `confidence` scores from OCR
- `quality` metrics (brightness, blur, resolution)
- `raw_text` OCR text

**Example (SA ID):**

```json
{
  "doc_type": "sa_id_card",
  "fields": {
    "id_number": "8801015800082",
    "surname": "SMITH",
    "names": "JOHN PETER",
    "date_of_birth": "1988-01-01",
    "sex": "Male",
    "nationality": "South African",
    "country_of_birth": "RSA",
    "citizenship_status": "SA Citizen"
  },
  "barcode_data": { "...": "..." },
  "mrz_data": null,
  "extraction_method": "barcode+vlm",
  "checks": {
    "image_quality": "passed",
    "id_number_valid": "passed",
    "barcode_valid": "passed",
    "data_crosscheck": "passed",
    "dob_crosscheck": "passed",
    "gender_crosscheck": "passed",
    "mrz_valid": "not_applicable"
  },
  "overall_result": "pass"
}
```

**Example (Passport):**

```json
{
  "doc_type": "passport",
  "fields": {
    "passport_number": "A12345678",
    "surname": "Smith",
    "given_names": "John Peter",
    "date_of_birth": "1988-01-01",
    "sex": "Male",
    "nationality": "ZAF",
    "expiry_date": "2030-01-01",
    "issuing_country": "ZAF",
    "id_number": "8801015800082"
  },
  "mrz_data": { "...": "..." },
  "extraction_method": "mrz+vlm",
  "checks": {
    "image_quality": "passed",
    "mrz_valid": "passed",
    "id_number_valid": "passed"
  },
  "overall_result": "pass"
}
```

### `POST /api/serial`

Extract a product serial number from an image.

**Request:** `multipart/form-data`

| Field | Required | Description |
|---|---|---|
| `image` | Yes | Product image file |

**Response:**

```json
{
  "serial_number": "WPHK002510002632",
  "bounding_box": null,
  "cropped_image": null,
  "extraction_source": "vlm",
  "confidence": "medium",
  "validation": {
    "valid": true,
    "normalized": "WPHK002510002632",
    "issues": [],
    "prefix_match": null
  },
  "failure_reason": null,
  "regions_detected": 0,
  "quality": { "width": 640, "height": 480, "usable": true, "issues": [] }
}
```

The serial extraction pipeline uses VLM to read all visible text, then scores candidates by digit density, rejects known non-serial patterns (IMEI, MAC, English phrases, sequential digits), and boosts values near `S/N` or `Serial` labels.

### `POST /api/barcode/parse`

Parse raw barcode text (decoded client-side) into structured SA ID fields. Used by the web UI when the browser's BarcodeDetector API decodes a PDF417 barcode from the camera feed.

**Request:** `application/json`

```json
{ "text": "<raw barcode data>" }
```

**Response:** Same format as `/api/ocr` with `extraction_method: "barcode"`.

## Checks and Quality

The `checks` object includes a per-check status: `passed`, `failed`, or `not_applicable`.

- `image_quality`: minimum resolution check per doc type
- `id_number_valid`: country-specific validation (Luhn for SA, regex for others)
- `mrz_valid`: MRZ check digit validation
- `barcode_valid`: barcode extraction succeeded
- `data_crosscheck`: compare machine-readable data against VLM/OCR values
- `dob_crosscheck`, `gender_crosscheck`: SA ID number-encoded validation

The `quality` object includes:

- `resolution_ok`, `width`, `height`
- `brightness` and `blur_score`
- `issues` list for low-light, blur, or very small images

## Country ID Validation

ID numbers are validated per detected nationality:

| Country | Code | Format | Validation |
|---------|------|--------|-----------|
| South Africa | ZAF | 13 digits | SA Luhn + encoded fields |
| Nigeria | NGA | 11 digits | Regex |
| Kenya | KEN | 1-9 digits | Regex |
| Zimbabwe | ZWE | 8-9 digits + letter + 2 digits | Regex |
| Uganda | UGA | 14 alphanumeric | Regex |
| Zambia | ZMB | 10 digits | Regex |

## Configuration

| Variable | Default | Description |
|---|---|---|
| `OCR_API_KEY` | *(empty)* | API key (disabled if empty) |
| `OCR_HOST` | `0.0.0.0` | Bind address |
| `OCR_PORT` | `7860` | Bind port |
| `OCR_DEBUG` | `false` | Enable Flask debug mode |
| `OCR_MAX_REQUEST_SIZE` | `10485760` | Max request size in bytes (default 10MB) |
| `OCR_UPLOAD_DIR` | `./uploads` | Temporary upload directory |
| `HF_API_TOKEN` | *(empty)* | HF Inference API token (for VLM) |
| `OCR_VLM_MODEL` | `Qwen/Qwen3-VL-8B-Instruct` | VLM model ID (for ID documents) |
| `OCR_SERIAL_VLM_MODEL` | `Qwen/Qwen3-VL-8B-Instruct` | VLM model ID (for serial numbers) |
| `OCR_LOG_LEVEL` | `INFO` | Logging level |
| `OCR_LOG_FORMAT` | `json` | Log format (`json` or `text`) |
| `OCR_MAX_REQUEST_SIZE` | `10485760` | Max upload size in bytes (default 10MB) |
| `OCR_UPLOAD_DIR` | `./uploads` | Temporary file storage directory |
| `OCR_WORKERS` | `min(CPU_count, 2)` | Gunicorn worker count |

## Running

**Development:**
```bash
python main.py
```

**Production:**
```bash
gunicorn -c gunicorn.conf.py wsgi:app
```

**Docker Compose:**
```bash
docker compose up --build
```

**Docker:**
```bash
docker build -t id-ocr .
docker run -p 7860:7860 -e OCR_API_KEY=your-key -e HF_API_TOKEN=hf_xxx id-ocr
```

## Example Requests

```bash
# SA Smart Card (front only)
curl -X POST -H "X-API-Key: key" \
  -F "front=@front.jpg" -F "doc_type=sa_id_card" \
  http://localhost:7860/api/ocr

# SA Smart Card (front + back)
curl -X POST -H "X-API-Key: key" \
  -F "front=@front.jpg" -F "back=@back.jpg" -F "doc_type=sa_id_card" \
  http://localhost:7860/api/ocr

# Passport
curl -X POST -H "X-API-Key: key" \
  -F "front=@passport.jpg" -F "doc_type=passport" \
  http://localhost:7860/api/ocr

# Product serial number
curl -X POST -H "X-API-Key: key" \
  -F "image=@product.jpg" \
  http://localhost:7860/api/serial
```

## Data Handling and Privacy

```
β”œβ”€β”€ main.py                  # Flask app factory, routes, middleware
β”œβ”€β”€ config.py                # Environment-based configuration
β”œβ”€β”€ wsgi.py                  # Gunicorn WSGI entry point
β”œβ”€β”€ gunicorn.conf.py         # Gunicorn worker configuration
β”œβ”€β”€ logging_config.py        # Structured JSON logging
β”œβ”€β”€ engine/
β”‚   β”œβ”€β”€ ocr.py               # Image preprocessing + EasyOCR wrapper
β”‚   └── id_parser.py         # SA ID parsing, Luhn validation, field extraction
β”œβ”€β”€ static/
β”‚   β”œβ”€β”€ index.html           # Web dashboard UI
β”‚   β”œβ”€β”€ css/
β”‚   β”‚   └── style.css        # Dashboard styles
β”‚   └── js/
β”‚       β”œβ”€β”€ app.js           # App initialization
β”‚       β”œβ”€β”€ api.js           # API client wrapper
β”‚       β”œβ”€β”€ upload.js        # File upload & batch processing
β”‚       β”œβ”€β”€ results.js       # Results rendering
β”‚       β”œβ”€β”€ history.js       # Processing history management
β”‚       β”œβ”€β”€ stats.js         # Statistics calculation
β”‚       └── utils.js         # Utility functions
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ test_api.py          # API integration tests
β”‚   └── test_id_parser.py    # ID parser unit tests
β”œβ”€β”€ Dockerfile
└── docker-compose.yaml
```

## How It Works

### OCR Pipeline

1. **Image intake** β€” validates format and size, saves to a temporary directory
2. **Preprocessing** β€” auto-brightness correction (gamma), CLAHE contrast enhancement, bilateral noise filtering, intelligent resizing (800–2000px width)
3. **OCR pass 1** β€” standard preprocessing with EasyOCR
4. **Fallback passes** β€” if the ID number is not found, retries with aggressive preprocessing (sharpening + adaptive thresholding) and high-contrast preprocessing (histogram stretch + Otsu's thresholding)
5. **Field extraction** β€” spatial matching of bounding boxes to locate field values relative to labels
6. **Validation** β€” Luhn check on ID number, cross-validation of DOB/gender/citizenship against encoded values
7. **Cleanup** β€” temporary image is deleted immediately after processing

### OCR Error Correction

Common character confusions are automatically corrected during ID number extraction (e.g., `O`β†’`0`, `I`β†’`1`, `S`β†’`5`).

## SA ID Number Format

- Logs can be JSON or text format.
- `X-Request-ID` is accepted on requests and echoed in responses.
- Each request is logged with latency and request ID.

## Testing

```bash
python -m pytest tests/ -v
```