SimpleLPR Java QuickStart Guide

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.

1. Installation

Requirements

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.

Alternative: Maven Central, no SDK installation

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.

2. Basic Setup

Minimal Example

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)) { ... }

License Key Setup

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"));

How the native library is located

At SimpleLPR.setup(...) time the wrapper looks for the native binaries in this order:

  1. an explicit folder passed to SimpleLPR.setup(params, nativeFolderPath);
  2. the simplelpr.native.path system property or the SIMPLELPR_NATIVE_PATH environment variable;
  3. folders relative to the wrapper's location and to the working directory, matching the SDK layout — which is why programs run from inside the SDK tree need no configuration at all.

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

3. Your First Recognition

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:

4. Working with Images

Different Input Methods

// 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);

Batch Processing Multiple Images (sequential)

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.

5. Processing Video Files

Basic Video Processing

// 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.

Saving Frame Thumbnails

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
}

6. Real-time Video Streams

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));
        }
    }
}

7. Multi-threaded Processing

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)");
}
Pools also accept in-memory images (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.

8. License Plate Tracking

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.

9. Advanced Configuration

Country-Specific Configuration

// 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.

Region of Interest

// 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

Contrast Sensitivity

proc.setContrastSensitivityFactor(0.3f);  // [0,1]; LOW values help with shadowed
                                          // plates; higher values otherwise

10. Patterns and Best Practices

11. Error Handling

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());
}

12. Performance Optimization

Reference material: the SimpleLPR Java API Reference in the SDK's 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.