License plate recognition (MAUI) – camera + overlay toggle
Applies to SDK 2.2.x. The MAUI SDK supports Android and iOS only, using .NET MAUI 9 or 10. See requirements and licensing.
Goals:
- build the recognition options via
LPRRecognitionOptionHelper - use a
RecognitionViewin XAML - start/stop LPR recognition automatically when the camera opens
- toggle overlays (bounding boxes for plates and vehicles)
- display the cropped plate bitmap in a preview image
- handle model download progress events
Prerequisites
- Package(s):
CapCognition.MauiandCapCognition.Maui.LPR - This example uses local models. Bundle the selected
.ccmlfiles asMauiAssetresources inResources/Raw, preserving their logical filenames. See model placement and exact filenames. Missing assets cause an error; these local helpers do not fall back to downloads. Include every size/model selected by your UI (including the wide-text model for split-text mode).
Install the current modular packages and initialize their licensed modules once in MauiProgram.cs:
dotnet add package CapCognition.Maui
dotnet add package CapCognition.Maui.LPR
using CapCognition.Maui.Helpers;
using CapCognition.Maui.Common;
using CapCognition.Maui.LPR;
using SkiaSharp.Views.Maui.Controls.Hosting;
builder
.UseMauiApp<App>()
.UseSkiaSharp()
.InitializeCapCognition(
[
Capture.Use(/* Add your capture license here */),
Recognition.Use(/* Add your recognition license here */),
LicensePlateDetection.Use(/* Add your LPR license here */),
]);
1) Create options (MainPage)
This method matches the setup from OnOpenLPRRecognition and uses LPRRecognitionOptionHelper.PrepareOptions to build the recognition options with default settings (320×320 plate model, 320n text model, country detection on, vehicle detection on, overlays on).
using CapCognition.Maui.Core.Shared.Common;
private void OnOpenLPRRecognition(object sender, EventArgs e)
{
var recogOptions = LPRRecognitionOptionHelper.PrepareOptions(
LPRResolution.R320,
LPRTextResolution.R320n,
isCountryMode: true,
isVehicleDetection: true,
hasOverlay: true);
Navigation.PushAsync(new LPRRecognitionPage(recogOptions));
}
2) Option helper
The helper centralises option building and maps the enum values to concrete model types. It prepares and loads the bundled models synchronously before returning the options. Download-progress UI is relevant only if you adapt the example to use automatic model downloading.
using CapCognition.Maui.Core.Shared.Common;
using CapCognition.Maui.LPR;
using CapCognition.Maui.LPR.Shared.Extensions;
public static class LPRRecognitionOptionHelper
{
public static List<RecognitionOptions> PrepareOptions(
LPRResolution resolution,
LPRTextResolution textResolution,
bool isCountryMode,
bool isVehicleDetection,
bool hasOverlay)
{
var recognitionMode = isVehicleDetection
? isCountryMode
? LicensePlateDetectionRecognitionOptions.RecognitionModeType.CountryLicencePlateThenVehicle
: LicensePlateDetectionRecognitionOptions.RecognitionModeType.NoCountryLicencePlateThenVehicle
: isCountryMode
? LicensePlateDetectionRecognitionOptions.RecognitionModeType.CountryLicencePlateOnly
: LicensePlateDetectionRecognitionOptions.RecognitionModeType.NoCountryLicencePlateOnly;
var lpModelType = resolution switch
{
LPRResolution.R320 => LPModelType.LPModelType320n,
LPRResolution.R640 => LPModelType.LPModelType640n,
LPRResolution.R1280 => LPModelType.LPModelType1280n,
_ => LPModelType.LPModelType320n
};
var textModelType = textResolution switch
{
LPRTextResolution.R320n => LPTextModelType.LPTextModelType320n,
LPRTextResolution.R320s => LPTextModelType.LPTextModelType320s,
LPRTextResolution.R640n => LPTextModelType.LPTextModelType640n,
LPRTextResolution.RSplit512 => LPTextModelType.LPTextModelTypeSplit512n,
_ => LPTextModelType.LPTextModelType320n
};
var vehicleModelType = resolution switch
{
LPRResolution.R320 => VehicleModelType.VehicleModelType320n,
LPRResolution.R640 => VehicleModelType.VehicleModelType640n,
LPRResolution.R1280 => VehicleModelType.VehicleModelType1280n,
_ => VehicleModelType.VehicleModelType320n
};
var recognitionOptions = new RecognitionOptionBuilder()
.AddLicensePlateDetectionRecognitionOption()
.UseCroppedImageForRecognition()
.DoAutomaticDetectionOptimization()
.UseLocalModelsForRecognitionMode(recognitionMode)
.UseLocalLicensePlateModelFromType(lpModelType)
.UseLocalLicensePlateTextModelFromType(textModelType)
.UseLocalVehicleModelFromType(vehicleModelType)
.Done()
.ConfigureOption(option =>
{
option.ClearOldModelsFromCacheFolder();
option.CreateAndPrepareModelsAsync().GetAwaiter().GetResult();
})
.AddLicensePlateRecognitionOverlayDrawingOption()
.SetOverlays(hasOverlay)
.DisplayLicensePlateSurroundingBox()
.DisplayVehicleSurroundingBox()
.SetVehicleSurroundingRectColor(Color.FromRgb(0, 255, 0))
.SetVehicleSurroundingRectStrokeWidth(1)
.SetLicensePlateSurroundingRectColor(Color.FromRgb(255, 0, 0))
.SetLicensePlateSurroundingRectStrokeWidth(1)
.Done()
.Done()
.Build();
return recognitionOptions;
}
}
public enum LPRResolution { R320, R640, R1280 }
public enum LPRTextResolution { R320n, R320s, R640n, RSplit512 }
3) UI (XAML)
The layout is a Grid that places the RecognitionView as a full-size background layer. Control buttons are overlaid in the top row and in lower rows. A small Image (LPPreview) shows the cropped plate bitmap from the last recognized result.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:CapCognition.Maui.Views;assembly=CapCognition.Maui"
x:Class="YourApp.LPRRecognitionPage"
Title="LPR Recognition">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="80"/>
<RowDefinition Height="*"/>
<RowDefinition Height="60"/>
<RowDefinition Height="60"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Full-screen camera preview -->
<views:RecognitionView
x:Name="RecognitionView"
Grid.Row="0" Grid.RowSpan="4"
Grid.Column="0" Grid.ColumnSpan="5"
RequestedPreviewResolutionWidth="1920"
RequestedCaptureResolutionWidth="1080"
CaptureIntervalInMs="0"
UseFrontCamera="False"
FillPreviewToView="False"
DisplayWatermark="False" />
<!-- Top row: toggle buttons -->
<Button Grid.Row="0" Grid.Column="0" BackgroundColor="LightGray"
LineBreakMode="WordWrap"
Text="{Binding ResolutionText}" Clicked="OnToggleResolution" />
<Button Grid.Row="0" Grid.Column="1" BackgroundColor="LightGray"
LineBreakMode="WordWrap"
Text="{Binding TextResolutionText}" Clicked="OnToggleTextResolution" />
<Button Grid.Row="0" Grid.Column="2" BackgroundColor="LightGray"
LineBreakMode="WordWrap"
Text="{Binding CountryText}" Clicked="OnToggleHasCountryDetection" />
<Button Grid.Row="0" Grid.Column="3" BackgroundColor="LightGray"
LineBreakMode="WordWrap"
Text="{Binding VehicleText}" Clicked="OnToggleHasVehicleDetection" />
<Button Grid.Row="0" Grid.Column="4" BackgroundColor="LightGray"
LineBreakMode="WordWrap"
Text="{Binding OverlayText}" Clicked="OnToggleOverlay" />
<!-- Cropped plate preview (bottom-right) -->
<Image x:Name="LPPreview"
Grid.Row="1" Grid.Column="3"
Aspect="AspectFit" />
<!-- Camera controls -->
<Button Grid.Row="2" Grid.Column="0" BackgroundColor="LightGray"
Text="{Binding TorchText}" Clicked="OnToggleTorch" />
<Button Grid.Row="2" Grid.Column="1" BackgroundColor="LightGray"
Text="Close Camera" Clicked="OnCameraClose" />
<Button Grid.Row="2" Grid.Column="2" BackgroundColor="LightGray"
Text="Open Camera" Clicked="OnCameraOpen" />
<!-- Recognition controls -->
<Button Grid.Row="3" Grid.Column="0" BackgroundColor="LightGray"
Text="Single Recogn." Clicked="OnRecognizeSingleImage" />
<Button Grid.Row="3" Grid.Column="1" BackgroundColor="LightGray"
Text="{Binding ContinuousCaptureText}" Clicked="OnRecognizeContinuousImages" />
</Grid>
</ContentPage>
4) Code-behind (simplified)
Key points:
- Options are injected and registered on the
RecognitionViewviaRecognitionView.AddOption(...). - The camera is opened in
OnLoaded/OnAppearing. - Continuous recognition starts automatically inside
OnCameraOpened. - Model download progress is reported via three events on
LicensePlateDetectionRecognitionOptions. - Changing resolution/mode at runtime replaces options and reopens the camera.
using CapCognition.Maui.Core.Shared.Common;
using CapCognition.Maui.LPR;
using SkiaSharp;
using SkiaSharp.Views.Maui.Controls;
namespace YourApp;
public partial class LPRRecognitionPage : ContentPage
{
public LPRRecognitionPage(List<RecognitionOptions> options)
{
_options = options;
InitializeComponent();
BindingContext = this;
DefineTorchText();
DefineContRecognitionText();
foreach (var option in options)
{
RecognitionView.AddOption(option);
}
_licensePlateOptions = RecognitionView.GetOption<LicensePlateDetectionRecognitionOptions>();
Loaded += OnLoaded;
}
// --- Bindable properties ---
public string ResolutionText { get; set; } = "Res: 320x320";
public string TextResolutionText { get; set; } = "Mod n: 320x320";
public string CountryText { get; set; } = "C: On";
public string VehicleText { get; set; } = "V: On";
public string OverlayText { get; set; } = "O: On";
public string TorchText { get; set; } = "Torch On";
public string ContinuousCaptureText { get; set; } = "Start Cont.";
// --- Lifecycle ---
protected override void OnAppearing()
{
base.OnAppearing();
OpenCamera();
}
private async void OnLoaded(object? sender, EventArgs e)
{
var result = await RecognitionView.RequestAllPermissionAsync();
if (!result)
{
await Navigation.PopAsync();
}
OpenCamera();
}
protected override void OnDisappearing()
{
RecognitionView.CloseCamera();
base.OnDisappearing();
}
// --- Camera ---
private void OpenCamera()
{
if (!RecognitionView.Initialized || RecognitionView.CameraIsOpen)
{
return;
}
// Subscribe to model download events if models still need preparation
if (_licensePlateOptions != null && _licensePlateOptions.CheckIfModelsNeedsPreparation())
{
_licensePlateOptions.ModelsDownloadStartedEvt += OnModelsDownloadStarted;
_licensePlateOptions.PreparationProgressEvt += OnModelLoadingProgress;
_licensePlateOptions.ModelsDownloadedEvt += OnModelsDownloaded;
}
RecognitionView.CameraOpenedEvt -= OnCameraOpened;
RecognitionView.CameraOpenedEvt += OnCameraOpened;
RecognitionView.OpenCamera();
}
private void OnCameraOpened(bool success)
{
RecognitionView.CameraOpenedEvt -= OnCameraOpened;
if (!success)
{
Console.WriteLine("Camera could not be opened!");
return;
}
// Automatically start continuous recognition after camera opens
DoStartContinuousRecognition();
DefineTorchText();
DefineContRecognitionText();
}
private void CloseCamera()
{
if (!RecognitionView.CameraIsOpen)
{
return;
}
RecognitionView.CloseCamera();
}
// --- Recognition ---
private void OnRecognizeSingleImage(object? sender, EventArgs e)
{
if (!RecognitionView.CameraIsOpen)
{
return;
}
RecognitionView.SingleRecognition();
}
private void OnRecognizeContinuousImages(object? sender, EventArgs e)
{
if (!RecognitionView.CameraIsOpen)
{
return;
}
if (RecognitionView.CapturePending)
{
DoStopContinuousRecognition();
}
else
{
DoStartContinuousRecognition();
}
DefineContRecognitionText();
}
private void DoStartContinuousRecognition()
{
var success = RecognitionView.StartContinuousRecognition();
if (success)
{
RecognitionView.RecognitionResultEvt += OnImageRecognized;
}
}
private void DoStopContinuousRecognition()
{
var success = RecognitionView.StopContinuousRecognition();
if (success)
{
RecognitionView.RecognitionResultEvt -= OnImageRecognized;
}
}
private void OnImageRecognized(RecognitionResult result, SKBitmap bitmap)
{
var lpResult = result.GetResult<RecognitionProcessorLicensePlateDetectionResult>();
if (lpResult == null)
{
Console.WriteLine($"No LP result: {result}");
return;
}
Console.WriteLine(
$"Plate: {lpResult.PlateNumberValidated} ({lpResult.PlateConfidence}), " +
$"Country: {lpResult.PlateCountryCode}, " +
$"Vehicle: {lpResult.VehicleType} ({lpResult.VehicleConfidence})");
// Show cropped plate in the preview image (must marshal to main thread)
var bitmapClone = lpResult.PlateBitmap?.Copy();
MainThread.BeginInvokeOnMainThread(() =>
{
LPPreview.Source = (SKBitmapImageSource)bitmapClone;
});
}
// --- Toggle controls ---
private void OnToggleTorch(object? sender, EventArgs e)
{
RecognitionView.SetTorch(!RecognitionView.TorchIsOn);
DefineTorchText();
}
private void OnToggleOverlay(object? sender, EventArgs e)
{
_hasOverlay = !_hasOverlay;
OverlayText = _hasOverlay ? "O: On" : "O: Off";
if (_licensePlateOptions != null)
{
_licensePlateOptions.DrawingOptions.EnableOverlays = _hasOverlay;
}
}
private void OnToggleResolution(object? sender, EventArgs e)
{
_currentResolution = _currentResolution switch
{
LPRResolution.R320 => LPRResolution.R640,
LPRResolution.R640 => LPRResolution.R1280,
_ => LPRResolution.R320
};
ResolutionText = _currentResolution switch
{
LPRResolution.R640 => "Res: 640x640",
LPRResolution.R1280 => "Res: 1280x1280",
_ => "Res: 320x320"
};
ChangeToNewRecognitionType();
}
private void OnToggleTextResolution(object? sender, EventArgs e)
{
_currentTextResolution = _currentTextResolution switch
{
LPRTextResolution.R320n => LPRTextResolution.R320s,
LPRTextResolution.R320s => LPRTextResolution.R640n,
LPRTextResolution.R640n => LPRTextResolution.RSplit512,
_ => LPRTextResolution.R320n
};
TextResolutionText = _currentTextResolution switch
{
LPRTextResolution.R320s => "Mod s: 320x320",
LPRTextResolution.R640n => "Mod n: 640x640",
LPRTextResolution.RSplit512 => "Split: 160x512",
_ => "Mod n: 320x320"
};
ChangeToNewRecognitionType();
}
private void OnToggleHasCountryDetection(object? sender, EventArgs e)
{
_hasCountryDetection = !_hasCountryDetection;
CountryText = _hasCountryDetection ? "C: On" : "C: Off";
ChangeToNewRecognitionType();
}
private void OnToggleHasVehicleDetection(object? sender, EventArgs e)
{
_hasVehicleDetection = !_hasVehicleDetection;
VehicleText = _hasVehicleDetection ? "V: On" : "V: Off";
ChangeToNewRecognitionType();
}
// --- Runtime option swap ---
private void ChangeToNewRecognitionType()
{
var recognitionOptions = LPRRecognitionOptionHelper.PrepareOptions(
_currentResolution, _currentTextResolution,
_hasCountryDetection, _hasVehicleDetection, _hasOverlay);
RecognitionView.StopContinuousRecognition();
RecognitionView.CloseCamera();
foreach (var option in _options)
{
RecognitionView.RemoveOption(option);
}
_options = recognitionOptions;
foreach (var option in recognitionOptions)
{
RecognitionView.AddOption(option);
}
RecognitionView.OpenCamera();
RecognitionView.StartContinuousRecognition();
}
// --- Model download events ---
private void OnModelsDownloadStarted(int noOfModels)
{
Console.WriteLine($"Start downloading {noOfModels} license plate models");
}
private void OnModelLoadingProgress(long readBytes, int indexOfModel, int noOfModels)
{
Console.WriteLine($"Model ({indexOfModel}/{noOfModels}) downloading – {readBytes} bytes read");
}
private void OnModelsDownloaded(bool success, int noOfModels)
{
Console.WriteLine($"Finished downloading {noOfModels} models – success={success}");
_licensePlateOptions!.ModelsDownloadStartedEvt -= OnModelsDownloadStarted;
_licensePlateOptions.PreparationProgressEvt -= OnModelLoadingProgress;
_licensePlateOptions.ModelsDownloadedEvt -= OnModelsDownloaded;
}
// --- Helpers ---
private void DefineTorchText() => TorchText = RecognitionView.TorchIsOn ? "Torch Off" : "Torch On";
private void DefineContRecognitionText() => ContinuousCaptureText = RecognitionView.IsRecognizing ? "Stop Cont." : "Start Cont.";
// --- Fields ---
private LPRResolution _currentResolution = LPRResolution.R320;
private LPRTextResolution _currentTextResolution = LPRTextResolution.R320n;
private bool _hasVehicleDetection = true;
private bool _hasCountryDetection = true;
private bool _hasOverlay = true;
private List<RecognitionOptions> _options;
private LicensePlateDetectionRecognitionOptions? _licensePlateOptions;
}
Step-by-step explanation
Choose a recognition mode
- The mode is derived from two boolean flags:
isCountryMode(enables country-code extraction) andisVehicleDetection(enables vehicle-type classification). The four combinations map directly toLicensePlateDetectionRecognitionOptions.RecognitionModeType.
- The mode is derived from two boolean flags:
Select model sizes
- Three independent model groups are configured: the plate-detection model (
LPModelType), the text-reading model (LPTextModelType), and the vehicle-detection model (VehicleModelType). Larger sizes (640 / 1280) improve accuracy at the cost of speed.
- Three independent model groups are configured: the plate-detection model (
Prepare models
ClearOldModelsFromCacheFolder()removes stale cached versions.CreateAndPrepareModelsAsync()downloads and loads the models. Subscribe toModelsDownloadStartedEvt,PreparationProgressEvt, andModelsDownloadedEvtto track download progress.
Configure overlay drawing
AddLicensePlateRecognitionOverlayDrawingOption()enables bounding-box overlays for both the plate and the vehicle with configurable colours and stroke widths.
Register options in the RecognitionView
- Each
RecognitionOptionsobject is registered viaRecognitionView.AddOption(...)in the page constructor.
- Each
Open camera and start recognition
OnCameraOpenedis called once the camera is ready. Continuous recognition is started there automatically so the page immediately begins scanning.
Display results
OnImageRecognizedextractsRecognitionProcessorLicensePlateDetectionResultfrom the result. It logs plate number, confidence, country code, and vehicle type, and copies the cropped plate bitmap to anImagecontrol on the main thread.
Change options at runtime
ChangeToNewRecognitionTypedemonstrates how to swap the active recognition options: stop recognition, close the camera, remove all old options, add new ones, then reopen the camera.
Optional: stabilize results with LicensePlateTemporalTracker
The tracker is included in the LPR package. Keep a tracker for the page lifetime and feed it every recognition callback, including callbacks with no detections. Use its stable result event for UI output instead of displaying each raw OCR result.
using CapCognition.Maui.LPR.CapCognitionLicensePlateTracking;
// Set false to keep displaying raw per-frame recognition results.
private const bool UseTemporalTracker = true;
private LicensePlateTemporalTracker? _tracker;
private void InitializeTracking()
{
if (!UseTemporalTracker) return;
_tracker = new LicensePlateTemporalTracker(
LicensePlateTrackerOptions.Create(LicensePlateTrackingMode.CarDetection));
_tracker.ResultChanged += OnStableResultChanged;
RecognitionView.RecognitionExecutedEvt += OnRecognitionExecuted;
}
private void OnRecognitionExecuted(RecognitionResult? result, SKBitmap bitmap)
{
try
{
_tracker?.Process(bitmap, result ?? new RecognitionResult());
}
finally
{
// The tracker copies retained images; release the callback's plate crops.
if (result != null)
{
foreach (var plate in result.Results.OfType<RecognitionProcessorLicensePlateDetectionResult>())
plate.PlateBitmap?.Dispose();
}
}
}
private void OnStableResultChanged(
LicensePlateTemporalTracker tracker,
StableLicensePlateResult result)
{
MainThread.BeginInvokeOnMainThread(() =>
{
using (result)
{
StablePlateLabel.Text = result.State == StableLicensePlateState.Removed
? "—"
: result.PlateNumber ?? "—";
}
});
}
protected override void OnDisappearing()
{
if (_tracker != null)
{
RecognitionView.RecognitionExecutedEvt -= OnRecognitionExecuted;
_tracker.ResultChanged -= OnStableResultChanged;
_tracker.Dispose();
_tracker = null;
}
RecognitionView.CloseCamera();
base.OnDisappearing();
}
Call InitializeTracking() once after InitializeComponent() and replace the sample's raw-result handler with this callback while tracking is enabled. Add a Label named StablePlateLabel to display the stabilized plate. Choose ParkingOfficer, Barrier, or CarDetection to match the vehicle lifecycle; LicensePlateTrackerOptions.Create(mode) supplies starting thresholds.