The SimpleLPR Java wrapper gives Java 11+ applications access to the full SimpleLPR automatic number plate recognition engine: image analysis, concurrent processing pools, video sources and plate tracking. It is a pure-Java binding over the same native library used by the C/C++, .NET and Python interfaces — no native compilation is ever required on your side.
Maven does not need to be installed: the mvnw /
mvnw.cmd wrapper scripts bundled with the samples download and run
it automatically. The quickest start is running the bundled samples from the
SDK's samples/java folder:
cd "C:\Program Files\Warelogic\SimpleLPR 3.6\samples\java" mvnw.cmd -pl simplelpr-sample-platereader exec:java -Dexec.args="--country Spain C:\path\to\car.jpg"
To use the wrapper in your own Maven project, declare the SDK's local repository and the dependency (or simply copy a sample folder as a template):
<repositories>
<repository>
<id>simplelpr-sdk</id>
<!-- Adjust to your installation; encode spaces as %20 -->
<url>file:///C:/Program%20Files/Warelogic/SimpleLPR%203.6/samples/java/lib/repo</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.warelogic</groupId>
<artifactId>simplelpr</artifactId>
<version>3.6.5</version>
</dependency>
</dependencies>
The repository also carries the -sources and
-javadoc jars, so IDEs such as IntelliJ IDEA show the wrapper's
documentation and source code as you navigate.
The wrapper is also published on Maven Central. To use SimpleLPR without an installed SDK, declare the wrapper plus the natives artifact for your platform (or both, for a portable build):
<dependencies>
<dependency>
<groupId>com.warelogic</groupId>
<artifactId>simplelpr</artifactId>
<version>3.6.6</version>
</dependency>
<dependency>
<groupId>com.warelogic</groupId>
<artifactId>simplelpr</artifactId>
<version>3.6.6</version>
<classifier>natives-win-x64</classifier> <!-- or natives-linux-x64 -->
<scope>runtime</scope>
</dependency>
</dependencies>
On first run the native binaries are extracted from the classpath to
~/.cache/simplelpr/<version>/<platform> (override with the
simplelpr.cache.dir system property). A 60-day evaluation license
is included; for licensed use, register your product key with
setProductKey. An installed SDK, when present, takes precedence
over the classpath natives.
import com.warelogic.simplelpr.*;
public class Version {
public static void main(String[] args) {
// try-with-resources (the Java equivalent of C#'s 'using') guarantees
// native resources are released; the engine implements AutoCloseable.
try (SimpleLPR lpr = SimpleLPR.setup(new EngineSetupParams())) {
System.out.println("SimpleLPR version: " + lpr.getVersionNumber());
System.out.println("Supported countries: " + lpr.getSupportedCountries().size());
}
}
}
EngineSetupParams defaults select CPU-only operation with the
recommended concurrency. When a CUDA-compatible GPU is available, image
processing and classification can be off-loaded independently:
EngineSetupParams params = new EngineSetupParams()
.setCudaDeviceId(0) // -1 = CPU (default)
.setEnableImageProcessingWithGPU(true)
.setEnableClassificationWithGPU(true)
.setMaxConcurrentImageProcessingOps(4); // 0 = recommended value
try (SimpleLPR lpr = SimpleLPR.setup(params)) { ... }
Without a product key the engine runs in evaluation mode for a limited period. To register a key, call right after setup:
lpr.setProductKey(java.nio.file.Paths.get("C:/licenses/simplelpr_key.xml"));
At SimpleLPR.setup(...) time the wrapper looks for the native
binaries in this order:
SimpleLPR.setup(params, nativeFolderPath);simplelpr.native.path system property or the
SIMPLELPR_NATIVE_PATH environment variable;For applications deployed outside the SDK tree, option 2 is usually the most
convenient, pointing at the SDK's bin (Windows) or
lib (Linux) folder:
java -Dsimplelpr.native.path="C:\Program Files\Warelogic\SimpleLPR 3.6\bin" -jar yourapp.jar
import com.warelogic.simplelpr.*;
import java.nio.file.Paths;
import java.util.List;
public class HelloPlate {
public static void main(String[] args) {
try (SimpleLPR lpr = SimpleLPR.setup(new EngineSetupParams())) {
// Enable the countries you expect; disable the rest.
for (String c : lpr.getSupportedCountries()) lpr.setCountryWeight(c, 0f);
lpr.setCountryWeight("Spain", 1f);
lpr.realizeCountryWeights(); // builds lookup tables; call once after configuring
try (Processor proc = lpr.createProcessor()) {
proc.setPlateRegionDetectionEnabled(true);
proc.setCropToPlateRegionEnabled(true);
List<Candidate> found = proc.analyzeFile(Paths.get(args[0]));
for (Candidate cand : found) {
System.out.printf("Plate region confidence %.2f, %s background%n",
cand.getPlateRegionDetectionConfidence(),
cand.isBrightBackground() ? "bright" : "dark");
for (CountryMatch m : cand.getMatches()) {
System.out.printf(" %-12s [%s] confidence %.3f%n",
m.getText(), m.getCountryISO(), m.getConfidence());
for (Element e : m.getElements()) {
System.out.printf(" '%c' (%.2f) at [%d, %d]%n",
e.getGlyph(), e.getConfidence(),
e.getBoundingBox().getLeft(), e.getBoundingBox().getTop());
}
}
}
}
}
}
}
How to read the results:
Candidate is one plate-like region, with a bounding box,
region-detection confidence and quadrilateral vertices.getMatches() list holds the country interpretations.
The last entry is the raw text before syntax validation
(empty country/ISO code) — the reason a plate often appears twice in
simple listings. getCountryISO() returns the ISO Alpha-2 code
("ES", "DE"...).getElements() lists the recognized characters
with per-character confidence and position. The match confidence is the
product of its element confidences — a ranking value in [0, 1], not
a probability.// 1. From a file (.jpg, .png or .tif; 24-bit RGB or 8-bit grayscale)
List<Candidate> r1 = proc.analyzeFile(Paths.get("car.jpg"));
// 2. From the encoded file bytes held in memory
byte[] encoded = java.nio.file.Files.readAllBytes(Paths.get("car.jpg"));
List<Candidate> r2 = proc.analyzeEncodedImage(encoded);
// 3. From raw 8-bit grayscale pixels (top-down, widthStep bytes per row)
byte[] gray = ...;
List<Candidate> r3 = proc.analyzeGray8(gray, width, width, height);
// 4. From raw BGR24 pixels; the weights define the grayscale conversion
// L = w0*C0 + w1*C1 + w2*C2 (BGR: 0.114, 0.587, 0.299 / RGB: reversed)
byte[] bgr = ...;
List<Candidate> r4 = proc.analyzeC3(bgr, width * 3, width, height, 0.114f, 0.587f, 0.299f);
// 5. Zero-copy from native memory via a DIRECT ByteBuffer -- the Java analog
// of the .NET IntPtr overloads. Ideal when pixels come from a camera SDK,
// OpenCV, or a VideoFrame's data buffer.
java.nio.ByteBuffer direct = ...; // e.g. frame.getDataBuffer()
List<Candidate> r5 = proc.analyzeGray8(direct, width, width, height);
try (Processor proc = lpr.createProcessor()) {
try (var files = java.nio.file.Files.list(Paths.get("C:/images"))) {
for (var img : (Iterable<java.nio.file.Path>) files::iterator) {
List<Candidate> found = proc.analyzeFile(img);
System.out.println(img.getFileName() + ": " + found.size() + " candidate(s)");
}
}
}
For high throughput, use a processor pool instead.
// GRAY8 is the cheapest format when frames are only analyzed, never displayed.
try (VideoSource source = lpr.openVideoSource("traffic.mp4", FrameFormat.GRAY8, 1920, -1);
Processor proc = lpr.createProcessor()) {
for (VideoFrame frame; (frame = source.nextFrame()) != null; ) {
// One try-with-resources per frame: frames wrap native pixel memory
// the Java garbage collector cannot see, so close each one promptly.
try (VideoFrame f = frame) {
List<Candidate> found = proc.analyzeVideoFrame(f);
if (!found.isEmpty()) {
System.out.printf("frame %d (t=%.2fs): %s%n",
f.getSequenceNumber(), f.getTimestamp(),
found.get(0).getMatches().get(0).getText());
}
}
}
// nextFrame() returned null: EOF or error -- getState() tells which.
System.out.println("Stream ended in state " + source.getState());
}
The maxWidthCap/maxHeightCap arguments of
openVideoSource (here 1920 / uncapped) isotropically downscale
larger frames before delivery — a cheap way to bound processing cost.
try (VideoFrame f = frame) {
f.saveAsJPEG(Paths.get("frame_" + f.getSequenceNumber() + ".jpg"), 90); // quality 0..100, -1 = default
byte[] pixels = f.copyData(); // or copy the raw pixels instead
java.nio.ByteBuffer view = f.getDataBuffer(); // zero-copy view, valid while open
}
try (VideoSource source = lpr.openVideoSource("rtsp://camera.local/stream",
FrameFormat.GRAY8, 1280, -1);
Processor proc = lpr.createProcessor()) {
System.out.println("Live source: " + source.isLiveSource()); // true for streams
while (true) {
VideoFrame frame = source.nextFrame();
if (frame == null) {
VideoSourceState state = source.getState();
if (state == VideoSourceState.EOF || state == VideoSourceState.IO_ERROR) {
System.out.println("Connection lost (" + state + "), reconnecting...");
if (source.reconnect()) continue; // live sources only
}
break; // unrecoverable
}
try (VideoFrame f = frame) {
process(proc.analyzeVideoFrame(f));
}
}
}
ProcessorPool runs several processors on internal worker
threads: you launch requests and poll results, without
managing threads yourself.
try (ProcessorPool pool = lpr.createProcessorPool()) { // no-arg = sized by CPU cores
pool.setPlateRegionDetectionEnabled(true);
pool.setCropToPlateRegionEnabled(true);
long requestId = 0;
try (var files = java.nio.file.Files.list(Paths.get("C:/images"))) {
for (var img : (Iterable<java.nio.file.Path>) files::iterator) {
// Blocks while all processors are busy (TIMEOUT_INFINITE) -- a
// natural throttle. streamId groups related requests; results
// within one stream come back in launch order.
pool.launchAnalyzeFile(0, requestId++, 0.0,
ProcessorPool.TIMEOUT_INFINITE, img);
// Drain whatever is already finished, without blocking.
for (PoolResult r;
(r = pool.pollNextResult(0, ProcessorPool.TIMEOUT_IMMEDIATE)) != null; ) {
report(r);
}
}
}
// Wait for the stragglers.
while (pool.getOngoingRequestCount(ProcessorPool.STREAM_ID_ANY) > 0) {
report(pool.pollNextResult(ProcessorPool.STREAM_ID_ANY, ProcessorPool.TIMEOUT_INFINITE));
}
}
static void report(PoolResult r) {
if (!r.isSuccess()) { // per-request failures are reported, not thrown
System.out.println("request " + r.getRequestId() + " FAILED: " + r.getError().getMessage());
return;
}
System.out.println("request " + r.getRequestId() + ": " + r.getCandidates().size() + " candidate(s)");
}
launchAnalyzeEncodedImage, launchAnalyzeGray8/C3/C4)
and video frames (launchAnalyzeVideoFrame). Because the pool reads
image buffers on a worker thread after the launch returns:
byte[] and heap-buffer data is copied at launch (reuse the array
immediately), while direct ByteBuffers are passed zero-copy and must
not be modified until the matching result has been polled — the wrapper
retains them until then, so they cannot be garbage collected early.The PlateCandidateTracker turns frame-by-frame detections into
one event per physical plate, resistant to OCR misreadings and partial
detections, with a representative thumbnail per plate.
PlateCandidateTrackerSetupParams tp = new PlateCandidateTrackerSetupParams();
// defaults: trigger window 3.0s, max idle 3.5s, min 3 detections, 256x128 thumbnails
try (VideoSource source = lpr.openVideoSource("traffic.mp4", FrameFormat.BGR24, 1920, -1);
ProcessorPool pool = lpr.createProcessorPool();
PlateCandidateTracker tracker = lpr.createPlateCandidateTracker(tp)) {
pool.setPlateRegionDetectionEnabled(true);
pool.setCropToPlateRegionEnabled(true);
// A frame must stay open from its launch until the tracker has consumed
// its result (the thumbnail is cut from it): correlate by request id.
var pendingFrames = new java.util.HashMap<Long, VideoFrame>();
for (VideoFrame frame; (frame = source.nextFrame()) != null; ) {
long id = frame.getSequenceNumber();
pendingFrames.put(id, frame);
pool.launchAnalyzeVideoFrame(0, id, ProcessorPool.TIMEOUT_INFINITE, frame, null);
for (PoolTrackingResult r;
(r = pool.pollNextTrackingResult(0, ProcessorPool.TIMEOUT_IMMEDIATE)) != null; ) {
handle(r, pendingFrames, tracker);
}
}
while (pool.getOngoingRequestCount(0) > 0) {
handle(pool.pollNextTrackingResult(0, ProcessorPool.TIMEOUT_INFINITE), pendingFrames, tracker);
}
try (TrackerResult flushed = tracker.flush()) { // close whatever is still tracked
reportTracks(flushed);
}
}
static void handle(PoolTrackingResult result, java.util.Map<Long, VideoFrame> pending,
PlateCandidateTracker tracker) {
VideoFrame frame = pending.remove(result.getRequestId());
try (PoolTrackingResult r = result; VideoFrame f = frame) {
if (!r.isSuccess()) return;
try (TrackerResult events = tracker.processFrameCandidates(r, f)) {
reportTracks(events);
}
}
}
static void reportTracks(TrackerResult events) {
for (TrackedPlateCandidate t : events.getNewTracks()) {
System.out.printf("NEW %-12s frames %d..%d%n",
t.getBestText(), t.getFirstDetectionFrameId(), t.getNewestDetectionFrameId());
VideoFrame thumb = t.getRepresentativeThumbnail();
if (thumb != null) thumb.saveAsJPEG(Paths.get(t.getBestText() + ".jpg"), 90);
}
for (TrackedPlateCandidate t : events.getClosedTracks()) {
System.out.println("END " + t.getBestText());
}
}
The same loop works unchanged for RTSP streams; add the
reconnect pattern around nextFrame() for
long-running deployments.
// Weights break ties when a candidate fits several countries' syntaxes.
for (String c : lpr.getSupportedCountries()) lpr.setCountryWeight(c, 0f);
lpr.setCountryWeight("Spain", 1.0f);
lpr.setCountryWeight("France", 0.7f); // enabled, but Spain wins ties
lpr.realizeCountryWeights();
realizeCountryWeights() rebuilds internal lookup
tables and can be time consuming; call it once after configuring, never while
another thread is analyzing. The engine-level call affects all existing and new
processors; Processor and ProcessorPool expose the
same methods to configure one instance independently.// Restrict video-frame analysis to a region (e.g. one traffic lane): Rect lane = new Rect(400, 300, 800, 400); // left, top, width, height List<Candidate> found = proc.analyzeVideoFrame(frame, lane); // null region = whole frame; also available on pool.launchAnalyzeVideoFrame
proc.setContrastSensitivityFactor(0.3f); // [0,1]; LOW values help with shadowed
// plates; higher values otherwise
AutoCloseable. A safety-net cleaner reclaims leaked objects
eventually, but deterministic close is the supported pattern.Candidate,
CountryMatch, Element and PoolResult
hold no native resources: keep them, collect them, share them between
threads, never close them. The two deliberate exceptions:
VideoFrame (close promptly) and PoolTrackingResult
(close after feeding the tracker).SimpleLPR for the process lifetime. A Processor
is single-threaded — one per thread — while a
ProcessorPool manages concurrency for you.try (SimpleLPR lpr = SimpleLPR.setup(new EngineSetupParams())) {
try (Processor proc = lpr.createProcessor()) {
List<Candidate> found = proc.analyzeFile(Paths.get("car.jpg"));
}
} catch (SimpleLPRException e) {
// Engine-reported failures: unsupported image, expired evaluation, ...
if (e.hasErrorCode()) System.err.println("engine error " + e.getErrorCode());
System.err.println(e.getMessage());
} catch (IllegalStateException e) {
// Using an object after close()
System.err.println(e.getMessage());
}
SimpleLPRException
on failure.isSuccess() and
inspect getError(), so one bad image never tears down a
batch.openVideoSource's
maxWidthCap (e.g. 1280–1920): recognition rarely needs
more, and cost scales with pixels.createProcessorPool() sizes it from the CPU cores.ByteBuffers
when pixels already live in native memory (cameras, OpenCV) to avoid
per-frame copies.EngineSetupParams) when a
CUDA device is available; raise
maxConcurrentImageProcessingOps accordingly.doc folder covers every class and method; the
samples/java folder contains the complete, runnable versions of the
programs shown here. Questions? www.warelogic.com.