All articles
Engineering

Choosing a Vision Model for Business Card Scanning: 4 Models, 29 Cards

We tested four vision models on the same 29 visiting-card images and shipped a mid-size one: 21 of 29 cards usable as-is, phones right on 23 of 24, at $0.00139 a card. The model only transcribes; our code checks every fact.

Surya Kumar MuniOctober 20, 20269 min read1 views
Choosing a Vision Model for Business Card Scanning: 4 Models, 29 Cards

We tested four vision models on the same 29 visiting-card images and shipped a mid-size one. It turned 21 of the 29 cards into a usable contact, read the phone numbers right on 23 of 24 cards and the company on 17 of 20, at $0.00139 a card and 2.4 seconds on average. The cheapest, a small model, cost $0.00020 a card and managed 17 of 29.

The feature belongs to the admin panel we built for BogarInfra, a Chennai engineering and construction firm (the BogarInfra case study covers the whole build). Its directors had a backlog of visiting cards from people they had met, and asked for a phone link that turns a photo of a card into an entry in a private list of potential clients. This is how we chose the model, and why it ended up doing less than you might expect.

What did the model actually have to do?

We split the job in two. The model transcribes what is printed into a strict JSON schema: names, company, phones with their printed labels, emails, websites, addresses, GSTIN and a legibility rating. Everything that can be checked is then checked by our own code. That split shaped the test: we needed the model that copies most faithfully, not the one that reasons best.

How did we build a fair test set?

We did not have the directors' cards when we ran the test on 11 September 2026, and we would not invent any. So the 29 images came from public sources under their licences, with every field hand-transcribed as ground truth: photos and flat scans of real printed cards, Tamil and Hindi designs, cards with handwritten additions, back sides, a few non-Indian cards as controls, and two images that are not cards at all, a receipt and a blank card. The images stay internal; several licences forbid redistribution.

The scoring was strict. A field counts as right only when every expected value is present after formatting is normalised: case, spaces, the +91 prefix. Names are compared letter by letter in any script, so a Tamil name cannot normalise to an empty string and match everything. Company suffixes such as Pvt and Ltd are ignored. Anything marked illegible in the ground truth is skipped, not scored. Each model read each card once, through the same image preparation and fact-checking code as production, with no fallback.

"Usable as-is" is the production rule, not a test invention: a card is complete when our code finds a name or a company, and at least one valid phone number or email.

How did the four models compare?

Field Small model A Small model B Mid-size model (shipped) Small model C
Is it a card? 27/29 (93%) 26/29 (90%) 28/29 (97%) 27/29 (93%)
Name 12/21 (57%) 12/21 (57%) 16/21 (76%) 12/21 (57%)
Company 9/20 (45%) 11/20 (55%) 17/20 (85%) 13/20 (65%)
Phones 18/24 (75%) 17/24 (71%) 23/24 (96%) 19/24 (79%)
Handwritten 0/3 (0%) 0/3 (0%) 1/3 (33%) 0/3 (0%)
Emails 8/11 (73%) 10/11 (91%) 10/11 (91%) 6/11 (55%)
Website 9/10 (90%) 8/10 (80%) 10/10 (100%) 9/10 (90%)
PIN code 6/13 (46%) 9/13 (69%) 10/13 (77%) 9/13 (69%)
Usable as-is 17/29 16/29 21/29 18/29
Average time 2.0 s 1.6 s 2.4 s 2.3 s
Tokens in / out 2725 / 148 3863 / 130 2869 / 151 2370 / 145
Cost per card $0.00020 $0.00044 $0.00139 $0.00066
Cost, whole run $0.0057 $0.0127 $0.0403 $0.0190

None of the runs returned an error. GSTIN scored n/a for every model, because no card under a usable licence had one printed.

Why did the mid-size model win when it cost seven times as much?

Because the cost that matters is not the model's. It costs about seven times as much per card as the cheapest small model, yet the whole 29-card run came to four cents. The expensive part of a misread card is a director's time: opening it, finding what is wrong and typing it in. Four more usable cards out of 29 is worth far more than a fraction of a cent.

The plan we started with was the cheapest small model, with a stronger model retrying any card that came back incomplete. We wrote our predictions down before the run so we could score them. Two failed outright. We expected the small model to read printed names, companies and mobiles on at least 90% of clear cards; it managed 57% of names, 45% of companies and 75% of phones, and often returned no person for a clearly printed name. We expected the fallback to be needed on at most 15% of scans; it was incomplete on 12 of 29. A design that needs its fallback on four cards in ten is really a design built on the fallback model. On top of that, the fallback we had picked could not run on the account we were testing with, which had not been through the provider's organisation verification. So we shipped the mid-size model on its own, with the fallback switched off until the client's account can use it.

Two smaller findings changed the code too. Our first prompt told models to return nothing for a name printed only in Tamil; for a Chennai firm that was simply the wrong instruction, and the prompt now copies names in the script they are printed in. And the bake-off script itself printed a model with no price on file as $0. An unpriced run is not free, so the cost function now returns "no price on file" instead of zero.

Handwriting stayed weak for every model: the best managed 1 of 3. The review screen marks any number that came from handwriting for a person to check.

Why does code own the facts?

A model that is right 96% of the time on phone numbers is wrong on some of them, and a wrong number in a contact list fails silently. So the model's output is treated as a draft and every checkable fact is re-derived by deterministic code:

  • Phones are normalised to E.164. Indian mobiles start 6 to 9; landlines carry an area code that never starts with 0 or 9; toll-free numbers start 1800 or 1860. The label the model read (M, Tel, Fax, a WhatsApp icon) decides only where the digits cannot, such as Bengaluru's 080 area code against mobile numbers that start with 80.
  • PIN codes must be six digits that never start with 0, and the first digits map to a state through India Post's postal circles. The state is a suggestion; the printed address stays the source of truth.
  • GSTINs are checked against the 15-character pattern and the mod-36 check character, then split into the state code and the embedded PAN. A GSTIN registered in one state on a card whose PIN code is in another raises a flag, since it is often a branch address.
  • Sector is a keyword suggestion onto the site's own sectors, never applied automatically.
  • QR codes are decoded in code. A vCard on the card outranks the model's read of the print, because it is exact.

These checks flag; they never silently fix. A GSTIN that fails its checksum is kept as read, with the reason, and a person decides. When a director corrects a card, the edit goes through the same checks as the model's read.

The same principle covers prompt injection. The text on a card is data, and anyone can print anything on a card. The instructions say so, the reply is constrained to a strict JSON schema by the API's structured-output mode, and nothing in the reply is ever executed or used to choose an action. The worst a hostile card can do is fill in a wrong field, which the checks above exist to catch. OWASP ranks this risk first in its Top 10 for LLM applications.

How do duplicates get caught without paying twice?

Each director's contacts are private, a decision we took over a shared list, so two directors who met the same person get two contacts. De-duplication runs inside one director's list at three points:

  1. The same photo sent twice. The prepared image is hashed with SHA-256. A match is stored as a duplicate that inherits the original's contact, with no model call and no tokens.
  2. A card already read. The reading queue only ever claims scans waiting to be read, and only a failed scan can be retried.
  3. The same card photographed again. It is read again, then matched on its normalised phones, emails and GSTIN. One match with no conflicting company name merges automatically, filling gaps without overwriting anything a person kept. Several matches, or a shared number under a different company, someone who changed jobs, go to a person.

Converting a contact into an enquiry puts it on the shared sales board, so that step checks the board for the same phone or email and warns.

How is the capture link protected without a login?

Directors scan through a phone link, not a login. Each link is an unguessable token of 18 random bytes plus a six-digit PIN; rotating the link replaces both, and disabling it makes it disappear at once. An unknown link and a disabled one return the same "doesn't exist", and a wrong PIN is compared in constant time. A six-digit PIN has a million combinations, and the link allows 40 failed attempts per IP every 10 minutes. Successful uploads do not count towards that limit, so a director working through a stack at 20 cards a minute, the per-link limit, is never locked out by the brute-force guard.

Photos are re-encoded on the server and stripped of EXIF data, which on a phone includes GPS, then stored privately and shown only through short-lived signed URLs. Our first live check found card photos readable without signing in, because the storage bucket's public-read rule covered everything. We replaced it with an explicit list of the folders that really are public, so any private folder added later is private by default.

What is live, and what is still open?

The feature was built and deployed on 11 and 12 September 2026. The phone link, uploads and private storage work in production, and the whole flow, reading included, runs end to end in our own environment. Reading in production waits on one thing that is not code: the client's own AI API key, since the key we tested with belongs to another of our products and stays off their server. Until then, production keeps every card, says so on screen, and reads each waiting card once a key is in place.

What we'd do again, and what we'd do differently

Again: score models against hand-checked ground truth through the production code path; write predictions down before the run; keep the model to transcription and let code own every fact it can check; make "no price on file" different from zero.

Differently: test on the directors' own cards as soon as there are any. Our set had no GSTIN example and no photographed Tamil card, and those are exactly what a Chennai construction firm's contacts will carry. The production model choice should be re-run on a handful of their real cards before the backlog is trusted to it.

If you are weighing a vision model for your own document or card workflow, our AI app development page explains how we approach builds like this one.

Write to us

Want us to write about
something specific?

Send a topic, a question, or a project you'd like a deep-dive on. If it makes sense, it goes on the queue.