OpenCV 4.13.0
Open Source Computer Vision
読み込み中...
検索中...
見つかりません
🤖 AIによる機械翻訳(非公式) — これは OpenCV 4.13.0 公式リファレンス(英語)を AI (Claude) で自動翻訳したものです。訳に誤りを含む場合があります。正確な情報は 公式英語版(原文) を参照してください。
Mat - 基本的な画像コンテナ

次のチュートリアル: OpenCV による画像の走査、ルックアップテーブル、時間計測の方法

原著者Bernát Gábor
互換性OpenCV >= 3.0

目的

現実世界からデジタル画像を取得する方法は複数ある。デジタルカメラ、スキャナ、コンピュータ断層撮影、磁気共鳴画像法などがその一例である。いずれの場合も、我々(人間)が見るのは画像である。しかし、これをデジタル機器に変換すると、記録されるのは画像の各点に対応する数値である。

例えば上の画像を見ると、車のミラーは、ピクセル点の強度値をすべて格納した行列にほかならないことがわかる。ピクセル値をどのように取得し格納するかは必要に応じて変わり得るが、最終的にコンピュータの世界の中ではすべての画像は数値行列と、その行列自体を記述するその他の情報に還元される。OpenCV は、この情報を処理し操作することを主目的とするコンピュータビジョンライブラリである。したがって、まず最初に理解しておくべきは、OpenCV がどのように画像を格納し扱うかである。

Mat

OpenCV は 2001 年から存在している。当時、このライブラリは C インターフェースを中心に構築され、画像をメモリに格納するために IplImage と呼ばれる C の構造体を使っていた。これは、古いチュートリアルや教材の多くで目にするものである。これの問題点は、C 言語のあらゆる欠点を持ち込んでしまうことである。最大の問題は手動でのメモリ管理である。これは、メモリの確保と解放をユーザーが責任を持って行うという前提に基づいている。小規模なプログラムでは問題にならないが、コードベースが大きくなると、開発目標の解決に集中するよりも、こうした管理に手を取られる方が大きな負担になる。

幸いにも C++ が登場し、クラスという概念を導入したことで、(多かれ少なかれ)自動的なメモリ管理を通じてユーザーにとって扱いやすくなった。喜ばしいことに、C++ は C と完全な互換性があるため、移行によって互換性の問題が生じることはない。そこで OpenCV 2.0 は新しい C++ インターフェースを導入し、メモリ管理をいじる必要のない新しいやり方を提供した。これによりコードが簡潔になる(少ない記述でより多くのことを実現できる)。C++ インターフェースの主な欠点は、現時点で多くの組み込み開発システムが C しかサポートしていないことである。したがって、組み込みプラットフォームを対象としているのでない限り、古い方法を使う意味はない(マゾヒストのプログラマで、トラブルを自ら望むのでない限り)。

Mat についてまず知っておくべきは、もはやそのメモリを手動で確保したり、不要になったらすぐに解放したりする必要がないということである。これを行うことは依然として可能だが、ほとんどの OpenCV 関数は出力データを自動的に確保する。さらに嬉しいことに、すでに行列に必要な領域を確保済みの既存の Mat オブジェクトを渡せば、それが再利用される。言い換えれば、タスクの実行に必要なだけのメモリを常に使用するのである。

Mat は基本的に、2 つのデータ部分からなるクラスである。すなわち、行列ヘッダ(行列のサイズ、格納に使われる方法、行列が格納されているアドレスなどの情報を含む)と、ピクセル値を含む行列へのポインタ(格納方法に応じて任意の次元を取る)である。行列ヘッダのサイズは一定だが、行列自体のサイズは画像ごとに異なり、通常は桁違いに大きい。

OpenCV は画像処理ライブラリである。画像処理関数の大規模なコレクションを含んでいる。計算上の課題を解決するには、ほとんどの場合、ライブラリの複数の関数を使うことになる。このため、関数に画像を渡すことは一般的な作業である。我々が扱っているのは画像処理アルゴリズムであり、それらは概して計算負荷が高い傾向にあることを忘れてはならない。最も避けたいのは、潜在的に大きい画像の不要なコピーを作ってプログラムの速度をさらに低下させることである。

この問題に対処するため、OpenCV は参照カウント方式を用いる。その考え方は、各 Mat オブジェクトは自身のヘッダを持つが、2 つの Mat オブジェクトの行列ポインタを同じアドレスを指すようにすることで、行列を共有できるというものである。さらに、コピー演算子はヘッダと大きな行列へのポインタだけをコピーし、データ自体はコピーしない。

Mat A, C; // creates just the header parts
A = imread(argv[1], IMREAD_COLOR); // here we'll know the method used (allocate matrix)
Mat B(A); // Use the copy constructor
C = A; // Assignment operator

上記のオブジェクトはすべて、最終的に同一のデータ行列を指しており、そのいずれかを使って変更を加えると、他のすべてにも影響する。実際には、異なるオブジェクトは同じ基盤データへの異なるアクセス手段を提供しているにすぎない。とはいえ、それらのヘッダ部分は異なる。本当に興味深いのは、データ全体の一部分だけを参照するヘッダを作成できる点である。例えば、画像内に注目領域(ROI)を作るには、新しい境界を持つ新しいヘッダを作るだけでよい。

Mat D (A, Rect(10, 10, 100, 100) ); // using a rectangle
Mat E = A(Range::all(), Range(1,3)); // using row and column boundaries

ここで疑問が湧くかもしれない。行列自体が複数の Mat オブジェクトに属し得るなら、不要になったときに誰がそれを片付ける責任を負うのか、と。手短に言えば、それを最後に使ったオブジェクトである。これは参照カウント機構によって処理される。誰かが Mat オブジェクトのヘッダをコピーするたびに、その行列のカウンタが増加する。ヘッダが片付けられるたびに、このカウンタは減少する。カウンタがゼロに達すると、行列は解放される。行列自体もコピーしたい場合があるため、OpenCV は cv::Mat::clone()cv::Mat::copyTo() の関数を提供している。

Mat F = A.clone();
Mat G;
A.copyTo(G);

ここで FG を変更しても、A のヘッダが指す行列には影響しない。これらすべてから覚えておくべきことは次の点である。

  • OpenCV 関数の出力画像の確保は(特に指定しない限り)自動である。
  • OpenCV の C++ インターフェースではメモリ管理について考える必要はない。
  • 代入演算子とコピーコンストラクタはヘッダだけをコピーする。
  • 画像の基盤となる行列は cv::Mat::clone()cv::Mat::copyTo() の関数を使ってコピーできる。

格納方法

これはピクセル値をどのように格納するかについての話である。使用する色空間とデータ型を選択できる。色空間とは、ある色を符号化するために色成分をどのように組み合わせるかを指す。最も単純なのはグレースケールで、扱える色は黒と白である。これらの組み合わせにより、多くの灰色の濃淡を作り出すことができる。

カラフルな方法については、選択肢となる手法がさらに多くある。それぞれが色を 3 つまたは 4 つの基本成分に分解し、それらの組み合わせによって他の色を作り出せる。最も普及しているのは RGB で、これは主に我々の目が色を構成するのと同じ仕組みだからである。基本となる色は赤、緑、青である。色の透明度を符号化するために、4 つ目の要素であるアルファ (A) が追加されることもある。

ただし、他にも多くの色系があり、それぞれに固有の利点がある。

  • RGB は、我々の目も同様の仕組みを使っているため最も一般的である。ただし、OpenCV の標準的な表示系は BGR 色空間を使って色を構成すること(赤と青のチャンネルが入れ替わっている)に注意してほしい。
  • HSV と HLS は色を色相、彩度、明度/輝度の成分に分解する。これは我々が色を表現するうえでより自然な方法である。例えば、最後の成分を無視することで、アルゴリズムを入力画像の照明条件に対して鈍感にすることができる。
  • YCrCb は普及している JPEG 画像フォーマットで使われている。
  • CIE L*a*b* は知覚的に均一な色空間であり、ある色から別の色までの距離を測定する必要がある場合に便利である。

構成要素のそれぞれには固有の有効範囲がある。これが使用するデータ型につながる。成分をどのように格納するかが、その範囲をどれだけ制御できるかを決める。可能な最小のデータ型は char で、これは 1 バイトすなわち 8 ビットを意味する。これは符号なし(0 から 255 までの値を格納できる)にも符号付き(-127 から +127 までの値)にもできる。この幅でも、3 成分(RGB など)の場合にはすでに 1600 万色を表現できるが、各成分に float (4 バイト = 32 ビット) や double (8 バイト = 64 ビット) のデータ型を使えば、さらにきめ細かい制御が得られる。ただし、成分のサイズを大きくすると、メモリ上での画像全体のサイズも大きくなることを忘れてはならない。

Mat オブジェクトを明示的に生成する

画像ことはじめ のチュートリアルでは、cv::imwrite() 関数を使って行列を画像ファイルに書き出す方法をすでに学んだ。しかし、デバッグの目的では実際の値を確認する方がずっと便利である。これは Mat の << 演算子を使って行える。これは 2 次元行列に対してのみ機能することに注意してほしい。

Mat は画像コンテナとして非常によく機能するが、汎用的な行列クラスでもある。したがって、多次元行列を作成して操作することも可能である。Mat オブジェクトは複数の方法で作成できる。

2 次元かつ多チャンネルの画像については、まずそのサイズを行数と列数で定義する。

次に、要素を格納するために使用するデータ型と、行列の点ごとのチャンネル数を指定する必要がある。これを行うために、次の規約に従って構築された複数の定義がある。

CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]

例えば CV_8UC3 は、8 ビット長の符号なし char 型を使い、各ピクセルがこれを 3 つ持つことで 3 チャンネルを構成することを意味する。チャンネル数が 4 までの型があらかじめ定義されている。cv::Scalar は 4 要素の短いベクトルである。これを指定すれば、すべての行列の点を任意の値で初期化できる。さらに必要なら、下記のように括弧内にチャンネル数を設定して、上記のマクロで型を作成できる。

  • C/C++ の配列を使い、コンストラクタで初期化する

    int sz[3] = {2,2,2};
    Mat L(3,sz, CV_8UC(1), Scalar::all(0));

    上記の例は、2 次元を超える行列の作成方法を示している。その次元数を指定し、続いて各次元のサイズを含むポインタを渡す。残りは同じである。

  • cv::Mat::create 関数:

    M.create(4,4, CV_8UC(2));
    cout << "M = "<< endl << " " << M << endl << endl;

この構築方法では行列の値を初期化できない。新しいサイズが古いものに収まらない場合にのみ、行列データのメモリを再確保する。

  • MATLAB スタイルの初期化子: cv::Mat::zeros , cv::Mat::ones , cv::Mat::eye 。使用するサイズとデータ型を指定する:

    Mat E = Mat::eye(4, 4, CV_64F);
    cout << "E = " << endl << " " << E << endl << endl;
    Mat O = Mat::ones(2, 2, CV_32F);
    cout << "O = " << endl << " " << O << endl << endl;
    Mat Z = Mat::zeros(3,3, CV_8UC1);
    cout << "Z = " << endl << " " << Z << endl << endl;
  • 小さな行列の場合は、カンマ区切りの初期化子または初期化子リストを使える(後者の場合は C++11 のサポートが必要):

    Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
    cout << "C = " << endl << " " << C << endl << endl;
    C = (Mat_<double>({0, -1, 0, -1, 5, -1, 0, -1, 0})).reshape(3);
    cout << "C = " << endl << " " << C << endl << endl;
  • 既存の Mat オブジェクトに対して新しいヘッダを作成し、cv::Mat::clone または cv::Mat::copyTo する。

    Mat RowClone = C.row(1).clone();
    cout << "RowClone = " << endl << " " << RowClone << endl << endl;
覚え書き
cv::randu() 関数を使って行列をランダムな値で埋めることができる。ランダムな値の下限と上限を与える必要がある:
Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar::all(0), Scalar::all(255));

出力の整形

上記の例では、デフォルトの整形オプションを確認できた。しかし OpenCV では、行列の出力を整形することもできる。

  • デフォルト
    cout << "R (default) = " << endl << R << endl << endl;
  • Python
    cout << "R (python) = " << endl << format(R, Formatter::FMT_PYTHON) << endl << endl;
  • カンマ区切り値 (CSV)
    cout << "R (csv) = " << endl << format(R, Formatter::FMT_CSV ) << endl << endl;
  • Numpy
    cout << "R (numpy) = " << endl << format(R, Formatter::FMT_NUMPY ) << endl << endl;
  • C
    cout << "R (c) = " << endl << format(R, Formatter::FMT_C ) << endl << endl;

その他の一般的な要素の出力

OpenCV は、<< 演算子を通じて、他の一般的な OpenCV データ構造の出力もサポートしている:

  • 2D 点
    Point2f P(5, 1);
    cout << "Point (2D) = " << P << endl << endl;
  • 3D 点
    Point3f P3f(2, 6, 7);
    cout << "Point (3D) = " << P3f << endl << endl;
  • cv::Mat を介した std::vector
    vector<float> v;
    v.push_back( (float)CV_PI); v.push_back(2); v.push_back(3.01f);
    cout << "Vector of floats via Mat = " << Mat(v) << endl << endl;
  • 点の std::vector
    vector<Point2f> vPoints(20);
    for (size_t i = 0; i < vPoints.size(); ++i)
    vPoints[i] = Point2f((float)(i * 5), (float)(i % 7));
    cout << "A vector of 2D Points = " << vPoints << endl << endl;

ここで示したサンプルのほとんどは、小さなコンソールアプリケーションに含まれている。ここから、または cpp サンプルの core セクションからダウンロードできる。

これについての簡単な動画デモも YouTube で見ることができる。