Skip to main content

Document OCR

Experimental

Offline NIN extraction is accurate on 2 of 10 real sample cards, against a target of 80%. Do not depend on this in production yet. The NIN validator is a separate module and is unaffected.

Reads the NIN, name and date of birth from a Sierra Leone National eID card.

import { ocr, crossCheck } from '@sierrafy/sdk/ocr';

const result = await ocr(imageBuffer);
// {
// document_type: 'SL_NATIONAL_EID',
// extracted: { nin: 'ABCD1234', name: 'AMINATA KAMARA', dob: '1992-04-17' },
// ocr_confidence: 0.82
// }

Needs the optional peers: npm install sharp tesseract.js. See Install.

Why the accuracy is low​

The cause is understood and is not a preprocessing problem. The NIN is 8 alphanumeric characters with no checksum, so nothing disambiguates 0 from O, 1 from I, or 5 from S. Most misses are off by one or two such glyphs, and grayscale, green-channel and binarised preprocessing all produce the same substitutions.

Three things would move it: an NCRA-confirmed checksum, which would let the engine auto-correct single-character errors; higher-resolution, flat, axis-aligned captures; or the optional Google Vision engine.

Checking against what the user typed​

const check = crossCheck(result, {
nin: 'ABCD1234',
name: 'Aminata Kamara',
dob: '1992-04-17',
});
// { nin_format_valid, nin_matches_card, name_matches, dob_matches, ... }

The NIN must match exactly. Names are compared with Levenshtein similarity against NAME_MATCH_THRESHOLD, so case and ordering differences pass but a different person does not. Dates are normalised first, so 17/04/1992 and 1992-04-17 compare equal.

Errors​

ocr() does not throw for unreadable input. It returns a result with error set and extracted as null.

CodeMeaning
IMAGE_UNREADABLENo usable text in any zone
DOCUMENT_UNSUPPORTEDA document type this milestone does not handle

It does throw if an optional peer dependency is missing, because that is a setup error rather than bad input.

Staying offline​

No identity data leaves your machine on the default engine. Recognition runs locally on WebAssembly, and no image, NIN or extracted field is sent anywhere.

There is one caveat, and it is worth stating precisely rather than glossing: eng.traineddata, the Tesseract language model, is not bundled in this package. On the first ocr() call tesseract.js downloads it from the jsdelivr CDN (about 3 MB gzipped, for the LSTM model the default engine mode uses) and caches it in the process working directory. That request carries no identity data and no image, but it is a network call, and on a machine with no internet access the first OCR run fails.

To remove it, give the engine a local copy of the model. Download eng.traineddata once (3.9 MB from tessdata_fast), put it in a directory, and name that directory as the cache:

import { ocr } from '@sierrafy/sdk';

const result = await ocr(imageBuffer, {
tesseract: { cachePath: '/opt/tessdata' }, // holds eng.traineddata
});

Or set it once for the process, which is usually what you want in a container:

VariableEffect
TESSERACT_CACHE_PATHDirectory the model is read from before any download, and written to after one. Put a plain eng.traineddata here and nothing is ever fetched.
TESSERACT_LANG_PATHWhere the model is loaded from on a cache miss: a URL (your own mirror), or a local directory.
TESSERACT_CACHE_PATH=/opt/tessdata node app.js

An explicit option beats the environment variable, which beats the CDN default.

The two paths want different filenames

cachePath reads eng.traineddata, plain and uncompressed. That is the file you get from tessdata_fast, so this route is copy-and-go.

langPath pointed at a local directory reads eng.traineddata.gz, because tesseract.js appends .gz by default. Point langPath at a directory holding an uncompressed file and it fails with ENOENT on the .gz name. Use cachePath for a plain file, and langPath for a URL or a gzipped copy.

With cachePath set to a directory that holds the model, the OCR path is genuinely offline and works air-gapped. If you are building a Docker image, copying eng.traineddata in and setting TESSERACT_CACHE_PATH is the whole job.

Sending data off the machine​

Setting OCR_ENGINE=google-vision with a GOOGLE_VISION_KEY routes to Google Cloud Vision, which is markedly more accurate and is the only code path in Sierrafy that sends identity data anywhere. Card images, including the portrait, leave your infrastructure and leave Sierra Leone. It is off unless you turn it on. See Privacy for what enabling it commits you to.

Zone maps​

Field positions come from a JSON zone map with relative coordinates, bundled with the package and licensed CC0 separately from the MIT code. SL_NATIONAL_EID is calibrated against 10 real card fronts and includes the document number and expiry zones that NFC chip reading will need.

Coordinates assume a reasonably framed, axis-aligned photo. Heavily rotated or margin-padded captures lose leading or trailing characters.