JavaScriptで画像処理!OpenCV.jsの基本を紹介

JavaScriptで画像処理!OpenCV.jsの基本を紹介

はじめに:ブラウザで画像処理を実現する力

現代のウェブアプリケーションにおいて、画像処理は単なる装飾を超え、ユーザー体験を向上させる重要な要素となっています。写真フィルター、画像編集ツール、顔認識、物体検出、さらにはAR(拡張現実)まで、画像処理技術は多岐にわたる分野で活用されています。

これまで、本格的な画像処理はサーバーサイドやデスクトップアプリケーションで行われるのが一般的でした。しかし、Webブラウザの性能向上とWebAssemblyのような技術の登場により、クライアントサイド、つまりブラウザ上で複雑な画像処理をリアルタイムに行うことが現実的になってきています。

ここで紹介するのが、OpenCV.jsです。OpenCV(Open Source Computer Vision Library)は、画像処理およびコンピュータービジョン分野で最も広く利用されているオープンソースライブラリの一つです。C++やPythonをはじめとする様々な言語に対応しており、その高機能さと豊富なアルゴリズムで知られています。OpenCV.jsは、この強力なOpenCVライブラリをJavaScriptから利用できるようにしたものです。これにより、開発者はサーバーとの通信なしに、ブラウザ上で高度な画像処理機能を実装することが可能になります。

ブラウザ上で画像処理を行うことには、いくつかの大きな利点があります。まず、サーバー負荷の軽減です。画像処理は計算コストの高い処理が多いため、クライアント側で行うことでサーバーのリソースを節約できます。次に、リアルタイム性の向上です。サーバーとのデータ送受信の遅延なしに処理結果をすぐに表示できるため、インタラクティブなアプリケーションやリアルタイム処理が求められる場面で威力を発揮します。また、ユーザーのプライバシー保護にも貢献します。画像をサーバーにアップロードすることなく処理できるため、機密性の高い情報を含む画像を扱う場合でも、ユーザーは安心して利用できます。

この記事では、OpenCV.jsを使ってJavaScriptで画像処理を始めるための基本的な手順、概念、そしていくつかの代表的な画像処理手法の実装方法を、詳細な解説と豊富なコード例と共に紹介します。OpenCV.jsの導入から、画像の読み込みと表示、基本的な画像処理アルゴリズム(グレースケール変換、二値化、平滑化、エッジ検出など)の適用、そして重要なメモリ管理について学びます。この記事を読み終える頃には、あなたもブラウザ上で動くパワフルな画像処理アプリケーションを開発する第一歩を踏み出せるようになっているでしょう。

OpenCV.jsとは?

OpenCV.jsは、前述の通り、世界的に有名な画像処理ライブラリであるOpenCVのJavaScriptバインディングです。OpenCVの豊富なC++APIを、WebAssembly技術を利用してJavaScriptから呼び出せるようにしています。これにより、OpenCVの高度な機能をウェブブラウザ環境でそのまま活用できます。

OpenCVの概要

OpenCVは、1999年にIntelによって開発が開始され、現在は非営利団体のOpenCV Foundationによってサポートされています。画像処理、コンピュータービジョン、機械学習に関連する2500以上の最適化されたアルゴリズムが含まれており、物体検出、顔認識、画像からの3Dモデル構築、ステレオビジョン、ジェスチャー認識、移動物体のトラッキング、ライブラリ拡張機能、機械学習モデルなど、幅広い用途で利用されています。C++, Python, Java, MATLABといった多様なプログラミング言語に対応しています。

OpenCV.jsの特徴

  • WebAssemblyの利用: OpenCV.jsは、OpenCVのC++コードをWebAssemblyというバイナリ形式にコンパイルすることで実現されています。WebAssemblyは、ウェブブラウザ上でネイティブに近い速度で実行可能なコード形式であり、JavaScriptから呼び出すことができます。これにより、従来のJavaScriptのみでの画像処理に比べて、格段に高速な処理が可能になります。
  • ブラウザ上での実行: サーバーサイドに依存せず、クライアント(ユーザーのブラウザ)上で画像処理が完結します。
  • OpenCVの豊富な機能: C++版OpenCVで提供されている多くの画像処理関数やデータ構造(特にcv::Mat)がJavaScriptから利用できます。
  • JavaScriptとの親和性: HTMLの<img>タグや<canvas>要素と容易に連携できます。

なぜOpenCV.jsを使うのか?

  • 高速性: WebAssemblyによる実行速度の向上。
  • 機能性: OpenCVが長年培ってきた高度な画像処理アルゴリズムをすぐに利用できる。
  • 開発効率: JavaScriptに慣れた開発者であれば、新しい言語を習得することなく本格的な画像処理を実装できる。
  • クライアントサイド処理: サーバー負荷軽減、リアルタイム処理、プライバシー保護といったメリットを享受できる。

OpenCV.jsは、ウェブベースの画像編集ツール、リアルタイムビデオ処理、ウェブカメラを使ったインタラクティブアプリケーション、教育用ツールなど、様々なアプリケーション開発の可能性を広げます。

OpenCV.jsの導入方法

OpenCV.jsを利用するための最も簡単な方法は、提供されているJavaScriptファイルをHTMLファイルに読み込むことです。WebAssemblyのロードやOpenCVライブラリの初期化は、このファイルが自動的に行ってくれます。

1. opencv.js ファイルの入手

OpenCV.jsは、OpenCVの公式リリースに含まれています。
* OpenCVの公式サイトから最新版のソースコードまたはリリース済みパッケージをダウンロードします。
* ダウンロードしたアーカイブを展開し、build/js ディレクトリの中に opencv.js ファイルを見つけます。このファイルをあなたのプロジェクトのウェブサーバーからアクセスできる場所に配置します(例: プロジェクトルートの js ディレクトリなど)。

代替手段:CDNの利用

公式のCDNは提供されていませんが、非公式または特定のベンダーが提供するCDNが存在する場合があります。ただし、安定性やセキュリティを考慮すると、公式ソースからファイルをダウンロードして自身のサーバーでホストするのが最も推奨される方法です。開発中など一時的な利用であれば、信頼できるCDNの情報を探すこともできますが、本番環境での利用は慎重に判断してください。この記事では、ローカルにファイルを置いてホストする方法を前提とします。

2. HTMLファイルからの読み込み

opencv.js ファイルをプロジェクトに配置したら、それをHTMLファイル内の <script> タグで読み込みます。通常は <body> タグの閉じタグの直前あたりに配置します。

“`html




OpenCV.js Example

OpenCV.js Test







“`

3. OpenCV.js の初期化と待機

opencv.js ファイルが読み込まれると、WebAssemblyモジュールのダウンロードとコンパイル、そしてOpenCVライブラリの初期化が非同期で行われます。これらの処理が完了するまで、OpenCVの機能(例えば cv グローバルオブジェクト)は利用できません。初期化が完了したことを確認してからOpenCVのコードを実行する必要があります。

opencv.js は、初期化が完了すると onRuntimeInitialized というグローバル関数があれば、それを自動的に呼び出すように設計されています。このコールバック関数を利用するのが、初期化完了を待つ最も一般的な方法です。

“`html




OpenCV.js Example

OpenCV.js Test



“`

このHTML構造とJavaScriptコードでは、opencv.js が読み込まれた後に onRuntimeInitialized 関数が実行され、その中で画像処理の準備(この例では簡単なMat作成と解放)が行われます。実際の画像処理コードは processImage 関数の内部に記述していくことになります。

注意点:
* opencv.js ファイルは、ウェブサーバー(Apache, Nginx, Node.jsのExpressなど)を介して配信する必要があります。ローカルファイルとしてブラウザで直接開くと、セキュリティ上の制約(CORSなど)により正常に動作しない場合があります。開発時には、VS CodeのLive Server拡張機能など、簡単なローカルウェブサーバーを利用するのが便利です。
* opencv.js ファイルは比較的サイズが大きい(数MBになることもあります)ため、読み込みに時間がかかる場合があります。ユーザーエクスペリエンスを考慮し、読み込み中のインジケーターを表示するなど工夫すると良いでしょう。

これで、OpenCV.jsを使った画像処理を始めるための準備が整いました。次に、画像データを扱うための基本的な概念である Mat オブジェクトについて見ていきましょう。

基本概念:Matオブジェクト

OpenCVにおける画像データは、基本的に Mat (Matrix) オブジェクトという形式で扱われます。Mat は多次元配列を表し、画像処理においてはピクセル値を格納するための行列として機能します。OpenCV.jsでは、C++版の cv::Mat に対応するJavaScriptオブジェクトが提供されています。

Matオブジェクトの構造

Mat オブジェクトは、画像データの様々な情報を含んでいます。
* Dimensions (次元): 画像は通常2次元(幅と高さ)ですが、Mat はN次元を扱えます。
* Size (サイズ): 画像の幅(列数)と高さ(行数)。
* Type (型): ピクセル値のデータ型とチャンネル数。例えば、CV_8UC3 は8ビット符号なし整数(0〜255)で、3チャンネル(カラー画像、例えばBGR)であることを示します。
* Data (データ): 実際のピクセル値を格納しているメモリブロックへのポインタ(OpenCV.js内部ではWebAssemblyのヒープ上のアドレス)。JavaScriptからは直接ポインタを操作するのではなく、data プロパティや ucharAt, floatAt などのメソッドを通じてアクセスします。

主要なMatタイプ

OpenCVでは、ピクセル値のデータ型とチャンネル数を組み合わせて様々な Mat タイプが定義されています。一般的なものには以下のようなものがあります。

  • CV_8U: 8ビット符号なし整数 (0-255)。グレースケール画像などで使用。
  • CV_16U: 16ビット符号なし整数 (0-65535)。
  • CV_16S: 16ビット符号付き整数 (-32768-32767)。
  • CV_32S: 32ビット符号付き整数。
  • CV_32F: 32ビット浮動小数点数。
  • CV_64F: 64ビット浮動小数点数(倍精度)。

チャンネル数は、タイプ名の末尾に C とチャンネル数を付けて表します。例えば、CV_8UC1 は8ビット符号なし整数で1チャンネル(グレースケール)、CV_8UC3 は8ビット符号なし整数で3チャンネル(カラー、OpenCVのデフォルトはBGR)、CV_8UC4 は8ビット符号なし整数で4チャンネル(RGBA、アルファチャンネル付き)です。

Matオブジェクトの作成

OpenCV.jsで Mat オブジェクトを作成するにはいくつかの方法があります。

1. 空のMatを作成する

指定したサイズとタイプの空の Mat を作成します。

“`javascript
let rows = 100; // 高さ
let cols = 200; // 幅
let type = cv.CV_8UC3; // 8ビット符号なし整数、3チャンネル(カラー)
let mat1 = new cv.Mat(rows, cols, type);
console.log(“Empty Mat created:”, mat1);
console.log(“Size:”, mat1.size().width, “x”, mat1.size().height);
console.log(“Type:”, mat1.type()); // 例: 16 (CV_8UC3の内部表現値)
console.log(“Channels:”, mat1.channels()); // 3
console.log(“Depth:”, mat1.depth()); // 0 (CV_8Uの内部表現値)

// Matオブジェクトの解放(重要!)
mat1.delete();
“`

2. 特定の値で初期化されたMatを作成する

指定した単一の値(Scalarオブジェクト)で全ての要素を初期化します。

“`javascript
let rows = 50;
let cols = 50;
let type = cv.CV_8UC3;
// Scalar(B, G, R, A) – チャンネル数に応じて引数を指定
let color = new cv.Scalar(255, 0, 0); // B=255 (青)
let mat2 = new cv.Mat(rows, cols, type, color); // 50×50の青い画像Matを作成
console.log(“Colored Mat created:”, mat2);

// Matオブジェクトの解放
mat2.delete();

// グレースケールの場合(1チャンネル)
let grayMat = new cv.Mat(50, 50, cv.CV_8UC1, new cv.Scalar(128)); // 50×50のグレー(値128)画像Matを作成
console.log(“Gray Mat created:”, grayMat);

// Matオブジェクトの解放
grayMat.delete();
“`

cv.Scalar は、ピクセルの値を表すオブジェクトです。チャンネル数に応じて1つから4つの値(通常はB, G, R, αの順)を指定します。

3. ゼロまたは単位行列で初期化されたMatを作成する

cv.Mat.zeroscv.Mat.ones, cv.Mat.eye といった静的メソッドを利用して、全ての要素が0、全ての要素が1、または単位行列(対角成分が1、それ以外が0)のMatを作成できます。

“`javascript
// 全て0のMat (黒画像など)
let zeroMat = cv.Mat.zeros(100, 150, cv.CV_8UC1); // 100×150の黒いグレースケール画像
console.log(“Zero Mat created:”, zeroMat);
zeroMat.delete();

// 全て1のMat
let oneMat = cv.Mat.ones(50, 50, cv.CV_8UC3); // 50×50のカラー画像 (全てのチャンネルが1)
console.log(“One Mat created:”, oneMat);
oneMat.delete();

// 単位行列 (正方行列である必要があります)
let eyeMat = cv.Mat.eye(100, 100, cv.CV_32F); // 100×100の浮動小数点型単位行列
console.log(“Eye Mat created:”, eyeMat);
eyeMat.delete();
“`

4. 既存のデータからMatを作成する

JavaScriptのTypedArray(Uint8Array, Float32Array など)から Mat を作成することも可能です。これは、Canvasからピクセルデータを取得した場合などによく使用します。

“`javascript
// 2×2のグレースケール画像データ (0-255)
let pixelData = new Uint8Array([
0, 100,
200, 255
]);
let rows = 2;
let cols = 2;
let type = cv.CV_8UC1;

// Uint8ArrayからMatを作成
let dataMat = new cv.Mat(rows, cols, type);
dataMat.data.set(pixelData); // データをMatにコピー
console.log(“Mat from data created:”, dataMat);
console.log(“Pixel at (0, 0):”, dataMat.ucharAt(0, 0));
console.log(“Pixel at (0, 1):”, dataMat.ucharAt(0, 1));
console.log(“Pixel at (1, 0):”, dataMat.ucharAt(1, 0));
console.log(“Pixel at (1, 1):”, dataMat.ucharAt(1, 1));

dataMat.delete();

// カラー画像 (3チャンネル、BGR順)
let colorPixelData = new Uint8Array([
255, 0, 0, 0, 255, 0, // 1行目: 青, 緑
0, 0, 255, 255, 255, 255 // 2行目: 赤, 白
]);
let colorRows = 2;
let colorCols = 2;
let colorType = cv.CV_8UC3;

let colorDataMat = new cv.Mat(colorRows, colorCols, colorType);
colorDataMat.data.set(colorPixelData);
console.log(“Color Mat from data created:”, colorDataMat);

// ピクセル値へのアクセス (カラーの場合)
// 各チャンネルごとにアクセスする必要があります
// ucharAt(row, col * channels + channel_index)
console.log(“Pixel at (0, 0) [B, G, R]:”,
colorDataMat.ucharAt(0, 0 * 3 + 0), // Blue
colorDataMat.ucharAt(0, 0 * 3 + 1), // Green
colorDataMat.ucharAt(0, 0 * 3 + 2) // Red
); // 255, 0, 0

console.log(“Pixel at (1, 1) [B, G, R]:”,
colorDataMat.ucharAt(1, 1 * 3 + 0), // Blue
colorDataMat.ucharAt(1, 1 * 3 + 1), // Green
colorDataMat.ucharAt(1, 1 * 3 + 2) // Red
); // 255, 255, 255

colorDataMat.delete();
“`

Matdata プロパティは、WebAssemblyメモリ上のピクセルデータのビューを提供する Uint8Array またはそれに対応するTypedArrayです。.set() メソッドを使ってデータをコピーしたり、.get() (または .ucharAt, .floatAt など) を使ってデータを読み出したりできます。チャンネル数が多い場合、ピクセルへのアクセスは row * cols * channels + col * channels + channel_index といったオフセット計算が必要になります。

.ucharAt(), .floatAt() などを使ったピクセル値へのアクセス

より直感的に特定のピクセルにアクセスするために、OpenCV.jsはいくつかのヘルパーメソッドを提供しています。

  • mat.ucharAt(row, col): CV_8UタイプのMatの指定したピクセル値を取得/設定。
  • mat.ushortAt(row, col): CV_16UタイプのMatの指定したピクセル値を取得/設定。
  • mat.shortAt(row, col): CV_16SタイプのMatの指定したピクセル値を取得/設定。
  • mat.intAt(row, col): CV_32SタイプのMatの指定したピクセル値を取得/設定。
  • mat.floatAt(row, col): CV_32FタイプのMatの指定したピクセル値を取得/設定。
  • mat.doubleAt(row, col): CV_64FタイプのMatの指定したピクセル値を取得/設定。

これらのメソッドは、1チャンネルのMatに対して使用するのが最もシンプルです。カラー画像のような複数チャンネルのMatの場合、.data プロパティに直接アクセスするか、OpenCVの関数(cv.split, cv.mergeなど)を使ってチャンネルごとに分離してから処理するのが一般的です。

例えば、グレースケール画像のピクセル値を変更する場合:

“`javascript
let grayMat = new cv.Mat(10, 10, cv.CV_8UC1, new cv.Scalar(100));

// (5, 5)の位置のピクセル値を200に変更
grayMat.ucharAt(5, 5, 200); // 設定
let pixelValue = grayMat.ucharAt(5, 5); // 取得
console.log(“Pixel at (5, 5):”, pixelValue); // 200

grayMat.delete();
“`

重要:メモリ管理 (.delete())

C++でOpenCVを使う場合と同様に、OpenCV.jsで作成した Mat オブジェクトやその他のOpenCVオブジェクト(Scalar, Point, Sizeなど)は、不要になったら必ず .delete() メソッドを呼び出してメモリを解放する必要があります。これは、これらのオブジェクトがJavaScriptのガベージコレクションの管理下にないWebAssemblyメモリ(ヒープ)上にデータを保持しているためです。解放を忘れるとメモリリークが発生し、アプリケーションのパフォーマンス低下やクラッシュの原因となります。

画像処理を行う関数では、関数の開始時に一時的な Mat を作成し、関数の終了直前にそれらを全て .delete() で解放するというパターンが一般的です。

“`javascript
function processImage(inputMat) {
// 出力用のMatを作成
let outputMat = new cv.Mat();
let tempMat1 = new cv.Mat();
let tempMat2 = new cv.Mat(); // 中間処理用のMat

try {
    // ここでOpenCVの関数を使って inputMat を処理し、outputMat に結果を格納
    // 例: cv.cvtColor(inputMat, tempMat1, cv.COLOR_RGBA2GRAY);
    // 例: cv.threshold(tempMat1, tempMat2, 100, 255, cv.THRESH_BINARY);
    // 例: tempMat2.copyTo(outputMat);

    // 処理が完了したら outputMat を返す
    // inputMat と outputMat は呼び出し元で管理する
    return outputMat;

} catch (e) {
    console.error("Error processing image:", e);
    // エラーが発生した場合も、作成したMatは解放する必要がある
    // ただし、return する outputMat は解放しない
    return null; // またはエラーを示す値を返す

} finally {
    // 関数内で新規に作成した一時的なMatを全て解放
    tempMat1.delete();
    tempMat2.delete();
    // outputMat は呼び出し元で利用するためここでは解放しない!
    // return null の場合は outputMat も解放が必要
    if (outputMat && !outputMat.isDeleted()) { // isDeleted() で既に解放されていないか確認できる
         // outputMat は呼び出し元に渡すのでここでは解放しない
         // エラーで outputMat が作成されなかった場合などを考慮
         if (!outputMat.empty() && !outputMat.isDeleted()) {
            // outputMat は解放しない
         } else if (outputMat && outputMat.isDeleted()) {
            // pass
         } else if (outputMat) {
             // 作成されたが空またはエラーで利用不可になった場合など
             outputMat.delete();
         }
    }
}

}

// 呼び出し元
let srcMat = new cv.Mat(…); // 入力画像Mat
let dstMat = null;
try {
dstMat = processImage(srcMat);
if (dstMat) {
// dstMat を利用した処理 (Canvasへの表示など)
// …
}
} finally {
// 最後に srcMat と dstMat を解放
if (srcMat && !srcMat.isDeleted()) srcMat.delete();
if (dstMat && !dstMat.isDeleted()) dstMat.delete();
}
“`

try...finally ブロックを利用すると、エラーが発生した場合でも確実に一時的なMatを解放できるため、推奨されるパターンです。ただし、return するMatは呼び出し元が責任を持って解放する必要があります。

Canvas要素との連携

OpenCV.jsを使ってブラウザ上で画像処理を行う場合、HTMLの<canvas>要素との連携が非常に重要になります。<canvas> はブラウザ上でグラフィックスを描画するための要素であり、OpenCV.jsで処理した画像を表示したり、逆にCanvasに描画された画像を取得してOpenCV.jsで処理したりするために利用します。

1. HTMLでのCanvas要素の準備

まず、HTMLファイルに<canvas>要素を配置します。OpenCV.jsは、このCanvas要素に画像を描画する機能を提供します。

“`html

“`

必要に応じて、入力元となる<img>タグも用意しておくと便利です。<img>タグで読み込んだ画像をCanvasに描画し、そのCanvasからOpenCV.jsのMatに変換して処理を行う、というワークフローがよく使われます。

“`html

“`

2. 画像(<img>要素)をCanvasに描画し、Matとして読み込む (cv.imread)

OpenCV.jsの cv.imread() 関数は、HTML要素(<img> または <canvas>) から画像データを読み込み、新しい cv.Mat オブジェクトを作成します。

“`javascript
// onRuntimeInitialized 関数内、または初期化完了後に実行されるコード内で使用

function processImage() {
// 入力元の要素を取得
let imgElement = document.getElementById(‘inputImage’);

// cv.imread() を使って<img>要素からMatオブジェクトを作成
// imgElement が持っている画像データがMatに変換される
let srcMat = cv.imread(imgElement);
console.log("Image Mat created:", srcMat);
console.log("Size:", srcMat.size().width, "x", srcMat.size().height);
console.log("Type:", srcMat.type()); // <img>からの読み込みは通常 CV_8UC4 (RGBA) になる

// 作成したMatオブジェクトは不要になったら必ず解放する
// この例では srcMat を後続処理に使うので、ここでは解放しない
// 処理が終わった後で解放する必要がある

// 例として、グレースケールに変換してみる
let grayMat = new cv.Mat();
// cv.COLOR_RGBA2GRAY は RGBA からグレースケールへの変換コード
cv.cvtColor(srcMat, grayMat, cv.COLOR_RGBA2GRAY);
console.log("Gray Mat created:", grayMat);
console.log("Gray Mat Type:", grayMat.type()); // CV_8UC1 になる

// Canvasに表示するためにRGBAに戻す必要がある場合(後述のcv.imshowのため)
// または、グレースケールMatを直接表示する場合もある
// cv.imshow は CV_8U, CV_16U, CV_32F の1チャンネルまたは3チャンネル、4チャンネルに対応
// グレースケール(CV_8UC1)はそのまま表示可能

// 処理結果をCanvasに表示
let outputCanvas = document.getElementById('outputCanvas');
cv.imshow(outputCanvas, grayMat); // グレースケールMatをCanvasに表示

// 作成したMatオブジェクトを解放
srcMat.delete();
grayMat.delete(); // grayMat も不要になったので解放

}

// onRuntimeInitialized の定義と processImage() の呼び出し
function onRuntimeInitialized() {
console.log(“OpenCV.js is ready.”);
processImage();
}
“`

cv.imread() は非同期ではありません。呼び出し時点で画像のロードが完了している必要があります。そのため、<img> 要素の onload イベントを待ってから cv.imread() を呼び出すのがより確実な方法です。

“`html
Input Image


“`

3. MatオブジェクトをCanvasに表示する (cv.imshow)

OpenCV.jsの cv.imshow() 関数は、指定した cv.Mat オブジェクトの内容を、指定した <canvas> 要素に描画します。

javascript
// 前述の processImage 関数内で使用した例
let outputCanvas = document.getElementById('outputCanvas');
let processedMat = ...; // ここに表示したいMatオブジェクト
cv.imshow(outputCanvas, processedMat);

cv.imshow() は、CV_8U, CV_16U, CV_32F の1チャンネル、3チャンネル、または4チャンネルのMatを扱うことができます。
* 1チャンネル (CV_8UC1, CV_16UC1, CV_32FC1): グレースケール画像として表示されます。CV_16UCV_32F の場合、値は表示用にスケーリングされます。
* 3チャンネル (CV_8UC3, CV_16UC3, CV_32FC3): カラー画像として表示されます。OpenCVのデフォルトであるBGR順のデータは、表示用にRGBA順に変換されます。
* 4チャンネル (CV_8UC4, CV_16UC4, CV_32FC4): アルファチャンネルを含むカラー画像として表示されます。データはRGBA順である必要があります。

注意点: cv.imshow() は内部的にCanvasの2Dコンテキストを取得し、ピクセルデータをCanvasに書き込んでいます。特に大きな画像を頻繁に表示すると、パフォーマンスに影響を与える可能性があります。

4. CanvasからMatへの手動変換 (getImageDataputImageData)

cv.imread() だけでなく、Canvasの getContext('2d').getImageData() メソッドを使ってピクセルデータを取得し、そのデータから手動で cv.Mat を作成することも可能です。これは、Canvas上に既に描画された内容(ユーザーの落書きなど)をOpenCV.jsで処理したい場合に有用です。

getImageData() は、Canvasの指定領域のピクセルデータを ImageData オブジェクトとして返します。ImageData オブジェクトの data プロパティは Uint8ClampedArray で、RGBA順の8ビット整数(0-255)の一次元配列です。

OpenCV.jsはデフォルトでBGR順を扱いますが、getImageData() のデータはRGBA順である点に注意が必要です。多くのOpenCV関数はRGBA入力も受け付けますが、明示的にBGRに変換したい場合は cv.cvtColor(src, dst, cv.COLOR_RGBA2BGR) を使用します。

“`javascript
// CanvasからMatを作成する例
function matFromCanvas(canvasId) {
let canvas = document.getElementById(canvasId);
let ctx = canvas.getContext(‘2d’);
let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

// ImageDataのデータ (Uint8ClampedArray) から Mat を作成
// MatのサイズはCanvasと同じ、タイプはCV_8UC4 (RGBA)
let srcMat = new cv.Mat(canvas.height, canvas.width, cv.CV_8UC4);

// ImageDataのデータをMatにコピー
// srcMat.data は Uint8Array なので、Uint8ClampedArrayから直接コピー可能
srcMat.data.set(imageData.data);

return srcMat; // 作成したMatを返す (呼び出し元で解放が必要)

}

// 使用例
let canvasMat = null;
try {
canvasMat = matFromCanvas(‘myDrawingCanvas’); // 例えばユーザーが絵を描いたCanvas
console.log(“Mat from Canvas created:”, canvasMat);

// この Mat を使って画像処理を行う
// ...

} catch (e) {
console.error(“Error:”, e);
} finally {
if (canvasMat && !canvasMat.isDeleted()) canvasMat.delete();
}
“`

5. MatからCanvasへの手動変換 (getImageDataputImageData)

cv.imshow() を使わずに、OpenCV.jsで処理した Mat オブジェクトのデータをCanvasに手動で描画することも可能です。これは、Canvasの特定の領域にのみ描画したい場合や、より細かな制御を行いたい場合に有用です。

手動で描画するには、Canvasの getContext('2d').createImageData() または既存の ImageData オブジェクトを利用して putImageData() メソッドを使用します。putImageData()ImageData オブジェクトを受け取るため、Mat のデータをRGBA順の Uint8ClampedArray に変換して ImageData を作成する必要があります。

OpenCVの多くの処理結果はBGRまたはグレースケールであるため、CanvasのRGBA形式に合わせるための変換が必要になることが多いです。

“`javascript
// MatをCanvasに描画する例
function matToCanvas(mat, canvasId) {
let canvas = document.getElementById(canvasId);
let ctx = canvas.getContext(‘2d’);

// 表示用にRGBA形式に変換する必要がある場合が多い
// 入力Matのタイプを確認し、必要なら変換
let displayMat = mat;
let needsConversion = false;
if (mat.type() !== cv.CV_8UC4) {
    // グレースケール (CV_8UC1) から RGBA へ変換
    if (mat.type() === cv.CV_8UC1) {
        displayMat = new cv.Mat();
        cv.cvtColor(mat, displayMat, cv.COLOR_GRAY2RGBA);
        needsConversion = true;
    }
    // BGR (CV_8UC3) から RGBA へ変換
    else if (mat.type() === cv.CV_8UC3) {
         displayMat = new cv.Mat();
         cv.cvtColor(mat, displayMat, cv.COLOR_BGR2RGBA);
         needsConversion = true;
    }
    // その他のタイプはここでは扱わない
    else {
        console.error("Unsupported Mat type for display:", mat.type());
        return;
    }
}

// CanvasのサイズとMatのサイズが異なる場合、Canvasのサイズを調整
if (canvas.width !== displayMat.cols || canvas.height !== displayMat.rows) {
     canvas.width = displayMat.cols;
     canvas.height = displayMat.rows;
}

// ImageDataオブジェクトを作成
let imageData = ctx.createImageData(displayMat.cols, displayMat.rows);

// Matのデータ (RGBA順) を ImageDataのデータにコピー
// displayMat.data は Uint8Array、imageData.data は Uint8ClampedArray
// どちらも同じバイト表現なので直接コピー可能
imageData.data.set(displayMat.data);

// ImageDataをCanvasに描画
ctx.putImageData(imageData, 0, 0);

// 変換のために一時的に作成したMatを解放
if (needsConversion && displayMat && !displayMat.isDeleted()) {
    displayMat.delete();
}

}

// 使用例
let processedMat = …; // 処理済みのMat (例えばグレースケール)
matToCanvas(processedMat, ‘outputCanvas’);
// processedMat は呼び出し元で解放する必要がある
“`

手動でのデータ転送は、cv.imshow() よりも柔軟性がありますが、色のチャンネル順やデータ型に注意が必要です。特に cv.cvtColor を適切に利用して、CanvasのRGBA形式にデータを合わせる必要があります。

基本的な画像処理操作

ここでは、OpenCV.jsを使って行う基本的な画像処理操作をいくつか紹介します。

1. グレースケール変換 (cv.cvtColor)

カラー画像をグレースケール画像に変換するのは、画像処理の最初のステップとしてよく行われます。OpenCVでは cv.cvtColor() 関数を使用します。

“`javascript
// Assuming srcMat is a color image (e.g., CV_8UC3 or CV_8UC4)
let srcMat = cv.imread(document.getElementById(‘inputImage’)); // RGBAで読み込まれることが多い

let grayMat = new cv.Mat(); // グレースケール結果を格納するMat
// cv.COLOR_RGBA2GRAY は RGBA からグレースケールへの変換コード
cv.cvtColor(srcMat, grayMat, cv.COLOR_RGBA2GRAY);

// grayMat は CV_8UC1 タイプになります

// Canvasに表示
cv.imshow(‘outputCanvas’, grayMat);

// Matを解放
srcMat.delete();
grayMat.delete();
“`

cv.cvtColor() 関数は、入力Mat、出力Mat、そして変換コードを引数に取ります。OpenCVでは様々な色空間変換(BGR↔HSV, BGR↔HLSなど)が提供されており、対応する変換コードを指定することで実行できます。ウェブで一般的なのはRGBA形式なので、cv.COLOR_RGBA2GRAYcv.COLOR_RGBA2BGR などがよく使われます。

2. 二値化 (cv.threshold)

二値化は、画像を白と黒の2色のみで表現する処理です。指定した閾値よりも明るいピクセルを白(最大値)、それ以外を黒(0)にします。ノイズ除去や特徴抽出の前処理として有用です。グレースケール画像に対して行うのが一般的です。

“`javascript
// Assuming grayMat is a grayscale image (CV_8UC1)
// 先にグレースケール変換を行います
let srcMat = cv.imread(document.getElementById(‘inputImage’));
let grayMat = new cv.Mat();
cv.cvtColor(srcMat, grayMat, cv.COLOR_RGBA2GRAY);

let binaryMat = new cv.Mat(); // 二値化結果を格納するMat
let thresholdValue = 128; // 閾値 (0-255)
let maxValue = 255; // 閾値を超えたピクセルに設定する値

// cv.THRESH_BINARY は基本的な二値化手法
cv.threshold(grayMat, binaryMat, thresholdValue, maxValue, cv.THRESH_BINARY);

// binaryMat は CV_8UC1 タイプになります (0または255の値を持つ)

// Canvasに表示
cv.imshow(‘outputCanvas’, binaryMat);

// Matを解放
srcMat.delete();
grayMat.delete();
binaryMat.delete();
“`

cv.threshold() 関数は、入力Mat、出力Mat、閾値、最大値、そして閾値処理の種類を引数に取ります。閾値処理の種類にはいくつかバリエーションがあります(cv.THRESH_BINARY, cv.THRESH_BINARY_INV, cv.THRESH_TRUNC, cv.THRESH_TOZERO, cv.THRESH_TOZERO_INV)。また、画像のヒストグラムから最適な閾値を自動的に決定する大津の二値化 (cv.THRESH_OTSU) を使用することも可能です。この場合、cv.THRESH_BINARY | cv.THRESH_OTSU のようにフラグを組み合わせます。

3. 平滑化(ぼかし) (cv.GaussianBlur, cv.medianBlur, cv.blur)

平滑化は、画像のノイズを軽減したり、細かいディテールを除去したりする処理です。様々なフィルタリング手法があり、代表的なものにガウシアンぼかし、メディアンぼかし、単純平均ぼかしなどがあります。

ガウシアンぼかし (cv.GaussianBlur)

最も一般的なぼかし手法の一つで、ガウス関数に基づいたカーネル(重み付け行列)を用いて画像を畳み込みます。画像の高周波成分(エッジなど)を滑らかにします。

“`javascript
// Assuming srcMat is the input image Mat (e.g., CV_8UC4)
let srcMat = cv.imread(document.getElementById(‘inputImage’));

let dstMat = new cv.Mat(); // 結果格納用Mat
let ksize = new cv.Size(5, 5); // カーネルサイズ (奇数、例えば 3×3, 5×5, 7×7)
let sigmaX = 0; // X方向の標準偏差 (0を指定するとカーネルサイズから自動計算)

// ガウシアンぼかしを実行
cv.GaussianBlur(srcMat, dstMat, ksize, sigmaX, 0, cv.BORDER_DEFAULT);

// dstMat は srcMat と同じタイプ、同じサイズになります

// Canvasに表示
cv.imshow(‘outputCanvas’, dstMat);

// Matを解放
srcMat.delete();
dstMat.delete();
“`

cv.GaussianBlur() は、入力Mat、出力Mat、カーネルサイズ、X方向の標準偏差、Y方向の標準偏差(通常はXと同じか0)、境界処理方法を引数に取ります。

メディアンぼかし (cv.medianBlur)

カーネル内のピクセル値の中央値で中心ピクセルを置き換える手法です。特にソルト&ペッパーノイズのような突発的なノイズに効果的です。カーネルサイズは奇数でなければなりません。

“`javascript
// Assuming srcMat is the input image Mat (e.g., CV_8UC4)
let srcMat = cv.imread(document.getElementById(‘inputImage’));

let dstMat = new cv.Mat(); // 結果格納用Mat
let ksize = 5; // カーネルサイズ (奇数)

// メディアンぼかしを実行
cv.medianBlur(srcMat, dstMat, ksize);

// Canvasに表示
cv.imshow(‘outputCanvas’, dstMat);

// Matを解放
srcMat.delete();
dstMat.delete();
“`

cv.medianBlur() は、入力Mat、出力Mat、カーネルサイズ(単一の整数で幅と高さを指定)を引数に取ります。

単純平均ぼかし (cv.blur)

カーネル内のピクセル値の単純平均で中心ピクセルを置き換える最も基本的な手法です。

“`javascript
// Assuming srcMat is the input image Mat (e.g., CV_8UC4)
let srcMat = cv.imread(document.getElementById(‘inputImage’));

let dstMat = new cv.Mat(); // 結果格納用Mat
let ksize = new cv.Size(5, 5); // カーネルサイズ

// 単純平均ぼかしを実行
cv.blur(srcMat, dstMat, ksize, new cv.Point(-1, -1), cv.BORDER_DEFAULT);

// Canvasに表示
cv.imshow(‘outputCanvas’, dstMat);

// Matを解放
srcMat.delete();
dstMat.delete();
“`

cv.blur() は、入力Mat、出力Mat、カーネルサイズ、カーネルの中心点のオフセット(通常は-1,-1で自動計算)、境界処理方法を引数に取ります。

4. エッジ検出 (cv.Canny)

エッジ検出は、画像の輪郭線や境界線を見つける処理です。OpenCVでは、Cannyエッジ検出アルゴリズムがよく利用されます。

“`javascript
// Assuming srcMat is the input image Mat (e.g., CV_8UC4)
let srcMat = cv.imread(document.getElementById(‘inputImage’));

// Cannyエッジ検出はグレースケール画像に対して行うのが一般的です
let grayMat = new cv.Mat();
cv.cvtColor(srcMat, grayMat, cv.COLOR_RGBA2GRAY);

let edgesMat = new cv.Mat(); // エッジ検出結果を格納するMat
let lowThreshold = 50; // 閾値1
let highThreshold = 150; // 閾値2
let apertureSize = 3; // Sobelオペレーターのサイズ (3, 5, 7)
let L2gradient = false; // L2勾配を使用するかどうか

// Cannyエッジ検出を実行
cv.Canny(grayMat, edgesMat, lowThreshold, highThreshold, apertureSize, L2gradient);

// edgesMat は CV_8UC1 タイプになります (エッジ部分は白(255), それ以外は黒(0))

// 結果をカラーで表示したい場合(オプション)
// エッジ画像を元の画像に重ねるなど
// ここではシンプルにグレースケール結果を表示
cv.imshow(‘outputCanvas’, edgesMat);

// Matを解放
srcMat.delete();
grayMat.delete();
edgesMat.delete();
“`

cv.Canny() は、入力グレースケールMat、出力Mat、2つの閾値(lowThresholdとhighThreshold)、Sobelオペレーターのサイズ、L2勾配の使用フラグを引数に取ります。Cannyエッジ検出はヒステリシス閾値処理を使用しており、lowThresholdとhighThresholdの間隔を調整することで、検出されるエッジの量を制御できます。

5. リサイズ (cv.resize)

画像のサイズを変更します。拡大・縮小が可能です。

“`javascript
// Assuming srcMat is the input image Mat
let srcMat = cv.imread(document.getElementById(‘inputImage’));

let dstMat = new cv.Mat(); // リサイズ結果を格納するMat
let newSize = new cv.Size(320, 240); // 新しいサイズ (幅, 高さ)

// リサイズを実行
// srcMat のサイズを newSize に変更して dstMat に格納
cv.resize(srcMat, dstMat, newSize, 0, 0, cv.INTER_AREA);

// dstMat は新しいサイズになりますが、タイプは srcMat と同じです

// Canvasに表示
cv.imshow(‘outputCanvas’, dstMat);

// Matを解放
srcMat.delete();
dstMat.delete();
“`

cv.resize() は、入力Mat、出力Mat、新しいサイズ、X方向のリサイズ倍率(使用しない場合は0)、Y方向のリサイズ倍率(使用しない場合は0)、補間方法を引数に取ります。補間方法にはいくつか種類があり、cv.INTER_NEAREST, cv.INTER_LINEAR, cv.INTER_CUBIC, cv.INTER_AREA, cv.INTER_LANCZOS4 などがあります。一般的に、縮小には cv.INTER_AREA、拡大には cv.INTER_LINEARcv.INTER_CUBIC が推奨されます。

メモリ管理:OpenCV.jsを使う上での最も重要な注意点

既に何度か触れていますが、OpenCV.jsを安全かつ効率的に利用するためには、メモリ管理が非常に重要です。これは、OpenCV.jsがWebAssemblyのメモリヒープ上で画像データ(Matオブジェクトの中身など)を管理しており、JavaScriptの通常のガベージコレクションの対象とならないためです。

なぜメモリ解放が必要なのか?

JavaScriptで let obj = {}; のようにオブジェクトを作成した場合、そのオブジェクトが不要になった(どこからも参照されなくなった)と判断されると、JavaScriptエンジンが自動的にメモリを解放してくれます(ガベージコレクション)。

しかし、new cv.Mat(...) のようにOpenCV.jsのオブジェクトを作成した場合、そのオブジェクト自身はJavaScriptヒープ上にありますが、それが参照している画像データなどの実体は、WebAssemblyのメモリヒープ上に確保されます。JavaScriptのガベージコレクターは、WebAssemblyヒープ内のメモリ使用状況を直接管理できません。

そのため、OpenCV.jsで作成した cv.Mat オブジェクトなどが不要になったら、開発者自身が明示的に .delete() メソッドを呼び出し、WebAssemblyヒープ上の関連するメモリを解放してあげる必要があります。

解放を忘れるとどうなるか?

.delete() を呼び出さずにOpenCV.jsオブジェクトを使い捨てていくと、WebAssemblyヒープ上のメモリはどんどん消費されていきます。これがメモリリークです。

  • パフォーマンス低下: メモリの使用量が増えると、ブラウザの動作が遅くなったり、他のタブに影響を与えたりする可能性があります。
  • タブやブラウザのクラッシュ: 使用可能なメモリ上限を超えると、ブラウザタブやブラウザ全体がクラッシュする可能性があります。
  • 不安定な動作: メモリ不足により、予期しないエラーが発生したり、処理結果が不正になったりすることがあります。

特に、動画処理のように毎フレーム大量の Mat オブジェクトを作成・破棄するようなアプリケーションでは、適切なメモリ管理を怠るとすぐに深刻な問題が発生します。

いつ .delete() を呼び出すべきか?

OpenCV.jsオブジェクトが不要になった時点.delete() を呼び出す必要があります。

  • ある関数内で一時的に使用するために作成した Mat は、その関数の処理が完了する直前。
  • 処理パイプラインの中間結果を保持する Mat は、その結果が別の Mat にコピーされたり、次のステップで使用されなくなったりした後。
  • 最終的な処理結果を保持する Mat は、その結果がCanvasに表示されたり、JavaScriptで利用されたりして、もうその Mat オブジェクトが必要なくなった時点。

重要な原則: new で作成したOpenCV.jsオブジェクト、またはOpenCV関数が戻り値として新しいオブジェクトを返す場合(例: mat.clone(), mat.roi(...) など)、それらは全て明示的に .delete() で解放する必要があります。

効果的なメモリ管理パターン

1. try...finally を使用する

関数内で一時的な Mat を複数使用する場合、処理の途中でエラーが発生しても確実に解放するために try...finally ブロックを使うのが推奨されます。

“`javascript
function processAndFilter(inputMat) {
let grayMat = null;
let blurredMat = null;
let edgesMat = null;
let outputMat = new cv.Mat(); // 最終結果は呼び出し元に返すので、ここでは解放しない

try {
    grayMat = new cv.Mat();
    cv.cvtColor(inputMat, grayMat, cv.COLOR_RGBA2GRAY);

    blurredMat = new cv.Mat();
    let ksize = new cv.Size(5, 5);
    cv.GaussianBlur(grayMat, blurredMat, ksize, 0, 0, cv.BORDER_DEFAULT);

    edgesMat = new cv.Mat();
    cv.Canny(blurredMat, edgesMat, 50, 150);

    // エッジ画像をカラー画像に変換して、出力Matにコピーする例
    cv.cvtColor(edgesMat, outputMat, cv.COLOR_GRAY2RGBA);


} catch (e) {
    console.error("Error in processing:", e);
    // エラー時はoutputMatも無効になる可能性が高いので解放
    if (outputMat && !outputMat.isDeleted()) outputMat.delete();
    outputMat = null; // 参照をクリア
} finally {
    // 関数内で一時的に作成したMatを解放
    if (grayMat && !grayMat.isDeleted()) grayMat.delete();
    if (blurredMat && !blurredMat.isDeleted()) blurredMat.delete();
    if (edgesMat && !edgesMat.isDeleted()) edgesMat.delete();
    // outputMat は呼び出し元に渡すため、ここでは解放しない (エラー時を除く)
}

return outputMat; // 最終結果Matを返す

}

// 関数の呼び出し側
let src = cv.imread(‘inputImage’);
let result = null;
try {
result = processAndFilter(src);
if (result && !result.empty()) {
cv.imshow(‘outputCanvas’, result);
} else {
console.warn(“Processing failed or returned empty result.”);
}
} catch (e) {
console.error(“Error calling processAndFilter:”, e);
} finally {
// 呼び出し側で、入力と最終結果のMatを解放
if (src && !src.isDeleted()) src.delete();
if (result && result !== src && !result.isDeleted()) result.delete(); // src と result が同じオブジェクトでないか確認
}
“`

このパターンでは、関数内で作成した中間的な grayMat, blurredMat, edgesMatfinally ブロックで確実に解放しています。関数の戻り値である outputMat は、関数呼び出し側がその利用を終えた後に解放する責任を持ちます。

2. Helper関数やクラスを利用する

複数のOpenCV処理を組み合わせる場合、多くの Mat オブジェクトが生成され、どれをいつ解放すべきか追跡するのが難しくなります。これを管理しやすくするために、Matオブジェクトのリストを保持し、まとめて解放するヘルパー関数やクラスを作成することも有効です。

“`javascript
// 解放が必要なMatを管理するシンプルなクラス例
class MatManager {
constructor() {
this.mats = [];
}

// 新しいMatを作成し、管理対象に追加
newMat(rows, cols, type, scalar) {
    let mat = scalar ? new cv.Mat(rows, cols, type, scalar) : new cv.Mat(rows, cols, type);
    this.mats.push(mat);
    return mat;
}

// 既存のMatを管理対象に追加
add(mat) {
    this.mats.push(mat);
    return mat; // チェーン可能にするため
}

// 管理対象の全てのMatを解放し、リストをクリア
releaseAll() {
    for (let i = 0; i < this.mats.length; ++i) {
        if (this.mats[i] && !this.mats[i].isDeleted()) {
            this.mats[i].delete();
        }
    }
    this.mats = []; // リストを空にする
    console.log("All managed Mats released.");
}

}

// 使用例
function processImageManaged() {
const manager = new MatManager();
let outputMat = null;

try {
    let srcMat = manager.add(cv.imread(document.getElementById('inputImage')));
    let grayMat = manager.newMat();
    let blurredMat = manager.newMat();
    let edgesMat = manager.newMat();
    outputMat = manager.newMat(); // 最終結果も manager に追加

    cv.cvtColor(srcMat, grayMat, cv.COLOR_RGBA2GRAY);
    cv.GaussianBlur(grayMat, blurredMat, new cv.Size(5, 5), 0);
    cv.Canny(blurredMat, edgesMat, 50, 150);
    cv.cvtColor(edgesMat, outputMat, cv.COLOR_GRAY2RGBA);

    // ここで outputMat を使用(例えば Canvas に表示)
    cv.imshow('outputCanvas', outputMat);

} catch (e) {
    console.error("Error in processing:", e);
    // エラー時も manager.releaseAll() で解放される
} finally {
    // 作成・管理した全てのMatをまとめて解放
    manager.releaseAll();
    // outputMat は manager によって解放されるので、ここでは解放しない
    // outputMat の参照は processImageManaged 終了後も保持されないように注意
}

}

// onRuntimeInitialized の中で processImageManaged() を呼び出す
// 注意: この設計では、outputMat を関数の外で利用できません。
// もし outputMat を関数の外で利用したい場合は、manager に追加せず、
// 関数の戻り値として返し、呼び出し側で manager とは別に解放する必要があります。
“`

この MatManager の例では、関数の最後に manager.releaseAll() を呼ぶことで、関数内で作成された全ての Mat をまとめて解放できます。より洗練された実装としては、processImageManagedoutputMat を戻り値として返し、それを manager に追加せず、呼び出し側で manager.releaseAll() を呼んだ後、最後に outputMat を解放するというパターンも考えられます。

どちらのパターンを採用するにしても、new したMatやOpenCV関数が返した新しいMatは、不要になったら必ず .delete() する」という基本原則を常に意識することが最も重要です。コードを書くたびに「このMatはいつ、誰が解放するのか?」と考える習慣をつけましょう。

実践的なサンプルコード:画像のアップロードと基本処理

ここでは、ユーザーが画像をアップロードし、OpenCV.jsを使っていくつかの基本的な画像処理を適用して結果をCanvasに表示する、より実践的なサンプルコードを紹介します。

HTML構造

ユーザーがファイルを選択するための <input type="file">、画像をロード・処理するための非表示の <img>、処理結果を表示するための <canvas>、そして処理の種類を選択するためのボタンなどを配置します。

“`html




OpenCV.js Image Processor


OpenCV.js Image Processor






OpenCV.js loading…


Input Image






“`

JavaScriptコード (script.js)

このJavaScriptファイルでは、opencv.js のロード完了を待ち、ファイル選択イベントを処理し、各ボタンのクリックイベントに対応する画像処理関数を実装します。メモリ管理に注意しながらコードを記述します。

“`javascript
// 全てのMatオブジェクトの参照を保持するリスト (メモリ管理のため)
let mats = [];

// Matを安全に作成し、リストに追加するヘルパー関数
function createMat(…args) {
let mat = new cv.Mat(…args);
mats.push(mat);
return mat;
}

// 管理対象の全てのMatを解放する関数
function releaseAllMats() {
for (let i = 0; i < mats.length; ++i) {
if (mats[i] && !mats[i].isDeleted()) {
mats[i].delete();
}
}
mats = []; // リストをクリア
console.log(“All managed Mats released.”);
}

// OpenCV.jsの初期化完了後に呼び出される関数
function onRuntimeInitialized() {
console.log(“OpenCV.js is ready!”);
document.getElementById(‘status’).innerText = ‘OpenCV.js ready.’;

// ファイル入力とボタンのイベントリスナーを設定
const fileInput = document.getElementById('fileInput');
const inputImage = document.getElementById('inputImage');
const outputCanvas = document.getElementById('outputCanvas');
const grayButton = document.getElementById('grayButton');
const binaryButton = document.getElementById('binaryButton');
const blurButton = document.getElementById('blurButton');
const cannyButton = document.getElementById('cannyButton');

// ファイル選択時のイベント
fileInput.addEventListener('change', (e) => {
    const file = e.target.files[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = function(event) {
        // ファイルの内容を<img>要素に読み込む
        inputImage.onload = function() {
            console.log("Image loaded into img tag.");
            // 画像がロードされたら、初期表示としてそのままCanvasに描画
            displayOriginalImage();
            // 処理ボタンを有効化
            grayButton.disabled = false;
            binaryButton.disabled = false;
            blurButton.disabled = false;
            cannyButton.disabled = false;
            document.getElementById('status').innerText = 'Image loaded. Select a process.';
        };
        inputImage.src = event.target.result; // <img>要素に画像データをセット
    };
    reader.readAsDataURL(file); // ファイルをData URLとして読み込む
}, false);

// 各処理ボタンのイベントリスナー
grayButton.addEventListener('click', () => processImage('gray'));
binaryButton.addEventListener('click', () => processImage('binary'));
blurButton.addEventListener('click', () => processImage('blur'));
cannyButton.addEventListener('click', () => processImage('canny'));

// 初期表示用の関数 (アップロードされた画像をそのまま表示)
function displayOriginalImage() {
     // 以前の処理で作成されたMatを解放
     releaseAllMats();

     try {
         const imgElement = document.getElementById('inputImage');
         const canvas = document.getElementById('outputCanvas');

         // img要素からMatを作成 (OpenCV.jsの内部でRGBAに変換される)
         let srcMat = createMat(); // createMatを使用してMatを作成し、管理リストに追加
         cv.imread(imgElement, srcMat); // imgElementの内容をsrcMatに読み込む

         // Canvasに表示
         cv.imshow(canvas, srcMat);
         console.log("Original image displayed.");

     } catch (e) {
         console.error("Error displaying original image:", e);
         document.getElementById('status').innerText = 'Error displaying image.';
     }
     // Mat解放は releaseAllMats() が行う
}


// 画像処理を実行する汎用関数
function processImage(processType) {
    // 以前の処理で作成されたMatを解放
    releaseAllMats();
    document.getElementById('status').innerText = `Processing (${processType})...`;

    try {
        const imgElement = document.getElementById('inputImage');
        const canvas = document.getElementById('outputCanvas');

        // 常にRGBA形式で入力Matを作成
        let srcMat = createMat();
        cv.imread(imgElement, srcMat); // img要素からRGBA形式で読み込む

        let resultMat = null; // 処理結果を格納するMat

        switch (processType) {
            case 'gray':
                resultMat = createMat(); // 新しいMatを作成
                // RGBA -> Gray
                cv.cvtColor(srcMat, resultMat, cv.COLOR_RGBA2GRAY);
                break;

            case 'binary':
                let grayMat = createMat(); // 中間Matも作成
                cv.cvtColor(srcMat, grayMat, cv.COLOR_RGBA2GRAY);
                resultMat = createMat(); // 新しいMatを作成
                // Gray -> Binary (大津の二値化を使用)
                cv.threshold(grayMat, resultMat, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU);
                break;

            case 'blur':
                resultMat = createMat(); // 新しいMatを作成
                let ksize = new cv.Size(5, 5);
                // RGBA 画像に直接ぼかしを適用
                cv.GaussianBlur(srcMat, resultMat, ksize, 0, 0, cv.BORDER_DEFAULT);
                 break;

            case 'canny':
                let grayMatCanny = createMat(); // 中間Matも作成
                cv.cvtColor(srcMat, grayMatCanny, cv.COLOR_RGBA2GRAY);
                resultMat = createMat(); // 新しいMatを作成
                // Gray -> Edges
                cv.Canny(grayMatCanny, resultMat, 50, 150, 3, false);
                 break;

            default:
                console.error("Unknown process type:", processType);
                document.getElementById('status').innerText = 'Invalid process.';
                return; // 不明な場合は処理を終了
        }

        // 処理結果をCanvasに表示
        // cv.imshow は結果Matのタイプに応じて適切に表示してくれる
        if (resultMat && !resultMat.empty()) {
            cv.imshow(canvas, resultMat);
            document.getElementById('status').innerText = `${processType} process applied.`;
        } else {
             console.error("Processing failed or returned empty result.");
             document.getElementById('status').innerText = `Error in ${processType} process.`;
        }

    } catch (e) {
        console.error(`Error during ${processType} process:`, e);
        document.getElementById('status').innerText = `Error during ${processType} process.`;
    }
     // Mat解放は releaseAllMats() が行う
}

}

// opencv.js が読み込まれる前に onRuntimeInitialized を定義しておく
// opencv.js ファイルの中で onRuntimeInitialized() が呼び出される
“`

このコードでは、ファイルが選択されると FileReader を使って画像を読み込み、非表示の <img> タグにセットします。<img> タグの onload イベントで画像がブラウザにロードされたことを確認し、OpenCV.jsを使ってCanvasに表示します。

各処理ボタンがクリックされると processImage 関数が呼び出されます。processImage 関数は、まず前回までの処理で作成された Mat オブジェクトを全て解放します(releaseAllMats())。次に、<img> タグから現在の画像を読み込み srcMat を作成します。その後、選択された処理に応じて適切なOpenCV関数を呼び出し、結果を resultMat に格納します。この際、処理の中間結果が必要な場合は、それらも createMat() を使って作成し、管理リストに追加します。

処理が完了したら cv.imshow()resultMat をCanvasに表示します。最後に、processImage 関数が終了する際に(そして新しい処理が始まる前に)、releaseAllMats() を再度呼び出して、今回の処理で作成された全ての Mat オブジェクトを解放します。

createMat ヘルパー関数と releaseAllMats 関数を導入することで、複雑な処理の中で発生する一時的な Mat オブジェクトのメモリ管理を単純化しています。新しい Mat を作成する際は必ず createMat を使用し、各処理の開始時や終了時に releaseAllMats を呼び出すことで、メモリリークを防ぐことができます。

このサンプルコードは、OpenCV.jsを使った画像処理の基本的なワークフローを示しています。ユーザーインタラクション(ファイルのアップロード、ボタンクリック)からOpenCV.jsの関数呼び出し、そしてCanvasへの表示、そして重要なメモリ管理までが含まれています。

デバッグとパフォーマンス

デバッグ

OpenCV.jsアプリケーションのデバッグは、通常のJavaScriptデバッグと同様にブラウザの開発者ツール(Console、Sources、Performanceタブなど)を使用します。

  • Console: エラーメッセージ、警告、console.log による出力などを確認します。OpenCV.jsに関連するエラー(例えば .delete() されていないオブジェクトがある、無効なMatを関数に渡したなど)もここに表示されます。エラーメッセージには、OpenCV側の情報(関数名、エラーコードなど)が含まれていることがあります。
  • Sources: JavaScriptコードにブレークポイントを設定し、実行を一時停止して変数の値などを確認できます。
  • Memory: パフォーマンスモニタリングの一部として、メモリの使用状況を確認できます。時間の経過と共にWebAssemblyヒープの使用量が増え続ける場合は、メモリリークが発生している可能性が高いです。
  • Performance: 関数呼び出しにかかる時間や、処理のスレッド状況などをプロファイルできます。特定のOpenCV関数がボトルネックになっているかなどを特定するのに役立ちます。

よくあるデバッグのヒント:
* cv オブジェクトの確認: OpenCV.jsの初期化が完了しているか確認します。typeof cv !== 'undefined' でチェックできます。
* Matの有効性チェック: OpenCV関数に渡す前に mat && !mat.empty() && !mat.isDeleted() のような条件でMatが有効であることを確認すると、クラッシュを防ぐのに役立ちます。
* Matのタイプとサイズ: 関数の入力として要求されるMatのタイプとサイズが出力と一致しているか、また期待通りのタイプ・サイズになっているか mat.type(), mat.size() で確認します。
* メモリリークの監視: ブラウザの開発者ツールのメモリタブで、定期的にヒープスナップショットを取得したり、パフォーマンスモニタリングでメモリ使用量の推移を観察したりします。解放忘れが最も一般的な問題です。

パフォーマンス

WebAssemblyを使用しているため、OpenCV.jsはJavaScriptのみで画像処理を行うよりも高速ですが、ネイティブのC++版OpenCVと比較すると、ブラウザ環境の制約やJavaScript/WebAssembly間のデータ転送オーバーヘッドなどにより遅くなる場合があります。

  • WebAssemblyのメリット: 計算集約的な画像処理アルゴリズム(フィルタリング、変換、特徴点検出など)は、WebAssembly上で高速に実行されます。
  • JavaScript/WebAssembly間のデータ転送: JavaScriptのメモリとWebAssemblyのメモリ間でデータをやり取り(例: CanvasのImageDataとMatの間)する際にはオーバーヘッドが発生します。この転送を最小限に抑えることがパフォーマンス向上の鍵です。cv.imread, cv.imshow はこの転送を効率的に行いますが、手動で行う場合は注意が必要です。
  • 大規模画像処理の課題: 高解像度画像や多数の画像を処理する場合、メモリ使用量と処理時間が問題になりやすくなります。
  • 最適化のヒント:
    • 必要な処理のみ行う: 不要な色空間変換やフィルタリングを避けます。
    • ROI (Region of Interest) 処理: 画像全体ではなく、必要な領域 (Mat.roi()) だけを処理することで計算量を削減できます。
    • データ型の選択: 可能な限り CV_8U を使用します。浮動小数点型 (CV_32F, CV_64F) はより多くのメモリを消費し、演算も遅くなる傾向があります。
    • リサイズ: 高解像度画像を処理する前に、必要に応じて適切なサイズにリサイズすることで、以降の処理コストを大幅に削減できます。
    • 中間Matの再利用: 頻繁に呼び出される関数(例: 動画処理)では、毎回新しい中間Matを作成・解放するのではなく、一度作成したMatを使い回すことで、メモリ割り当て・解放のオーバーヘッドを減らせる場合があります(ただし、サイズの変更が必要ない場合に限る)。

リアルタイム処理(例えばウェブカメラ映像の処理)を行う場合は、処理速度がフレームレートに直結するため、特にパフォーマンスのチューニングが重要になります。開発者ツールのPerformanceプロファイラを活用し、処理に時間がかかっている箇所を特定して最適化を行います。

注意点と制限

OpenCV.jsを利用する上で、いくつかの注意点と制限事項があります。

  • 非同期ロード: opencv.js ファイルのロードとWebAssemblyの初期化は非同期で行われます。onRuntimeInitialized コールバックを使用するなどして、準備が完了するまでOpenCVの機能にアクセスしないようにする必要があります。
  • メモリ管理: 前述の通り、Mat などのOpenCVオブジェクトは明示的な解放(.delete()) が必要です。これは最もよく遭遇する問題の一つです。
  • ブラウザ互換性: OpenCV.jsはWebAssemblyを使用するため、WebAssemblyをサポートしている比較的モダンなブラウザが必要です。古いブラウザ(Internet Explorerなど)では動作しません。
  • JavaScript APIの差異: C++版OpenCVの全ての関数や機能がOpenCV.jsで利用できるわけではありません。また、APIのシグネチャ(引数の順序や型)がC++版と微妙に異なる場合があります。公式ドキュメント(特にOpenCV.jsのドキュメント)を参照して確認する必要があります。
  • ファイルサイズ: opencv.js ファイル(および関連するWebAssemblyファイル)はサイズが大きくなる傾向があります。ユーザーのダウンロード時間や帯域幅に影響を与える可能性があります。必要に応じてカスタムビルドで不要なモジュールを削るなどの対応も考えられますが、ビルド環境の構築が必要です。
  • 同期的なAPI: OpenCV.jsのAPIは基本的に同期的に動作します(非同期なのは初期化部分)。計算に時間のかかる処理を実行すると、ブラウザのメインスレッドをブロックし、UIがフリーズすることがあります。長い処理を行う場合は、Web Workerを利用してバックグラウンドで実行することを検討する必要があります。
  • Web Workerでの利用: OpenCV.jsはWeb Worker内でも利用可能です。これにより、メインスレッドをブロックせずにバックグラウンドで画像処理を実行できます。ただし、Workerとメインスレッド間のデータ転送(特に大きな画像データ)にはコストがかかるため、SharedArrayBufferなどを使った効率的なデータ共有や、Worker内での処理完結などの工夫が必要になります。

これらの注意点を理解し、適切な設計やコーディングパターンを用いることで、OpenCV.jsを使った安定した高性能なウェブアプリケーションを開発することが可能です。

まとめ:ブラウザ画像処理の可能性を切り拓く

この記事では、JavaScriptからOpenCVの強力な画像処理機能を活用できるOpenCV.jsについて、その基本的な概念から具体的な使い方までを詳しく解説しました。

OpenCV.jsを利用することで、ウェブブラウザ上で画像の読み込み、表示、そしてグレースケール変換、二値化、平滑化、エッジ検出といった様々な基本処理を高速に実行できるようになります。HTMLの<canvas>要素との連携はOpenCV.jsの中核をなす機能であり、ブラウザ上の画像データとOpenCV.jsのMatオブジェクトの間で効率的なデータのやり取りが可能です。

特に重要な点として、WebAssemblyメモリ上で管理されるMatオブジェクトなどのOpenCVオブジェクトは、JavaScriptのガベージコレクションの対象とならないため、開発者自身が不要になった時点で明示的に.delete()メソッドを呼び出してメモリを解放する必要があることを強調しました。適切なメモリ管理は、OpenCV.jsアプリケーションの安定性とパフォーマンスを確保するために不可欠です。

この記事で紹介した内容はOpenCV.jsの機能のほんの一部に過ぎません。OpenCVは顔検出、物体認識、特徴点検出、画像スティッチング、ビデオ解析など、さらに高度な機能を提供しており、OpenCV.jsを通じてこれらの多くがJavaScriptからも利用可能です。

ブラウザ上で動く画像処理アプリケーションは、ユーザーにとってよりインタラクティブで、サーバーリソースに依存しない、そしてプライバシーに配慮した体験を提供できます。OpenCV.jsは、ウェブ開発者がこのような先進的なアプリケーションを構築するための強力なツールとなります。

もちろん、大規模で計算負荷の高い処理や、リアルタイム性の厳しい要件を持つアプリケーションでは、パフォーマンスの最適化やWeb Workerの活用といったさらに進んだ知識や技術が必要になります。しかし、この記事で学んだOpenCV.jsの基本とメモリ管理の重要性を理解していれば、それらの課題に取り組むための強固な基盤となります。

ぜひ、OpenCV.jsを使って、あなたのアイデアをブラウザ上で動く画像処理アプリケーションとして実現してみてください。公式ドキュメントやOpenCVの豊富なリソースも参考にしながら、コンピュータビジョンの世界をウェブで探索する旅を始めましょう。ブラウザ画像処理の未来は、今まさにあなたの手の中にあります。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール