@wasmedia/wopencv
v0.1.0
Published
OpenCV compiled to WebAssembly for browsers
Maintainers
Readme
wopencv
WebAssembly build of OpenCV, pinned to 4.14.0,
scoped as the computer-vision stage of the wasmedia
photo pipeline. Single-threaded + SIMD, no SharedArrayBuffer, no
COOP/COEP headers — works on any site, following the house pattern of
sibling ports wvips and wlensfun.
How this differs from opencv.js
OpenCV already ships an official WASM build, opencv.js. wopencv is not
a replacement for it and does not try to be. There is no Mat class at the
JS boundary and no way to call an arbitrary cv:: function — wopencv
exposes a small, curated, strongly-typed task API over the specific
operations the wasmedia sibling ports need (geometry, features and
homography, computational photography, automatic stitching, detection),
each taking and returning plain pixel buffers.
If you want general-purpose access to OpenCV's API from the browser, use
opencv.js
instead. wopencv's small export table (curated rather than generated) is
what lets dead-code elimination keep the binary small; that trade is the
whole reason this project exists rather than just depending on upstream.
Status
The core bundle's capabilities are complete. Geometry, colour, feature
detection, matching, model fitting, HDR, computational photography,
dense optical flow and automatic stitching exist, built as a core bundle
(dist/wopencv.{js,wasm}) plus a detection bundle
(dist/wopencv-detect.{js,wasm}): remap, resize, warpAffine,
warpPerspective, cvtColor, convertDepth, detectFeatures,
matchFeatures, findHomography, estimateAffinePartial2D,
mergeExposures, mergeDebevec, tonemap, denoise, inpaint,
seamlessClone, detailEnhance, edgePreservingFilter,
opticalFlowFarneback, flowToMap,
createStitcher/Stitcher.stitch, and the stitching stages one by one
for layers warped elsewhere: compensateExposure, findSeams and
multibandBlend. Detection is complete too, in its own
entry point: loadCascade (Haar cascades) and loadFaceDetector (YuNet),
imported from @wasmedia/wopencv/detect. Both run off the main thread
in the Web Worker at @wasmedia/wopencv/worker, driven by
WOpencvWorker, and npm run playground serves a page that uses it.
PixelBuffer is validated at runtime, not just at compile time. See
briefs/INITIAL.md for the roadmap and design
rationale.
Installation
npm install @wasmedia/wopencvModels and cascades used by detection features are not bundled in the npm package. Their licences are permissive (BSD-3-Clause-style for the cascades, MIT for YuNet), so bundling them would be lawful. They are fetched separately to keep the package small and let model versions move independently of releases, as a deliberate manual step that is not part of install or build, into a local directory. You pass their bytes in, and wopencv mounts them into the wasm module's virtual filesystem:
npx wopencv-fetch-modelsIt writes models/ and a THIRD-PARTY-LICENSES.md, and you pass the
bytes to loadCascade and loadFaceDetector.
Usage
The shape wopencv is building toward — stateless tasks as one-shot functions, stateful things (feature sets, stitcher, DNN net) as handles with explicit disposal:
import { initWOpencv } from '@wasmedia/wopencv';
const cv = await initWOpencv();
// The data contract: tightly packed, RGB(A) at the boundary.
interface PixelBuffer {
data: Uint8Array | Uint16Array | Float32Array;
width: number;
height: number;
channels: number;
depth: 'u8' | 'u16' | 'f32';
}
// Stateless tasks are one-shot functions.
const smaller = cv.resize(img, { width: 1024 });
const clean = cv.denoise(img, { strength: 7 });
const fused = cv.mergeExposures([under, mid, over]); // Mertens
const display = cv.tonemap(cv.mergeDebevec(stack, times), { method: 'reinhard' });
// The wlensfun composition point: apply someone else's correction maps.
const corrected = cv.remap(img, { mapX, mapY, interpolation: 'linear' });
// Stateful things are handles with explicit disposal.
const a = cv.detectFeatures(left, { detector: 'akaze' });
const b = cv.detectFeatures(right, { detector: 'akaze' });
const matches = cv.matchFeatures(a, b, { ratio: 0.75 });
const fit = cv.findHomography(matches, { method: 'ransac' });
if (fit) { const { matrix, inliers } = fit; /* … */ }
a.dispose(); b.dispose();
// The automatic panorama path.
const stitcher = cv.createStitcher({ mode: 'panorama' });
const result = stitcher.stitch([left, mid, right]); // { ok: true, image } | { ok: false, status }
stitcher.dispose();
// Errors are thrown, and the module survives them.
try { cv.resize(img, { width: -1 }); }
catch (e) { /* WOpencvError — cv is still usable */ }Every call in the sketch above works today. Detection is a separate bundle
with its own import, and takes model bytes you fetched with
npx wopencv-fetch-models:
import { readFile } from 'node:fs/promises';
import { initWOpencvDetect } from '@wasmedia/wopencv/detect';
const det = await initWOpencvDetect();
const faces = det.loadFaceDetector(
new Uint8Array(await readFile('models/face_detection_yunet_2023mar.onnx')),
);
const found = faces.detect(img); // [{ x, y, width, height, score, landmarks }]
faces.dispose();In a browser, run it in a Web Worker so no operation blocks the page. Each bundle loads in the worker on first use, and results' buffers are transferred rather than copied:
import { WOpencvWorker } from '@wasmedia/wopencv/worker-client';
const cv = new WOpencvWorker(() =>
new Worker(new URL('@wasmedia/wopencv/worker', import.meta.url), { type: 'module' }));
const small = await cv.call('resize', img, { width: 1024 });
const pano = await cv.stitch([left, mid, right]);
const yunet = await cv.loadFaceDetector(yunetBytes);
const found = await cv.detect(yunet, img);
await cv.release(yunet);Errors reject as WOpencvError, rebuilt on the page. If your bundler
does not resolve new URL('@wasmedia/wopencv/worker', import.meta.url),
see Worker in docs/api.md for alternatives.
See docs/api.md for exactly what ships:
import { initWOpencv } from '@wasmedia/wopencv';
const cv = await initWOpencv();
const gray = cv.cvtColor(image, 'rgb2gray');
const half = cv.resize(gray, { width: Math.round(gray.width / 2) });Building from source
All native compilation happens inside Docker; see
docs/building.md for the full build and size-budget
notes.
npm install
npm run build
npm testnpm test downloads the detection models into models/ on its first run
(see docs/building.md); after that it runs offline.
Family boundaries
wopencv owns the automatic panorama path (Stitcher, one call, no
configuration) and OpenCV's stitching stages as separate calls
(exposure compensation, graph-cut seams, multiband blending) for callers
that need OpenCV's output, plus features, homography, remap, photo ops
and detection. The controllable panorama path — control-point editing,
projection choice, bundle adjustment — belongs to whugin. Enblend's
blending and enfuse's exposure fusion are wenblend's. wlensfun
hands wopencv its lens-correction maps to apply via remap. See
briefs/INITIAL.md for the full
family-boundaries table.
License
Apache-2.0, matching upstream OpenCV — see LICENSE. The
wasm bundles also contain code under other permissive licences: zlib,
Protocol Buffers and FLANN, vendored by OpenCV; OpenCV files contributed
under their authors' own BSD-style licences (FAST, AGAST, BRISK, ORB,
KAZE/AKAZE, SIFT and others); and Emscripten's runtime (with musl
libc and Node.js's path code). Their notices
are in THIRD-PARTY-NOTICES.md, which ships
with the package; docs/licences.md records the
audit. Models
and cascades fetched via wopencv-fetch-models carry their own licences,
separate from wopencv's own, and are not covered by this project's
Apache-2.0 grant.
