A user points a camera at a faded receipt in dim light. Without image preprocessing, standard OCR libraries produce up to 50% errors. The solution is to apply contrast stretching (vImageContrastStretch on iOS, OpenCV on Android) and binarization at the capture stage. Only then feed the frame to VNRecognizeTextRequest or ML Kit Text Recognition. In our practice, this chain raises accuracy from 70% to 98-99% on printed text.
Over five years, we have delivered more than 20 OCR projects for retail (price tags and receipts), logistics (waybill tracking), and fintech (passport data verification). Every project had its own set of filters and post-processing steps. A common mistake is expecting the OCR library to produce perfect results out of the box. Without image preprocessing and result post-processing, accuracy drops to 50–70%. Therefore, the first stage of any project is collecting real document samples and testing on them.
Why Preprocessing Is Critical for Accurate OCR
Preprocessing is a key stage that determines final accuracy. In poor lighting or blur, we use:
- Contrast enhancement via
vImageContrastStretch(iOS) orOpenCV(Android). - Grayscale conversion followed by
AdaptiveThreshold. -
SharpenCIFilter before feeding into OCR.
For handwritten text, standard frameworks show 40–60% accuracy. In such cases, custom models based on TensorFlow Lite help — this is a separate task requiring labeled data and training.
How Native OCR Frameworks Work
iOS: Vision + VNRecognizeTextRequest
Since iOS 13, the Vision framework can recognize text offline. VNRecognizeTextRequest supports two modes: .fast (approximate, instant) and .accurate (slower but 15% more accurate for complex fonts). For complex fonts, the .accurate mode gives a 15% accuracy boost.
func recognizeText(in image: UIImage) {
guard let cgImage = image.cgImage else { return }
let request = VNRecognizeTextRequest { [weak self] request, error in
guard let observations = request.results as? [VNRecognizedTextObservation] else { return }
let text = observations.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\n")
DispatchQueue.main.async { self?.handleRecognized(text: text) }
}
request.recognitionLevel = .accurate
request.usesLanguageCorrection = true
request.recognitionLanguages = ["ru-RU", "en-US"] // order = priority
let handler = VNImageRequestHandler(cgImage: cgImage, options: [: ])
try? handler.perform([request])
}
usesLanguageCorrection helps with typos, but sometimes "corrects" abbreviations and article numbers — for technical documents it is better to disable it.
Android: ML Kit Text Recognition v2
com.google.mlkit:text-recognition supports Latin, Cyrillic, Chinese, Japanese, Korean via separate modules. It downloads on first use (~5 MB for Latin).
val recognizer = TextRecognition.getClient(
TextRecognizerOptions.DEFAULT_OPTIONS // or RussianTextRecognizerOptions
)
val image = InputImage.fromBitmap(bitmap, 0)
recognizer.process(image)
.addOnSuccessListener { visionText ->
val fullText = visionText.textBlocks
.joinToString("\n") { block -> block.text }
handleRecognized(fullText)
}
.addOnFailureListener { e -> handleError(e) }
ML Kit also returns bounding boxes for each text block — useful for highlighting recognized areas in the UI.
Live Mode: Real-Time Text from Video Stream
For live-overlay (text highlighted directly in the video stream) on iOS we use AVCaptureSession + CMSampleBuffer:
// Delegate method AVCaptureVideoDataOutput
func captureOutput(_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
// Do not start a new request if previous is still running
guard !isProcessing else { return }
isProcessing = true
let request = VNRecognizeTextRequest { [weak self] request, _ in
defer { self?.isProcessing = false }
// handle results...
}
request.recognitionLevel = .fast // speed matters for live
try? VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [: ]).perform([request])
}
The isProcessing flag is mandatory — without it at 30 FPS, the request queue piles up and memory grows until crash.
On Android — CameraX + ImageAnalysis.Analyzer. ML Kit is optimized to work directly with ImageProxy without converting to Bitmap.
| Parameter | Static Recognition | Live Recognition |
|---|---|---|
| Processing speed | 100-200 ms | up to 30 ms per frame |
| Accuracy | up to 99% | up to 95% (due to speed compromise) |
| Battery consumption | low | medium (continuous processing) |
| Application | document scanning, receipts | pointing at business cards, license plates |
Improving Recognition Quality in Difficult Conditions
Contrast enhancement and binarization are standard techniques. For specific documents (e.g., faded receipts), we add custom filters.
Common integration mistakes:
- Forgetting the
isProcessingflag in live mode → memory leak. - Leaving
usesLanguageCorrectionenabled for technical texts → corrupts abbreviations. - Not checking bounding boxes for overlap with UI → text overlays on interface.
Post-Processing: From Raw Text to Structured Data
Raw OCR output is a stream of strings. Most tasks require structuring:
- Receipts: extract lines with prices via regex, parse final amount.
- Business cards:
NSDataDetector(iOS) orPatterns(Android) for phones, emails, addresses. - Passports/documents: MRZ zone read by ICAO 9303 standard, parsers available.
- License plates: separate task — better to use specialized models (OpenALPR, PlateRecognizer API).
For poor-quality Cyrillic text, preprocessing helps: increase contrast via vImageContrastStretch, grayscale, Sharpen CIFilter before OCR.
Comparison of Native Frameworks
| Parameter | Vision (iOS) | ML Kit (Android) |
|---|---|---|
| Modes | .fast, .accurate | base model |
| Languages | up to 15 in one request | modules: Lat, Cyr, Chi, Jap, Kor |
| Offline | yes | yes (model ~5 MB) |
| Accuracy on printed | ~98% | ~97% |
| Bounding boxes | yes | yes |
| Speed (full HD) | 100-200 ms | 80-150 ms |
Click for more details on preprocessing techniques
For extreme conditions, we use adaptive thresholding (e.g., cv2.adaptiveThreshold with block size 11) and morphological operations to remove noise. These steps can improve accuracy by an additional 10-15%.
Deliverables Included in Development
When ordering this service, you receive:
- Integration of Vision or ML Kit into your app.
- Tuning recognition parameters for your document type.
- Live mode with text highlighting on camera (optional).
- Data post-processing: parsing receipts, business cards, numbers.
- Integration and testing documentation.
- Support during App Store / Google Play review.
Work Process
- Define use cases: document types, languages, need for live mode or only static photo.
- Implement image capture (camera + gallery), preprocessing.
- Integrate OCR: native Vision/ML Kit or cloud (Google Vision API, AWS Textract) if higher accuracy is needed for complex documents.
- Post-processing for the specific task: data structuring, regex, NER.
- Test on real samples in different lighting conditions.
Timeline Estimates
Basic static text recognition via native framework — 2-3 days (starting from $2,000). Live mode with overlay + data structuring for a specific document type — 1-2 weeks (approx $5,000–$8,000). Complex scenarios with custom models — from one month (budget $15,000+). Contact us for an exact estimate for your project.
Our guaranteed accuracy on printed text is 98% with preprocessing; we provide a certified integration with documented results. Over 5 years of experience ensures reliable project delivery.
Get a consultation for your project — we will select the optimal solution. Contact us to discuss your task and estimate the cost of OCR development for your app. Our solution supports multilingual OCR offline including Russian, English, Chinese, and Japanese.







