【Java】OpenCVの導入と使い方を解説

はい、承知いたしました。JavaでOpenCVを導入し、基本的な使い方から応用的な部分までを網羅した、約5000語の詳細な解説記事を作成します。


Javaで始めるOpenCV:導入から高度な画像処理まで徹底解説

はじめに

コンピュータビジョンは、画像や動画をコンピュータで解析し、人間のように内容を理解させようとする技術分野です。顔認識、物体検出、自動運転、医療画像解析など、その応用範囲は多岐にわたります。このコンピュータビジョンを実現するための強力なライブラリとして、OpenCV(Open Source Computer Vision Library)があります。

OpenCVはC++で書かれていますが、Javaを含む多くのプログラミング言語向けのバインディング(ラッパー)が提供されており、それぞれの言語からOpenCVの機能を利用できます。この記事では、特にJava開発者向けに、OpenCVをJavaプロジェクトに導入する方法から、画像処理、動画処理の基本的な使い方、さらにはGUIアプリケーションでの画像表示方法まで、詳細かつ実践的に解説します。

約5000語にわたるこの解説を通じて、あなたはJavaでコンピュータビジョンアプリケーションを開発するための強固な基盤を築くことができるでしょう。さあ、JavaとOpenCVの世界に飛び込みましょう!

OpenCVとは何か?

OpenCVは、インテルによって開発が開始され、現在はオープンソースとしてBSDライセンスのもとで提供されているコンピュータビジョンライブラリです。高性能なアルゴリズムが多数実装されており、画像処理、画像解析、機械学習、物体検出、立体視など、コンピュータビジョンに関する幅広い機能を提供しています。

OpenCVの最大の魅力は、そのパフォーマンスと豊富な機能、そして活発なコミュニティによるサポートです。C++で書かれているため高速な処理が可能であり、Javaなどの他の言語から利用する際も、そのネイティブなパフォーマンスの恩恵を受けることができます。

なぜJavaでOpenCVを使うのか?

Javaは広く普及しているプログラミング言語であり、その強力なオブジェクト指向性、豊富な標準ライブラリ、クロスプラットフォーム性、安定した実行環境(JVM)といった特徴を持っています。OpenCVのJavaバインディングを利用することで、これらのJavaの利点を活かしつつ、OpenCVの高速なコンピュータビジョン機能を利用できるようになります。

JavaでOpenCVを使う主なメリットは以下の通りです。

  • クロスプラットフォーム開発: Javaの「Write Once, Run Anywhere」の原則により、OpenCVを使ったアプリケーションも様々なOS上で動作させやすくなります。
  • 豊富なJavaエコシステム: Javaの既存ライブラリ(GUIライブラリ、ネットワークライブラリなど)とOpenCVの機能を組み合わせて、複雑なアプリケーションを構築できます。
  • Java開発者の馴染みやすさ: 既にJavaでの開発経験がある人にとって、新しい言語を学ぶことなくコンピュータビジョン開発に参入できます。
  • ガベージコレクション: メモリ管理についてC++ほど厳密に気にすることなく開発を進められます(ただし、OpenCVのMatオブジェクトの扱いは注意が必要です)。

JavaバインディングはC++のAPIをJavaのクラスやメソッドとしてラップしたものです。したがって、OpenCVの基本的な概念やアルゴリズムは、C++版OpenCVと共通しています。

この記事で学べること

この記事では、以下の内容を習得することを目指します。

  1. JavaプロジェクトへのOpenCVライブラリの正しい導入方法(Windows, macOS, Linuxを想定)
  2. OpenCVの基本的なデータ構造であるMatクラスの理解と操作方法
  3. 画像の読み込み、表示、保存といった基本的な画像ファイル操作
  4. グレースケール変換、サイズ変更、フィルタ処理などの基本的な画像処理アルゴリズムの適用方法
  5. 二値化、エッジ検出、輪郭検出、図形描画などの応用的な画像処理手法
  6. カメラ入力や動画ファイルからのフレーム取得、処理方法
  7. JavaFXやSwingといったGUIライブラリとOpenCVを連携させて画像を表示する方法
  8. OpenCV使用時によく発生するエラーとその対処法

それでは、早速OpenCVの導入から始めていきましょう。

OpenCVの導入

JavaでOpenCVを使うためには、以下の準備が必要です。

  1. Java Development Kit (JDK): Javaのコンパイルと実行環境が必要です。最新のLTSバージョン(例: JDK 11, 17, 21)を推奨します。
  2. 開発環境 (IDE): Eclipse、IntelliJ IDEA、VS CodeなどのJava開発が可能なIDEを使用すると、ライブラリの設定やコード記述が容易になります。この記事ではIDE(特にEclipseやIntelliJ IDEA)を使用することを前提とします。
  3. OpenCVライブラリ: Javaバインディングとネイティブライブラリ(DLL, .so, .dylib ファイル)が必要です。

OpenCVライブラリのダウンロード

OpenCVの公式サイトから、使用するOSに合ったリリース版パッケージをダウンロードします。

  1. OpenCV公式サイト (https://opencv.org/) にアクセスします。
  2. 「Releases」またはダウンロードページに移動します。
  3. 最新の安定版リリースを選択し、お使いのOSに対応するパッケージ(例: “Windows”, “macOS”, “Linux”)をダウンロードします。多くの場合、プラットフォームごとのインストーラーまたはアーカイブファイル(.zip, .tar.gz)として提供されます。

ダウンロードしたファイルを展開(インストール)します。インストール先は任意ですが、パスに日本語や特殊文字を含まない、分かりやすい場所に展開することをお勧めします(例: C:\opencv, /Users/username/opencv, /opt/opencv)。

展開されたフォルダの中に、以下のような構造があることを確認してください。

  • build/
    • java/
      • opencv-x.y.z.jar (JavaバインディングのJARファイル)
    • x64/ (またはx86などアーキテクチャ別のフォルダ)
      • vcXX/ (WindowsのVisual Studioバージョン) または bin/ (Linux/macOS)
        • ネイティブライブラリファイル (例: opencv_javaX.y.z.dll (Windows), libopencv_javaX.y.z.so (Linux), libopencv_javaX.y.z.dylib (macOS))

ここで重要なのは、opencv-x.y.z.jar ファイルと、お使いのOS・アーキテクチャに対応したネイティブライブラリファイルです。x.y.z はOpenCVのバージョン番号です。

JavaプロジェクトへのOpenCVの設定 (IDEを使用する場合)

JavaプロジェクトでOpenCVを使用するには、ダウンロードしたOpenCVライブラリをプロジェクトのビルドパスに追加し、さらに実行時にネイティブライブラリの場所をJVMに知らせる必要があります。

ここでは、主要なIDEであるEclipseとIntelliJ IDEAでの設定方法を説明します。

Eclipseでの設定:

  1. 新しいJavaプロジェクトを作成するか、既存のプロジェクトを開きます。
  2. プロジェクトを右クリックし、「Properties」を選択します。
  3. 左側のメニューから「Java Build Path」を選択します。
  4. 「Libraries」タブを選択します。
  5. 「Add External JARs…」ボタンをクリックします。
  6. ダウンロードしたOpenCVフォルダ内の build/java/opencv-x.y.z.jar ファイルを選択し、「開く」をクリックします。JARファイルがライブラリリストに追加されます。
  7. 追加した opencv-x.y.z.jar を展開し、「Native library location」を選択します。
  8. 「Edit…」ボタンをクリックします。
  9. 「External folder…」を選択し、ダウンロードしたOpenCVフォルダ内のネイティブライブラリが格納されているフォルダ(例: build/x64/vc16/binbuild/lib など、OSによって異なります)を指定します。
  10. 「OK」を何度かクリックしてプロパティウィンドウを閉じます。

これでビルドパスの設定は完了ですが、実行時にネイティブライブラリをロードする必要があります。これはJavaコード内で行うのが一般的です。

OpenCVを使用するクラスの先頭、またはアプリケーションのエントリポイントで、以下のコードを記述します。

“`java
import org.opencv.core.Core;

public class YourApplication {

static {
    // OpenCVのネイティブライブラリをロードする
    // ライブラリ名はOSによって異なります
    // 例: Windows: opencv_javaX.y.z.dll -> "opencv_javaX.y.z"
    // 例: Linux: libopencv_javaX.y.z.so -> "opencv_javaX.y.z"
    // 例: macOS: libopencv_javaX.y.z.dylib -> "opencv_javaX.y.z"

    // System.loadLibrary("opencv_java455"); // 例:バージョンに合わせて変更
    // もしくは System.load("C:\\opencv\\build\\java\\x64\\vc16\\bin\\opencv_java455.dll"); // 絶対パスを指定

    // 推奨されるロード方法 (JAR内のパスに依存しない)
    // ただし、ネイティブライブラリの場所をJVMに認識させる必要がある
    // 例えば、VM引数に -Djava.library.path="ネイティブライブラリのパス" を設定する
    try {
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        System.out.println("OpenCV library loaded successfully.");
    } catch (UnsatisfiedLinkError e) {
        System.err.println("Failed to load OpenCV library. Check your system path and library name.");
        e.printStackTrace();
    }
}

public static void main(String[] args) {
    // ここからOpenCVを使ったコードを記述
    System.out.println("OpenCV version: " + Core.VERSION);

    // その他の処理...
}

}
“`

System.loadLibrary(Core.NATIVE_LIBRARY_NAME); を使用する場合、JVMがネイティブライブラリを見つけられるように、ネイティブライブラリが含まれるフォルダのパスをJavaのシステムプロパティ java.library.path に追加する必要があります。Eclipseの場合、実行構成(Run Configurations)の「Arguments」タブにある「VM arguments」に -Djava.library.path="C:\opencv\build\java\x64\vc16\bin" のように記述します(パスはご自身の環境に合わせて変更してください)。

IntelliJ IDEAでの設定:

  1. 新しいJavaプロジェクトを作成するか、既存のプロジェクトを開きます。
  2. 「File」->「Project Structure…」を選択します。
  3. 左側のメニューから「Modules」を選択します。
  4. お使いのモジュールを選択し、右側の「Dependencies」タブを選択します。
  5. 右側の「+」ボタンをクリックし、「JARs or Directories…」を選択します。
  6. ダウンロードしたOpenCVフォルダ内の build/java/opencv-x.y.z.jar ファイルを選択し、「OK」をクリックします。JARファイルが依存関係に追加されます。
  7. 「OK」をクリックしてProject Structureウィンドウを閉じます。

Eclipseと同様に、実行時にネイティブライブラリをロードする必要があります。これはJavaコード内で行うか、実行構成で設定します。

コード内でロードする場合: Eclipseと同様に System.loadLibrary() または System.load() を使用します。

実行構成で設定する場合(推奨):
1. 「Run」->「Edit Configurations…」を選択します。
2. お使いのアプリケーションの実行構成を選択します。
3. 「VM options」フィールドに -Djava.library.path="C:\opencv\build\java\x64\vc16\bin" のように記述します(パスはご自身の環境に合わせて変更してください)。
4. 「Apply」または「OK」をクリックします。

設定確認:

設定が正しく行われたかを確認するために、以下の簡単なコードを実行してみましょう。

“`java
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;

public class OpenCVTest {

static {
    // ネイティブライブラリのロード
    try {
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        System.out.println("OpenCV library loaded successfully.");
    } catch (UnsatisfiedLinkError e) {
        System.err.println("Failed to load OpenCV library. Ensure java.library.path is set correctly.");
        e.printStackTrace();
        System.exit(1); // ロード失敗したら終了
    }
}

public static void main(String[] args) {
    System.out.println("Welcome to OpenCV " + Core.VERSION);

    // 簡単なMatオブジェクトの作成テスト
    Mat mat = new Mat(5, 10, CvType.CV_8UC1, new org.opencv.core.Scalar(0));
    System.out.println("mat = " + mat.dump()); // Matの内容を表示

    // リソース解放(OpenCVのMatは手動またはGCで解放されるが、明示的な解放メソッドはない)
    // Matはネイティブメモリを参照しているため、大きな画像を扱う場合は注意が必要
    // 通常はGCに任せるが、finalize()での解放は保証されないため、不安ならclose()メソッドがあるか確認
    // OpenCV 4.x以降のJava Matにはclose()メソッドがないため、基本的にはGCに任せる

    System.out.println("Test finished.");
}

}
“`

このコードを実行して、「OpenCV library loaded successfully.」と「Welcome to OpenCV x.y.z」というメッセージが表示され、mat = ... の出力があれば、OpenCVの導入と基本設定は成功です。

OpenCVの基本概念:Matクラス

OpenCVにおける最も基本的なクラスは Mat (Matrix) クラスです。これは画像や行列データを表現するために使用されます。画像はピクセル値の行列として扱われるため、画像処理はこの Mat オブジェクトに対する操作として行われます。

Mat オブジェクトは、以下の情報を持ちます。

  • Dimension (次元数): 画像の場合は通常2次元(高さと幅)。
  • Size (サイズ): 行数(高さ)と列数(幅)。Mat.rows()Mat.cols()Mat.size() メソッドで取得できます。
  • Data Type (データ型): 行列の要素(ピクセル)の型。OpenCVでは様々なデータ型が定義されています(例: CV_8UC1, CV_16SC3, CV_32FC1)。
    • CV_ + ビット数 + 型文字 + チャンネル数
    • 型文字: U (Unsigned Integer), S (Signed Integer), F (Float)
    • チャンネル数: C1 (1チャンネル – グレースケール), C3 (3チャンネル – カラー画像 BGR), C4 (4チャンネル – RGBAなど)
  • Number of Channels (チャンネル数): 各ピクセルが持つ値の数。グレースケール画像は1チャンネル、カラー画像(RGBやBGR)は3チャンネルです。Mat.channels() メソッドで取得できます。

Matオブジェクトの生成:

Mat オブジェクトは様々な方法で生成できます。

“`java
// 1. サイズと型を指定して空のMatを生成(すべての要素は0で初期化されない)
Mat img = new Mat(480, 640, CvType.CV_8UC3); // 高さ480, 幅640, 8ビット符号なし整数, 3チャンネルのカラー画像

// 2. サイズ、型、初期値を指定して生成
Mat blackImg = new Mat(200, 300, CvType.CV_8UC3, new org.opencv.core.Scalar(0, 0, 0)); // 真っ黒な画像
Mat whiteImg = new Mat(100, 100, CvType.CV_8UC1, new org.opencv.core.Scalar(255)); // 真っ白なグレースケール画像

// 3. 既存のMatからクローンを作成
Mat imgClone = img.clone();

// 4. 既存のMatの特定領域(ROI)を抽出して新しいMatとして参照(データは共有される)
org.opencv.core.Rect roi = new org.opencv.core.Rect(10, 20, 50, 60); // x=10, y=20から幅50, 高さ60の領域
Mat roiMat = img.submat(roi);

// 5. ゼロ行列や1行列を作成するヘルパーメソッド
Mat zeros = Mat.zeros(100, 100, CvType.CV_8UC3);
Mat ones = Mat.ones(50, 50, CvType.CV_32FC1);
“`

ピクセル値へのアクセス:

Mat オブジェクトのピクセル値には、get() および put() メソッドを使用してアクセスできます。ただし、これらのメソッドはパフォーマンスが低いため、可能な限りOpenCVの提供する高速な行列操作関数(例: Core.add(), Core.subtract(), フィルタ関数など)を使用することが推奨されます。

“`java
// 8ビット1チャンネル(グレースケール)の場合
Mat grayImg = new Mat(100, 100, CvType.CV_8UC1, new org.opencv.core.Scalar(128));
double[] pixelValue = grayImg.get(10, 20); // (行, 列) -> (y, x) の順序に注意
System.out.println(“Pixel value at (20, 10): ” + pixelValue[0]); // 1チャンネルなので要素は1つ

// 8ビット3チャンネル(カラー)の場合
Mat colorImg = new Mat(100, 100, CvType.CV_8UC3, new org.opencv.core.Scalar(255, 0, 0)); // 青色の画像 (BGR順)
double[] colorPixel = colorImg.get(30, 40); // (行, 列) -> (y, x)
System.out.println(“Pixel value at (40, 30): B=” + colorPixel[0] + “, G=” + colorPixel[1] + “, R=” + colorPixel[2]);

// ピクセル値の設定
double[] newColor = {0, 255, 0}; // 緑
colorImg.put(50, 50, newColor); // (行, 列) -> (y, x)
“`

get() メソッドは double[] を返します。これは、様々な型のピクセル値を統一的に扱うためです。取得した値を本来の型にキャストして利用します。put() メソッドも double[] または可変長引数で値を設定します。

注意点として、Mat はC++側でネイティブメモリを確保しており、Java側のオブジェクトはネイティブメモリへのポインタを保持しています。JavaのガベージコレクタはJavaオブジェクトのメモリのみを解放するため、ネイティブメモリは明示的に解放する必要があります。しかし、OpenCV 4.x以降のJavaバインディングでは、Mat オブジェクトに release()close() といった明示的な解放メソッドはありません。これは、ガベージコレクタがJavaオブジェクトを収集する際に、関連付けられたネイティブメモリも解放するように設計されているためです。ただし、巨大なMatオブジェクトを大量に生成するようなケースでは、GCのタイミングによってはメモリ不足を引き起こす可能性もゼロではありません。通常の使い方であればGCに任せて問題ありません。

基本的な画像処理

OpenCVを使った基本的な画像処理のステップは、大まかに以下のようになります。

  1. 画像をファイルから読み込むか、新しい画像を生成して Mat オブジェクトとして取得する。
  2. 取得した Mat オブジェクトに対して必要な画像処理関数を適用する。これらの関数は通常、入力 Mat と出力 Mat を引数にとります。
  3. 処理結果の Mat オブジェクトをファイルに保存するか、画面に表示する。

画像の読み込み (Imgcodecs.imread)

ファイルから画像を読み込むには、org.opencv.imgcodecs.Imgcodecs クラスの静的メソッド imread() を使用します。

“`java
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.imgcodecs.Imgcodecs;

public class LoadImageExample {

static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
    String imagePath = "path/to/your/image.jpg"; // 読み込む画像のパスを指定

    // 画像を読み込む
    // Imgcodecs.IMREAD_COLOR: カラー画像として読み込む(デフォルト)
    // Imgcodecs.IMREAD_GRAYSCALE: グレースケール画像として読み込む
    // Imgcodecs.IMREAD_UNCHANGED: アルファチャンネルを含め、そのまま読み込む
    Mat image = Imgcodecs.imread(imagePath, Imgcodecs.IMREAD_COLOR);

    // 画像が正しく読み込まれたか確認
    if (image.empty()) {
        System.err.println("Error: Could not open or find the image: " + imagePath);
        return;
    }

    System.out.println("Image loaded successfully: " + imagePath);
    System.out.println("Image dimensions: " + image.cols() + "x" + image.rows());
    System.out.println("Number of channels: " + image.channels());
    System.out.println("Data type: " + image.type()); // 例: 16 (CV_8UC3)

    // 画像を表示または処理する...

    // MatオブジェクトはGCによって解放される
}

}
“`

imread() は指定されたパスの画像を Mat オブジェクトとして返します。読み込みに失敗した場合(ファイルが存在しない、パスが間違っているなど)、空の Mat オブジェクトを返します。image.empty() メソッドで確認できます。

第2引数で読み込み時のフラグを指定できます。カラー画像が必要な場合は Imgcodecs.IMREAD_COLOR(または何も指定しない)、グレースケールが必要な場合は Imgcodecs.IMREAD_GRAYSCALE を指定します。

画像の表示 (HighGui.imshow)

OpenCVには、デバッグや簡単な確認のために画像を表示するための HighGui モジュールがあります。これは基本的なウィンドウ機能を提供します。

“`java
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.highgui.HighGui;

public class ShowImageExample {

static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
    String imagePath = "path/to/your/image.jpg"; // 読み込む画像のパスを指定
    Mat image = Imgcodecs.imread(imagePath, Imgcodecs.IMREAD_COLOR);

    if (image.empty()) {
        System.err.println("Error: Could not open or find the image.");
        return;
    }

    // 画像をウィンドウに表示
    String windowName = "My Image";
    HighGui.imshow(windowName, image);

    // ウィンドウを閉じずに待機
    System.out.println("Press any key to close the window...");
    HighGui.waitKey(); // キー入力を待つ(ミリ秒単位、0を指定すると無限に待つ)

    // ウィンドウを閉じる
    HighGui.destroyAllWindows();
}

}
“`

HighGui.imshow(windowName, image) は、指定された名前のウィンドウに Mat オブジェクトを表示します。ウィンドウが存在しない場合は新しく作成されます。

HighGui.waitKey() は、指定されたミリ秒数の間、キー入力を待ちます。引数を省略するか 0 を指定すると、任意のキーが押されるまで無限に待機します。これは、画像を表示したウィンドウがすぐに消えてしまわないようにするために重要です。

HighGui.destroyAllWindows() は、OpenCVによって作成されたすべてのウィンドウを閉じます。

ただし、HighGui は非常に基本的な機能しか提供しません。JavaFXやSwingといったより高機能なGUIフレームワークと連携して画像を表示したい場合は、後述の「JavaFX/Swingとの連携」セクションを参照してください。

画像の保存 (Imgcodecs.imwrite)

処理した画像をファイルに保存するには、Imgcodecs.imwrite() を使用します。

“`java
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.imgcodecs.Imgcodecs;

public class SaveImageExample {

static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
    String inputImagePath = "path/to/your/input_image.jpg";
    String outputImagePath = "path/to/your/output_image.png"; // 保存するパスとファイル形式を指定

    Mat image = Imgcodecs.imread(inputImagePath, Imgcodecs.IMREAD_COLOR);

    if (image.empty()) {
        System.err.println("Error: Could not open or find the image.");
        return;
    }

    // ここで画像処理を行う... (例: グレースケール変換)
    Mat grayImage = new Mat();
    org.opencv.imgproc.Imgproc.cvtColor(image, grayImage, org.opencv.imgproc.Imgproc.COLOR_BGR2GRAY);

    // 処理結果をファイルに保存
    boolean success = Imgcodecs.imwrite(outputImagePath, grayImage);

    if (success) {
        System.out.println("Image saved successfully to: " + outputImagePath);
    } else {
        System.err.println("Error: Failed to save the image.");
    }

    // HighGui.imshow("Original", image);
    // HighGui.imshow("Grayscale", grayImage);
    // HighGui.waitKey();
    // HighGui.destroyAllWindows();
}

}
“`

Imgcodecs.imwrite(outputPath, image) は、指定されたパスに Mat オブジェクトを画像ファイルとして保存します。保存されるファイル形式は、ファイル名の拡張子によって自動的に判断されます(例: .jpg, .png, .bmp)。成功した場合は true、失敗した場合は false を返します。

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

カラー画像をグレースケール画像に変換するのは、画像処理で非常によく行われる操作です。org.opencv.imgproc.Imgproc クラスの cvtColor() メソッドを使用します。

“`java
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.highgui.HighGui;

public class ConvertToGrayscale {

static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
    String imagePath = "path/to/your/color_image.jpg";
    Mat colorImage = Imgcodecs.imread(imagePath, Imgcodecs.IMREAD_COLOR);

    if (colorImage.empty()) {
        System.err.println("Error: Could not open or find the image.");
        return;
    }

    Mat grayImage = new Mat(); // 変換後の画像を受け取るMatオブジェクト

    // カラー (BGR) からグレースケールへ変換
    Imgproc.cvtColor(colorImage, grayImage, Imgproc.COLOR_BGR2GRAY);

    System.out.println("Image converted to grayscale.");

    // 結果を表示
    HighGui.imshow("Original Color Image", colorImage);
    HighGui.imshow("Grayscale Image", grayImage);
    HighGui.waitKey();
    HighGui.destroyAllWindows();
}

}
“`

Imgproc.cvtColor(src, dst, code) は、入力画像 src を、指定された変換コード code に基づいて dst に変換します。

  • src: 入力画像 (Mat オブジェクト)。
  • dst: 変換結果を格納する出力画像 (Mat オブジェクト)。型やサイズはOpenCVが自動的に調整してくれますが、あらかじめ new Mat() でオブジェクトを作成しておく必要があります。
  • code: 変換の種類を指定するフラグ。Imgproc.COLOR_BGR2GRAY はカラー(BGR順)画像をグレースケールに変換します。他にも COLOR_BGR2HSV, COLOR_GRAY2BGR など、様々な変換コードがあります。

サイズ変更 (Imgproc.resize)

画像のサイズを変更(リサイズ)するには、Imgproc.resize() メソッドを使用します。

“`java
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.Size; // OpenCVのSizeクラス
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.highgui.HighGui;

public class ResizeImageExample {

static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
    String imagePath = "path/to/your/image.jpg";
    Mat originalImage = Imgcodecs.imread(imagePath);

    if (originalImage.empty()) {
        System.err.println("Error: Could not open or find the image.");
        return;
    }

    Mat resizedImage = new Mat();
    Size newSize = new Size(320, 240); // 新しい幅と高さを指定 (幅, 高さ)

    // 画像のサイズを変更
    // Imgproc.INTER_LINEAR: 双一次補間 (デフォルト)
    // Imgproc.INTER_CUBIC: 双三次補間 (より高品質だが遅い)
    // Imgproc.INTER_NEAREST: 最近傍補間 (最も高速だが品質が低い)
    Imgproc.resize(originalImage, resizedImage, newSize, 0, 0, Imgproc.INTER_LINEAR);

    System.out.println("Image resized to: " + resizedImage.cols() + "x" + resizedImage.rows());

    HighGui.imshow("Original Image", originalImage);
    HighGui.imshow("Resized Image", resizedImage);
    HighGui.waitKey();
    HighGui.destroyAllWindows();
}

}
“`

Imgproc.resize(src, dst, dsize, fx, fy, interpolation) は、入力画像 src を、指定されたサイズまたはスケールファクタに基づいて dst にリサイズします。

  • src: 入力画像 (Mat オブジェクト)。
  • dst: リサイズ結果を格納する出力画像 (Mat オブジェクト)。
  • dsize: 出力画像のサイズ (Size オブジェクト)。new Size(width, height) で指定します。dsize がゼロの場合、fxfy を使ってサイズが計算されます。
  • fx, fy: x軸およびy軸方向のスケールファクタ。dsize がゼロでない場合は無視されます。
  • interpolation: 補間方法。画像の拡大・縮小時にピクセル値を計算する方法を指定します。代表的なものに INTER_LINEAR(双一次補間)があります。

通常は dsize に希望のサイズを指定し、fx, fy0 とします。

切り抜き (ROI – Region of Interest)

画像の特定領域(Region of Interest)を切り抜くには、Mat オブジェクトの submat() メソッドを使用します。submat() は新しい Mat オブジェクトを返しますが、これは元の画像の指定された領域を参照しているだけです。つまり、submat で取得したオブジェクトを変更すると、元の画像も変更されます。

“`java
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.Rect; // OpenCVのRectクラス
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.highgui.HighGui;

public class CropImageExample {

static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
    String imagePath = "path/to/your/image.jpg";
    Mat originalImage = Imgcodecs.imread(imagePath);

    if (originalImage.empty()) {
        System.err.println("Error: Could not open or find the image.");
        return;
    }

    // 切り抜きたい領域を定義 (x座標, y座標, 幅, 高さ)
    // 座標系の原点は画像の左上
    int x = 50;
    int y = 60;
    int width = 200;
    int height = 150;

    // 画像のサイズを超えないようにチェック
    if (x + width > originalImage.cols() || y + height > originalImage.rows()) {
        System.err.println("Error: ROI exceeds image bounds.");
        return;
    }

    Rect roi = new Rect(x, y, width, height);

    // ROIを新しいMatオブジェクトとして取得
    Mat croppedImage = originalImage.submat(roi);

    System.out.println("Image cropped successfully.");

    HighGui.imshow("Original Image", originalImage);
    HighGui.imshow("Cropped Image (Reference)", croppedImage); // croppedImageを変更するとoriginalImageも変わる

    // croppedImage のコピーを作成したい場合 (元の画像を保持しつつ切り抜き部分を変更したい)
    Mat croppedCopy = new Mat();
    croppedImage.copyTo(croppedCopy); // または croppedImage.clone();

    // croppedCopy を表示または保存する...
    // HighGui.imshow("Cropped Image (Copy)", croppedCopy);
    // HighGui.waitKey();
    // HighGui.destroyAllWindows();

    // submat()で取得したMatは元のMatと同じネイティブメモリを参照しているため、
    // 明示的な解放は不要。元のMatが解放されれば参照しているメモリも解放される。
    // ただし、croppedImage を使い続ける間は originalImage を解放しないこと。
    // copyTo()やclone()で作成したMatは独立したメモリを持つため、それぞれ独立して扱える。
}

}
“`

submat(roi) メソッドは、指定された Rect オブジェクトによって定義される領域の Mat オブジェクトを返します。Rect(x, y, width, height) で定義され、x, y は領域の左上隅の座標です。

submat は元の画像のデータへの参照を返すため、croppedImage を変更すると originalImage の対応するピクセルも変更されます。もし切り抜き領域の独立したコピーが必要な場合は、submat で得られた Mat に対して clone() または copyTo() メソッドを使用します。

フィルタ処理 (例: ガウシアンブラー)

画像をぼかしたり、ノイズを除去したりするフィルタ処理も Imgproc クラスのメソッドで行います。代表的なものとして、ガウシアンブラーがあります。

“`java
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.Size;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.highgui.HighGui;

public class GaussianBlurExample {

static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
    String imagePath = "path/to/your/noisy_image.jpg";
    Mat originalImage = Imgcodecs.imread(imagePath);

    if (originalImage.empty()) {
        System.err.println("Error: Could not open or find the image.");
        return;
    }

    Mat blurredImage = new Mat();
    Size kernelSize = new Size(5, 5); // ガウシアンカーネルのサイズ (幅, 高さ)。奇数である必要がある。
    double sigmaX = 0; // X方向の標準偏差。0を指定するとカーネルサイズから自動的に計算される。
    double sigmaY = 0; // Y方向の標準偏差。0を指定するとsigmaXと同じ値が使われる。

    // ガウシアンブラーを適用
    // Imgproc.GaussianBlur(src, dst, ksize, sigmaX, sigmaY, borderType)
    Imgproc.GaussianBlur(originalImage, blurredImage, kernelSize, sigmaX, sigmaY);

    System.out.println("Image blurred with Gaussian filter.");

    HighGui.imshow("Original Image", originalImage);
    HighGui.imshow("Blurred Image", blurredImage);
    HighGui.waitKey();
    HighGui.destroyAllWindows();
}

}
“`

Imgproc.GaussianBlur(src, dst, ksize, sigmaX, sigmaY) は、入力画像 src にガウシアンフィルタを適用し、結果を dst に格納します。

  • src: 入力画像。
  • dst: 出力画像。
  • ksize: ガウシアンカーネルのサイズ。幅と高さを指定する Size オブジェクトです。幅と高さは奇数である必要があります(例: (3, 3), (5, 5), (7, 7))。
  • sigmaX: X方向の標準偏差。値を大きくすると、より強くぼやけます。0を指定すると、カーネルサイズから自動的に計算されます。
  • sigmaY: Y方向の標準偏差。通常は sigmaX と同じ値を使用します。0を指定すると sigmaX と同じ値が使われます。
  • borderType: 画像の端の処理方法(通常はデフォルト値で良い)。

ガウシアンブラー以外にも、Imgproc.medianBlur(メディアンフィルタ)、Imgproc.blur(平均化フィルタ)など、様々なフィルタ関数があります。

より実践的な画像処理

基本的な操作に加えて、オブジェクトの検出や画像の解析に使われる、より実践的な画像処理手法を見てみましょう。

二値化 (Imgproc.threshold)

二値化は、画像を特定のしきい値に基づいて白または黒のピクセルのみを持つ画像(二値画像)に変換する処理です。これにより、オブジェクトと背景を分離しやすくなります。

“`java
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.highgui.HighGui;

public class ThresholdExample {

static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
    String imagePath = "path/to/your/image.jpg";
    Mat originalImage = Imgcodecs.imread(imagePath, Imgcodecs.IMREAD_GRAYSCALE); // グレースケールで読み込む

    if (originalImage.empty()) {
        System.err.println("Error: Could not open or find the image.");
        return;
    }

    Mat binaryImage = new Mat();
    double thresholdValue = 128; // しきい値 (0-255)
    double maxVal = 255; // 最大値 (しきい値より大きいピクセルに設定される値)

    // 二値化処理
    // Imgproc.threshold(src, dst, thresh, maxval, type)
    // type: しきい値処理の種類 (例: THRESH_BINARY, THRESH_BINARY_INV, THRESH_TOZEROなど)
    Imgproc.threshold(originalImage, binaryImage, thresholdValue, maxVal, Imgproc.THRESH_BINARY);

    System.out.println("Image binarized.");

    HighGui.imshow("Original Grayscale Image", originalImage);
    HighGui.imshow("Binary Image (Threshold: " + thresholdValue + ")", binaryImage);
    HighGui.waitKey();
    HighGui.destroyAllWindows();
}

}
“`

Imgproc.threshold(src, dst, thresh, maxval, type) は、入力画像 src を二値化し、結果を dst に格納します。

  • src: 入力画像(通常はグレースケール画像)。
  • dst: 出力画像(二値画像)。
  • thresh: しきい値。
  • maxval: ピクセル値がしきい値を超えた場合に設定される値(通常は255)。
  • type: しきい値処理の種類。THRESH_BINARY は、しきい値を超えるピクセルを maxval に、それ以下を0にします。THRESH_BINARY_INVはその逆です。

適切な thresholdValue を選択することが重要です。画像の内容に応じて、固定値、または大津の方法 (Imgproc.THRESH_OTSU) などの自動しきい値決定アルゴリズムを使用できます。

エッジ検出 (Imgproc.Canny)

エッジ検出は、画像の輝度値が急激に変化する場所(物体の輪郭など)を見つけ出す処理です。Cannyエッジ検出は、その精度と信頼性から広く使用されています。

“`java
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.highgui.HighGui;

public class CannyEdgeDetection {

static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
    String imagePath = "path/to/your/image.jpg";
    Mat originalImage = Imgcodecs.imread(imagePath, Imgcodecs.IMREAD_GRAYSCALE); // グレースケールで読み込む

    if (originalImage.empty()) {
        System.err.println("Error: Could not open or find the image.");
        return;
    }

    Mat edges = new Mat();
    double threshold1 = 100; // 最小しきい値
    double threshold2 = 200; // 最大しきい値

    // Cannyエッジ検出を適用
    // Imgproc.Canny(image, edges, threshold1, threshold2, apertureSize, L2gradient)
    Imgproc.Canny(originalImage, edges, threshold1, threshold2); // apertureSize=3, L2gradient=false がデフォルト

    System.out.println("Canny edge detection applied.");

    HighGui.imshow("Original Grayscale Image", originalImage);
    HighGui.imshow("Canny Edges", edges);
    HighGui.waitKey();
    HighGui.destroyAllWindows();
}

}
“`

Imgproc.Canny(image, edges, threshold1, threshold2) は、入力画像 image(通常はグレースケール)からエッジを検出し、結果を edges に格納します。

  • image: 入力画像。
  • edges: 出力画像(8ビットのシングルチャンネル画像で、エッジは白、背景は黒)。
  • threshold1, threshold2: ヒステリシスしきい値処理で使用される2つのしきい値。エッジとして確定されるには、ピクセル勾配が threshold2 より大きいか、threshold1 より大きくかつ threshold2 より大きいエッジに連結している必要があります。

輪郭検出 (Imgproc.findContours)

輪郭は、同じ色や輝度を持つ連続した点(ピクセル)の曲線であり、オブジェクトの境界を表すためによく使用されます。Imgproc.findContours() は画像内の輪郭を見つけ出す機能です。

“`java
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint; // 輪郭の点を格納するクラス
import org.opencv.core.Scalar; // 色を表現するクラス
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.highgui.HighGui;

import java.util.ArrayList;
import java.util.List;

public class FindContoursExample {

static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
    String imagePath = "path/to/your/binary_image.png"; // 二値化された画像を使うのが一般的
    Mat binaryImage = Imgcodecs.imread(imagePath, Imgcodecs.IMREAD_GRAYSCALE);

    if (binaryImage.empty()) {
        System.err.println("Error: Could not open or find the image.");
        return;
    }

    // findContoursは入力画像を書き換える可能性があるため、コピーを使う
    Mat imageForContours = binaryImage.clone();
    List<MatOfPoint> contours = new ArrayList<>();
    Mat hierarchy = new Mat(); // 輪郭の階層構造を格納

    // 輪郭を検出
    // Imgproc.findContours(image, contours, hierarchy, mode, method)
    // mode: 輪郭の取得モード (RETR_LIST, RETR_EXTERNALなど)
    // method: 輪郭の近似方法 (CHAIN_APPROX_SIMPLE, CHAIN_APPROX_NONEなど)
    Imgproc.findContours(imageForContours, contours, hierarchy, Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);

    System.out.println("Number of contours found: " + contours.size());

    // 検出された輪郭を描画
    Mat resultImage = new Mat(binaryImage.size(), CvType.CV_8UC3, new Scalar(255, 255, 255)); // 白い背景のカラー画像
    // Imgproc.drawContours(image, contours, contourIdx, color, thickness, lineType, hierarchy, maxLevel, offset)
    // contourIdx: 描画する輪郭のインデックス (-1で全ての輪郭)
    Imgproc.drawContours(resultImage, contours, -1, new Scalar(0, 0, 255), 2); // 全ての輪郭を赤色で太さ2で描画

    HighGui.imshow("Original Binary Image", binaryImage);
    HighGui.imshow("Contours", resultImage);
    HighGui.waitKey();
    HighGui.destroyAllWindows();
}

}
“`

Imgproc.findContours(image, contours, hierarchy, mode, method) は、入力画像 image から輪郭を見つけ、その情報を出力します。

  • image: 入力画像(8ビットのシングルチャンネル画像、通常は二値画像またはCannyエッジ検出の結果)。この関数は入力画像を書き換える可能性があるので、オリジナルの画像を保持したい場合はコピーを渡してください。
  • contours: 検出された輪郭を格納するための List<MatOfPoint>。各 MatOfPoint は1つの輪郭を構成する点のリストです。
  • hierarchy: 輪郭の階層構造を格納する Mat。必要ない場合は無視できます。
  • mode: 輪郭の取得モード。RETR_LIST はすべての輪郭をフラットなリストとして取得します。RETR_EXTERNAL は最も外側の輪郭のみを取得します。
  • method: 輪郭点の近似方法。CHAIN_APPROX_SIMPLE は水平、垂直、斜めの線分を圧縮し、端点のみを格納します。CHAIN_APPROX_NONE はすべての点を格納します。

検出された輪郭は List<MatOfPoint> に格納されます。これらの輪郭を画像に描画するには Imgproc.drawContours() を使用します。

Imgproc.drawContours(image, contours, contourIdx, color, thickness) は、指定された画像 imagecontours リストから輪郭を描画します。

  • image: 輪郭を描画する画像。
  • contours: 輪郭のリスト (List<MatOfPoint>)。
  • contourIdx: 描画する輪郭のリスト内でのインデックス。-1 を指定するとリスト内のすべての輪郭を描画します。
  • color: 描画する輪郭の色 (Scalar オブジェクト)。カラー画像の場合は new Scalar(B, G, R)、グレースケール画像の場合は new Scalar(GrayValue)
  • thickness: 輪郭線の太さ。-1 または Core.FILLED を指定すると、輪郭で囲まれた領域を塗りつぶします。

図形の描画

画像上に直線、円、矩形、テキストなどの図形を描画することも可能です。これらも Imgproc クラスのメソッドを使用します。

“`java
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.Point; // OpenCVのPointクラス
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.core.CvType; // 追加: 新しいMatの型を指定するために必要
import org.opencv.imgproc.Imgproc;
import org.opencv.highgui.HighGui;

public class DrawShapesExample {

static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
    // 描画用の新しいカラー画像を生成 (幅600, 高さ400)
    Mat blankImage = new Mat(400, 600, CvType.CV_8UC3, new Scalar(255, 255, 255)); // 白い背景

    // 直線の描画
    Point p1 = new Point(50, 50);
    Point p2 = new Point(200, 200);
    Scalar lineColor = new Scalar(255, 0, 0); // 青色 (BGR)
    int lineThickness = 2;
    Imgproc.line(blankImage, p1, p2, lineColor, lineThickness);

    // 矩形の描画
    Rect rect = new Rect(250, 50, 150, 100); // 左上隅 (250, 50), 幅150, 高さ100
    Scalar rectColor = new Scalar(0, 255, 0); // 緑色
    int rectThickness = -1; // -1 または Core.FILLED で塗りつぶし
    Imgproc.rectangle(blankImage, rect, rectColor, rectThickness);

    // 円の描画
    Point center = new Point(500, 100);
    int radius = 50;
    Scalar circleColor = new Scalar(0, 0, 255); // 赤色
    int circleThickness = 3;
    Imgproc.circle(blankImage, center, radius, circleColor, circleThickness);

    // テキストの描画
    String text = "Hello, OpenCV!";
    Point textOrigin = new Point(50, 350); // 左下隅の座標
    int fontFace = Imgproc.FONT_HERSHEY_SIMPLEX; // フォントの種類
    double fontScale = 1.0; // フォントサイズ
    Scalar textColor = new Scalar(0, 0, 0); // 黒色
    int textThickness = 2;
    // Imgproc.putText(img, text, org, fontFace, fontScale, color, thickness, lineType, bottomLeftOrigin)
    Imgproc.putText(blankImage, text, textOrigin, fontFace, fontScale, textColor, textThickness);


    HighGui.imshow("Shapes and Text", blankImage);
    HighGui.waitKey();
    HighGui.destroyAllWindows();
}

}
“`

Imgproc クラスには、様々な図形を描画するためのメソッドがあります。共通の引数としては、描画対象の Mat、座標を表す Point、色を表す Scalar、線の太さや塗りつぶしを指定する thickness などがあります。テキスト描画の場合はフォントやスケールも指定します。

動画処理の基本

OpenCVは静止画だけでなく、動画(カメラ入力や動画ファイル)の処理もサポートしています。動画は連続したフレーム(静止画)の集まりとして扱われます。

VideoCapture クラス

動画の読み込みやカメラからの入力には、org.opencv.videoio.VideoCapture クラスを使用します。

“`java
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.videoio.VideoCapture; // VideoCaptureクラス
import org.opencv.highgui.HighGui;

public class VideoCaptureExample {

static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
    // カメラからの入力の場合 (通常は0を指定)
    // カメラが複数ある場合は 1, 2... と指定
    VideoCapture camera = new VideoCapture(0);

    // 動画ファイルからの入力の場合
    // String videoPath = "path/to/your/video.mp4";
    // VideoCapture camera = new VideoCapture(videoPath);

    // カメラ/ファイルが開けたか確認
    if (!camera.isOpened()) {
        System.err.println("Error: Could not open camera/video.");
        return;
    }

    System.out.println("Camera/Video opened successfully.");

    Mat frame = new Mat(); // フレームを格納するMatオブジェクト
    String windowName = "Live Feed";
    HighGui.namedWindow(windowName, HighGui.WINDOW_AUTOSIZE); // ウィンドウを作成

    while (true) {
        // フレームを読み込む
        if (camera.read(frame)) { // frameに次のフレームが読み込まれたらtrueを返す
            // ここでフレームに対する画像処理を行う
            // 例: グレースケール変換
            Mat grayFrame = new Mat();
            Imgproc.cvtColor(frame, grayFrame, Imgproc.COLOR_BGR2GRAY);

            // 処理したフレームを表示
            HighGui.imshow(windowName, grayFrame);
            // HighGui.imshow(windowName, frame); // 元のカラーフレームを表示する場合

            // 'q' キーが押されたらループを終了
            if (HighGui.waitKey(1) == 'q') { // 1ミリ秒待機し、キー入力をチェック
                break;
            }
        } else {
            System.err.println("Error: Could not read frame.");
            break; // フレームが読み込めなくなったら終了 (動画ファイルの終わりなど)
        }
    }

    // カメラ/ファイルとウィンドウを解放
    camera.release();
    HighGui.destroyAllWindows();
    System.out.println("Application finished.");
}

}
“`

VideoCapture オブジェクトは、コンストラクタにカメラのインデックス(通常は0)または動画ファイルのパスを指定して作成します。

isOpened() メソッドで、カメラまたはファイルが正常に開かれたかを確認できます。

無限ループ内で camera.read(frame) を呼び出すことで、動画の次のフレームを Mat オブジェクトとして取得できます。read() はフレームを正常に取得できた場合に true を返します。

各フレーム(frame Mat オブジェクト)に対して、これまで学んだ画像処理関数(グレースケール変換、フィルタ、エッジ検出など)を適用できます。

処理結果のフレームを HighGui.imshow() で表示します。動画をリアルタイムに表示するためには、HighGui.waitKey() の引数に短い時間(例: 1ミリ秒)を指定します。これにより、ウィンドウシステムに処理を明け渡し、ウィンドウの更新とキー入力の検出が行われます。特定のキーが押されたらループを抜けるようにすると、アプリケーションを終了できます。

ループ終了後は、camera.release() でリソースを解放し、HighGui.destroyAllWindows() でウィンドウを閉じます。

JavaFX/Swingとの連携

HighGui.imshow は簡単な表示には便利ですが、本格的なGUIアプリケーションに画像を組み込むには、JavaのGUIフレームワーク(JavaFXやSwing)との連携が必要です。OpenCVの Mat オブジェクトをJavaの標準画像形式である java.awt.image.BufferedImage に変換することで、これが可能になります。

OpenCVの Mat はピクセルデータをB/G/Rの順序で格納することが多いですが、BufferedImage は通常R/G/BまたはB/G/Rのピクセルフォーマットを使用します。また、データ型やチャンネル数も考慮して変換する必要があります。

以下に、MatBufferedImage に変換するヘルパーメソッドを含むクラスの例を示します。

“`java
import org.opencv.core.Mat;
import org.opencv.core.CvType;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.WritableRaster;

/*
* OpenCVのMatオブジェクトとJavaのBufferedImageオブジェクトを相互変換するためのユーティリティクラス.
/
public class Mat2Image {

private Mat mat = null;
private BufferedImage img = null;
private byte[] dat = null;
private WritableRaster raster = null;

/**
 * 指定されたMatオブジェクトを保持する新しいMat2Imageオブジェクトを作成します。
 * 最初の変換時にBufferedImageが生成されます。
 * @param mat 変換元のMatオブジェクト
 */
public Mat2Image(Mat mat) {
    this.mat = mat;
}

/**
 * このオブジェクトが保持するBufferedImageを取得します。
 * 初回呼び出し時、またはMatの内容が変更された後にBufferedImageが生成または更新されます。
 * @return BufferedImageオブジェクト
 */
public BufferedImage getImage() {
    // Matのデータ型とチャンネル数を確認
    int type = 0;
    if (mat.channels() == 1) {
        type = BufferedImage.TYPE_BYTE_GRAY; // 1チャンネル (グレースケール)
    } else if (mat.channels() == 3) {
        type = BufferedImage.TYPE_3BYTE_BGR; // 3チャンネル (カラー - BGR順)
    } else {
        // 他のチャンネル数やデータ型には対応していない場合はエラー処理
        System.err.println("Unsupported Mat type or number of channels for BufferedImage conversion.");
        return null;
    }

    // Matのデータを行列としてバイト配列にコピー
    int bufferSize = mat.channels() * mat.cols() * mat.rows();
    if (dat == null || dat.length != bufferSize) {
         dat = new byte[bufferSize];
    }
    mat.get(0, 0, dat); // Matのデータをバイト配列にコピー

    // BufferedImageを生成または更新
    if (img == null || img.getWidth() != mat.cols() || img.getHeight() != mat.rows() || img.getType() != type) {
        img = new BufferedImage(mat.cols(), mat.rows(), type);
        raster = img.getRaster();
    }

    // バイト配列からBufferedImageのピクセルデータを設定
    raster.setDataElements(0, 0, mat.cols(), mat.rows(), dat);

    return img;
}

/**
 * このオブジェクトが保持するMatオブジェクトを取得します。
 * @return Matオブジェクト
 */
public Mat getMat() {
    return mat;
}

/**
 * このオブジェクトが保持するMatオブジェクトを設定します。
 * 次回getImage()呼び出し時にBufferedImageが再生成されます。
 * @param mat 新しいMatオブジェクト
 */
public void setMat(Mat mat) {
    this.mat = mat;
    // BufferedImageは次回getImage()呼び出し時に再生成される
    this.img = null;
    this.raster = null;
    this.dat = null;
}

// BufferedImage から Mat への変換 (ここでは省略 - 必要に応じて実装)
// public Mat toMat(BufferedImage img) { ... }

}
“`

この Mat2Image クラスを使用すると、OpenCVで処理した Mat オブジェクトを簡単に BufferedImage に変換し、JavaFXの ImageView やSwingの JLabel に表示できます。

JavaFXでの画像表示例:

JavaFXアプリケーションでフレームを表示する簡単な例を示します。これは、VideoCaptureで取得したフレームを表示する場合などに応用できます。

“`java
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.scene.image.Image;
import javafx.embed.swing.SwingFXUtils; // BufferedImageをJavaFX Imageに変換するユーティリティ

import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.videoio.VideoCapture;

import java.awt.image.BufferedImage;
import java.util.Timer;
import java.util.TimerTask;

public class JavaFXImageViewer extends Application {

static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

private ImageView imageView;
private VideoCapture capture;
private Mat frame;
private Timer timer;
private Mat2Image mat2ImageConverter; // MatをBufferedImageに変換するヘルパー

@Override
public void start(Stage primaryStage) {
    imageView = new ImageView();
    StackPane root = new StackPane(imageView);
    Scene scene = new Scene(root, 640, 480);

    primaryStage.setTitle("OpenCV JavaFX Viewer");
    primaryStage.setScene(scene);
    primaryStage.show();

    // VideoCaptureの初期化 (ここではカメラ0を使用)
    capture = new VideoCapture(0);
    frame = new Mat();
    mat2ImageConverter = new Mat2Image(frame); // フレームMatを渡してコンバーターを初期化

    if (!capture.isOpened()) {
        System.err.println("Camera failed to open.");
        // カメラが開けない場合、サンプル画像を表示するなど適切なフォールバック処理を行う
        // 例: Mat sampleImage = Imgcodecs.imread("path/to/sample.jpg");
        // if (!sampleImage.empty()) {
        //     mat2ImageConverter.setMat(sampleImage);
        //     updateImageView(mat2ImageConverter.getImage());
        // }
        return; // カメラが開けない場合は処理を終了
    }

    // フレームを取得して表示するためのタイマーを開始
    timer = new Timer();
    timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            if (capture.read(frame)) { // 新しいフレームが読み込めたら
                // ここでフレームに画像処理を適用することも可能
                // 例: Imgproc.cvtColor(frame, frame, Imgproc.COLOR_BGR2GRAY);

                // MatをBufferedImageに変換
                mat2ImageConverter.setMat(frame); // 現在のフレームMatを設定
                BufferedImage bImage = mat2ImageConverter.getImage();

                // BufferedImageをJavaFXのImageに変換し、ImageViewを更新
                if (bImage != null) {
                     Image fxImage = SwingFXUtils.toFXImage(bImage, null);
                     // UIの更新はJavaFX Application Threadで行う必要がある
                     javafx.application.Platform.runLater(() -> {
                         imageView.setImage(fxImage);
                         imageView.setFitWidth(root.getWidth());
                         imageView.setPreserveRatio(true);
                     });
                } else {
                    System.err.println("Failed to convert Mat to BufferedImage.");
                }
            } else {
                 System.err.println("Failed to read frame from camera.");
                 // エラー発生時や動画終了時の処理
                 cancel(); // タイマータスクをキャンセル
                 releaseResources(); // リソースを解放
                 javafx.application.Platform.runLater(() -> primaryStage.close()); // ウィンドウを閉じる
            }
        }
    }, 0, 33); // 最初の実行遅延0ms, 以降33msごと (約30fps)

    // ウィンドウが閉じられたときの処理
    primaryStage.setOnCloseRequest(event -> {
        releaseResources();
    });
}

// リソースを解放するメソッド
private void releaseResources() {
    if (timer != null) {
        timer.cancel();
    }
    if (capture != null && capture.isOpened()) {
        capture.release();
    }
    if (frame != null && !frame.empty()) {
        // frame MatはVideoCaptrueが管理しているので通常はrelease不要
        // ただし、clone()などで生成したMatは明示的な管理が必要になる場合がある
    }
    System.out.println("Resources released.");
}

public static void main(String[] args) {
    launch(args);
}

}
“`

このJavaFXの例では、VideoCapture でカメラからフレームを読み込み、Mat2Image クラスを使って BufferedImage に変換し、SwingFXUtils.toFXImage でJavaFXの Image に変換して ImageView に表示しています。GUIの更新は必ずJavaFX Application Thread (javafx.application.Platform.runLater) で行う必要があります。タイマーを使って定期的にフレームを取得・更新することで、動画表示を実現しています。

Swingの場合も基本的な考え方は同じです。MatBufferedImage に変換した後、ImageIcon を作成し、それを JLabel に設定することで画像を表示できます。

エラーシューティング

JavaでOpenCVを使用していると、いくつかの一般的なエラーに遭遇することがあります。

1. java.lang.UnsatisfiedLinkError: no opencv_javaX.y.z in java.library.path

  • 原因: JVMがOpenCVのネイティブライブラリファイル(.dll, .so, .dylib)を見つけられない場合に発生します。
  • 解決策:
    • java.library.path の設定ミス:
      • IDEの実行構成(VM arguments)で -Djava.library.path="ネイティブライブラリフォルダのパス" が正しく設定されているか確認します。パスはOSに合わせて、ネイティブライブラリファイル自体ではなく、それらが格納されているフォルダを指定します。
      • システム環境変数 PATH(Windows)または LD_LIBRARY_PATH(Linux)、DYLD_LIBRARY_PATH(macOS)にネイティブライブラリのフォルダパスを追加することも有効ですが、java.library.path の設定が推奨されます。
    • ライブラリ名の不一致: System.loadLibrary() の引数に指定するライブラリ名が、実際のファイル名(拡張子を除く)と一致しているか確認します。Core.NATIVE_LIBRARY_NAME を使用するのが最も安全です。
    • OS/アーキテクチャの不一致: ダウンロードしたOpenCVライブラリが、使用しているOS(Windows/macOS/Linux)およびCPUアーキテクチャ(32ビット/64ビット)と一致しているか確認します。JavaのバージョンとOpenCVライブラリのビルド環境(特にWindowsのVisual Studioバージョン)も関係する場合があります。
    • OpenCVのビルド方法: 公式サイトからダウンロードしたプレビルド版を使用せず、自身でOpenCVをビルドした場合、Javaバインディングとネイティブライブラリが正しく生成されているか確認が必要です。

2. java.lang.IllegalArgumentException: mat data type is not compatible with BufferedImage type

  • 原因: OpenCVの Mat とJavaの BufferedImage 間で、データ型やチャンネル数が変換ユーティリティが想定するものと一致しない場合に発生します。上記の Mat2Image クラスのようなカスタム変換コードを使用している場合、この例外が発生する可能性があります。
  • 解決策:
    • Mat の型とチャンネル数: 変換しようとしている Mat の型(mat.type())とチャンネル数(mat.channels())を確認します。
    • 変換コードの対応範囲: 使用している変換コードが、その Mat の型とチャンネル数(例: CV_8UC1, CV_8UC3)に対応しているか確認します。必要であれば、変換コードを修正して他の型にも対応させます。
    • 画像処理結果の型: 画像処理関数によっては出力の型が変更される場合があります。例えば、Imgproc.Sobel など勾配計算を行う関数は、デフォルトで CV_32FCV_64F の浮動小数点型を出力することがあります。これを BufferedImage に変換するには、Mat.convertTo() などで8ビット整数型に変換してから行う必要があります。

3. java.lang.CvException: OpenCV(xxxx): error: (-5:Bad argument)

  • 原因: OpenCVの関数呼び出し時に、引数に不正な値や無効な Mat オブジェクトが渡された場合に発生します。
  • 解決策:
    • Mat オブジェクトの有効性: 関数に渡す Mat オブジェクトが null でないか、また empty() でないか(画像が正しく読み込まれているかなど)を確認します。
    • 引数の値: 関数のパラメータ(例: カーネルサイズ、しきい値、座標など)が、関数のドキュメントで指定されている範囲内や条件(例: カーネルサイズは奇数である必要があるなど)を満たしているか確認します。
    • 入力/出力 Mat の型/サイズ: 関数の種類によっては、入力 Mat と出力 Mat の型やサイズに特定の要件がある場合があります。ドキュメントを参照して確認します。例えば、多くのフィルタ処理関数は入力と出力が同じサイズである必要があります。

4. メモリ関連の問題 (特に大きな画像を扱う場合)

  • 原因: OpenCVの Mat はネイティブメモリを使用します。大きな画像を扱う場合や、たくさんの Mat オブジェクトを生成する場合、ネイティブメモリが不足したり、JavaのGCによるネイティブメモリ解放が追いつかなかったりすることがあります。
  • 解決策:
    • 不要な Mat の削減: 中間結果を格納する Mat オブジェクトが必要以上に多くならないようにコードを最適化します。可能であれば、処理結果を同じ Mat オブジェクトに上書きするようにします。
    • Mat.release() の不在: Javaバインディングの Mat クラスには明示的な release() メソッドはありません。基本的にGCに任せることになりますが、メモリ使用量が気になる場合は、Java側の Mat オブジェクトへの参照を早めに null に設定するなどして、GCによる解放を促す工夫が必要になることがあります。ただし、submat など参照を共有する Mat を扱う際は注意が必要です。
    • JVMヒープサイズ: ネイティブメモリとは直接関係ありませんが、Java側のオブジェクトが多ければJVMヒープも増加します。必要に応じてJVMのヒープサイズを増やします (-Xmx オプション)。ただし、これはネイティブメモリ不足の根本的な解決にはなりません。

エラーメッセージの内容をよく読み、OpenCVのドキュメント(特に使用している関数の仕様)を参照することが、問題解決への近道です。

OpenCVの高度な機能への展望

この記事では、JavaでOpenCVを使うための導入から基本的な画像・動画処理について解説しました。しかし、OpenCVの機能はこれだけにとどまりません。さらに高度な機能を利用することで、より洗練されたコンピュータビジョンアプリケーションを開発できます。

以下に、学習を進める上で興味を持つであろうOpenCVの高度な機能の例を挙げます。

  • 特徴点検出と記述: 画像内の特徴的な点(コーナー、ブロブなど)を見つけ出し、その周囲の領域を数値的に表現(記述子)することで、異なる画像間での物体追跡や画像マッチングが可能になります。SIFT, SURF, ORB, AKAZEなどのアルゴリズムがあります。
  • 物体検出: 画像中の特定の種類の物体(顔、車、猫など)の位置とサイズを検出する技術です。古典的なHaar-like特徴量を使ったカスケード分類器や、近年主流となっている深層学習ベースの手法(YOLO, SSDなど)があります。OpenCVのdnnモジュールは、様々な深層学習モデルの推論を実行できます。
  • 物体追跡: 動画中で一度検出した物体をフレーム間で追跡する技術です。MIL, KCF, CSRTなどの様々な追跡アルゴリズムが提供されています。
  • 機械学習: OpenCVには、SVM (Support Vector Machine), k-NN (k-Nearest Neighbors), Decision Treesなど、基本的な機械学習アルゴリズムを実装したmlモジュールがあります。これらを画像の特徴量と組み合わせて分類や回帰を行うことができます。
  • キャリブレーションと3D: カメラキャリブレーション(カメラの内部パラメータと外部パラメータの推定)、ステレオビジョン(2台のカメラで奥行き情報を得る)、Structure from Motion(複数の画像から3D構造を復元)など、3次元に関わる機能も豊富です。
  • Contribモジュール: 公式リリース版には含まれていない、開発途上またはライセンス上の制約がある機能群です。より新しいアルゴリズムや専門的な機能が含まれていることが多く、別途ビルドが必要です。

これらの高度な機能も、Javaバインディングを通じて利用可能です。まずはOpenCVの基本的な概念と使い方をしっかりと習得し、次に興味のある分野に進んでいくのが良いでしょう。

まとめ

この記事では、JavaでOpenCVを使用するための詳細な手順と、基本的な画像・動画処理の様々な手法について解説しました。

まず、JDK、IDE、そしてOpenCVライブラリ本体のダウンロードと、Javaプロジェクトへの設定方法を確認しました。特にネイティブライブラリのパス設定が重要であること、そして System.loadLibrary(Core.NATIVE_LIBRARY_NAME); でライブラリをロードする必要があることを学びました。

OpenCVの中心となるデータ構造である Mat クラスについて理解し、その生成方法やピクセル値へのアクセス方法を知りました。

そして、画像の読み込み、表示、保存といった基本的なファイル操作から始まり、グレースケール変換、サイズ変更、切り抜き、ガウシアンブラーなどの基本的な画像処理、さらに二値化、エッジ検出、輪郭検出、図形描画といったより実践的な手法を、具体的なJavaコード例とともに学びました。

動画処理に関しては、VideoCapture クラスを使ってカメラや動画ファイルからフレームを取得し、各フレームに対して画像処理を適用して表示する基本的な流れを解説しました。

JavaFXやSwingといったGUIフレームワークとの連携についても、OpenCVの Mat をJavaの BufferedImage に変換するためのユーティリティクラスの例を示し、GUIアプリケーションで画像を表示する方法の概略を説明しました。

最後に、JavaでOpenCVを使用する際によく遭遇するエラー(UnsatisfiedLinkError, IllegalArgumentException, CvException など)の原因と解決策、そしてOpenCVが提供する豊富な高度な機能への展望に触れました。

Javaは強力で広く利用されている言語であり、OpenCVはそのJavaバインディングを通じて、Java開発者に最先端のコンピュータビジョン機能を提供します。この記事で得た知識を基に、ぜひ様々な画像・動画処理アプリケーションの開発に挑戦してみてください。コンピュータビジョンの世界は広大で、創造的な可能性に満ちています。

この詳細な解説が、あなたのJavaとOpenCVを使った開発の出発点となり、さらなる学習の助けとなれば幸いです。頑張ってください!


コメントする

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

上部へスクロール