Building Robust OCR Extraction for Pakistani Documents
Built the core image processing, OCR, and field extraction pipeline for docling-pk, supporting Pakistani identity and education documents. Real-world testing exposed failures caused by image preprocessing assumptions and OCR layout variability, leading to more robust extraction strategies. The system now handles rotated documents, layout-dependent fields, and explicit extraction failures rather than relying on ideal document structure. The work reinforced a practical lesson: real document variability must drive OCR design, not assumptions from standard preprocessing techniques.
Lab Note & Technical Documentation
What I built
I built the image processing and field extraction core of docling-pk, a
library that reads structured data (name, CNIC number, dates, marks) from
photos of Pakistani identity and education documents. This includes
preprocessor.py (blur detection and deskew), ocr.py (an EasyOCR wrapper
that tries all four rotations and keeps the best result), the field
extraction logic in parsers/cnic.py, parsers/matric.py, and
parsers/intermediate.py, and extractor.py, which ties all of it
together.
One technical decision
I initially added adaptive thresholding to preprocessing because it is standard advice for OCR under uneven lighting. Later, while debugging low confidence scores, I ran a direct test: the same real photos, with and without that step. Removing it raised EasyOCR's own confidence on every sample, sometimes by 20 points. The threshold was converting images into a harsh black and white format the model was never trained on.
What I would do differently: test standard advice against real data from day one instead of trusting it by default.
What broke, and how I fixed it
CNIC number and date extraction worked on my first test image but failed inconsistently on others. I assumed my regex was wrong and spent time tightening it. The real cause became clear only when I printed the raw OCR text directly: EasyOCR read all three date labels as one group, then all three date values as a separate group further down, not as label-value pairs. My code assumed a value always sits right after its label, which worked for some fields and silently failed for others depending on layout. The fix was to stop assuming label adjacency. I extract every date shaped string from the raw text and assign them by fixed position (birth, issue, expiry), since that order holds across real cards even when layout scrambles their position relative to their labels.
What I could not have learned from a tutorial
The same field can fail for different reasons on different real documents, and that is a feature of the problem, not a bug in my code. Gender extracted cleanly as text on one CNIC and was completely unrecoverable on another, due to a difference in the card's print sub-format. This taught me to write extraction code that records why a field failed, not just that it did, and to stop treating one successful test image as proof of a working solution.
Project Repositories & Artifacts
Lead Engineer
Building a Reproducible NLP Dataset Quality Auditor
We built `inference-audit`, a reproducible Python toolkit for systematically auditing NLP dataset quality across five core dimensions. The pipeline combines deterministic checks for label distribution, missing values, near-duplicates, language contamination, and annotation consistency. Extensive testing and real-corpus evaluation exposed performance and reliability issues, leading to targeted fixes and measurable optimizations. The final system emphasizes reproducibility, explicit failure states, citable evidence, and transparent limitations.
Read Report llm-eval-kitBuilding a Deterministic Offline Evaluation Engine for LLM Systems
We redesigned llm-eval-kit into a deterministic, zero-network evaluation pipeline built for reliable continuous evaluation in CI/CD environments. The new architecture introduces a decoupled criteria registry, fail-fast orchestration, hybrid semantic and symbolic verification, and graceful handling of inapplicable evaluation criteria. The implementation reached 93% test coverage across 88 tests, with cross-version CI validation from Python 3.9 to 3.12. The work demonstrates how carefully defined evaluation contracts and offline heuristics can provide reproducible, privacy-preserving model assessment without relying on external LLM-as-a-judge APIs.
Read Report