ANPR/LPR License Plate Recognition in Mobile Applications
Automatic Number Plate Recognition on mobile—mature task with clear tools. Main engineering problem isn't OCR itself but the pipeline from frame capture to reliable read number: plate detection in real conditions (night, dirt, reflections, CIS non-standard fonts).
Approach Selection
Two paths by requirements:
On-device—for high-frequency scanning (parking, checkpoints) or no guaranteed internet. Models: OpenALPR (open source, supports 60+ countries including RU/BY/UA), Plate Recognizer Edge SDK, Google ML Kit Text Recognition v2 (simple standard plates).
Cloud API—Plate Recognizer API, OpenALPR Cloud, AWS Rekognition. More accurate on complex cases (non-standard angle, unclear font), better handles various CIS regions.
// iOS: on-device ANPR via Vision + custom YOLOv8 for plate detection
class LicensePlateRecognizer {
// Step 1: plate detection via CoreML (YOLOv8n—fast variant)
private let plateDetector: VNCoreMLModel
// Step 2: OCR via Vision Text Recognition
private func recognizeText(in croppedImage: CGImage,
completion: @escaping (String?) -> Void) {
let request = VNRecognizeTextRequest { request, _ in
let text = (request.results as? [VNRecognizedTextObservation])?
.compactMap { $0.topCandidates(1).first?.string }
.joined()
completion(text)
}
request.recognitionLevel = .accurate
request.usesLanguageCorrection = false // disable—plates aren't words
request.minimumTextHeight = 0.1
try? VNImageRequestHandler(cgImage: croppedImage).perform([request])
}
func recognize(sampleBuffer: CMSampleBuffer) async -> PlateResult? {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil }
// Detect plate
let plateBBox = await detectPlate(in: pixelBuffer)
guard let bbox = plateBBox else { return nil }
// Crop and OCR
let croppedCGImage = cropCGImage(pixelBuffer: pixelBuffer, rect: bbox)
let rawText = await recognizeTextAsync(croppedCGImage)
// Normalize and validate
return normalizePlate(rawText)
}
}
CIS License Plate Normalization
OCR produces raw text. For CIS plates, post-processing is critical:
struct PlateNormalizer {
// Replace visually similar characters (typical OCR errors)
static let ocrCorrections: [Character: Character] = [
"0": "O", // sometimes reversed
"1": "I",
"8": "B",
]
// Patterns for different countries
static let patterns: [(country: String, regex: String)] = [
("RU", #"^[АВЕКМНОРСТУХ]{1}\d{3}[АВЕКМНОРСТУХ]{2}\d{2,3}$"#),
("BY", #"^\d{4}[ABCEHIKMOPTX]{2}-\d{1}$"#),
("UA", #"^[АВСЕКМНРОТХBCEKMNOPTX]{2}\d{4}[АВСЕКМНРОТХBCEKMNOPTX]{2}$"#),
("KZ", #"^\d{3}[A-Z]{3}\d{2}$"#)
]
func normalize(_ rawText: String) -> PlateResult? {
let cleaned = rawText.uppercased()
.replacingOccurrences(of: " ", with: "")
.replacingOccurrences(of: "-", with: "")
for (country, pattern) in Self.patterns {
if cleaned.range(of: pattern, options: .regularExpression) != nil {
return PlateResult(text: cleaned, country: country, confidence: .high)
}
}
// No pattern match—low confidence, return as-is
return PlateResult(text: cleaned, country: nil, confidence: .low)
}
}
Continuous Video Stream Mode
For parking or checkpoint—continuous frame scanning without button tap:
// Android: CameraX + continuous analysis via ImageAnalysis
class ContinuousPlateAnalyzer(
private val onPlateDetected: (PlateResult) -> Unit
) : ImageAnalysis.Analyzer {
private val frameThrottler = FrameThrottler(maxFps = 5) // 5 fps sufficient
private val consecutiveMatchThreshold = 3 // 3 consecutive same
private val recentResults = ArrayDeque<String>(maxOf = 5)
override fun analyze(image: ImageProxy) {
if (!frameThrottler.shouldProcess()) { image.close(); return }
val bitmap = image.toBitmap()
val result = plateRecognizer.recognize(bitmap)
image.close()
result?.let { plate ->
recentResults.add(plate.text)
if (recentResults.size >= consecutiveMatchThreshold &&
recentResults.takeLast(consecutiveMatchThreshold).all { it == plate.text }) {
onPlateDetected(plate)
recentResults.clear()
}
}
}
}
Threshold of 3 consecutive identical results eliminates false positives on random objects resembling plates.
Timeline Estimates
On-device ANPR with Vision/ML Kit, normalization for one country, basic UI—3–5 days. Multi-country system with RU/BY/UA/KZ support, continuous video analysis, scan history, external database integration (stolen cars, clients), iOS + Android—1–2 weeks.







