OpenCV.jsは、C++の画像処理ライブラリOpenCVをEmscriptenでWebAssemblyへ変換し、JavaScriptから呼べるようにした公式ビルドです。ブラウザ内でグレースケール変換やエッジ検出、Webカメラ映像の処理ができ、画像をサーバへ送る必要がありません。この記事では、配布ファイルの入手先と版の選び方、配布形式によって書き方が変わる初期化の待ち方、動くコード、そしてメモリ管理の落とし穴を、OpenCV 5.0.0と4.14.0の実ファイルをNode.js 26.5.0で動かした結果をもとに説明します。
まとめ
- 公式ファイルは
https://docs.opencv.org/{版}/opencv.js。ただし4.xは2026年10月16日時点でも4.13.0へ転送され、7月19日公開の4.14.0にはなりません。版は番号で固定します。 - npmで使うなら
@techstark/opencv-js(5.0.0-release.1)。名前がopencv.jsのパッケージは2017年で止まっています。 - 初期化の待ち方は配布形式で違います。公式ファイルは読み込み前に
Module.onRuntimeInitializedを用意し、npm版はPromiseをawaitします。公式ファイルのcvをawaitすると処理が戻らなくなります。 cv.Matはdelete()が必須です。640×480の処理で解放を省くと、約694フレーム(30fpsで約23秒)でヒープが1GiBに達して失敗しました。- npm版の
clone()は元のMatとメモリを共有します。独立したコピーが要るときはmat_clone()かcopyTo()を使います。 - ファイルは11〜16MBあります。リサイズやグレースケールだけならCanvas APIで足り、OpenCV.jsを入れる理由はありません。
OpenCV.jsとは:WebAssembly化したOpenCV本体
Python版と同じC++実装をwasmへ変換したもの
OpenCV.jsはJavaScriptで書き直した別実装ではなく、Python版(opencv-python)と同じC++のソースをwasmにコンパイルしたものです。関数名は cv.imread、cv.cvtColor、cv.Canny のようにPython版とほぼ共通で、同じアルゴリズムが使えます。結果を比べるときは、版・入力形式・ビルド設定をそろえてください。主な違いは入出力とメモリ管理です。入力はファイルパスではなく <img> や <canvas> 要素で、Matの実体はwasmのメモリ上にあるため、JavaScriptのガベージコレクタでは解放されません。wasmとJavaScriptの間でメモリをやり取りする仕組みはWebAssembly(Wasm)とは?JavaScript連携とMemory・Table操作の実装で扱っています。
使えるモジュールと4.xから5.xでの名前の変化
JavaScriptへ公開される関数は、ソースの platforms/js/opencv_js.config.py にある white_list で決まります。4.14.0の設定は core・imgproc・objdetect・video・dnn・features2d・photo・calib3d の8モジュールです。5.0.0の設定は9モジュールになり、features2dは features に、calib3dは _3d と calib に分かれました。自前ビルドで設定ファイルを書き換えるときは、どちらの版のソースかでモジュール名が変わります。フィルタ、輪郭抽出、特徴点マッチング、カメラキャリブレーション、DNN推論まで、デスクトップ版の主要機能は一通りブラウザで動きます。
OpenCV.jsをダウンロードする:配布元と版の選び方
docs.opencv.orgの配布URLと4.xの転送先
公式ドキュメントサイトが、版ごとのビルド済みファイルを https://docs.opencv.org/{版}/opencv.js で配信しています。4.14.0の公式チュートリアル「Using OpenCV.js」は「最新のビルドが欲しければ4.xを使う」と案内しています。ところが2026年10月16日に確認すると、4.x/opencv.js は301で 4.13.0/opencv.js へ転送されました。4.14.0は2026年7月19日に公開済みなので、4.x と書いても4系の最新は届きません。5.x/opencv.js は 5.0/opencv.js へ転送されます。
<!-- 版を番号で固定する(本番はこちら) -->
<script async src="https://docs.opencv.org/4.14.0/opencv.js"></script>
<!-- 4.x は 2026-10-16 時点で 4.13.0 へ転送される -->
<script async src="https://docs.opencv.org/4.x/opencv.js"></script>
エイリアスは転送先が予告なく変わります。本番では番号で固定し、そのURLがブラウザで200を返すことを確かめてから載せてください。
リリースのdocs zip:ファイル名と格納場所
自社サーバから配信したい場合は、GitHubリリースのドキュメント一式に同梱されたファイルを使えます。zipの名前は4.14.0から変わっており、チュートリアルにある opencv-{版}-docs.zip の形は5.0.0と4.13.0にしか当てはまりません。
| 版 | 公開日 | zip名 | opencv.jsのサイズ |
|---|---|---|---|
| 5.0.0 | 2026-06-06 | opencv-5.0.0-docs.zip | 16,211,109バイト |
| 4.14.0 | 2026-07-19 | opencv-docs-4.14.0.zip | 11,059,007バイト |
| 4.13.0 | 2025-12-31 | opencv-4.13.0-docs.zip | (未計測) |
ファイルはzip内の js/bin/opencv.js と doc/doxygen/html/opencv.js の2か所にあり、中身は同じです。wasmはbase64で1ファイルに埋め込まれているので、この1本を置けば動きます。
npm・CDNの導入:@techstark/opencv-jsの選定理由
バンドラやTypeScriptから使う場合、本記事では、型定義を同梱しOpenCV 5.0.0に追随している @techstark/opencv-js を使います。
| パッケージ | 最新版 | 公開日 | 週間DL | 型定義 |
|---|---|---|---|---|
| @techstark/opencv-js | 5.0.0-release.1 | 2026-06-24 | 228,895 | あり |
| opencv.js | 1.2.1 | 2017-11-01 | 3,625 | なし |
| opencv-wasm | 4.3.0-10 | 2021-01-15 | 27,570 | あり |
週間DLは2026年10月6日〜12日のnpm公式の集計です。@techstark/opencv-jsのREADMEは、中身を「docs.opencv.org/5.0.0/opencv.jsからダウンロードしたもの」と説明し、型定義はmirada由来としています。ただし同梱ファイルは13,298,869バイトで、上のzip版5.0.0とはサイズも初期化の振る舞いも違います(次章)。同梱の opencv.js.patch によると、ESMの <script type="module"> とwebpackのstrictモードで落ちないよう、UMDの呼び出しを2か所書き換えています。
npm install @techstark/opencv-js
<!-- ビルドせずにCDNから読む場合(jsDelivr) -->
<script async src="https://cdn.jsdelivr.net/npm/@techstark/[email protected]/dist/opencv.js"></script>
webpackでブラウザ向けにまとめるときは、READMEの指示どおり resolve.fallback に fs: false, path: false, crypto: false を設定しないとビルドが通りません。READMEは型定義が最新のOpenCV.jsに追いついていない場合があるとも書いており、実行時に使える関数は同リポジトリの doc/cvKeys.json で確かめられます。
OpenCV.jsの初期化:配布形式別の待機方法
<script> の読み込みが終わっても、wasmの初期化は終わっていません。初期化前に関数を呼ぶとエラーになります。しかも公式ファイルとnpm版では cv の正体が違います。Node.js 26.5.0で3つのビルドを読み込み、比べた結果が次の表です。
| 配布形式 | 読み込み直後のcv | 正しい待ち方 | 処理が戻らなくなる書き方 |
|---|---|---|---|
| 公式(docs zip 4.14.0・5.0.0) | Promiseでないthen付きオブジェクト | 読み込み前にModuleを用意 | await cv/async関数でcvをreturn |
| npm版(@techstark 5.0.0-release.1) | Promise | await | 後からonRuntimeInitializedを代入 |
公式ファイルの初期化:読み込み前のModule設定
公式チュートリアルの完成形も、opencv.js を読み込むscriptより前で var Module を定義しています。この順序なら、OpenCV.jsが自分で仕込んでいる初期化後の処理(後述の clone() の差し替え)も生きたまま呼ばれます。
<script>
var Module = {
onRuntimeInitialized() {
document.getElementById('status').textContent = 'OpenCV.js is ready.';
// ここから cv.* を呼んでよい
}
};
</script>
<script async src="https://docs.opencv.org/4.14.0/opencv.js"></script>
公式ファイルの cv は then メソッドを持つのにPromiseではありません。これを await cv すると、then が自分自身を返し続けてマイクロタスクが空にならず、20秒のタイマーすら発火しないままCPUを使い続けました。チュートリアルの cv = (cv instanceof Promise) ? await cv : cv; が安全なのは、instanceof Promise が偽になって await しないからです。代わりに初期化は待たないため、画像の読み込み完了が初期化より早いと cv.imread が未定義になります。
npm版の初期化:Promiseのawaitによる待機
@techstark/opencv-js 5.0.0-release.1を import すると、得られるのはPromiseです。await すれば初期化済みのモジュールが返ります。逆に、このPromiseへ onRuntimeInitialized を代入しても呼ばれることはなく、20秒待っても解決しませんでした。npm版はファクトリ内で var Module を宣言し直すため、ページ側で用意したグローバルの Module も読まれません。
import cvModule from "@techstark/opencv-js";
const cv = await cvModule; // 5.0.0-release.1 は Promise を返す
console.log(cv.getBuildInformation().includes("5.0.0"));
両配布形式に対応するローダー:戻り値のラップ
どちらのファイルが来ても動くようにするなら、次の形にします。注意点は戻り値です。公式ファイルの cv をasync関数からそのまま return すると、await cv と同じ理由で処理が戻りません。{ cv } のようにオブジェクトで包めば解決します。@techstarkのREADMEが return { cv }; と書いているのはこのためです。
<script>
let resolveCv;
const cvReady = new Promise((resolve) => { resolveCv = resolve; });
var Module = { onRuntimeInitialized() { resolveCv(); } }; // 公式ファイル用
async function getCv() {
if (window.cv instanceof Promise) {
return { cv: await window.cv }; // npm版(@techstark)
}
await cvReady; // 公式ファイル
return { cv: window.cv }; // そのまま return すると戻らない
}
async function onOpenCvLoaded() {
const { cv } = await getCv();
// ここから cv.* を使う
}
</script>
<script async src="opencv.js" onload="onOpenCvLoaded()"></script>
このローダーを公式4.14.0・公式5.0.0・npm版5.0.0-release.1の3つで動かし、いずれも初期化を待ってから cv.Mat を使えることを確かめました。
画像処理を実装する:imreadからCannyまで
ブラウザの画像処理:imread・cvtColor・Cannyの連携
ブラウザでは cv.imread() が <img> か <canvas>(またはそのid)を受け取り、cv.imshow() がcanvasへ描き戻します。グレースケール変換からCannyエッジ検出までは次のとおりです。
<img id="imageSrc" src="sample.png">
<canvas id="canvasOutput"></canvas>
<script>
function detectEdges(cv) {
const src = cv.imread('imageSrc');
const gray = new cv.Mat();
const edges = new cv.Mat();
try {
cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);
cv.Canny(gray, edges, 50, 150); // 下位・上位しきい値
cv.imshow('canvasOutput', edges);
} finally {
src.delete();
gray.delete();
edges.delete();
}
}
</script>
変換コードは COLOR_BGR2GRAY ではなく COLOR_RGBA2GRAY です。canvasの画素は常にRGBAの4チャンネルで、imread の戻りも CV_8UC4 になります。この関数は、OpenCV.jsの初期化と画像の読み込みが両方終わってから呼びます。前章のローダーと組み合わせるなら、onOpenCvLoaded の中で const { cv } = await getCv(); await document.getElementById('imageSrc').decode(); detectEdges(cv); の順に書きます。Cannyの第3・第4引数はヒステリシスの下位・上位しきい値で、上位を下げるほど拾うエッジが増えます。しきい値の決め方と、SobelやLaplacianとの使い分けはエッジ検出とは?Canny法と微分フィルタの使い分けをOpenCVの実装で解説で詳しく扱っています。
Node.jsの画像処理:DOM不要の画素配列入力
Node.jsには <img> も <canvas> もないため、cv.imread() と cv.imshow() をそのまま使うには、DOMやCanvasを補う環境が必要です。公式チュートリアル「Using OpenCV.js In Node.js」はjsdomとnode-canvasで補う方法を載せています。補助ライブラリを入れずに済ませるなら、RGBAの画素配列を用意し、cv.matFromImageData() でMatを作ります。次のコードは中央に白い正方形を置いた64×64の画像を作り、エッジの画素数を数えます。Node.js 26.5.0とnpm版5.0.0-release.1で実行すると edge pixels: 124 と出力されました。
// edge.mjs(node edge.mjs で実行)
import cvModule from "@techstark/opencv-js";
const cv = await cvModule;
const width = 64, height = 64;
const data = new Uint8ClampedArray(width * height * 4);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const v = (x >= 16 && x < 48 && y >= 16 && y < 48) ? 255 : 0;
data.set([v, v, v, 255], (y * width + x) * 4);
}
}
const src = cv.matFromImageData({ data, width, height });
const gray = new cv.Mat();
const edges = new cv.Mat();
try {
cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);
cv.Canny(gray, edges, 50, 150);
console.log("edge pixels:", cv.countNonZero(edges));
} finally {
src.delete(); gray.delete(); edges.delete();
}
実ファイルを読むときは、画像デコード用のライブラリでRGBAの配列と幅・高さを取り出し、同じ形のオブジェクトを渡します。サーバ側で大量の画像を処理するのが目的なら、ネイティブのOpenCVを使うPython版も候補に入れ、対象の処理で速度と運用環境を比べてください(OpenCVとは?Pythonでの使い方入門|pip installから画像処理・顔検出まで【5.0対応】)。
メモリ管理:delete()とclone()の落とし穴
Matの解放漏れ:640×480・30fps換算で約23秒の測定例
公式チュートリアルは、Emscriptenのヒープに確保したメモリを解放するために delete を呼ぶ必要があると明記しています。省くとどうなるかを、640×480のRGBA画像をグレースケール化する処理を繰り返して測りました(公式4.14.0・5.0.0、Node.js 26.5.0)。
| 条件 | 処理フレーム数 | 処理後のヒープ |
|---|---|---|
| 毎フレームdelete() | 3,000(完走) | 128MiBのまま |
| delete()なし | 300 | 512MiBへ拡大 |
| delete()なし | 693〜694で失敗 | 上限の1GiB |
1フレームあたりRGBAとグレーで約1.5MBが残り続け、30fpsのカメラなら23秒前後でメモリ確保に失敗します。静止画1枚では気づかず、動画にした途端にタブが落ちるのはこのためです。上のコードのように try と finally で囲み、例外が出ても必ず解放される形にしておきます。
clone()が元のMatとメモリを共有するケース
OpenCV.jsの Mat.clone() は、ビルドと初期化の方法によって中身をコピーしないことがあります。Embindが全オブジェクトに付ける同名の clone()(同じC++オブジェクトを指す参照を増やすだけ)と衝突するため、OpenCV側は本来のコピーを mat_clone という名前で公開し、初期化後に clone を差し替えています。変更履歴によると、この深いコピーの動作は4.13.0で入りました。2×2のMatを複製し、複製側の1画素を書き換えて元に波及するかを見た結果です。
| ビルド・初期化 | clone() | mat_clone()・copyTo() |
|---|---|---|
| 公式・読み込み前にModuleを用意 | 独立したコピー | 独立したコピー |
| 公式・読み込み後にonRuntimeInitializedを代入 | 元とメモリを共有 | 独立したコピー |
| npm版(@techstark 5.0.0-release.1) | 元とメモリを共有 | 独立したコピー |
読み込み後に onRuntimeInitialized を代入すると、OpenCV.jsが仕込んだ差し替え処理が上書きされて呼ばれなくなります。npm版の同梱ファイルには差し替え処理そのものがありません。元画像を残したまま加工結果を作るコードで「元画像まで変わる」症状が出たら、これを疑ってください。mat_clone() は3つのビルドすべてで独立したコピーを返したので、配布形式を問わず使えます。
Webカメラ映像の処理:フレーム取得とMatの再利用
カメラ映像を1フレームずつ処理するのが、OpenCV.jsをブラウザで使う価値がいちばん出る場面です。getUserMedia で取得した <video> を cv.VideoCapture に渡し、read() でMatへ書き出します。
<video id="videoInput" autoplay playsinline muted></video>
<canvas id="canvasOutput"></canvas>
<button id="startButton" disabled>開始</button>
<button id="stopButton" disabled>停止</button>
<script>
const FPS = 30;
let cv = null, src = null, gray = null, cap = null, stream = null, timer = null;
// 前章のローダーの onOpenCvLoaded をこの形に置き換える
async function onOpenCvLoaded() {
({ cv } = await getCv());
document.getElementById('startButton').disabled = false;
}
async function start() {
document.getElementById('startButton').disabled = true;
const video = document.getElementById('videoInput');
try {
stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
await video.play();
// VideoCapture は video.width/height で内部canvasとMatの大きさを決める
video.width = video.videoWidth;
video.height = video.videoHeight;
src = new cv.Mat(video.height, video.width, cv.CV_8UC4);
gray = new cv.Mat(video.height, video.width, cv.CV_8UC1);
cap = new cv.VideoCapture(video);
document.getElementById('stopButton').disabled = false;
processVideo();
} catch (e) {
stop();
console.error(e);
}
}
function processVideo() {
if (!cap) return;
const begin = Date.now();
cap.read(src);
cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);
cv.imshow('canvasOutput', gray);
timer = setTimeout(processVideo, Math.max(0, 1000 / FPS - (Date.now() - begin)));
}
function stop() {
clearTimeout(timer);
stream?.getTracks().forEach((t) => t.stop());
src?.delete();
gray?.delete();
timer = stream = src = gray = cap = null;
document.getElementById('stopButton').disabled = true;
document.getElementById('startButton').disabled = !cv;
}
document.getElementById('startButton').addEventListener('click', start);
document.getElementById('stopButton').addEventListener('click', stop);
</script>
Matはループの外で1回だけ確保し、停止時にまとめて解放します。processVideo の中で new cv.Mat() を繰り返し、不要になったMatを解放しなければ、メモリ使用量が増え続けます。注意したいのは映像の大きさです。cv.VideoCapture は生成時の video.width と video.height で内部のcanvasを作り、read() でもMatの大きさをこの値と照合します。この2つはHTML属性の値なので、タグに書かないと0のままで、read() 内部の getImageData が例外になります。上のコードは再生開始後に video.videoWidth と videoHeight(カメラの実寸)を属性へ代入してから、MatとVideoCaptureを作っています。開始前や開始に失敗した後に停止しても落ちないよう、stop() は未生成の値を飛ばし、解放後に参照を空に戻します。
getUserMedia はセキュアコンテキストで利用でき、通常はHTTPSまたはlocalhostで配信します。python3 -m http.server でlocalhostから開けば手元では足りますが、同じLANのスマートフォンからIPアドレスで開くとHTTPになりカメラが起動しません。映像を相手に送る仕組みまで作るなら、WebRTCとは?P2Pの仕組み・ICE/STUN/TURNからSFU選定・実装・運用判断まで実装者向けに解説が前提知識になります。
本番投入の判断:サイズとビルド
11〜16MBの配布ファイル:導入が見合う処理
OpenCV.jsの最大の弱点はサイズです。公式4.14.0で11,059,007バイト、公式5.0.0で16,211,109バイト、npm版5.0.0-release.1で13,298,869バイトあり、wasmをbase64で埋め込むと元のバイナリの約1.33倍になるため、そのぶん重くなっています。モバイル回線の一般向けページで、グレースケール変換やぼかし、リサイズのためだけにこれを読み込むのは割に合いません。その程度の処理ならOpenCV.jsは使わず、Canvasの getImageData と putImageData、またはWebGLで書きます。
OpenCV.jsが見合うのは、輪郭抽出、特徴点マッチング、射影変換による書類の傾き補正、カメラキャリブレーションのように、自前で書くと数百行になるアルゴリズムを使うときです。もう1つの理由は、本人確認書類や医療画像のように原本をサーバへ送りたくない場合です。前処理の後も画像を外部へ送らない設計にすれば、処理を端末内で完結できます。人物の姿勢推定や顔のランドマーク検出のように学習済みモデルが主役なら、TensorFlow.jsとは?ブラウザとNode.jsで動かす導入手順と動くコード【4.22系・2026年10月】のほうが近道です。ブラウザ内の画像処理を業務システムへ組み込む場合は、フルスクラッチ開発として要件から相談いただけます。
自前ビルドの軽量化:wasm分離とC++17の指定条件
使う関数が決まっているなら、Emscriptenで自分でビルドして不要なモジュールを落とします。
# 4.14.0 をビルドするなら --branch 4.14.0 に変える
git clone --branch 5.0.0 --depth 1 https://github.com/opencv/opencv.git
emcmake python ./opencv/platforms/js/build_js.py build_js --disable_single_file
# 5.0 を Emscripten 4.0.20 以降でビルドする場合
emcmake python ./opencv/platforms/js/build_js.py build_js --disable_single_file \
--cmake_option="-DCMAKE_CXX_STANDARD=17"
効くのは2点です。--disable_single_file はwasmを別ファイルに分け、base64のぶんだけ総転送量を減らします。もう1つは opencv_js.config.py の white_list を必要なモジュールだけに絞ることで、編集したファイルは --config で渡します。5.0.0のビルド手順には、Emscripten 4.0.20以降はEmbindがC++17以上を要求するため上の --cmake_option を付けるよう追記されています(4.14.0の手順には無い記述です)。--simd は公式が実験的と位置づけ、--threads はブラウザ専用でNode.jsでは動かないとされているので、まずはサイズを削る目的だけでビルドするのが無難です。
4.14と5.0の選択基準:既存コードとDNNの要件
既存のコードを動かし続けたいなら、4.14.0を番号固定で使うのが無難です。4.14.0の変更履歴には、wasmのSIMD型変換での範囲外読み取りの修正と、EmscriptenのJSビルドでの版判定の修正が入っています。ファイルも5.0.0より約5MB小さく済みます。5.0.0を選ぶのは、5.0で作り直されたDNNエンジンを使う場合や、Python側も5.0にそろえる場合です。cv.Mat・cv.imread・cv.cvtColor・cv.Canny などの基本関数は両版で同じ名前で動きましたが、features2dやcalib3dの関数を使うコードは前述のモジュール再編の影響を確認してから切り替えてください。
よくある質問
OpenCV.jsとPython版のOpenCVで結果は同じですか?
同じC++実装をコンパイルしたものなので、同じ関数・同じ引数なら結果は基本的に一致します。違うのは入出力とメモリです。ブラウザでは cv.imread() に要素を渡し、canvasの画素がRGBAなので COLOR_RGBA2GRAY を使います。Matは自動では解放されず、delete() を呼ぶ必要があります。
TypeScriptの型定義はどのパッケージにありますか?
@techstark/opencv-js に同梱されています(package.jsonの types は dist/src/index.d.ts)。TypeScriptでデフォルトimportするときは、READMEのとおり tsconfig.json で "esModuleInterop": true を設定します。型定義がOpenCV.js本体より古い場合があるため、型に無い関数は実行時に存在を確かめてから使います。
opencv4nodejsとOpenCV.jsはどう違いますか?
opencv4nodejsはネイティブのOpenCVをNode.jsから呼ぶバインディングで、ブラウザでは動きません。npmの opencv4nodejs は2020年5月の5.6.0が最終で、フォークの @u4/opencv4nodejs も2024年9月の7.1.2で止まっています。OpenCV.jsはwasmなのでネイティブのビルドが要らず、ブラウザとNode.jsの両方で同じファイルが動きます。
Web Workerの中で動かせますか?
動かせます。公式のopencv.jsは読み込み時に環境を判定し、importScripts がある環境ではWorkerのグローバルに cv を置きます。重い処理をWorkerへ移すとメインスレッドが固まりません。画素の受け渡しは ImageData の data を転送し、Worker側で cv.matFromImageData() に渡します。
ローカルのHTMLファイルを直接開いても動きますか?
公式チュートリアルはローカルのWebサーバで配信するよう勧めています。wasmを埋め込んだ既定のビルドは file:// でも動くことがありますが、EmscriptenのFAQは、file://での読み込みはブラウザによって動かないことがあるとしています。Webカメラを使うなら getUserMedia の制約もあるため、python3 -m http.server などでlocalhostから開くのが確実です。