OpenCV

【C++】OpenCVでテクスチャ分類を実装:LBPとGLCMで特徴量抽出しPCA+SVMで高精度分類

画像の局所領域からLBPやグレーレベル共起行列で特徴量を抽出し、PCAで次元削減したあと、SVMなどの学習モデルでテクスチャを判定します。

OpenCVのcalcLBPml::SVMを活用すると効率的です。

画像前処理

画像前処理は、テクスチャ分類の精度を向上させるために非常に重要なステップです。

適切な前処理を行うことで、特徴量抽出の精度が高まり、分類器の性能も向上します。

ここでは、まず画像をグレースケールに変換し、その後ノイズ除去を行う方法について詳しく解説します。

グレースケール化

カラー画像はRGBやBGRといった複数のチャネルを持ち、情報量が多いため、テクスチャの分析には適さない場合があります。

したがって、まずは画像をグレースケールに変換します。

OpenCVではcv::cvtColor関数を用いて簡単に変換可能です。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // 画像の読み込み
    cv::Mat colorImage = cv::imread("texture_image.jpg");
    if (colorImage.empty()) {
        std::cerr << "画像が見つかりません。" << std::endl;
        return -1;
    }
    // グレースケールに変換
    cv::Mat grayImage;
    cv::cvtColor(colorImage, grayImage, cv::COLOR_BGR2GRAY);
    // 変換結果の表示
    cv::imshow("グレースケール画像", grayImage);
    cv::waitKey(0);
    return 0;
}

このコードでは、cv::imreadで画像を読み込み、cv::cvtColorを使ってBGRからグレースケールに変換しています。

グレースケール化により、計算コストが削減されるだけでなく、テクスチャのパターンに集中できるため、特徴量抽出の精度が向上します。

ノイズ除去

画像にはさまざまなノイズが含まれていることがあり、これが特徴量の抽出や分類の妨げとなる場合があります。

ノイズを除去することで、より正確なテクスチャ情報を得ることが可能です。

次に、ノイズ除去のための平滑化フィルタの選択と、そのパラメータ調整について解説します。

平滑化フィルタの選択

ノイズ除去にはいくつかの平滑化フィルタが利用されます。

代表的なものは以下の通りです。

  • 平均値フィルタ(平均化フィルタ):指定したカーネル内のピクセル値の平均をとることで平滑化します。計算が高速ですが、エッジのぼやけが生じやすいです
  • ガウシアンフィルタ:正規分布に基づく重み付けを行い、滑らかにします。エッジの保存性が高く、ノイズ除去に適しています
  • メディアンフィルタ:カーネル内のピクセル値の中央値を採用します。塩胡椒ノイズに対して非常に効果的です

これらの中から、目的や画像の特性に応じて選択します。

フィルタサイズの調整

フィルタの効果はカーネルサイズに大きく依存します。

一般的に、カーネルサイズが大きいほど平滑化の効果は高まりますが、エッジや細かいテクスチャ情報も失われやすくなります。

例えば、ガウシアンフィルタのカーネルサイズを3×3、5×5、7×7と変えてみて、その効果を比較します。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // 画像の読み込み
    cv::Mat grayImage = cv::imread("texture_image.jpg", cv::IMREAD_GRAYSCALE);
    if (grayImage.empty()) {
        std::cerr << "画像が見つかりません。" << std::endl;
        return -1;
    }
    // 3x3ガウシアンフィルタ
    cv::Mat smooth3x3;
    cv::GaussianBlur(grayImage, smooth3x3, cv::Size(3, 3), 0);
    // 5x5ガウシアンフィルタ
    cv::Mat smooth5x5;
    cv::GaussianBlur(grayImage, smooth5x5, cv::Size(5, 5), 0);
    // 7x7ガウシアンフィルタ
    cv::Mat smooth7x7;
    cv::GaussianBlur(grayImage, smooth7x7, cv::Size(7, 7), 0);
    // 画像の表示
    cv::imshow("元画像", grayImage);
    cv::imshow("3x3平滑化", smooth3x3);
    cv::imshow("5x5平滑化", smooth5x5);
    cv::imshow("7x7平滑化", smooth7x7);
    cv::waitKey(0);
    return 0;
}

この例では、cv::GaussianBlurを用いて異なるカーネルサイズの平滑化を行っています。

実際の画像や用途に応じて最適なサイズを選択します。

これらの前処理を適切に行うことで、次の特徴量抽出や分類の段階でより良い結果を得ることができます。

次のステップでは、具体的な特徴量の抽出方法について詳しく解説します。

LBP特徴量抽出

局所二値パターン(LBP)は、画像の局所的なテクスチャ情報を効率的に表現する手法です。

画像の各ピクセル周辺のパターンを符号化し、テクスチャの特徴を抽出します。

ここでは、LBPの原理とその実装方法について詳しく解説します。

LBPの原理

LBPは、対象ピクセルの周囲のピクセル値と中心ピクセル値を比較し、その結果を二進数のパターンに変換します。

これにより、局所的なテクスチャのパターンを符号化し、画像全体の特徴量として利用します。

標準LBPアルゴリズム

標準的なLBPのアルゴリズムは以下の手順で行われます。

  1. 画像の各ピクセルを中心とし、その周囲にP個のサンプル点を配置します。これらの点は半径Rの円周上に配置されます。
  2. 各サンプル点の値と中心ピクセルの値を比較します。
  3. 比較結果が大きい場合は1、小さい場合は0とし、Pビットの二進数パターンを作成します。
  4. この二進数を10進数に変換し、その値を特徴量として記録します。

この方法は、局所的なパターンを効率的に符号化でき、回転やスケールに対して比較的頑健です。

// 標準LBPの実装例(簡略化版)
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <cmath>
int main() {
    // 画像の読み込み(グレースケール)
    cv::Mat grayImage = cv::imread("texture_image.jpg", cv::IMREAD_GRAYSCALE);
    if (grayImage.empty()) {
        std::cerr << "画像が見つかりません。" << std::endl;
        return -1;
    }
    // パラメータ設定
    int P = 8; // 隣接点数
    int R = 1; // 半径
    cv::Mat lbpImage = cv::Mat::zeros(grayImage.size(), CV_8UC1);
    // 画像の中央ピクセルを除き、LBP計算
    for (int y = R; y < grayImage.rows - R; ++y) {
        for (int x = R; x < grayImage.cols - R; ++x) {
            unsigned char center = grayImage.at<uchar>(y, x);
            unsigned char code = 0;
            // 8つのサンプル点の角度
            for (int n = 0; n < P; ++n) {
                double theta = 2.0 * CV_PI * n / P;
                int sampleX = static_cast<int>(x + R * cos(theta));
                int sampleY = static_cast<int>(y - R * sin(theta));
                // サンプル点の値
                unsigned char sampleVal = grayImage.at<uchar>(sampleY, sampleX);
                // 比較してビットを設定
                code |= (sampleVal >= center) << n;
            }
            lbpImage.at<uchar>(y, x) = code;
        }
    }
    // 結果の表示
    cv::imshow("LBP画像", lbpImage);
    cv::waitKey(0);
    return 0;
}

このコードは、8点のサンプルを用いた標準LBPの基本的な実装例です。

回転不変LBP

回転不変LBPは、画像の回転に対して頑健な特徴量を得るための拡張です。

標準LBPは回転に敏感であり、同じパターンでも回転角度によって異なる符号になることがあります。

回転不変LBPは、符号化したパターンのビット列を左回りまたは右回りにシフトさせて、最小の値を持つパターンを代表値とします。

これにより、回転に対して同じ特徴量を得ることが可能です。

// 回転不変LBPの例(符号の最小化)
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <algorithm>
unsigned char rotateRight(unsigned char val, int shift) {
    return (val >> shift) | (val << (8 - shift));
}
unsigned char getUniformPattern(unsigned char pattern) {
    unsigned char minPattern = pattern;
    for (int i = 1; i < 8; ++i) {
        unsigned char rotated = rotateRight(pattern, i);
        if (rotated < minPattern) {
            minPattern = rotated;
        }
    }
    return minPattern;
}
int main() {
    // 画像の読み込み(グレースケール)
    cv::Mat grayImage = cv::imread("texture_image.jpg", cv::IMREAD_GRAYSCALE);
    if (grayImage.empty()) {
        std::cerr << "画像が見つかりません。" << std::endl;
        return -1;
    }
    int P = 8;
    int R = 1;
    cv::Mat lbpRotInvImage = cv::Mat::zeros(grayImage.size(), CV_8UC1);
    for (int y = R; y < grayImage.rows - R; ++y) {
        for (int x = R; x < grayImage.cols - R; ++x) {
            unsigned char center = grayImage.at<uchar>(y, x);
            unsigned char pattern = 0;
            for (int n = 0; n < P; ++n) {
                double theta = 2.0 * CV_PI * n / P;
                int sampleX = static_cast<int>(x + R * cos(theta));
                int sampleY = static_cast<int>(y - R * sin(theta));
                unsigned char sampleVal = grayImage.at<uchar>(sampleY, sampleX);
                pattern |= (sampleVal >= center) << n;
            }
            // 回転不変パターンの取得
            unsigned char uniformPattern = getUniformPattern(pattern);
            lbpRotInvImage.at<uchar>(y, x) = uniformPattern;
        }
    }
    // 結果の表示
    cv::imshow("回転不変LBP画像", lbpRotInvImage);
    cv::waitKey(0);
    return 0;
}

この例では、ビット列を右にシフトさせて最小値を求めることで、回転に対して頑健な特徴量を得ています。

パラメータ設定

LBPの性能は、隣接点数Pと半径Rの設定に大きく依存します。

隣接点数 (P)

  • Pは、中心ピクセルの周囲に配置するサンプル点の数です
  • 一般的には8点P=8や16点P=16がよく使われます
  • Pが増えると、より詳細な局所パターンを捉えられますが、計算コストも増加します

半径 (R)

  • Rは、サンプル点の円周上の距離です
  • 小さな値(例:1ピクセル)では、局所的なパターンを捉えます
  • 大きな値(例:2ピクセルや3ピクセル)では、より広範囲のパターンを捉えることができ、スケールの変化に対しても頑健になります

これらのパラメータは、対象とする画像の性質や目的に応じて調整します。

例えば、細かいテクスチャにはP=8R=1が適している場合が多いです。

一方、より大きなパターンを捉えたい場合は、P=16R=2を選択します。

これでLBP特徴量抽出の基本的な仕組みと実装例について解説しました。

GLCM特徴量抽出

グレイレベル共起行列(GLCM)は、画像のテクスチャ情報を定量的に表現するための代表的な手法です。

画像内のピクセルの空間的な関係性を解析し、さまざまな統計量を抽出します。

ここでは、GLCMの基礎と、その特徴量の計算方法について詳しく解説します。

GLCMの基礎

共起行列の定義

共起行列(Gray-Level Co-occurrence Matrix)は、画像のピクセル値のペアの出現頻度を表す行列です。

具体的には、ある距離と角度において、あるグレイレベルのピクセルと隣接するピクセルの組み合わせの出現回数をカウントします。

例えば、画像のグレイレベルが0からN-1までの範囲にあるとき、共起行列はN×Nの行列となり、行列の要素P(i,j)は、距離と角度においてグレイレベルiとjが隣接している回数を表します。

この行列は、画像のテクスチャのパターンや構造を反映しており、さまざまな統計量の計算に利用されます。

距離と角度の指定

共起行列を計算する際には、ピクセル間の空間的関係を定義するために距離と角度を設定します。

一般的な設定は以下の通りです。

  • 距離(d):ピクセル間の距離。通常は1ピクセル(d=1)を用いますが、2ピクセルや3ピクセルも使用されます
  • 角度(θ):ピクセル間の方向。代表的な角度は0°(水平右向き)、45°、90°(垂直下向き)、135°です

これらのパラメータを変えることで、異なる方向性やスケールのテクスチャ情報を抽出できます。

// 例:距離1ピクセル、角度0度の共起行列の設定
int distance = 1;
double angle = 0.0; // 0度

テクスチャ統計量

共起行列から抽出される統計量は、画像のテクスチャの性質を定量的に表現します。

代表的な統計量は以下の通りです。

コントラスト

画像の局所的な変化の大きさを表す指標です。

値が大きいほど、画像内のコントラストやエッジの強さが高いことを示します。

コントラスト=i,j(ij)2P(i,j)

相関

ピクセル値の間の線形関係を示します。

値が高いと、ピクセル間に一定の関係性があることを意味します。

相関=i,j(iμi)(jμj)P(i,j)σiσj

ただし、μi,μjは平均値、σi,σjは標準偏差です。

エネルギー

共起行列の値の二乗和の平方根で、画像の均一性や平滑性を示します。

値が高いほど、画像は均一であることを示します。

エネルギー=i,jP(i,j)2

同質性(逆差異性)

ピクセル値の差が小さいほど高くなる指標です。

画像の滑らかさや均一性を表します。

同質性=i,jP(i,j)1+|ij|

実装手順

GLCM行列の生成

  1. 画像のグレイレベルを定義(例:8ビット画像なら256レベル)。
  2. 指定した距離と角度に基づき、各ピクセルとその隣接ピクセルのグレイレベルのペアを収集。
  3. それらのペアの出現頻度をカウントし、共起行列を作成。
// 例:距離1、角度0度の共起行列の生成
#include <opencv2/opencv.hpp>
#include <vector>
#include <iostream>
cv::Mat computeGLCM(const cv::Mat& image, int distance, double angle) {
    int levels = 256; // 8ビット画像
    cv::Mat glcm = cv::Mat::zeros(levels, levels, CV_32F);
    int dx = static_cast<int>(round(distance * cos(angle)));
    int dy = static_cast<int>(round(distance * sin(angle)));
    for (int y = 0; y < image.rows - dy; ++y) {
        for (int x = 0; x < image.cols - dx; ++x) {
            int i = image.at<uchar>(y, x);
            int j = image.at<uchar>(y + dy, x + dx);
            glcm.at<float>(i, j) += 1.0f;
        }
    }
    // 正規化
    glcm /= cv::sum(glcm)[0];
    return glcm;
}

統計量の算出

  1. GLCMから各統計量を計算。
  2. それぞれの式に従い、行列の値を集計。
// 例:コントラストの計算
double computeContrast(const cv::Mat& glcm) {
    double contrast = 0.0;
    for (int i = 0; i < glcm.rows; ++i) {
        for (int j = 0; j < glcm.cols; ++j) {
            contrast += (i - j) * (i - j) * glcm.at<float>(i, j);
        }
    }
    return contrast;
}

このようにして、画像のテクスチャの特徴を定量的に表現し、分類や解析に役立てます。

これで、GLCMの基礎と特徴量の抽出方法についての解説を終えます。

特徴量統合と前処理

複数の特徴量抽出手法(例:LBP、GLCM、HOGなど)を用いて得られた特徴ベクトルは、それぞれの情報を統合し、より表現力の高い特徴量セットを作成することが重要です。

また、これらの特徴量は異なるスケールや分布を持つことが多いため、適切な前処理を行うことで、分類器の性能を最大化できます。

ここでは、特徴量の結合とスケーリングについて詳しく解説します。

ベクトル結合

複数の特徴量ベクトルを一つの大きな特徴ベクトルに結合する操作を指します。

例えば、LBPのヒストグラム、GLCMの統計量、HOGの特徴ベクトルをそれぞれ抽出した場合、それらを単純に連結します。

// 例:複数の特徴量ベクトルを結合する
#include <vector>
#include <iostream>
std::vector<double> concatenateFeatures(const std::vector<double>& feature1,
                                       const std::vector<double>& feature2,
                                       const std::vector<double>& feature3) {
    std::vector<double> combined;
    combined.reserve(feature1.size() + feature2.size() + feature3.size());
    combined.insert(combined.end(), feature1.begin(), feature1.end());
    combined.insert(combined.end(), feature2.begin(), feature2.end());
    combined.insert(combined.end(), feature3.begin(), feature3.end());
    return combined;
}
int main() {
    // 仮の特徴量ベクトル
    std::vector<double> lbpHist = {0.1, 0.2, 0.3};
    std::vector<double> glcmStats = {0.5, 0.6};
    std::vector<double> hogFeatures = {0.7, 0.8, 0.9};
    // 結合
    std::vector<double> featureVector = concatenateFeatures(lbpHist, glcmStats, hogFeatures);
    // 結果表示
    for (double val : featureVector) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
    return 0;
}
0.1 0.2 0.3 0.5 0.6 0.7 0.8 0.9 

この操作により、異なる特徴量の情報を一つのベクトルにまとめ、分類器に入力します。

スケーリング

結合した特徴量は、異なるスケールや分布を持つことが多いため、スケーリングを行うことで、学習の安定性や精度を向上させることができます。

代表的なスケーリング手法には以下の2つがあります。

標準化 (Z-score)

各特徴量の平均値を0、標準偏差を1に変換します。

これにより、すべての特徴量が同じ尺度で表現され、学習アルゴリズムの収束が早くなります。

z=xμσ

  • x:元の特徴量
  • μ:特徴量の平均値
  • σ:標準偏差
// 標準化の例
#include <vector>
#include <numeric>
#include <cmath>
#include <iostream>
void standardize(std::vector<double>& data) {
    double sum = std::accumulate(data.begin(), data.end(), 0.0);
    double mean = sum / data.size();
    double accum = 0.0;
    for (double val : data) {
        accum += (val - mean) * (val - mean);
    }
    double stddev = std::sqrt(accum / data.size());
    for (double& val : data) {
        val = (val - mean) / stddev;
    }
}
int main() {
    std::vector<double> features = {10.0, 20.0, 15.0, 25.0, 30.0};
    standardize(features);
    // 結果表示
    for (double val : features) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
    return 0;
}
-1.41421 0 -0.707107 0.707107 1.41421 

最小 -最大正規化

特徴量を指定した範囲(通常は0から1)にスケーリングします。

異なる特徴量の値の範囲を揃えることで、学習の効率化や比較が容易になります。

x=xmin(x)max(x)min(x)

// 最小  -最大正規化の例
#include <vector>
#include <algorithm>
#include <iostream>
void minMaxNormalize(std::vector<double>& data) {
    auto minmax = std::minmax_element(data.begin(), data.end());
    double minVal = *minmax.first;
    double maxVal = *minmax.second;
    for (double& val : data) {
        val = (val - minVal) / (maxVal - minVal);
    }
}
int main() {
    std::vector<double> features = {10.0, 20.0, 15.0, 25.0, 30.0};
    minMaxNormalize(features);
    // 結果表示
    for (double val : features) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
    return 0;
}
0 0.5 0.25 0.75 1 

これらのスケーリング手法は、特徴量の性質や分類器の種類に応じて使い分けます。

標準化は正規分布に近いデータに適しており、最小 -最大正規化は値の範囲を明示的に制御したい場合に有効です。

これらの操作を適切に行うことで、複数の特徴量を効果的に統合し、分類モデルの性能を最大化できます。

次元削減 (PCA)

主成分分析(Principal Component Analysis, PCA)は、多次元の特徴量空間において、情報をできるだけ損なわずに次元を削減する手法です。

高次元のデータは計算コストが増大し、過学習のリスクも高まるため、適切な次元削減はモデルの性能向上に寄与します。

ここでは、PCAの目的とその具体的な実装方法について詳しく解説します。

PCAの目的と効果

PCAの主な目的は、次の通りです。

  • 次元削減:高次元の特徴空間から、情報の大部分を保持したまま低次元の空間に写像します
  • ノイズ除去:不要な変動やノイズを除去し、重要な特徴を抽出します
  • 可視化:高次元データを2次元や3次元に投影し、データの分布やクラスタリングを視覚的に理解しやすくします

効果としては、計算コストの削減、分類器の性能向上、過学習の抑制などが挙げられます。

実装方法

特徴行列の構築

まず、複数のサンプルから抽出した特徴量を行列にまとめます。

各サンプルの特徴ベクトルを行に並べた行列を作成します。

// 例:特徴行列の構築
#include <vector>
#include <iostream>
int main() {
    // 例:3サンプル、4特徴量のデータ
    std::vector<std::vector<double>> featureMatrix = {
        {1.0, 2.0, 3.0, 4.0},
        {2.0, 3.0, 4.0, 5.0},
        {3.0, 4.0, 5.0, 6.0}
    };
    // ここでは、行列の平均や分散計算に進む
    // 実際にはEigenやOpenCVのcv::Matを使うと便利
    return 0;
}

この行列を用いて、次のステップに進みます。

固有値/固有ベクトルの計算

次に、特徴行列の共分散行列を計算し、その固有値と固有ベクトルを求めます。

固有ベクトルは、データの分散が最大となる方向を示し、これが主成分となります。

// Eigenライブラリを用いた例
#include "Eigen/Dense"
#include <iostream>
int main() {
    Eigen::MatrixXd data(3, 4);
    data << 1, 2, 3, 4,
            2, 3, 4, 5,
            3, 4, 5, 6;
    // 各特徴量の平均を引く
    Eigen::RowVectorXd mean = data.colwise().mean();
    Eigen::MatrixXd centered = data.rowwise() - mean;
    // 共分散行列の計算
    Eigen::MatrixXd cov = (centered.adjoint() * centered) / double(data.rows() - 1);
    // 固有値・固有ベクトルの計算
    Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> eig(cov);
    Eigen::VectorXd eigenvalues = eig.eigenvalues();
    Eigen::MatrixXd eigenvectors = eig.eigenvectors();
    // 固有値と固有ベクトルの出力
    std::cout << "固有値:\n" << eigenvalues << std::endl;
    std::cout << "固有ベクトル:\n" << eigenvectors << std::endl;
    return 0;
}
固有値:
-4.44089e-16
-2.16412e-16
           0
           4
固有ベクトル:
           0    -0.866025            0          0.5
    0.816497     0.288675 -8.75605e-17          0.5
   -0.408248     0.288675    -0.707107          0.5
   -0.408248     0.288675     0.707107          0.5

Eigenライブラリは、固有値・固有ベクトルの計算に便利です。

Eigenライブラリは別途導入が必要ですが、ヘッダーオンリーなため、ライブラリのリンクなどは必要ありません。

これを導入すると使えるようになる

主成分の選択基準

固有値の大きさに基づいて、重要な主成分を選択します。

一般的な基準は以下の通りです。

  • 累積寄与率:全固有値の合計に対する、上位k個の固有値の合計の割合を計算し、一定の閾値(例:95%)を超えるまで主成分を選択します
  • 固有値の閾値:固有値が一定の値以上のものだけを選択します
// Eigenライブラリを用いた例:累積寄与率の計算
#include <iostream>
#include <numeric>
#include <vector>
#include "Eigen/Dense"
int main() {
    Eigen::VectorXd eigenvalues(4);
    eigenvalues << 3.0, 2.0, 0.5, 0.2;
    double total = eigenvalues.sum();
    std::vector<double> cumulative;
    double sum = 0.0;
    for (int i = 0; i < eigenvalues.size(); ++i) {
        sum += eigenvalues(i);
        cumulative.push_back(sum / total);
    }
    // 累積寄与率が95%以上になる最小の主成分数を探す
    int numComponents = 0;
    for (size_t i = 0; i < cumulative.size(); ++i) {
        if (cumulative[i] >= 0.95) {
            numComponents = i + 1;
            break;
        }
    }
    std::cout << "選択する主成分の数: " << numComponents << std::endl;
    return 0;
}
選択する主成分の数: 3

このようにして、重要な情報を保持しつつ次元を削減します。

これで、PCAによる次元削減の基本的な流れと実装例について解説しました。

SVMによる分類

サポートベクターマシン(SVM)は、二値分類問題において高い性能を発揮する強力な機械学習モデルです。

非線形なデータにも対応できるカーネル関数を用いることで、複雑な決定境界を学習します。

ここでは、SVMのモデル選択、ハイパーパラメータ調整、学習とモデルの保存について詳しく解説します。

モデル選択

線形カーネル

線形カーネルは、特徴空間においてデータが線形に分離可能な場合に適しています。

計算コストが低く、大規模なデータセットにも適用しやすいのが特徴です。

// OpenCVのSVMで線形カーネルを設定する例
#include <opencv2/opencv.hpp>
#include <opencv2/ml.hpp>
int main() {
    cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
    svm->setType(cv::ml::SVM::C_SVC);
    svm->setKernel(cv::ml::SVM::LINEAR);
    // 他のパラメータ設定や学習は後述
    return 0;
}

RBFカーネル

RBF(Radial Basis Function)カーネルは、非線形な決定境界を学習できるため、多くの実問題に適しています。

データの局所的な構造を捉えることができ、柔軟性が高いです。

// RBFカーネルの設定例
#include <opencv2/opencv.hpp>
#include <opencv2/ml.hpp>
int main() {
    cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
    svm->setType(cv::ml::SVM::C_SVC);
    svm->setKernel(cv::ml::SVM::RBF);
    // 他のパラメータ設定や学習は後述
    return 0;
}

ハイパーパラメータ調整

Cパラメータ

Cは誤分類を許容するペナルティの重み付けを制御します。

値が大きいほど、誤分類を避けるためにマージンを狭めて学習しますが、過剰に大きいと過学習のリスクがあります。

// Cパラメータの設定例
svm->setC(1.0); // 例:C=1.0

γパラメータ

γはRBFカーネルのパラメータで、データ点の影響範囲を決定します。

小さい値は広範囲にわたる影響、大きい値は局所的な影響を与えます。

// γパラメータの設定例
svm->setGamma(0.5); // 例:γ=0.5

ハイパーパラメータは、グリッドサーチや交差検証を用いて最適値を見つけることが一般的です。

学習と保存

トレーニングデータ準備

学習には、特徴量ベクトルと対応するラベルが必要です。

OpenCVのcv::ml::TrainDataを用いてデータを準備します。

// 例:特徴量とラベルの準備
#include <opencv2/opencv.hpp>
#include <opencv2/ml.hpp>
int main() {
    cv::Mat trainingData; // 特徴量行列(サンプル数×特徴量数)
    cv::Mat labels;       // ラベル(サンプル数×1)
    // 例:データの読み込みや作成は省略
    // trainingData = ...
    // labels = ...
    cv::Ptr<cv::ml::TrainData> tData = cv::ml::TrainData::create(trainingData, cv::ml::ROW_SAMPLE, labels);
    return 0;
}

モデルの保存・ロード

学習済みモデルはファイルに保存し、必要に応じて再利用できます。

// モデルの保存
svm->save("svm_model.xml");
// モデルのロード
cv::Ptr<cv::ml::SVM> svm_loaded = cv::ml::SVM::load("svm_model.xml");

これにより、学習済みモデルを効率的に管理し、分類処理に利用できます。

これで、SVMによる分類の基本的な流れと設定方法について解説しました。

分類モデルの評価

モデルの性能を正確に把握するためには、適切な評価指標を用いることが重要です。

特に、分類問題では、正解率だけでなく、クラスの偏りや誤分類の種類に応じた指標も考慮する必要があります。

ここでは、代表的な評価指標とその可視化方法について詳しく解説します。

評価指標

正確度 (Accuracy)

正確度は、全予測のうち正しく分類された割合を示します。

計算式は以下の通りです。

Accuracy=正解数総サンプル数

ただし、クラスの不均衡がある場合には、誤解を招くこともあります。

// 例:正確度の計算
#include <iostream>
#include <vector>
int main() {
    int correct = 85; // 正解数
    int total = 100;  // 総サンプル数
    double accuracy = static_cast<double>(correct) / total;
    std::cout << "正確度: " << accuracy * 100 << "%" << std::endl;
    return 0;
}

再現率 (Recall)

再現率は、実際に陽性(正例)であるサンプルのうち、正しく陽性と予測できた割合です。

特に、誤って陰性と判定される誤りを避けたい場合に重要です。

Recall=真陽性真陽性+偽陰性

// 例:再現率の計算
#include <iostream>
int main() {
    int true_positive = 40;
    int false_negative = 10;
    double recall = static_cast<double>(true_positive) / (true_positive + false_negative);
    std::cout << "再現率: " << recall * 100 << "%" << std::endl;
    return 0;
}

F1スコア

F1スコアは、精度(Precision)と再現率の調和平均であり、不均衡なクラス分布に対してバランスの取れた評価指標です。

F1=2×Precision×RecallPrecision+Recall

// 例:F1スコアの計算
#include <iostream>
int main() {
    double precision = 0.8; // 仮の値
    double recall = 0.75;   // 仮の値
    double f1_score = 2 * (precision * recall) / (precision + recall);
    std::cout << "F1スコア: " << f1_score << std::endl;
    return 0;
}

混同行列の可視化

混同行列は、実際のクラスと予測クラスの関係を表す表です。

二値分類の場合は4つの値(TP、FP、FN、TN)を持ち、多クラス分類ではクラスごとに行と列を持つ行列となります。

  • TP(True Positive):正例を正例と予測
  • FP(False Positive):負例を誤って正例と予測
  • FN(False Negative):正例を誤って負例と予測
  • TN(True Negative):負例を負例と予測
// 混同行列の例(出力のみ)
#include <iostream>
#include <vector>
int main() {
    // 例:2クラスの混同行列
    std::vector<std::vector<int>> confusionMatrix = {
        {50, 10},
        {5, 35}
    };
    std::cout << "混同行列:\n";
    std::cout << "          予測正例  予測負例\n";
    std::cout << "実際正例   " << confusionMatrix[0][0] << "        " << confusionMatrix[0][1] << "\n";
    std::cout << "実際負例   " << confusionMatrix[1][0] << "        " << confusionMatrix[1][1] << "\n";
    return 0;
}

ROC曲線とAUC

ROC(Receiver Operating Characteristic)曲線は、閾値を変化させたときの真陽性率(TPR)と偽陽性率(FPR)をプロットしたものです。

AUC(Area Under Curve)は、その曲線の下の面積であり、モデルの判別能力を数値化します。

  • TPR(感度):再現率と同じ
  • FPR:誤って陽性と判定された負例の割合
// ROC曲線とAUCの計算は、外部ライブラリやツールを用いることが一般的です。
// 例:Pythonのscikit-learnやRのpROCパッケージがよく使われます。
// C++では、OpenCVや他のライブラリを用いて計算・可視化します。

ROC曲線の描画とAUCの計算は、モデルの閾値を変化させながらTPRとFPRを計測し、その点をプロットします。

AUCは、0.5(ランダム予測)から1.0(完全分類)までの範囲で評価されます。

これらの評価指標と可視化手法を用いて、分類モデルの性能を総合的に判断し、最適なモデル選択やパラメータ調整に役立てます。

パラメータチューニング手法

モデルの性能を最大化するためには、ハイパーパラメータの適切な設定が不可欠です。

パラメータチューニングは、最適なハイパーパラメータの組み合わせを見つけるための重要な工程です。

ここでは、代表的な手法であるグリッドサーチと交差検証(k-fold)について詳しく解説します。

グリッドサーチ

グリッドサーチは、あらかじめ設定した複数のハイパーパラメータの候補値の全ての組み合わせを網羅的に試し、最も良い性能を示すパラメータの組み合わせを選択する方法です。

  • 手順
  1. チューニングしたいハイパーパラメータと、その候補値のリストを用意します。
  2. それらの全組み合わせを生成します。
  3. 各組み合わせについて、モデルを学習させ、評価指標(例:精度、F1スコア)を計算します。
  4. 最も良い評価指標を示したパラメータの組み合わせを選びます。
  • メリット
    • 全ての候補を試すため、最適解を見つけやすい
  • デメリット
    • 計算コストが高くなる場合があります
// 例:OpenCVのGridSearchは標準では提供されていませんが、手動で実装可能
// 例示のための擬似コード
#include <vector>
#include <iostream>
int main() {
    std::vector<double> C_values = {0.1, 1.0, 10.0};
    std::vector<double> gamma_values = {0.01, 0.1, 1.0};
    double bestScore = -1.0;
    double bestC = 0.0;
    double bestGamma = 0.0;
    for (double C : C_values) {
        for (double gamma : gamma_values) {
            // SVMの設定と学習
            // cv::ml::SVM::create() などを用いて設定
            // 学習と評価を行い、スコアを計算
            double score = evaluateModel(C, gamma); // 仮の関数
            if (score > bestScore) {
                bestScore = score;
                bestC = C;
                bestGamma = gamma;
            }
        }
    }
    std::cout << "最適なC: " << bestC << ", 最適なγ: " << bestGamma << std::endl;
    return 0;
}

交差検証 (k-fold)

交差検証は、データセットをk個のサブセット(fold)に分割し、モデルの汎用性を評価する手法です。

特に、モデルの過学習を防ぎ、パラメータの安定性を確認するのに有効です。

  • 手順
  1. データをランダムにシャッフルし、等しい大きさのk個のサブセットに分割します。
  2. そのうちの1つを検証用データとし、残りを学習用データとしてモデルを学習します。
  3. 検証用データでモデルの性能を評価します。
  4. これをk回繰り返し、各回の評価結果の平均を取ります。
  • メリット
    • データの偏りに左右されず、モデルの汎用性を正確に評価できます
  • デメリット
    • 計算コストが増加します
// 例:OpenCVには直接的なk-fold交差検証の関数はないため、自前で実装
#include <algorithm>
#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
void kFoldCrossValidation(const std::vector<cv::Mat>& data,
                          const std::vector<int>& labels, int k) {
    int foldSize = data.size() / k;
    std::vector<double> scores;
    for (int i = 0; i < k; ++i) {
        // データの分割
        std::vector<cv::Mat> trainData, testData;
        std::vector<int> trainLabels, testLabels;
        for (int j = 0; j < data.size(); ++j) {
            if (j >= i * foldSize && j < (i + 1) * foldSize) {
                testData.push_back(data[j]);
                testLabels.push_back(labels[j]);
            } else {
                trainData.push_back(data[j]);
                trainLabels.push_back(labels[j]);
            }
        }
        // 学習と評価
        double score =
            trainAndEvaluateModel(trainData, trainLabels, testData, testLabels);
        scores.push_back(score);
    }
    // 評価結果の平均
    double sum = std::accumulate(scores.begin(), scores.end(), 0.0);
    double meanScore = sum / scores.size();
    std::cout << "平均評価スコア: " << meanScore << std::endl;
}
int main() {
    // データとラベルの準備は省略
    std::vector<cv::Mat> data; // 例:特徴量の行列
    std::vector<int> labels;   // 例:クラスラベル
    int k = 5;                 // 5-fold交差検証
    kFoldCrossValidation(data, labels, k);
    return 0;
}

これらの手法を組み合わせて、最適なハイパーパラメータを見つけ出し、モデルの汎用性と性能を最大化します。

パフォーマンス最適化

画像処理や機械学習の処理は計算負荷が高いため、効率的なパフォーマンス最適化が重要です。

特に、大規模なデータやリアルタイム処理を行う場合には、並列処理やハードウェアの最適化を駆使して処理速度を向上させる必要があります。

ここでは、マルチスレッド処理、メモリ管理、そしてSIMD命令の活用について詳しく解説します。

マルチスレッド処理

マルチスレッド処理は、複数のCPUコアを活用して並列に処理を行うことで、処理時間を大幅に短縮します。

OpenCVやC++標準ライブラリには、スレッドを用いた並列処理を容易に行える仕組みが備わっています。

  • 実装例
    • OpenCVのcv::parallel_for_を用いると、画像のピクセル処理や特徴量抽出を並列化できます
    • C++11以降のstd::threadstd::asyncを用いて、処理を複数のスレッドに分散させることも可能です
// OpenCVのparallel_for_を用いた例
#include <opencv2/opencv.hpp>
#include <opencv2/core/parallel/parallel.hpp>
class MyParallelLoop : public cv::ParallelLoopBody {
public:
    void operator()(const cv::Range& range) const override {
        for (int i = range.start; i < range.end; ++i) {
            // ピクセルごとの処理
            // 例:LBPやGLCMの計算
        }
    }
};
int main() {
    cv::Mat image; // 画像データ
    // 画像の前処理や特徴抽出の並列化
    cv::parallel_for_(cv::Range(0, image.rows), MyParallelLoop());
    return 0;
}
  • メリット
    • CPUコアを最大限に活用でき、処理速度が向上
    • 大規模データの処理に適しています
  • 注意点
    • 競合状態やデータ競合を避けるために、適切な排他制御やデータ分割が必要でしょう

メモリ管理

効率的なメモリ管理は、パフォーマンス向上の鍵です。

不要なメモリ割り当てや解放を避け、データの局所性を高めることで、キャッシュ効率を向上させることができます。

  • ポイント
    • 事前に必要なメモリを確保し、再利用します
    • 大きなデータは連続したメモリブロックに格納し、キャッシュミスを減らす
    • OpenCVのcv::Matは、参照カウント方式でメモリ管理を行うため、コピーコストを抑えられます
// cv::Matのメモリ管理例
cv::Mat src = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
cv::Mat dst = src; // 参照カウント増加、コピーは遅くない
// 必要に応じて深いコピー
cv::Mat deepCopy;
src.copyTo(deepCopy);
  • メモリプールの利用
    • 頻繁に同じサイズのメモリを確保・解放する場合は、メモリプールを利用して効率化

SIMD命令の活用

Single Instruction Multiple Data(SIMD)は、1つの命令で複数のデータを同時に処理できる命令セットです。

これにより、ループ内の演算を高速化し、処理時間を短縮します。

  • 代表的な命令セット
    • SSE(Streaming SIMD Extensions)
    • AVX(Advanced Vector Extensions)
    • NEON(ARMアーキテクチャ)
  • 活用例
    • OpenCVは、内部的にSIMD命令を利用しており、cv::halcv::oclを通じて最適化された関数を呼び出すことが可能です
    • 自前でSIMD命令を使う場合は、インラインアセンブリやコンパイラの自動ベクトル化を利用します
// AVXを用いた例(コンパイラの自動ベクトル化を促すための例)
#include <immintrin.h>
#include <iostream>
int main() {
    __m256d vec1 = _mm256_set1_pd(1.0);
    __m256d vec2 = _mm256_set1_pd(2.0);
    __m256d result = _mm256_add_pd(vec1, vec2);
    double res[4];
    _mm256_storeu_pd(res, result);
    for (int i = 0; i < 4; ++i) {
        std::cout << res[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}
  • 注意点
    • SIMD命令は、データのアラインメントや型に依存するため、適切なデータ整列と型変換が必要でしょう
    • 高度な最適化には、コンパイラの最適化フラグや専用ライブラリの利用が推奨されます

これらの最適化手法を適切に組み合わせることで、画像処理や機械学習のパイプライン全体の処理速度を大幅に向上させることが可能です。

トラブルシューティング

画像処理や機械学習の実装においては、さまざまな問題が発生することがあります。

これらの問題を迅速に解決し、安定したシステム運用を維持するためには、原因の特定と適切な対策が必要です。

ここでは、特徴量抽出の異常値、モデルの収束しない問題、メモリ不足や処理遅延について詳しく解説します。

特徴量抽出の異常値

特徴量抽出の過程で異常値(アウトライア)が発生すると、モデルの性能に悪影響を及ぼすことがあります。

異常値は、ノイズや画像の破損、計算ミスなどが原因で生じることが多いです。

  • 原因
    • 画像のノイズや破損
    • パラメータの誤設定(例:LBPの半径やサンプル点数の不適切な値)
    • 計算時のオーバーフローやアンダーフロー
  • 対策
    • 前処理でノイズ除去や正規化を徹底する
    • 異常値を検出し、除去または修正する(例:ZスコアやIQRを用いたアウトライア検出)
    • パラメータの見直しと調整
// 異常値検出例(Zスコアを用いる)
#include <vector>
#include <cmath>
#include <iostream>
void detectOutliers(const std::vector<double>& data, double threshold = 3.0) {
    double sum = 0.0;
    for (double v : data) sum += v;
    double mean = sum / data.size();
    double accum = 0.0;
    for (double v : data) accum += (v - mean) * (v - mean);
    double stddev = std::sqrt(accum / data.size());
    for (size_t i = 0; i < data.size(); ++i) {
        double z = (data[i] - mean) / stddev;
        if (std::abs(z) > threshold) {
            std::cout << "異常値検出: インデックス " << i << " 値 " << data[i] << std::endl;
        }
    }
}

モデル収束しない問題

学習中にモデルが収束しない場合、以下の原因と対策を検討します。

  • 原因
    • ハイパーパラメータの設定不適切(例:Cやγが極端に大きいまたは小さい)
    • データのスケールがバラバラで正規化されていない
    • データにノイズや外れ値が多い
    • 学習データが少なく、情報不足
  • 対策
    • ハイパーパラメータの調整(グリッドサーチやランダムサーチを活用)
    • 特徴量の正規化や標準化
    • データのクリーニングと前処理
    • 学習データの増加やデータ拡張
// 例:SVMのハイパーパラメータ調整
#include <opencv2/opencv.hpp>
#include <opencv2/ml.hpp>
void tuneHyperparameters() {
    cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
    svm->setType(cv::ml::SVM::C_SVC);
    svm->setKernel(cv::ml::SVM::RBF);
    svm->setC(1.0);
    svm->setGamma(0.5);
    // 追加の調整や交差検証を行う
}

メモリ不足や処理遅延

大量のデータや複雑な処理により、メモリ不足や処理遅延が発生することがあります。

  • 原因
    • 大きな画像や特徴量のメモリ確保
    • 不必要なデータの保持やコピー
    • 非効率なループやアルゴリズムの使用
    • ハードウェアの制約(メモリ容量やCPU性能)
  • 対策
    • メモリの事前確保と再利用
    • 不要なデータの解放や遅延評価の導入
    • 画像や特徴量の解像度を下げる
    • 並列処理やハードウェアアクセラレーション(GPUやSIMD)の活用
    • 逐次処理やバッチ処理の導入
// メモリの効率的な管理例
#include <opencv2/opencv.hpp>
int main() {
    cv::Mat image = cv::imread("large_image.jpg");
    // 画像の解像度を縮小
    cv::Mat resized;
    cv::resize(image, resized, cv::Size(), 0.5, 0.5);
    // 必要な処理を行う
    return 0;
}
  • パフォーマンス監視
    • プロファイラやログを用いて、処理時間やメモリ使用量を監視し、ボトルネックを特定

これらのトラブルシューティングのポイントを押さえることで、システムの安定性と効率性を向上させ、開発や運用の効率化を図ることができます。

まとめ

この記事では、画像の特徴量抽出方法(LBP、GLCM)、次元削減(PCA)、分類(SVM)、評価指標、パラメータ調整、最適化技術、そしてトラブルシューティングについて詳しく解説しました。

これらの知識を活用すれば、効率的かつ高精度な画像分類システムを構築できるようになります。

関連記事

Back to top button
目次へ