# PP-OCRv6 Small LiteRT runtime examples Use these four unchanged FP32 files with **LiteRT 2.2.0**. The Android wrapper explicitly requests GPU FP32. That setting matters: the default GPU configuration failed numerical parity, while explicit FP32 passed all 37 tensor cases and all six complete image-pipeline cases on the tested Samsung S26 SM8850, Android 16. Those are small synthetic fixture gates, not a broad accuracy benchmark or a guarantee for another GPU. | File | Input float32 NCHW | Output float32 probabilities | |---|---|---| | `ppocrv6_small_det_640_fp32.tflite` | `[1,3,640,640]` | `[1,1,640,640]` | | `ppocrv6_small_rec_320_fp32.tflite` | `[1,3,48,320]` | `[1,40,18710]` | | `ppocrv6_small_rec_640_fp32.tflite` | `[1,3,48,640]` | `[1,80,18710]` | | `ppocrv6_small_rec_960_fp32.tflite` | `[1,3,48,960]` | `[1,120,18710]` | Every model uses signature `serving_default`, input `args_0`, output `output_0`. Both examples verify the model SHA-256 and query actual buffer or tensor types/shapes. Model hashes are pinned in the source. `characters.json` contains all **18,710** tokens, including blank at index 0 and the final space token. Do not append, delete or reorder tokens; its exact SHA-256 is `9c89db8aa95ad30c83ee305d53d366c6bba3f85ff2eab0375ef345fd50b0674a`. ## Android Kotlin Add the Google Maven repository and the versioned dependency to your own application. The tested app used Kotlin 2.2.10, Java/JVM target 17, minSdk 26, compileSdk 35 and arm64-v8a. This example needs no vendor SDK, NPU plugin, APK or precompiled cache. ```kotlin // Dependency repository: google() dependencies { implementation("com.google.ai.edge.litert:litert:2.2.0") } ``` Copy `examples/android/PpocrV6Gpu.kt`, `examples/android/OcrPreprocessing.kt` and `examples/android/TorchUint8Resize.kt` into your app. They use package `com.ppocrv6.litert`. Place the four models and unchanged dictionary in application-private files and pass those paths explicitly. If downloading or copying from assets, complete that operation before constructing the runtime. ```kotlin import com.ppocrv6.litert.* import java.io.File // Run this entire lifetime on one dedicated worker thread, off the UI thread. val modelDir = File(context.filesDir, "ppocrv6") val vocabulary = PpocrV6Gpu.loadVocabulary(File(modelDir, "characters.json")) PpocrV6Gpu(modelDir).use { runtime -> val creationMs = runtime.prepare(OcrGraph.DET_640) // For a prepared, finite FloatArray in the exact [1,3,640,640] NCHW order: val det = runtime.infer(OcrGraph.DET_640, detectorNchw) val map = det.probabilities // 640*640 values, fully read back to the CPU. // Or supply exactly 640x640 interleaved RGB uint8 pixels: val detFromPixels = runtime.detect(squareRgb640) // Supply an upright line crop from the DB/crop steps below: val text = runtime.recognize(cropRgb, cropWidth, cropHeight, vocabulary) println(text.text) } ``` The variables `detectorNchw`, `squareRgb640`, `cropRgb`, `cropWidth` and `cropHeight` are caller-supplied data. `ByteArray` pixels are packed R,G,B; Kotlin's signed bytes are interpreted as unsigned 0–255. No Bitmap, camera orientation, color conversion, DB geometry or crop extraction is hidden in `infer()`. `detect()` adds only the provided exact DET preprocessing, and `recognize()` adds exact REC resize/normalization/padding and greedy CTC. GPU selection is exactly: ```kotlin CompiledModel.Options(setOf(Accelerator.GPU)).apply { gpuOptions = CompiledModel.GpuOptions( precision = CompiledModel.GpuOptions.Precision.FP32) } ``` GPU backend selection stays automatic, as in the passing S26 run; program serialization is not enabled. The wrapper makes no CPU retry or NPU request. Creation/inference errors propagate. A successful creation alone does not prove accelerator placement or numerical correctness on a new device; compare its actual readback and review delegate logs. Do not switch to default/FP16 GPU precision while retaining the FP32 quality claim. `OcrInference.runAndReadbackMs` spans `run()` **and** `readFloat()`. GPU `run()` may only enqueue work. Input transfer is reported separately; preprocessing, DB/cropping and CTC are outside this graph-only timing. `prepare()` reports first model creation plus buffer allocation in this runtime instance; caches have not been proven cold. Models stay loaded until `close()`. ## Image-to-text integration contract The minimally packaged wrapper starts and ends at the model tensor boundary. To reproduce the tested complete pipeline, implement the following surrounding image steps exactly; this recipe does not claim that a new camera-image integration has already passed the six-page gate. 1. Decode an opaque image into interleaved RGB uint8. The tested detector pages are exactly **640×640**. `OcrPreprocessing.det()` requires that size. There is no implicitly validated arbitrary-size resize/letterbox or rotation policy. If adapting arbitrary images, define that policy and coordinate inversion explicitly and gate it separately. 2. Normalize and change layout with `OcrPreprocessing.det(rgb)`. The exact official processor's incoming RGB means are `[.406,.456,.485]`, standard deviations `[.225,.224,.229]`. Compute each mean/std as float32 times 255, subtract/divide in float32, and write channels in reverse order to BGR NCHW. These are not replaceable by rounded decimal shortcuts such as `57.12f`. 3. Read the DET probability map, then apply DB postprocessing with bitmap threshold **0.2**, box score threshold **0.45**, unclip ratio **1.4**, maximum candidates **3000**, and minimum short side **3**. Use the official analytic rectangle expansion, contour/min-area-box scoring and rounding. Do not replace it with an arbitrary polygon-offset library and assume equality. 4. Sort boxes by top-left y/x, with the existing within-row correction when the y distance is below **10 pixels**. For each accepted polygon, use the PaddleX min-area rectangle crop: order TL/TR/BR/BL, integer crop width/height from maximum opposite-edge lengths, OpenCV perspective transform to `(0,0),(w,0),(w,h),(0,h)`, `INTER_CUBIC` and `BORDER_REPLICATE`. Rotate **90° counterclockwise** when `height/width >= 1.5`. The validated implementation used OpenCV **5.0.0**. This wrapper intentionally does not substitute axis-aligned boxes or a different image resampler. 5. Choose the smallest exported REC width covering `ceil(48*cropWidth/cropHeight)`: 320, 640 or 960. Preserve aspect ratio at height 48. Width above 960 is rejected, not compressed; the caller must handle it explicitly. `OcrPreprocessing.rec()` uses the provided two-pass fixed-point PyTorch uint8 bilinear port, then BGR NCHW normalization `(value-127.5f)/127.5f`, with **normalized zero** right padding. Ordinary OpenCV resize differed by one pixel value on tested crops and is not interchangeable for exact parity. 6. Run REC and decode all timesteps, including padding: argmax each probability vector; collapse adjacent repeated IDs; remove ID 0; concatenate dictionary entries. A blank separates repetitions. Keep spaces. Outputs are already softmax probabilities; do not softmax again. Confidence is the mean probability of emitted tokens, or `null` (undefined) for an empty result. The exact preprocessing helper algorithms were already checked on the JVM (38 byte-exact resize vectors and 39 bitwise float32 preprocessing cases), and their original application pipeline passed the real CPU/GPU image tests. This release changes their package name only. New runtime glue is compiled against the actual LiteRT 2.2.0 API; this minimal wrapper itself has not been installed and re-executed on a phone. ## Python CompiledModel CPU For the complete square-image pipeline, install the root `requirements-lock.txt` and run `python examples/run_ocr.py fixtures/det/japanese.png` from this repository. The exact preprocessing, DB polygons, perspective crops and CTC are implemented in `examples/ocr.py`. Both DET and REC use explicit LiteRT CompiledModel CPU inference. The public package also replays all three verification stages on the included fixtures; see `conversion/README.md`. The smaller `examples/run_cpu.py` is a host CPU tensor runner. Install `examples/requirements-cpu.txt` into your own environment; this example was exercised with Python 3.12.12. Supply either a correctly shaped float32 `.npy` tensor or contiguous little-endian float32 `.f32` bytes. Inputs must already follow the preprocessing contract above. Neither Python example claims GPU execution. ```sh python examples/run_cpu.py \ --models /path/to/models \ --model-id rec_320 \ --input /path/to/prepared_rec_input.f32 \ --dictionary /path/to/characters.json \ --output /path/to/rec_output.npy ``` The example writes the output tensor and an adjacent JSON file with queried shapes, finite checks, hashes, decoded REC text, initialization and run/readback timing. DET accepts `--model-id det_640` without a dictionary. Optional `--reference /path/to/same_input_oracle.npy` applies max absolute error ≤0.001 and relative L2 ≤0.0001 and exits nonzero on failure. That comparison requires the same input and matching output shape. Local release verification executed REC320 on the previously saved `LiteRT` input and compared against its official CPU FP32 golden. The public verification summary is `release_verification.json`; detailed machine-local compiler and execution logs are retained separately. The CPU runtime may log discovery/registration of other installed accelerator libraries; the model execution request here is CPU-only. ## Quality and source notices Conversion parity does not remove source-model mistakes: on the six-page fixture set the official/CPU/FP32-GPU pipeline had 14 TP, 3 FP, 0 FN and region CER 5/181, including three nontext shape detections and two missing spaces. This release does not claim benchmark-scale accuracy. NPU and default-GPU quality failures do not qualify those configurations for this recipe. The runtime wrapper and CPU example are Apache-2.0. `OcrPreprocessing.kt` preserves the official PaddlePaddle/Hugging Face Apache-2.0 notice. `TorchUint8Resize.kt` preserves its PyTorch BSD-3-Clause notice and source attribution. Retain those notices when copying the files. The official models are Apache-2.0; runtime third-party dependencies retain their own licenses. No vendor binaries are included in these examples.