<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Muhamad Rizki on Medium]]></title>
        <description><![CDATA[Stories by Muhamad Rizki on Medium]]></description>
        <link>https://medium.com/@byrizki?source=rss-6819503738e0------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*VukhV4XG8WM89O3wPyz_CQ.png</url>
            <title>Stories by Muhamad Rizki on Medium</title>
            <link>https://medium.com/@byrizki?source=rss-6819503738e0------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 05 Nov 2026 04:44:42 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@byrizki/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Why We Built a Pure-Rust OCR Engine: Zero C++, Sub-100ms Latency, and a 5MB Binary]]></title>
            <link>https://medium.com/@byrizki/why-we-built-a-pure-rust-ocr-engine-zero-c-sub-100ms-latency-and-a-5mb-binary-e410269b4bd1?source=rss-6819503738e0------2</link>
            <guid isPermaLink="false">https://medium.com/p/e410269b4bd1</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[computer-vision]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[ocr]]></category>
            <category><![CDATA[rust]]></category>
            <dc:creator><![CDATA[Muhamad Rizki]]></dc:creator>
            <pubDate>Tue, 20 Oct 2026 04:53:22 GMT</pubDate>
            <atom:updated>2026-10-20T04:53:22.722Z</atom:updated>
            <content:encoded><![CDATA[<h3>Why We Built a Pure-Rust OCR Engine: Zero C++ and a 5MB Binary</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*SqBftU_U2YmY81uh" /><figcaption>Photo by <a href="https://unsplash.com/@patrickian4?utm_source=medium&amp;utm_medium=referral">Patrick Fore</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>If you have ever needed to pull text out of a document scan, receipt, or identity card in production, you know how quickly things turn ugly.</p><p>On paper, Optical Character Recognition sounds like a solved problem: pass a JPEG into an engine, get text back.</p><p>In practice? You find yourself chained to <strong>OpenCV and its decades-old C++ ecosystem</strong>.</p><p>For the past twenty years, nearly every production OCR pipeline — from Tesseract and EasyOCR to RapidOCR and PaddleOCR — has treated OpenCV as an inescapable law of physics. OpenCV handles the image loading, the resizing, the contour tracing, the polygon dilation, and the perspective warping.</p><p>OpenCV is an extraordinary piece of engineering. But when you try shipping it in modern production architectures, it quickly becomes an operational liability:</p><ul><li><strong>In Docker &amp; Serverless</strong>: Deploying an OpenCV-based OCR pipeline balloons container images to <strong>1.5–2.0 GB</strong>. On serverless platforms like AWS Lambda or Google Cloud Run, cold starts drag on for 8 to 12 seconds, while baseline memory overhead sits at <strong>300–400 MB RAM per worker</strong>.</li><li><strong>On Mobile (React Native, iOS, Android)</strong>: Bundling OpenCV means wrestling with Android NDK versions, CMake build scripts, and libc++_shared.so collisions. It adds <strong>40–60 MB</strong> of uncompressed native libraries to your application bundle.</li><li><strong>On the Web (WASM)</strong>: Compiling OpenCV to WebAssembly through Emscripten yields an unwieldy <strong>30MB+ </strong><strong>.wasm file</strong> that consumes hundreds of megabytes of heap space and freezes browser tabs.</li></ul><p>Late last year, we asked ourselves an honest question:</p><blockquote><strong>What if we completely removed OpenCV from the OCR pipeline and rebuilt the entire computer vision and inference stack in pure Rust?</strong></blockquote><p>That inquiry became <a href="https://github.com/byrizki/rusto-rs"><strong>RustO! (rusto-rs)</strong></a>.</p><p>Here is the engineering story of how we broke free from OpenCV, rebuilt every computer vision primitive in 100% safe Rust, and created a sub-100ms OCR engine with a self-contained ~5.4 MB binary that runs everywhere — from high-throughput cloud servers to client-side browser WebAssembly.</p><h3>The OpenCV Trap in Modern OCR</h3><p><strong>For decades, modern OCR frameworks have relied on OpenCV for image preprocessing, morphological operations, and perspective cropping, introducing massive binary bloat and dynamic linking headaches.</strong> While OpenCV excels at general computer vision, coupling an entire OCR engine to heavy C++ shared libraries creates severe deployment friction.</p><p>To understand why OpenCV became a bottleneck, you have to look at what an OCR pipeline actually does. Deep learning models like DBNet (detection) and SVTR/CRNN (recognition) only do part of the work. The glue holding them together is computer vision:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pX0EvtlaclQpYRavwzUVNg.png" /></figure><p>In conventional OpenCV-based architectures, every document scan undergoes an expensive dance:</p><ol><li><strong>Host-Level Dynamic Linking</strong>: The application process must dynamically link against heavy shared libraries (libopencv_core.so, libopencv_imgproc.so). A subtle glibc mismatch between your build runner and your production host crashes the service on startup with GLIBCXX_... not found.</li><li><strong>Buffer Marshalling &amp; Memory Copies</strong>: Raw images are loaded into OpenCV cv::Mat matrices, converted into intermediate arrays, copied into deep-learning tensor buffers, and then marshalled back into OpenCV structures for bounding-box crops. Every single image incurs multiple redundant heap allocations.</li><li><strong>Cross-Compilation Nightmares</strong>: If you want to support macOS (x86_64 and ARM64), Linux (glibc and musl), Windows, Android (four distinct ABIs), and iOS, maintaining CMake toolchain files for OpenCV feels like a full-time job.</li><li><strong>Thread-Safety Bottlenecks</strong>: Many OpenCV operations and C++ inference engine sessions maintain internal mutable state. Running OCR concurrently across a web worker pool requires coarse-grained mutex locking, killing CPU utilization.</li></ol><p>We initially attempted to wrap these OpenCV and C++ components with Rust FFI bindings. It took only a few weeks of debugging mysterious Android NDK linker errors and race conditions to realize the truth: <strong>Wrapping C++ in Rust doesn’t solve C++ problems.</strong></p><p>To get the reliability, portability, and performance we needed, OpenCV had to go.</p><h3>Rebuilding Computer Vision in Pure Rust</h3><p>By replacing OpenCV’s contour finding, morphological dilation, and affine transformations with pure-Rust implementations using image, geo-clipper, and Rayon, we matched OpenCV&#39;s accuracy pixel for pixel with zero C++ dependencies.</p><p>Eliminating OpenCV meant engineering pure-Rust equivalents for three foundational computer vision algorithms:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3JkyMO4TFLqfCjWKF9WLUA.png" /></figure><h4>1. Contour Detection Without cv::findContours</h4><p>The DBNet text detection model outputs a floating-point probability heatmap representing text centers. To convert this probability map into discrete text polygons, traditional engines call OpenCV’s cv::findContours.</p><p>We implemented a pure-Rust border-following algorithm based on Suzuki’s topological structural analysis. It scans binarized byte buffers directly in memory, identifying external boundaries and child holes without allocating intermediate cv::Mat objects or calling into foreign C code.</p><h4>2. Vatti Polygon Unclipping via geo-clipper</h4><p>Because DBNet intentionally shrinks detection ground truth during model training to separate adjacent text lines, raw detected contours are too small. They tightly hug the inner core of words.</p><p>Standard PaddleOCR uses Vatti polygon clipping with an unclip expansion ratio (typically 1.6) to expand the polygon back to its true text boundaries.</p><p>Instead of binding to OpenCV’s legacy polygon wrappers, we implemented this phase natively using geo-clipper. The resulting bounding polygons achieve <strong>99.3%+ geometric parity</strong> with upstream PaddleOCR, ensuring zero loss in text recognition accuracy:</p><pre>// Pure-Rust polygon dilation using Vatti clipping:<br>let unclipped_polygons = candidate_polygons<br>    .iter()<br>    .filter_map(|poly| unclip_polygon(poly, unclip_ratio))<br>    .collect();</pre><h4>3. Parallel Box Cropping &amp; Affine Warping with Rayon</h4><p>A single document scan — such as a complex invoice or receipt — can easily contain 30 to 60 distinct text bounding boxes. Each polygon must be cropped, rotated, and affine-warped into a standardized horizontal strip before entering the recognition model.</p><p>In OpenCV, orchestrating parallel crops across worker threads often runs into thread-pool contention. In Rust, safe multi-threading is built into the language. Using <strong>Rayon</strong>, perspective transformation and tensor preparation run in parallel across all available CPU cores in three lines of readable code:</p><pre>// Crop and rectify candidate text polygons concurrently across all CPU cores:<br>let crop_imgs: Vec&lt;RgbImage&gt; = padded_boxes<br>    .par_iter()<br>    .map(|b| get_rotate_crop_image(&amp;source_image, b))<br>    .collect();</pre><h3>The Engine Evolution — From MNN to Pure-Rust RTen</h3><p>Replacing proprietary C++ inference engines with Robert Knight’s pure-Rust RTen engine allowed RustO! to execute standard ONNX and FlatBuffer models natively with zero external runtimes.</p><p>Image processing is only half the battle; the engine still needs to run deep learning inference for detection and recognition.</p><p>In our early iterations, we experimented with <strong>Alibaba MNN</strong>. While MNN was fast, it reintroduced the exact C++ toolchain headaches we were trying to eliminate: it required CMake, proprietary .mnn model conversions, and mutable session configurations that broke multi-threaded web servers.</p><p>In <strong>RustO! v0.3</strong>, we migrated the core inference engine to <a href="https://github.com/robertknight/rten"><strong>RTen</strong></a>, a pure-Rust deep learning runtime:</p><h4>1. Zero C++ Compilers Required</h4><p>Because RTen is written in 100% safe Rust, the entire RustO! engine compiles using a simple cargo build. There are no CMake scripts, no GCC/Clang version checks, and no dynamic .so or .dylib library dependencies.</p><h4>2. FlatBuffers (.rten) &amp; Zero-Copy Memory Mapping</h4><p>RTen operates on open .onnx models and pre-compiled <strong>.rten</strong> FlatBuffer models. Using memory mapping (mmap), model weights are mapped directly from disk into virtual memory without duplication. Multiple OS processes can share the exact same physical memory pages for model weights, drastically cutting memory overhead in microVM and container deployments.</p><h4>3. The Pluggable InferenceSession Abstraction</h4><p>We designed RustO! so that developers aren’t locked into a single inference engine. All model execution is abstracted behind a thread-safe trait:</p><pre>pub trait InferenceSession: Send + Sync {<br>    fn run(&amp;self, inputs: &amp;[TensorView]) -&gt; Result&lt;Vec&lt;Tensor&gt;, EngineError&gt;;<br>    fn input_shape(&amp;self, index: usize) -&gt; Option&lt;&amp;[usize]&gt;;<br>}</pre><p>Our default EngineSession uses RTen out of the box. But if you have dedicated hardware requirements—such as running TensorRT on Nvidia servers or CoreML on Apple hardware—you can pass custom engine sessions into RustO! with zero pipeline rewrites:</p><pre>use rusto::engine::EngineSession;<br><br>// Default: Pure-Rust RTen session (zero C++ dependencies)<br>let det_session = EngineSession::from_det_config(&amp;config.det)?;<br>let rec_session = EngineSession::from_rec_config(&amp;config.rec)?;<br>// Or instantiate with custom execution providers:<br>let ocr = RustO::with_custom_engines(config, det_session, rec_session)?;</pre><h4>4. Lock-Free Concurrency: InitializeConfig vs. OcrRunOptions</h4><p>To eliminate the mutex bottlenecks of our early prototypes, we decoupled static initialization from per-image options:</p><ul><li><strong>InitializeConfig (Static, Read-Only, Loaded Once)</strong>: Loads model files, compiles computation graphs, and allocates the character vocabulary dictionary. This is initialized once and safely shared across threads (Arc&lt;RustO&gt;).</li><li><strong>OcrRunOptions (Dynamic, Stateless, Per-Request)</strong>: Configures binarization thresholds, polygon padding, and output formats for an individual document. It is passed by reference and contains no shared mutable state.</li></ul><pre>use std::sync::Arc;<br>use rusto::{InitializeConfig, OcrRunOptions, OutputGranularity, RustO};<br><br>// 1. Initialize once at startup (Thread-Safe, Read-Only)<br>let ocr = Arc::new(RustO::initialize(InitializeConfig::ppv6(<br>    &quot;models/det.rten&quot;,<br>    &quot;models/rec.rten&quot;,<br>    &quot;models/dict.txt&quot;,<br>))?);<br><br>// 2. Thread 1: Fast scan for document routing (line tokens only)<br>let res1 = {<br>    let mut opts = OcrRunOptions::default();<br>    opts.output = OutputGranularity::Lines;<br>    ocr.detect_text(&amp;receipt_img, &amp;opts)?<br>};<br><br>// 3. Thread 2: Detailed invoice scan with 2D spatial layout reconstruction<br>let res2 = {<br>    let mut opts = OcrRunOptions::default();<br>    opts.output = OutputGranularity::Spatial;<br>    opts.text_score = Some(0.60);<br>    ocr.detect_text(&amp;invoice_img, &amp;opts)?<br>};</pre><p>Zero locks. Zero mutex contention. Complete thread safety guaranteed at compile time by the Rust borrow checker.</p><h3>Benchmarks — Pure Rust vs. OpenCV C++ Pipeline</h3><p>On standard document scans, RustO! matches or outperforms traditional OpenCV and ONNXRuntime C++ pipelines, delivering 70–87ms latency on desktop CPUs, sub-130ms on mobile phones, and slashing RAM usage by 60%.</p><p>We benchmarked RustO! against an OpenCV + ONNXRuntime C++ pipeline on standard A4 invoice scans (150 DPI, ~1200×1700 px, ~25 text blocks) using <strong>PP-OCRv6 Tiny</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*D3wcKXkU7DGL5fsEfV_kYw.png" /><figcaption>Hardware Latency Matrix</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4Cz02i_Blpqe-P7cmvtdow.png" /><figcaption>Operational Comparison: The True Production Footprint</figcaption></figure><p>By eliminating inter-library memory copies between OpenCV cv::Mat structures and foreign C++ pointers, RustO! achieves competitive or superior latency while cutting peak memory consumption in half.</p><h3>One Engine, Every Ecosystem</h3><p>Because Rust compiles cleanly to native shared libraries and WebAssembly without C++ runtime baggage, RustO! provides first-class, symmetrical SDKs across all major software ecosystems under the @rustors scope.</p><h4>1. In Pure Rust</h4><pre>cargo add rusto-rs</pre><pre>use rusto::{ImageSource, InitializeConfig, OcrRunOptions, RustO};<br><br>let mut ocr = RustO::initialize(InitializeConfig::ppv6(&quot;det.rten&quot;, &quot;rec.rten&quot;, &quot;dict.txt&quot;))?;<br>let result = ocr.detect_text(&amp;ImageSource::Path(&quot;doc.jpg&quot;.into()), &amp;OcrRunOptions::default())?;</pre><h4>2. Client-Side in the Browser (WebAssembly)</h4><p>Run OCR 100% client-side in the user’s browser — no backend servers, no third-party API costs, and zero sensitive document data leaving the client device:</p><pre>npm install @rustors/web @rustors/model-ppocrv6-tiny</pre><pre>import { initialize, detectText } from &#39;@rustors/web&#39;;<br>import { getModelUrls } from &#39;@rustors/model-ppocrv6-tiny&#39;;<br><br>const urls = getModelUrls(&#39;/models/ppocrv6-tiny&#39;);<br><br>await initialize({<br>  preset: &#39;ppv6&#39;,<br>  models: { detection: urls.detection, recognition: urls.recognition, dictionary: urls.dictionary },<br>});<br><br>const results = await detectText(document.querySelector(&#39;img&#39;)!);<br><br>results.forEach(r =&gt; console.log(`${r.text} (${(r.score * 100).toFixed(1)}%)`));</pre><h4>3. In React Native (iOS &amp; Android)</h4><p>No manual CMake files, no Cocoapods C++ conflicts, and tiny binary impact:</p><pre>npm install @rustors/react-native</pre><pre>import { initialize, detectText } from &#39;@rustors/react-native&#39;;<br><br>await initialize();<br><br>const results = await detectText({ uri: &#39;file:///path/to/receipt.jpg&#39; });<br><br>console.log(results.map(r =&gt; r.text).join(&#39;\n&#39;));</pre><h4>4. In .NET / C#</h4><pre>using RustODotnet;<br><br>using var ocr = RustO.Initialize(InitializeConfig.Ppv6());<br><br>var result = ocr.DetectText(new UriImageSource(&quot;receipt.jpg&quot;), new OcrRunOptions());</pre><h3>Spatial 2D Layouts and PP-OCRv6 Multilingual Models</h3><p>Unlike standard OCR engines that discard document structure and output an unorganized bag of text, RustO! reconstructs 2D reading order and supports 50+ languages simultaneously.</p><h4>2D Spatial Layout Reconstruction</h4><p>When parsing invoices, financial statements, or receipts, the spatial relationship between words matters as much as the characters themselves. RustO! includes a built-in layout reconstructor that:</p><ul><li>Calculates horizontal baselines to cluster words into geometric lines.</li><li>Detects tabular columns and computes relative indentation.</li><li>Emits both rich bounding-box coordinate arrays and human-readable 2D spatial text (--format text-ordered).</li></ul><h4>50+ Languages in a Single Model</h4><p>Powered by <strong>PP-OCRv6</strong>, RustO! recognizes Latin, Cyrillic, Arabic, Devanagari, Chinese, Japanese, and Korean under a single unified vocabulary dictionary.</p><p>For edge devices, INT8 quantized models keep downloads tiny:</p><ul><li><strong>PP-OCRv6 Tiny (INT8)</strong>: <strong>~3.4 MB</strong> (Optimized for WASM, mobile, and IoT edge)</li><li><strong>PP-OCRv6 Small (INT8)</strong>: <strong>~16.2 MB</strong> (Balanced speed and accuracy)</li><li><strong>PP-OCRv6 Medium</strong>: <strong>~75.7 MB</strong> (Full server accuracy)</li></ul><h3>Conclusion: What Replacing OpenCV Taught Us</h3><p>Building RustO! confirmed what many systems engineers are beginning to discover:</p><ol><li><strong>OpenCV is not mandatory for computer vision</strong>: For domain-specific pipelines like OCR, implementing dedicated contour tracing, polygon clipping, and affine transforms in pure Rust delivers better safety, smaller binaries, and zero C++ toolchain headaches.</li><li><strong>Stateless APIs enable true concurrency</strong>: Separating static initialization (InitializeConfig) from per-image execution (OcrRunOptions) turned a mutex-choked bottleneck into a lock-free, multi-threaded engine.</li><li><strong>Pure-Rust machine learning is production-ready</strong>: Crates like rten, rayon, image, and geo-clipper prove that Rust can easily match C++ performance while providing superior developer ergonomics.</li></ol><h3>Get Started with RustO!</h3><p>RustO! is completely <strong>open source under the MIT License</strong>:</p><ul><li>⭐ <strong>GitHub Repository</strong>: <a href="https://github.com/byrizki/rusto-rs">github.com/byrizki/rusto-rs</a></li><li>📖 <strong>Documentation Site</strong>: <a href="https://byrizki.github.io/rusto-rs/">byrizki.github.io/rusto-rs</a></li><li>📦 <strong>Crates.io</strong>: <a href="https://crates.io/crates/rusto-rs">rusto-rs</a></li><li>🌐 <strong>NPM Packages</strong>: <a href="https://www.npmjs.com/package/@rustors/web">@rustors/web</a> &amp; <a href="https://www.npmjs.com/package/@rustors/react-native">@rustors/react-native</a></li><li>💼 <strong>NuGet</strong>: <a href="https://www.nuget.org/packages/RustODotnet">RustODotnet</a></li></ul><p>Have you wrestled with OpenCV or C++ dependencies in production? Share your thoughts and questions in the comments below!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e410269b4bd1" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>