Fast License Plate OCR inference in pure Rust. A high-performance port of fast-plate-ocr that provides fast and accurate license plate character recognition across multiple countries.
- Pure Rust default backend using
tract-onnxfor ONNX model inference - Optional Tencent NCNN backend for Raspberry Pi 5 / edge CPU inference from
.param/.binmodels - Multiple pre-trained models for global and regional license plates
- Character-level confidence scores for result validation
- Region detection for plates with regional information (e.g., country/state)
- Efficient batch processing with automatic image resizing
- Offline support with model caching and custom model paths
- Cross-platform support (Linux, macOS, Windows)
Add to your Cargo.toml:
[dependencies]
fpo-rust = "0.1"Or use the git repository:
[dependencies]
fpo-rust = { git = "https://github.com/alparslanahmed/fpo-rust" }Check the crates.io page for the latest version.
Clone the repository and build:
git clone https://github.com/alparslanahmed/fpo-rust
cd fpo-rust
cargo build --releaseThe binary will be available at target/release/fpo-rust.
use fpo_rust::{LicensePlateRecognizer, OcrModel, PlateInput};
fn main() -> anyhow::Result<()> {
// Load a model from the hub (downloads on first use, then cached)
let recognizer = LicensePlateRecognizer::from_hub(
OcrModel::CctSV2Global, // Recommended global model
false // force_download
)?;
// Run inference on a single image
let prediction = recognizer.run_one(
PlateInput::from("path/to/plate.png"),
true, // return_confidence scores
true // remove_pad_char
)?;
println!("Plate: {}", prediction.plate);
println!("Average Confidence: {:.2}", prediction.avg_char_confidence());
if let Some(region) = &prediction.region {
println!("Region: {} ({:.1}%)", region, prediction.region_prob.unwrap_or(0.0) * 100.0);
}
Ok(())
}# Using a hub model (downloads model on first run)
fpo-rust run --model cct-s-v2-global-model plate1.jpg plate2.png
# Using a custom model and config
fpo-rust run --onnx ./models/custom.onnx --config ./models/custom_config.yaml plate.jpg
# Keep padding characters in output
fpo-rust run --model cct-s-v2-global-model --keep-pad plate.jpg# After building with --features ncnn-cpu or --features ncnn-vulkan:
fpo-rust run --backend ncnn --model cct-s-v2-global-model --threads 4 plate.jpg
# Or with explicit converted files:
fpo-rust run --backend ncnn \
--param ./models/custom.ncnn.param \
--bin ./models/custom.ncnn.bin \
--config ./models/custom_config.yaml \
plate.jpg# Benchmark with 500 iterations, batch size 1
fpo-rust benchmark --model cct-s-v2-global-model
# Custom benchmark settings
fpo-rust benchmark --model cct-s-v2-global-model --iters 1000 --batch 32 --warmup 100
# Include pre/post-processing time in benchmark
fpo-rust benchmark --model cct-s-v2-global-model --include-processing
# Benchmark converted NCNN model on Raspberry Pi 5
fpo-rust benchmark --backend ncnn --model cct-s-v2-global-model --threads 4cct-s-v2-global-model⭐ - Compact Convolutional Transformer Small v2, optimized for global platescct-xs-v2-global-model- XSmall v2, faster but less accuratecct-s-v1-global-model- Small v1, previous versioncct-xs-v1-global-model- XSmall v1, lightweightcct-s-relu-v1-global-model- Small v1 ReLU variantcct-xs-relu-v1-global-model- XSmall v1 ReLU variant
european-plates-mobile-vit-v2-model- Optimized for European license platesglobal-plates-mobile-vit-v2-model- Mobile-optimized global modelargentinian-plates-cnn-model- CNN model for Argentinian platesargentinian-plates-cnn-synth-model- CNN model trained with synthetic data
Models are automatically downloaded and cached on first use. The default cache location is:
- Linux/macOS:
~/.cache/fpo-rust/ - Windows:
%APPDATA%\fpo-rust\
If you have your own ONNX model and config file:
use fpo_rust::{LicensePlateRecognizer, PlateInput};
use std::path::Path;
fn main() -> anyhow::Result<()> {
let recognizer = LicensePlateRecognizer::from_files(
Path::new("./models/my_model.onnx"),
Path::new("./models/my_config.yaml")
)?;
let prediction = recognizer.run_one(
PlateInput::from("plate.jpg"),
true,
true
)?;
println!("{}", prediction.plate);
Ok(())
}Download models to a specific directory for offline use:
use fpo_rust::{LicensePlateRecognizer, OcrModel};
use std::path::Path;
fn main() -> anyhow::Result<()> {
let custom_dir = Path::new("./offline_models");
// Downloads to ./offline_models/ instead of default cache
let recognizer = LicensePlateRecognizer::from_hub_to_dir(
OcrModel::CctSV2Global,
custom_dir,
false
)?;
// Later, you can use the cached models directly
let recognizer = LicensePlateRecognizer::from_files(
custom_dir.join("cct_s_v2_global.onnx"),
custom_dir.join("cct_s_v2_global_plate_config.yaml")
)?;
Ok(())
}Download all models to a directory for offline use:
# Create a models directory
mkdir models
# Run CLI with custom model path (this will download if not present)
fpo-rust run --onnx ./models/my_model.onnx --config ./models/my_config.yaml plate.jpgThe NCNN backend is optional. The default binary stays pure Rust and uses ONNX through tract-onnx; NCNN builds require a local Tencent NCNN install because the backend links to NCNN's C API.
Tencent's NCNN docs recommend pnnx for ONNX conversion and produce .ncnn.param plus .ncnn.bin files. See the official NCNN ONNX guide and PNNX options:
- https://github.com/tencent/ncnn/wiki/use-ncnn-with-pytorch-or-onnx
- https://github.com/Tencent/ncnn/tree/master/tools/pnnx
CPU-only is the most predictable path on Raspberry Pi 5. Vulkan can be experimented with separately, but NCNN's own Raspberry Pi notes call out driver maturity concerns.
sudo apt update
sudo apt install -y build-essential git cmake clang libclang-dev \
libprotobuf-dev protobuf-compiler libgomp1
git clone --recursive https://github.com/Tencent/ncnn.git
cd ncnn
mkdir build-rpi5-cpu
cd build-rpi5-cpu
cmake -DCMAKE_BUILD_TYPE=Release \
-DNCNN_VULKAN=OFF \
-DNCNN_C_API=ON \
-DNCNN_STRING=ON \
-DNCNN_STDIO=ON \
-DNCNN_BUILD_TOOLS=ON \
-DNCNN_BUILD_EXAMPLES=OFF \
..
cmake --build . -j"$(nproc)"
sudo cmake --install . --prefix /opt/ncnncd /path/to/fpo-rust
export NCNN_LIB_DIR=/opt/ncnn/lib
cargo build --release --features ncnn-cpuIf your NCNN install uses a non-standard link setup, these environment variables are supported:
NCNN_LIB_DIR: directory containinglibncnn.aorlibncnn.soNCNN_LINK_KIND:staticordylib(default:static)NCNN_LIB_NAME: library name without prefix/suffix (default:ncnn)NCNN_OPENMP_LIB: OpenMP runtime for static Linux builds (default:gomp; useompfor LLVM OpenMP ornonefor-DNCNN_OPENMP=OFF)NCNN_VULKAN: set to1to linklibvulkanwhen using a Vulkan-enabled NCNN buildNCNN_VULKAN_LIBS: override glslang/static shader compiler libraries for Vulkan builds. By default the build script usespkg-config --libs --static glslang spirv, then falls back toglslang,MachineIndependent,GenericCodeGen,SPIRV,OSDependent.NCNN_EXTRA_LIBS: extra libraries separated by commas, semicolons, or spaces
For an NCNN build compiled with -DNCNN_VULKAN=ON, install Vulkan and glslang development libraries and build with:
sudo apt install -y libvulkan-dev glslang-dev pkg-config
export NCNN_LIB_DIR=/opt/ncnn/lib
cargo build --release --features ncnn-vulkanOn older revisions of this crate, the equivalent manual workaround is:
NCNN_EXTRA_LIBS=gomp,vulkan,glslang,MachineIndependent,GenericCodeGen,SPIRV,OSDependent \
cargo build --release --features ncnn-cpuThe repository includes converted NCNN files for every built-in hub model under models/.
For those models, no conversion is required:
./target/release/fpo-rust run \
--backend ncnn \
--model cct-s-v2-global-model \
--threads 4 \
plate.jpgTo refresh a bundled model or convert into a custom directory, run:
./target/release/fpo-rust convert-ncnn --model cct-s-v2-global-model --pnnx pnnxFor custom models:
./target/release/fpo-rust convert-ncnn \
--onnx ./models/custom.onnx \
--config ./models/custom_config.yamlThe converter command passes inputshape=[1,img_height,img_width,channels] from the YAML config and writes:
<model>.ncnn.param
<model>.ncnn.bin
The default is fp16=0 for portability. Add --fp16 if you want PNNX to store fp16 weights and have verified accuracy/performance on your NCNN build.
If NCNN reports a missing blob name, inspect the .ncnn.param file or Netron graph and pass explicit names:
./target/release/fpo-rust run --backend ncnn \
--param ./models/custom.ncnn.param \
--bin ./models/custom.ncnn.bin \
--config ./models/custom_config.yaml \
--input-name in0 \
--plate-output out0 \
--region-output out1 \
plate.jpgThe built-in fast-plate-ocr ONNX models use NHWC input shape [1, H, W, C]; the NCNN backend preserves that layout for converted models.
Set FPO_NCNN_DEBUG=1 before run to print the NCNN load, input, and output-extraction steps. This is useful when diagnosing native NCNN crashes on a target device.
The main inference engine.
Methods:
-
from_hub(model: OcrModel, force_download: bool) -> Result<Self>- Load a model from the hub with automatic caching
-
from_hub_to_dir(model: OcrModel, save_dir: &Path, force_download: bool) -> Result<Self>- Load a model from hub, saving to a specific directory
-
from_files(onnx_path: impl AsRef<Path>, config_path: impl AsRef<Path>) -> Result<Self>- Load a custom ONNX model with its config
-
from_hub_ncnn(model: OcrModel, force_download: bool) -> Result<Self>(featurencnn)- Load a bundled NCNN hub model, falling back to converted cache files
-
from_hub_to_dir_ncnn(model: OcrModel, save_dir: &Path, force_download: bool) -> Result<Self>(featurencnn)- Load a converted NCNN model from a specific directory
-
from_ncnn_files(param_path, bin_path, config_path) -> Result<Self>(featurencnn)- Load a converted NCNN model with default blob-name inference
-
from_ncnn_files_with_options(param_path, bin_path, config_path, NcnnOptions) -> Result<Self>(featurencnn)- Load a converted NCNN model with explicit blob names / runtime options
-
run(inputs: &[PlateInput], return_confidence: bool, remove_pad_char: bool) -> Result<Vec<PlatePrediction>>- Run inference on multiple images
-
run_one(input: PlateInput, return_confidence: bool, remove_pad_char: bool) -> Result<PlatePrediction>- Run inference on a single image
Represents a single plate image input. Can be:
// From file path
PlateInput::from("plate.jpg")
// From Path object
PlateInput::from(Path::new("plate.jpg"))
// From pre-loaded DynamicImage
use image::open;
let img = open("plate.jpg")?;
PlateInput::from(img)Output of a single inference.
Fields:
plate: String- Recognized plate textregion: Option<String>- Region/country if detectedregion_prob: Option<f32>- Confidence of region (0.0-1.0)char_probs: Option<Vec<f32>>- Per-character confidence scores
Methods:
avg_char_confidence() -> f32- Average confidence across all characters
Enum of available hub models:
pub enum OcrModel {
CctSV2Global,
CctXsV2Global,
CctSV1Global,
CctXsV1Global,
CctSReluV1Global,
CctXsReluV1Global,
ArgentinianPlatesCnn,
ArgentinianPlatesCnnSynth,
EuropeanPlatesMobileVitV2,
GlobalPlatesMobileVitV2,
}let pred = recognizer.run_one(input, true, true)?;
println!("Plate: {}", pred.plate);
println!("Avg Confidence: {:.2}", pred.avg_char_confidence());
println!("Region: {}", pred.region.unwrap_or("Unknown".to_string()));plate.jpg: 34PE7523 [Turkey] (98.7%) - Char Confidence: 0.99
34PE7523- Recognized plate text[Turkey]- Detected region (if available)(98.7%)- Region confidence score0.99- Average character confidence (0.0-1.0)
Performance varies by model size and hardware. Example benchmarks on standard hardware:
| Model | Inference Time | Memory |
|---|---|---|
| cct-xs-v2-global | ~10-15ms | ~50MB |
| cct-s-v2-global | ~20-30ms | ~100MB |
| mobile-vit-v2-global | ~30-50ms | ~150MB |
Use fpo-rust benchmark --model <name> to measure performance on your hardware.
The library respects the XDG_CACHE_HOME environment variable on Linux/macOS. To use a custom cache directory:
# Use custom cache directory
export XDG_CACHE_HOME=/path/to/cache
cargo run -- run --model cct-s-v2-global-model plate.jpgcargo build --releaseThe optimized binary will be at target/release/fpo-rust.
For maximum portability, you can create a statically-linked binary. This requires additional setup depending on your platform.
- Check your internet connection
- Verify GitHub is not blocked
- Use
RUST_LOG=debugfor more details:RUST_LOG=debug cargo run -- run --model cct-s-v2-global-model plate.jpg
- Try a different model optimized for your region
- Ensure plate images have sufficient contrast and resolution
- Pre-process images (crop, rotate) if plates are at odd angles
- Use a smaller model (e.g.,
cct-xs-v2-global-model) - Reduce batch size in benchmarks:
--batch 1
cargo testcargo run --release -- benchmark --model cct-s-v2-global-model --iters 1000- tract-onnx - Pure-Rust ONNX runtime
- Tencent NCNN C API - Optional linked backend (feature
ncnn,ncnn-cpu, orncnn-vulkan) - image - Image loading and processing
- serde - Serialization framework
- serde_yml - YAML parsing for config files
- ureq - Lightweight HTTP client for model downloads
- anyhow - Error handling
This project is licensed under the MIT License - see the LICENSE file for details.
- fast-plate-ocr - Original Python implementation
- tract - Pure Rust ONNX runtime
- ONNX - Open Neural Network Exchange format
Contributions are welcome! Please feel free to submit issues and pull requests.