opencv-wrapper
/

React Native OpenCV Wrapper

A fluent, type-safe image-processing pipeline backed by native OpenCV on Android and iOS. Chain operations once, run them in a single native pass, and get a file path, a base64 string, or structured data back.

Getting started

Install

npm install @nijatk/react-native-opencv-wrapper
# or
yarn add @nijatk/react-native-opencv-wrapper

The library uses the React Native New Architecture (TurboModules). Rebuild your app after installing so the native module is linked. All paths are absolute filesystem paths — no file:// URIs.

A first pipeline

import { pipeline } from "@nijatk/react-native-opencv-wrapper";

const out = await pipeline()
  .input("/abs/in.jpg")
  .output("/abs/out.png")
  .resize(640, 480)
  .gray()
  .gaussianBlur(5)
  .canny(50, 150)
  .run(); // resolves with the output path

A pipeline reads once, transforms in memory, and writes once. It is optimized losslessly before running: adjacent point transforms (lut, bitwiseNot) are fused into a single cv::LUT pass, and redundant consecutive gray() calls are collapsed.

Usage patterns

1 · Fluent pipeline → image

Chain transforms and finish with .run(). Resolves with the output path, or a base64 string when you use outputBase64().

2 · Base64, in-memory I/O

const base64Jpg = await pipeline()
  .inputBase64(picker.base64)   // or "data:image/png;base64,…"
  .outputBase64("jpg")          // format optional; defaults to "png"
  .resize(256, 256)
  .gray()
  .run();

Combine inputBase64() with outputBase64() to keep the image entirely in memory — no disk I/O.

3 · Analysis terminals → structured data

Data ops are terminal: they run any queued transforms plus the analysis step and resolve with a typed result. They need only an input — no output() or run().

const { found, codes } = await pipeline()
  .input("/abs/photo.jpg")
  .crop(0, 0, 512, 512)
  .decodeQR();

4 · Standalone one-shot functions

import { gray, resize, canny } from "@nijatk/react-native-opencv-wrapper";

await gray("/abs/in.jpg", "/abs/out.jpg");
await resize("/abs/in.jpg", "/abs/out.jpg", 320, 240);
await canny("/abs/in.jpg", "/abs/out.jpg", 50, 150);

5 · Dynamic op selection

import { standaloneOps, runStandaloneOp } from "@nijatk/react-native-opencv-wrapper";

await runStandaloneOp("threshold", "/in.png", "/out.png", 127, 255, "binary");
await standaloneOps.rotate("/in.png", "/out.png", 90);

6 · Clone & branch

const base = pipeline().input("/in.jpg").gray();
await base.clone().output("/edges.jpg").canny(50, 150).run();
await base.clone().output("/blurred.jpg").gaussianBlur(7).run();

Recipes & presets

A pipeline’s steps are plain, serializable data. Convert any pipeline into a recipe — a JSON-friendly PipelineRecipe object — then store it, ship it over the network, or rebuild it later. Recipes are source-agnostic: they describe what to do, not which image to do it to.

type PipelineRecipe = {
  version: 1;
  ops: { type: string; [param: string]: unknown }[];
  input?: InputSource;   // present only if the pipeline captured one
  output?: OutputSink;   // present only if the pipeline captured one
};

Serialize — toJSON()

toJSON() snapshots a pipeline’s ops (plus its input/output if set) into a recipe. It is also called automatically by JSON.stringify(), so a pipeline persists with no extra ceremony.

const recipe = pipeline().gray().gaussianBlur(5).canny(50, 150).toJSON();
// { version: 1, ops: [ { type: "gray" }, { type: "gaussianBlur", ... }, ... ] }

const json = JSON.stringify(pipeline().gray().canny(50, 150)); // ready to store

Rebuild — Pipeline.fromJSON()

Pipeline.fromJSON() turns a recipe (object or JSON string) back into a runnable pipeline, restoring any captured input/output. Supply whatever the recipe didn’t include before calling .run().

import { Pipeline } from "@nijatk/react-native-opencv-wrapper";

await Pipeline.fromJSON(json)        // string or PipelineRecipe
  .input("/abs/in.jpg")
  .output("/abs/out.png")
  .run();

Because recipes often come from disk, the network, or a saved user filter, fromJSON() validates them defensively: it throws a descriptive error if the recipe is not an object, carries an unsupported version, has no ops array, or references an op type that isn’t registered — so a bad recipe fails fast instead of erroring opaquely at run time.

Compose — apply()

apply() drops a recipe’s ops into any existing chain (its captured input/output are ignored), returning the pipeline for further chaining. Pass a recipe or a bare ops array.

await pipeline()
  .input("/abs/in.jpg")
  .output("/abs/out.png")
  .resize(640, 480)
  .apply(recipe)        // splice the recipe's steps in here
  .bitwiseNot()
  .run();

Built-in presets

presets ships a few ready-made recipes. Each is built from the typed op builders, so it can never drift from an op’s parameter schema.

Preset Pipeline Use for
presets.edges gray → gaussianBlur(5) → canny(50,150) Grayscale edge map
presets.crispScan gray → adaptiveThreshold(255,15,5) High-contrast document scan
presets.softenPortrait bilateralFilter(9,75,75) → clahe(2,8) Gentle portrait cleanup
import { pipeline, presets } from "@nijatk/react-native-opencv-wrapper";

await pipeline()
  .inputBase64(picker.base64)
  .outputBase64("png")
  .apply(presets.edges)
  .run();

Custom recipes

Build your own reusable recipe by chaining the ops you want on a fresh pipeline() (no input/output) and calling toJSON(). Keep it source-agnostic so it can be applied to any image, shared between screens, or saved as a user-defined filter.

// Define once — a portable, reusable recipe.
export const punch = pipeline()
  .clahe(2, 8)              // local contrast
  .convertScaleAbs(1.1, 10) // gain + brightness
  .gaussianBlur(3)
  .toJSON();

// Apply it anywhere.
await pipeline()
  .input("/abs/photo.jpg")
  .output("/abs/punch.jpg")
  .apply(punch)
  .run();

// Or persist user-built filters and reload them later.
await AsyncStorage.setItem("filter", JSON.stringify(punch));
const saved = await AsyncStorage.getItem("filter");
await Pipeline.fromJSON(saved).input(src).output(dst).run();

A recipe is just data — it carries no image, so the same recipe is safe to reuse across many inputs and to send between a server and the app.

Batch processing

runBatch() applies one recipe (or preset) across many images in a single call — each item runs the same steps with its own input and output. It is the natural companion to recipes: define the steps once, then process a whole folder.

import { runBatch, presets } from "@nijatk/react-native-opencv-wrapper";

const results = await runBatch(presets.edges, [
  { input: "/abs/a.jpg", output: "/abs/a.png" },
  { input: "/abs/b.jpg", output: "/abs/b.png" },
  { input: "/abs/c.jpg", output: "/abs/c.png" },
]);

Per-item results — nothing fails the batch

Results come back in input order and mirror Promise.allSettled: one unreadable file never rejects the whole run, so you can report successes and failures individually.

for (const r of results) {
  if (r.status === "fulfilled") console.log(r.index, "→", r.output);
  else console.warn(r.index, "failed:", r.error.message);
}

const failed = results.filter((r) => r.status === "rejected");

Sources, sinks & concurrency

Each item’s input/output is an absolute path (string) or a base64 descriptor, so batches can run fully in memory. Cap parallelism with concurrency to bound peak memory when processing many large images (it defaults to all items at once).

await runBatch(
  recipe,
  [
    { input: { base64: picker.a }, output: { base64: "jpg" } },
    { input: { base64: picker.b }, output: { base64: "jpg" } },
  ],
  { concurrency: 4 }, // at most 4 images in flight at a time
);

Progress

Pass onProgress to drive a progress bar: it fires once per item as it finishes (in completion order), with the running count, the total, and that item’s result.

await runBatch(recipe, items, {
  onProgress: (completed, total, result) =>
    console.log(`${completed}/${total}`, result.status),
});

Input & output model

Sources and sinks are absolute file paths or base64 strings; they are interchangeable. The decoder/encoder is chosen from the file extension, or from an explicit format for base64 output.

Inputs

  • input(path) — absolute file path
  • inputBase64(data) — raw base64 or a data: URI

Outputs

  • output(path) — encoder from extension
  • outputBase64(format?) — resolves to a base64 string

Supported output formats: .

Error handling

Every async call rejects with a stable code you can branch on, plus a human-readable message.

Code Meaning

Operations