OpenCV

【C++】OpenCVで画像を自在に増やすデータ拡張テクニック集

C++とOpenCVで画像を回転、平行移動、スケーリング、反転、色調変換など多様に拡張すると、少量データでも汎化性能を高めやすいです。

cv::warpAffinecv::resizeといった標準APIのみで実装でき、追加ライブラリは不要。

マルチスレッドやGPUを併用すれば大規模データでもリアルタイム処理が期待できます。

目次から探す
  1. ランダム回転
  2. 平行移動
  3. スケーリング
  4. 反転
  5. 彩度・明度・コントラスト調整
  6. ノイズ付加
  7. ぼかしとシャープ化
  8. 部分切り抜き・ランダムクロップ
  9. アフィン変換
  10. パースペクティブ変換
  11. 擬似的照明変化
  12. 複合パイプライン構築
  13. セグメンテーションマスク対応
  14. キーポイント・ランドマーク対応
  15. 大規模データへの適用
  16. データ不均衡対策としての拡張比率
  17. オンライン学習との連携
  18. まとめ

ランダム回転

画像のランダム回転は、画像認識や物体検出のモデルに対して回転に対する頑健性を持たせるための基本的なデータ拡張手法です。

OpenCVではcv::getRotationMatrix2D関数を使って回転行列を生成し、cv::warpAffine関数で画像を回転させます。

ここでは、回転行列の設定方法や角度のサンプリング、境界処理の選択肢、さらに物体検出などで重要なバウンディングボックスの再計算方法について詳しく解説します。

cv::getRotationMatrix2Dの設定項目

cv::getRotationMatrix2Dは、画像の回転に必要な2×3のアフィン変換行列を生成する関数です。

主なパラメータは以下の3つです。

  • center: 回転の中心点をcv::Point2fで指定します。通常は画像の中心(cols/2, rows/2)を指定します
  • angle: 回転角度を度数法で指定します。正の値は反時計回りの回転を意味します
  • scale: 拡大縮小率を指定します。1.0で等倍、1.2なら20%拡大、0.8なら20%縮小となります

例えば、画像の中心を回転中心にして30度回転、スケール1.0で回転行列を作成するコードは以下のようになります。

cv::Point2f center(image.cols / 2.0f, image.rows / 2.0f);
double angle = 30.0;
double scale = 1.0;
cv::Mat rotMat = cv::getRotationMatrix2D(center, angle, scale);

この行列は、cv::warpAffineに渡すことで画像を回転させることができます。

角度分布のサンプリング方法

ランダム回転では、回転角度をどのように決めるかが重要です。

一般的には、回転角度を一定範囲内の連続的な値からランダムにサンプリングします。

例えば、-30度から+30度の範囲で均一分布からサンプリングする方法がよく使われます。

C++の標準ライブラリの乱数生成器を使う例を示します。

#include <random>
// 乱数生成器の初期化
std::random_device rd;
std::mt19937 gen(rd());
// -30度から30度の範囲で均一分布
std::uniform_real_distribution<> dis(-30.0, 30.0);
// ランダムな角度を取得
double angle = dis(gen);

この方法で得られた角度をcv::getRotationMatrix2Dに渡すことで、毎回異なる角度で画像を回転できます。

また、角度の分布を工夫することで、特定の角度に偏らせたり、回転角度を離散的に設定したりすることも可能です。

例えば、90度刻みの回転だけを行いたい場合は、以下のように離散的にサンプリングします。

std::uniform_int_distribution<> dis(0, 3);
int k = dis(gen);
double angle = 90.0 * k;

境界処理の選択肢

画像を回転すると、回転後の画像の四隅に元画像の外側の領域が現れます。

この部分のピクセル値をどう扱うかが境界処理(border mode)です。

OpenCVのcv::warpAffine関数では、borderModeパラメータで指定します。

主な境界処理の種類は以下の通りです。

定数名説明
cv::BORDER_CONSTANT指定した色(通常は黒)で埋める
cv::BORDER_REPLICATE端のピクセル値を繰り返す
cv::BORDER_REFLECT端のピクセルを鏡像反転して埋める
cv::BORDER_WRAP画像を繰り返す(ラップアラウンド)

BORDER_CONSTANTとBORDER_REFLECTの違い

特に多く使われるのがBORDER_CONSTANTBORDER_REFLECTです。

  • BORDER_CONSTANT

回転後の画像の外側領域を一定の色(デフォルトは黒)で埋めます。

画像の背景が黒や単色の場合は自然に見えますが、背景が複雑な場合は不自然な黒い領域ができてしまいます。

  • BORDER_REFLECT

画像の端のピクセルを鏡像反転して埋める方法です。

自然な境界を作りやすく、画像の連続性を保ちやすいので、データ拡張に適しています。

例えば、以下のコードはBORDER_REFLECTを使って回転処理を行っています。

cv::warpAffine(src, dst, rotMat, src.size(), cv::INTER_LINEAR, cv::BORDER_REFLECT);

このように境界処理を工夫することで、回転後の画像の不自然な部分を減らし、モデルの学習に悪影響を与えにくくできます。

バウンディングボックスの再計算手順

物体検出タスクなどで、画像の回転に伴いバウンディングボックス(物体の矩形領域)も正しく変換する必要があります。

単に画像を回転させるだけでなく、バウンディングボックスの座標も回転行列を使って変換しなければなりません。

バウンディングボックスの表現

通常、バウンディングボックスは左上の座標と右下の座標、または左上の座標と幅・高さで表されます。

ここでは左上(x1, y1)、右下(x2, y2)の形式を想定します。

変換手順

  1. 4つの頂点を取得

バウンディングボックスの4つの頂点をcv::Point2fの配列に格納します。

  1. 回転行列を拡張

cv::getRotationMatrix2Dは2×3行列ですが、座標変換には3×3の同次変換行列が必要です。

2×3行列を3×3行列に拡張します。

  1. 頂点を変換

4つの頂点を同次座標に変換し、3×3行列を使って回転させます。

  1. 新しいバウンディングボックスを計算

変換後の4点のx座標の最小値・最大値、y座標の最小値・最大値を求め、新しいバウンディングボックスを作成します。

#include <opencv2/opencv.hpp>
#include <vector>
// バウンディングボックスの4頂点を回転変換する関数
std::vector<cv::Point2f> rotateBoundingBox(const cv::Rect& bbox, const cv::Mat& rotMat) {
    // 4頂点を取得
    std::vector<cv::Point2f> points(4);
    points[0] = cv::Point2f(bbox.x, bbox.y);
    points[1] = cv::Point2f(bbox.x + bbox.width, bbox.y);
    points[2] = cv::Point2f(bbox.x + bbox.width, bbox.y + bbox.height);
    points[3] = cv::Point2f(bbox.x, bbox.y + bbox.height);
    // 2x3行列を3x3に拡張
    cv::Mat rotMat33 = cv::Mat::eye(3, 3, rotMat.type());
    rotMat.copyTo(rotMat33(cv::Rect(0, 0, 3, 2)));
    // 変換後の頂点を格納
    std::vector<cv::Point2f> rotatedPoints(4);
    for (int i = 0; i < 4; ++i) {
        cv::Mat pt = (cv::Mat_<double>(3,1) << points[i].x, points[i].y, 1);
        cv::Mat rpt = rotMat33 * pt;
        rotatedPoints[i] = cv::Point2f(static_cast<float>(rpt.at<double>(0,0)), static_cast<float>(rpt.at<double>(1,0)));
    }
    return rotatedPoints;
}
// 新しいバウンディングボックスを計算
cv::Rect getBoundingBoxFromPoints(const std::vector<cv::Point2f>& points) {
    float minX = points[0].x;
    float minY = points[0].y;
    float maxX = points[0].x;
    float maxY = points[0].y;
    for (const auto& pt : points) {
        if (pt.x < minX) minX = pt.x;
        if (pt.y < minY) minY = pt.y;
        if (pt.x > maxX) maxX = pt.x;
        if (pt.y > maxY) maxY = pt.y;
    }
    return cv::Rect(cv::Point2f(minX, minY), cv::Point2f(maxX, maxY));
}
int main() {
    cv::Mat image = cv::imread("image.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    // 元のバウンディングボックス(例)
    cv::Rect bbox(50, 50, 100, 150);
    // 回転行列を作成(中心は画像中心、角度は30度)
    cv::Point2f center(image.cols / 2.0f, image.rows / 2.0f);
    double angle = 30.0;
    double scale = 1.0;
    cv::Mat rotMat = cv::getRotationMatrix2D(center, angle, scale);
    // 画像を回転
    cv::Mat rotatedImage;
    cv::warpAffine(image, rotatedImage, rotMat, image.size(), cv::INTER_LINEAR, cv::BORDER_REFLECT);
    // バウンディングボックスの頂点を回転
    std::vector<cv::Point2f> rotatedPoints = rotateBoundingBox(bbox, rotMat);
    // 新しいバウンディングボックスを計算
    cv::Rect newBbox = getBoundingBoxFromPoints(rotatedPoints);
    // 回転後の画像にバウンディングボックスを描画
    cv::rectangle(rotatedImage, newBbox, cv::Scalar(0, 255, 0), 2);
    cv::imwrite("rotated_with_bbox.jpg", rotatedImage);
    return 0;
}
変換サンプル

このコードでは、元のバウンディングボックスを回転行列で変換し、新しいバウンディングボックスを計算して回転後の画像に描画しています。

これにより、回転した画像に対応した正しい物体領域を得られます。

以上のように、ランダム回転のデータ拡張では、回転行列の生成、角度のランダムサンプリング、境界処理の選択、そしてバウンディングボックスの正しい変換が重要なポイントです。

これらを適切に実装することで、モデルの回転に対する頑健性を高めることができます。

平行移動

2×3行列の生成と適用

平行移動は画像を水平方向や垂直方向にずらす変換で、OpenCVでは2×3のアフィン変換行列を使って実現します。

行列は以下の形で表されます。

[10dx01dy]

ここで、dxは水平方向の移動量(ピクセル単位)、dyは垂直方向の移動量です。

この行列をcv::warpAffine関数に渡すことで画像を平行移動できます。

具体的な生成例は以下の通りです。

cv::Mat transMat = (cv::Mat_<double>(2, 3) << 1, 0, dx, 0, 1, dy);

この行列を使って画像を変換するコード例は以下のようになります。

cv::Mat dst;
cv::warpAffine(src, dst, transMat, src.size(), cv::INTER_LINEAR, cv::BORDER_REFLECT);

src.size()は出力画像のサイズを元画像と同じに指定しています。

cv::INTER_LINEARは補間方法で、BORDER_REFLECTは境界処理の一例です。

ピクセルオフセット範囲の決定指針

平行移動のオフセット量dxdyは、画像の大きさやタスクの特性に応じて決める必要があります。

大きすぎる移動は重要な情報を画像外に追い出してしまい、逆に小さすぎると効果が薄くなります。

一般的な目安としては、画像の幅や高さの5%〜20%程度の範囲でランダムに移動量を決めることが多いです。

例えば、幅が200ピクセルの画像なら±10〜40ピクセルの範囲で移動させます。

C++で画像サイズに応じてランダムに移動量を決める例を示します。

int maxShiftX = static_cast<int>(src.cols * 0.1); // 横方向最大10%
int maxShiftY = static_cast<int>(src.rows * 0.1); // 縦方向最大10%
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> disX(-maxShiftX, maxShiftX);
std::uniform_int_distribution<> disY(-maxShiftY, maxShiftY);
int dx = disX(gen);
int dy = disY(gen);

このように画像サイズに比例した範囲で移動量を決めることで、画像の情報を大きく損なわずに多様な平行移動を実現できます。

アノテーション座標の更新フロー

物体検出やセグメンテーションなどで、画像の平行移動に伴いアノテーションの座標も正しく更新する必要があります。

平行移動は単純な座標の加算で済むため、更新は比較的簡単です。

バウンディングボックスの場合

バウンディングボックスの左上座標(x, y)に対して、移動量(dx, dy)を加算します。

幅と高さは変わらないため、以下のように更新します。

x=x+dx,y=y+dy

C++コード例:

cv::Rect updateBoundingBox(const cv::Rect& bbox, int dx, int dy) {
    return cv::Rect(bbox.x + dx, bbox.y + dy, bbox.width, bbox.height);
}

キーポイントやランドマークの場合

キーポイントはcv::Point2fの配列やベクターで管理されることが多いです。

各点に対してdxdyを加算します。

void updateKeypoints(std::vector<cv::Point2f>& keypoints, int dx, int dy) {
    for (auto& pt : keypoints) {
        pt.x += dx;
        pt.y += dy;
    }
}

注意点

  • 移動後の座標が画像の範囲外に出る場合があります。必要に応じて座標のクリッピングや無効化処理を行ってください
  • セグメンテーションマスクの場合は、画像と同じ変換をマスクにも適用する必要があります

このように、平行移動のアノテーション更新は座標の単純な加算で済むため、実装が容易です。

画像とアノテーションの整合性を保ちながらデータ拡張を行いましょう。

スケーリング

拡大縮小係数の乱数設計

画像のスケーリングでは、拡大縮小の係数(スケールファクター)をランダムに決めることが重要です。

これにより、モデルは異なるサイズの物体に対しても頑健になります。

スケールファクターは通常、0.8〜1.2の範囲でランダムにサンプリングされることが多いです。

0.8は20%縮小、1.2は20%拡大を意味します。

C++での乱数生成例は以下の通りです。

#include <random>
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> dis(0.8, 1.2);
double scaleFactor = dis(gen);

このscaleFactorcv::resize関数のfxfyに指定して画像を拡大縮小します。

スケールの範囲はタスクやデータセットに応じて調整してください。

例えば、物体のサイズ変化が大きい場合は0.5〜1.5など広い範囲に設定することもあります。

アスペクト比保持と非保持の切替

スケーリング時にアスペクト比(縦横比)を保持するかどうかは重要なポイントです。

  • アスペクト比保持

縦横のスケール係数を同じ値にして画像全体を均一に拡大縮小します。

元の形状を崩さず、物体の形状変化を避けたい場合に適しています。

  • アスペクト比非保持

縦横のスケール係数を別々に設定し、縦横で異なる倍率で拡大縮小します。

これにより、画像が縦長や横長に変形し、モデルの形状変化に対する耐性を高めることができます。

アスペクト比非保持の例として、縦横それぞれに異なる乱数を生成するコードを示します。

std::uniform_real_distribution<> disX(0.8, 1.2);
std::uniform_real_distribution<> disY(0.8, 1.2);
double scaleX = disX(gen);
double scaleY = disY(gen);
cv::resize(src, dst, cv::Size(), scaleX, scaleY, cv::INTER_LINEAR);

アスペクト比保持の場合はscaleXscaleYを同じ値にします。

補間アルゴリズム比較

スケーリング時の補間アルゴリズムは、画像の品質や処理速度に影響します。

OpenCVでは複数の補間方法が用意されていますが、代表的なものにINTER_LINEARINTER_AREAがあります。

INTER_LINEAR

INTER_LINEARはバイリニア補間で、拡大・縮小の両方に適しています。

計算コストが比較的低く、滑らかな画像を得られます。

特に拡大時に自然な見た目を保ちやすいです。

cv::resize(src, dst, cv::Size(), scaleX, scaleY, cv::INTER_LINEAR);
  • メリット
    • 拡大縮小両方に対応
    • 滑らかな画像を生成
    • 処理速度が速い
  • デメリット
    • 縮小時に若干のぼやけが生じることがある

INTER_AREA

INTER_AREAは主に縮小時に推奨される補間方法です。

ピクセルの領域平均を取るため、縮小時にモアレやジャギーを抑え、シャープでノイズの少ない画像を得られます。

ただし、拡大時には適していません。

cv::resize(src, dst, cv::Size(), scaleX, scaleY, cv::INTER_AREA);
  • メリット
    • 縮小時に高品質な画像を生成
    • モアレやジャギーを抑制
  • デメリット
    • 拡大時には不適切で画像がぼやける
    • 処理がやや重い

補間アルゴリズムの使い分け例

縮小時はINTER_AREA、拡大時はINTER_LINEARを使い分けるのがベストプラクティスです。

以下のコードはスケールファクターに応じて補間方法を切り替えています。

double scaleFactor = dis(gen);
int interpolation = (scaleFactor < 1.0) ? cv::INTER_AREA : cv::INTER_LINEAR;
cv::resize(src, dst, cv::Size(), scaleFactor, scaleFactor, interpolation);

このようにすることで、画像の品質を保ちながら効率的にスケーリングできます。

反転

横反転と縦反転のパターン

画像の反転は、データ拡張でよく使われる手法の一つで、画像を左右または上下に反転させることでモデルの反転不変性を高めます。

OpenCVのcv::flip関数を使うと簡単に実装できます。

cv::flipの第3引数flipCodeで反転の種類を指定します。

flipCode反転の種類
0縦方向(上下)反転
1横方向(左右)反転
-1縦横両方向反転

例えば、横反転はflipCode=1、縦反転はflipCode=0で実行します。

cv::Mat flippedImage;
cv::flip(src, flippedImage, 1); // 横反転
cv::flip(src, flippedImage, 0); // 縦反転

横反転は人物の左右対称性や物体の向きの多様性を増やすのに効果的です。

縦反転は自然界の画像ではあまり使われませんが、特定のタスクやデータセットによっては有効な場合もあります。

物体検出ラベルの反映方法

物体検出タスクでは、画像を反転した際にバウンディングボックスの座標も正しく更新する必要があります。

特に横反転の場合、x座標の変換が重要です。

横反転時のバウンディングボックス更新

画像幅をW、元のバウンディングボックスの左上座標を(x, y)、幅をwとすると、横反転後の左上x座標x'は以下のように計算します。

x=W(x+w)

y座標と高さは変わりません。

C++コード例:

cv::Rect flipBoundingBoxHorizontal(const cv::Rect& bbox, int imageWidth) {
    int x_new = imageWidth - (bbox.x + bbox.width);
    return cv::Rect(x_new, bbox.y, bbox.width, bbox.height);
}

縦反転時のバウンディングボックス更新

画像高さをH、元のバウンディングボックスの上端y座標をy、高さをhとすると、縦反転後のy座標y'は以下のように計算します。

y=H(y+h)

x座標と幅は変わりません。

cv::Rect flipBoundingBoxVertical(const cv::Rect& bbox, int imageHeight) {
    int y_new = imageHeight - (bbox.y + bbox.height);
    return cv::Rect(bbox.x, y_new, bbox.width, bbox.height);
}

複数バウンディングボックスの更新

複数のバウンディングボックスがある場合は、ループでそれぞれ更新します。

std::vector<cv::Rect> flipBoundingBoxesHorizontal(const std::vector<cv::Rect>& bboxes, int imageWidth) {
    std::vector<cv::Rect> flippedBboxes;
    for (const auto& bbox : bboxes) {
        flippedBboxes.push_back(flipBoundingBoxHorizontal(bbox, imageWidth));
    }
    return flippedBboxes;
}

ステレオペア利用時の注意点

ステレオペアとは、左右2台のカメラで同時に撮影した画像ペアのことです。

ステレオ画像を使ったデータ拡張では、反転処理に特別な注意が必要です。

左右画像の整合性

横反転を単純に左右両方の画像に適用すると、左右のカメラの位置関係が逆転してしまい、ステレオ視差情報が破壊されます。

これにより、ステレオマッチングや深度推定の精度が大きく低下します。

対応策

  • 片方の画像だけ反転しない

左右の画像の関係性を保つため、片方の画像だけ反転するのは避けます。

  • 両方の画像を同時に反転し、カメラパラメータも更新する

両方の画像を同じ反転処理で変換し、カメラの内部・外部パラメータも反転に合わせて調整します。

これにより、ステレオペアの整合性を保てます。

  • 反転を使わない

ステレオペアのデータ拡張では反転を避けるケースもあります。

ステレオペアの反転は単純な画像反転以上に複雑な処理が必要です。

反転を行う場合は、カメラパラメータの更新や視差マップの再計算などを考慮し、慎重に実装してください。

彩度・明度・コントラスト調整

convertToで一括スケーリング

OpenCVのconvertTo関数は、画像のピクセル値に対して一括でスケーリングとオフセットをかけることができ、明度やコントラストの調整に便利です。

具体的には、以下の式で変換を行います。

dst=src×α+β

  • α はコントラストの倍率(スケールファクター)
  • β は明度のオフセット(加算値)

例えば、コントラストを1.2倍にし、明度を20だけ上げる場合は以下のようにします。

cv::Mat adjustedImage;
double alpha = 1.2;  // コントラスト倍率
int beta = 20;       // 明度オフセット
src.convertTo(adjustedImage, -1, alpha, beta);

ここで、-1は出力画像のデータ型を元画像と同じにする指定です。

ランダムな調整例

データ拡張としてランダムに明度・コントラストを変化させる場合は、乱数でαβを決めます。

#include <random>
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> alphaDis(0.8, 1.2);
std::uniform_int_distribution<> betaDis(-30, 30);
double alpha = alphaDis(gen);
int beta = betaDis(gen);
src.convertTo(adjustedImage, -1, alpha, beta);

この方法は計算コストが低く、リアルタイム処理にも適しています。

HSV空間での色揺らぎ生成

RGB空間での明度や彩度の調整は複雑ですが、HSV(色相・彩度・明度)空間に変換すると、彩度や明度を独立して操作しやすくなります。

OpenCVではcv::cvtColorで色空間変換が可能です。

cvtColorとsplit活用

  1. RGBからHSVへ変換
cv::Mat hsvImage;
cv::cvtColor(src, hsvImage, cv::COLOR_BGR2HSV);
  1. HSVチャンネルの分割
std::vector<cv::Mat> hsvChannels;
cv::split(hsvImage, hsvChannels);

ここで、hsvChannels[0]が色相(H)、hsvChannels[1]が彩度(S)、hsvChannels[2]が明度(V)です。

  1. 彩度・明度の調整

彩度や明度のチャンネルに対してスケーリングやオフセットを加えます。

例えば、彩度を0.8〜1.2倍の範囲でランダムに変化させる場合は以下のようにします。

std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> satDis(0.8, 1.2);
std::uniform_real_distribution<> valDis(0.8, 1.2);
double satScale = satDis(gen);
double valScale = valDis(gen);
// 彩度のスケーリング
hsvChannels[1].convertTo(hsvChannels[1], -1, satScale, 0);
// 明度のスケーリング
hsvChannels[2].convertTo(hsvChannels[2], -1, valScale, 0);
  1. チャンネルのマージとHSVからRGBへの変換
cv::merge(hsvChannels, hsvImage);
cv::Mat adjustedImage;
cv::cvtColor(hsvImage, adjustedImage, cv::COLOR_HSV2BGR);

この方法で彩度や明度を独立して調整でき、より自然な色揺らぎを作り出せます。

ガンマ補正プロセス

ガンマ補正は画像の明るさを非線形に調整する手法で、特に暗部や明部の階調を強調したい場合に有効です。

ガンマ補正は以下の式で表されます。

Iout=255×(Iin255)γ

  • Iin は入力ピクセル値(0〜255)
  • Iout は出力ピクセル値(0〜255)
  • γ はガンマ値(1より小さいと明るく、1より大きいと暗くなる)

実装例

cv::Mat gammaCorrection(const cv::Mat& src, double gamma) {
    CV_Assert(gamma >= 0);
    cv::Mat lut(1, 256, CV_8UC1);
    for (int i = 0; i < 256; i++) {
        lut.at<uchar>(i) = cv::saturate_cast<uchar>(pow(i / 255.0, gamma) * 255.0);
    }
    cv::Mat dst;
    cv::LUT(src, lut, dst);
    return dst;
}

この関数はルックアップテーブル(LUT)を使って高速にガンマ補正を行います。

ランダムなガンマ値の設定例

std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> gammaDis(0.7, 1.5);
double gamma = gammaDis(gen);
cv::Mat correctedImage = gammaCorrection(src, gamma);

ガンマ補正を加えることで、画像の明るさのバリエーションが増え、モデルの明度変化に対する耐性が向上します。

ノイズ付加

ガウシアンノイズの実装例

ガウシアンノイズは、画像の各ピクセルに正規分布に従うランダムな値を加えるノイズで、センサーのノイズや撮影環境の揺らぎを模擬するのに適しています。

OpenCVでは直接ガウシアンノイズを加える関数はありませんが、cv::Matに乱数を生成して加算することで実装できます。

以下は、ガウシアンノイズを加えるサンプルコードです。

#include <opencv2/opencv.hpp>
#include <random>
cv::Mat addGaussianNoise(const cv::Mat& src, double mean, double stddev) {
    cv::Mat noise(src.size(), src.type());
    std::random_device rd;
    std::mt19937 gen(rd());
    std::normal_distribution<> dist(mean, stddev);
    // ノイズを生成してnoiseに格納
    for (int y = 0; y < noise.rows; ++y) {
        for (int x = 0; x < noise.cols; ++x) {
            for (int c = 0; c < noise.channels(); ++c) {
                double val = dist(gen);
                if (src.type() == CV_8UC3) {
                    noise.at<cv::Vec3b>(y, x)[c] = static_cast<uchar>(val);
                } else if (src.type() == CV_8UC1) {
                    noise.at<uchar>(y, x) = static_cast<uchar>(val);
                }
            }
        }
    }
    // ノイズを加算(型変換に注意)
    cv::Mat dst;
    cv::add(src, noise, dst, cv::noArray(), src.type());
    return dst;
}
int main() {
    cv::Mat image = cv::imread("image.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    double mean = 0.0;
    double stddev = 10.0; // ノイズの標準偏差
    cv::Mat noisyImage = addGaussianNoise(image, mean, stddev);
    cv::imwrite("gaussian_noisy.jpg", noisyImage);
    return 0;
}
ノイズを加算した画像

このコードでは、平均mean、標準偏差stddevの正規分布に従うノイズを生成し、元画像に加算しています。

ノイズの強さはstddevで調整可能です。

ソルトアンドペッパーノイズの適用範囲

ソルトアンドペッパーノイズは、画像のランダムなピクセルを白(ソルト)または黒(ペッパー)に置き換えるノイズです。

主にスパースなノイズを模擬し、ノイズ除去アルゴリズムの評価やロバスト性向上に使われます。

実装のポイント

  • ノイズを加えるピクセルの割合(ノイズ密度)を指定します。例えば、全ピクセルの1%をノイズ化するなど
  • ノイズ化するピクセルはランダムに選びます
  • 選ばれたピクセルはランダムに白(255)か黒(0)に設定します

適用範囲

  • グレースケール画像

0(黒)または255(白)に置き換えます。

  • カラー画像

各チャンネルを同時に0または255に設定するか、チャンネルごとに独立して設定します。

一般的には全チャンネルを同じ値にすることが多いです。

注意点

  • ノイズ密度が高すぎると画像の情報が失われすぎるため、通常は1〜5%程度に抑えます
  • 物体検出などのタスクでは、ノイズが物体の重要な部分にかからないように注意が必要です

パラメータチューニング指標

ノイズ付加のパラメータは、ノイズの種類や強度を決める重要な要素です。

適切なパラメータ設定は、モデルの汎化性能向上に寄与します。

ノイズ種類主なパラメータ調整ポイント・指標
ガウシアンノイズ平均(mean)、標準偏差(stddev)stddevを大きくするとノイズが強くなります。10〜30程度が一般的。
ソルトアンドペッパーノイズ密度(割合)0.01〜0.05(1〜5%)が多い。高すぎると画像劣化が激しい。

チューニングのコツ

  • モデルの性能評価

ノイズを加えたデータで学習し、検証データでの精度やロバスト性を比較します。

  • 視覚的確認

ノイズ付加後の画像を目視で確認し、ノイズが過剰でないかチェックします。

  • タスク依存性

物体検出やセグメンテーションでは、ノイズが物体の輪郭や特徴を潰さないように注意します。

  • 段階的増加

ノイズ強度を段階的に増やし、モデルの耐性を確認しながら最適値を探します。

これらの指標を参考に、ノイズ付加のパラメータを調整し、効果的なデータ拡張を実現してください。

ぼかしとシャープ化

平均・ガウシアン・モーションブラーの効果比較

画像のぼかし(ブラー)は、ノイズ除去やデータ拡張でよく使われる処理です。

代表的なぼかしには「平均ぼかし」「ガウシアンぼかし」「モーションブラー」があります。

それぞれの特徴と効果を比較します。

ぼかし種類特徴効果・用途
平均ぼかしカーネル内のピクセル値の単純平均を計算ノイズ除去に効果的だが、エッジもぼやけやすい
ガウシアンぼかしガウス関数に基づく重み付き平均を計算自然なぼかし効果でエッジの保持が比較的良い
モーションブラー一方向に線状のぼかしをかける動きのある画像のブレを模擬、動体認識に有効

平均ぼかし

cv::blur関数で実装され、カーネルサイズを指定して周囲のピクセルの平均を取ります。

計算が軽く、ノイズ除去に使いやすいですが、エッジがぼやけやすい欠点があります。

ガウシアンぼかし

cv::GaussianBlur関数で実装され、カーネルの中心に近いピクセルほど重みが大きくなるため、より自然なぼかしが得られます。

エッジの保持性能が平均ぼかしより優れているため、画像処理で広く使われています。

モーションブラー

動きのある被写体のブレを模擬するためのぼかしで、カーネルが一方向に伸びています。

OpenCVには直接の関数はありませんが、カスタムカーネルを作成してcv::filter2Dで適用します。

動体検出や動きのある映像のデータ拡張に適しています。

GaussianBlurカーネルサイズの決め方

cv::GaussianBlurのカーネルサイズは、ぼかしの強さを決める重要なパラメータです。

カーネルサイズは奇数の正方形(例:3×3、5×5、7×7)で指定し、サイズが大きいほどぼかしが強くなります。

選び方のポイント

  • 小さいカーネル(3×3)

軽いぼかしで、細かいノイズ除去に適しています。

エッジの保持も比較的良好です。

  • 中くらいのカーネル(5×5〜7×7)

ノイズ除去効果が高まり、ぼかしも目立ちます。

一般的な用途に多用されます。

  • 大きいカーネル(9×9以上)

強いぼかし効果で、画像のディテールが大きく失われます。

特殊な効果や大幅なノイズ除去に使います。

シグマ値(標準偏差)

カーネルサイズに加え、ガウス関数の標準偏差(シグマ)もぼかしの強さに影響します。

OpenCVではシグマを0にすると自動計算されますが、明示的に指定することも可能です。

cv::GaussianBlur(src, dst, cv::Size(5, 5), 1.5);

この例では5×5カーネル、シグマ1.5でぼかしをかけています。

カスタムカーネルによるシャープ化

シャープ化は画像のエッジや細部を強調する処理で、ぼかしとは逆の効果を持ちます。

OpenCVではカスタムカーネルを作成し、cv::filter2D関数で適用することでシャープ化が可能です。

代表的なシャープ化カーネル

以下は3×3の典型的なシャープ化カーネルの例です。

[010151010]

このカーネルは中央のピクセルを強調し、周囲のピクセルを減算することでエッジを際立たせます。

実装例

cv::Mat kernel = (cv::Mat_<float>(3,3) <<
                   0, -1,  0,
                  -1,  5, -1,
                   0, -1,  0);
cv::Mat sharpened;
cv::filter2D(src, sharpened, src.depth(), kernel);

カスタマイズ

  • カーネルの中央の値を大きくするとシャープ化の強度が増します
  • 周囲の負の値を調整することでエッジの強調具合をコントロールできます

注意点

  • 強すぎるシャープ化はノイズを増幅するため、適切な強度に調整してください
  • シャープ化後に画像が白飛びや黒つぶれしないように、cv::convertScaleAbsなどでクリッピング処理を行うこともあります

これらのぼかしとシャープ化の手法を使い分けることで、画像の質感や特徴を多様化し、モデルの汎化性能を高めることができます。

部分切り抜き・ランダムクロップ

中心クロップとランダムクロップの使い分け

部分切り抜き(クロップ)は、画像の一部を切り出すことでデータの多様性を増やす手法です。

代表的な方法に「中心クロップ」と「ランダムクロップ」があります。

  • 中心クロップ

画像の中心部分を指定したサイズで切り出します。

画像の重要な情報が中心に集まっている場合に有効で、切り出し位置が固定されるため、データのばらつきを抑えつつサイズを統一したいときに使います。

  • ランダムクロップ

画像内の任意の位置から指定サイズで切り出します。

切り出し位置がランダムなため、データの多様性が増し、モデルの汎化性能向上に寄与します。

ただし、重要な物体が切り取られるリスクもあります。

使い分けのポイントは以下の通りです。

クロップ方法特徴利用シーン例
中心クロップ安定した切り出し、情報損失が少ない画像の中心に注目したい場合、評価時
ランダムクロップ多様な切り出しでデータ拡張効果大学習時のデータ拡張、過学習防止

アスペクト比変化を伴う拡張クロップ

通常のクロップは元画像のアスペクト比を維持して切り出しますが、アスペクト比を変化させるクロップもデータ拡張として有効です。

これにより、モデルは異なる縦横比の物体やシーンに対応しやすくなります。

実装例

  1. 切り出すサイズの幅と高さをランダムに決定し、元画像のアスペクト比と異なる比率に設定します。
  2. 画像内の切り出し位置をランダムに決めます。
  3. 指定したサイズで切り出し、必要に応じてリサイズしてモデルの入力サイズに合わせます。
#include <opencv2/opencv.hpp>
#include <random>

cv::Mat randomAspectRatioCrop(const cv::Mat& src, int outputWidth,
                              int outputHeight) {
    std::random_device rd;
    std::mt19937 gen(rd());
    // アスペクト比の範囲を設定(例:0.75〜1.33)
    std::uniform_real_distribution<> aspectDis(0.75, 1.33);
    double aspectRatio = aspectDis(gen);

    // 出力サイズに基づき切り出しサイズを決定
    int cropWidth =
        std::min(src.cols, static_cast<int>(src.rows * aspectRatio));
    int cropHeight = static_cast<int>(cropWidth / aspectRatio);

    // 切り出し位置の範囲を計算
    int maxX = src.cols - cropWidth;
    int maxY = src.rows - cropHeight;
    std::uniform_int_distribution<> xDis(0, std::max(0, maxX));
    std::uniform_int_distribution<> yDis(0, std::max(0, maxY));
    int x = xDis(gen);
    int y = yDis(gen);

    cv::Rect cropRect(x, y, cropWidth, cropHeight);
    cv::Mat cropped = src(cropRect);

    // 出力サイズにリサイズ
    cv::Mat resized;
    cv::resize(cropped, resized, cv::Size(outputWidth, outputHeight));
    return resized;
}

int main() {
    // サンプル画像読み込み(自分の環境に応じてパスを変更してください)
    cv::Mat img = cv::imread("sample.jpg");
    if (img.empty()) {
        std::cerr << "画像が読み込めませんでした。" << std::endl;
        return -1;
    }

    cv::Mat result = randomAspectRatioCrop(img, 200, 200);

    // 結果表示
    cv::imshow("Original", img);
    cv::imshow("Random Aspect Ratio Crop", result);
    cv::waitKey(0);
    return 0;
}

この方法で、元画像のアスペクト比とは異なる比率の切り出しが可能になり、モデルの多様な入力に対する適応力が向上します。

物体検出タスク向け考慮点

物体検出タスクで部分切り抜きやランダムクロップを行う場合、以下の点に注意が必要です。

  • バウンディングボックスの更新

クロップにより画像の座標系が変わるため、バウンディングボックスの座標も切り出し領域に合わせて変換します。

具体的には、クロップ領域の左上座標を原点とした相対座標に変換し、切り出し外に出た部分は切り捨てます。

  • 物体の切断

クロップにより物体が部分的に切り取られることがあります。

切断された物体の扱いはタスクによって異なりますが、一般的には以下のように対応します。

  • 閾値による除外

バウンディングボックスの面積が元の面積に対して一定割合以下の場合、その物体を除外します。

  • 部分的なバウンディングボックスの更新

切り取られた部分を除いた新しいバウンディングボックスを計算し、ラベルを保持します。

  • アノテーションの整合性

クロップ後の画像サイズに合わせて、バウンディングボックスの座標を正規化(0〜1の範囲)する場合は、リサイズ後のサイズも考慮して更新します。

バウンディングボックス更新例

#include <opencv2/opencv.hpp>
#include <random>

cv::Rect updateBoundingBoxForCrop(const cv::Rect& bbox, const cv::Rect& cropRect) {
    int x1 = std::max(bbox.x, cropRect.x);
    int y1 = std::max(bbox.y, cropRect.y);
    int x2 = std::min(bbox.x + bbox.width, cropRect.x + cropRect.width);
    int y2 = std::min(bbox.y + bbox.height, cropRect.y + cropRect.height);
    int newWidth = x2 - x1;
    int newHeight = y2 - y1;
    if (newWidth <= 0 || newHeight <= 0) {
        // バウンディングボックスがクロップ外に完全に出ている場合は無効
        return cv::Rect();
    }
    // クロップ領域を原点とした座標に変換
    return cv::Rect(x1 - cropRect.x, y1 - cropRect.y, newWidth, newHeight);
}

このように、物体検出タスクではクロップ処理に伴うアノテーションの正確な更新が不可欠です。

適切に処理しないと、学習時に誤ったラベルが付与され、モデルの性能低下を招きます。

アフィン変換

3点指定による行列推定フロー

アフィン変換は、平行移動・回転・スケーリング・シアー(せん断)などを組み合わせた線形変換で、画像の幾何学的変形に広く使われます。

OpenCVでは3点の対応関係を指定してアフィン変換行列を推定し、cv::warpAffineで画像変換を行います。

3点指定の意味

アフィン変換は2×3の行列で表され、2D座標の変換は以下の式で表されます。

[xy]=[a11a12txa21a22ty][xy1]

この行列は3点の元座標と変換後座標の対応から一意に決定できます。

推定フロー

  1. 元画像の3点を選択

変換前の画像上の3点srcTricv::Point2fで指定します。

例えば、画像の左上、右上、左下の3点など。

  1. 変換後の3点を指定

変換後の画像上の対応する3点dstTriを指定します。

これにより、変換の種類(回転・シアーなど)が決まります。

  1. アフィン変換行列の計算

cv::getAffineTransform(srcTri, dstTri)を呼び出し、2×3の変換行列を取得します。

  1. 画像変換の実行

cv::warpAffineに変換行列を渡して画像を変換します。

#include <opencv2/opencv.hpp>
int main() {
    cv::Mat src = cv::imread("image.jpg");
    if (src.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    // 元画像の3点(左上、右上、左下)
    std::vector<cv::Point2f> srcTri = {
        cv::Point2f(0, 0),
        cv::Point2f(src.cols - 1, 0),
        cv::Point2f(0, src.rows - 1)
    };
    // 変換後の3点(例:シアー変形を含む)
    std::vector<cv::Point2f> dstTri = {
        cv::Point2f(0, 0),
        cv::Point2f(src.cols * 0.8f, src.rows * 0.2f),
        cv::Point2f(src.cols * 0.2f, src.rows * 0.9f)
    };
    // アフィン変換行列の計算
    cv::Mat affineMat = cv::getAffineTransform(srcTri, dstTri);
    // 変換後画像のサイズは元画像と同じ
    cv::Mat dst;
    cv::warpAffine(src, dst, affineMat, src.size(), cv::INTER_LINEAR, cv::BORDER_REFLECT);
    cv::imwrite("affine_transformed.jpg", dst);
    return 0;
}
アフィン変換で歪ませた画像

このコードでは、3点の対応からアフィン変換行列を推定し、画像にシアーを含む変形を適用しています。

シアー変形のパラメータ設計

シアー変形(せん断変形)は、画像を水平方向または垂直方向に傾ける変形で、アフィン変換の一種です。

シアー変形を加えることで、モデルは物体の傾きや歪みに対しても頑健になります。

シアー変形の行列

水平方向のシアー変形は以下の行列で表されます。

[1k0010]

垂直方向のシアー変形は以下の通りです。

[100k10]

ここで、kはシアー係数で、正負の値で傾きの方向が変わります。

パラメータ設計のポイント

  • シアー係数の範囲

通常は小さな値(例:-0.3〜0.3)をランダムに選びます。

大きすぎると画像が大きく歪み、学習に悪影響を与える可能性があります。

  • 方向の選択

水平方向、垂直方向、または両方のシアーを組み合わせることが可能です。

  • 中心点の考慮

シアー変形は原点を基準に行われるため、画像中心を基準に変形したい場合は座標の平行移動を組み合わせます。

シアー変形の実装例(水平方向)

#include <opencv2/opencv.hpp>
#include <random>
cv::Mat shearImage(const cv::Mat& src, double shearFactor) {
    cv::Mat shearMat = (cv::Mat_<double>(2, 3) << 1, shearFactor, 0, 0, 1, 0);
    // 出力サイズは元画像と同じ
    cv::Mat dst;
    cv::warpAffine(src, dst, shearMat, src.size(), cv::INTER_LINEAR, cv::BORDER_REFLECT);
    return dst;
}
int main() {
    cv::Mat image = cv::imread("image.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_real_distribution<> dis(-0.3, 0.3);
    double shearFactor = dis(gen);
    cv::Mat shearedImage = shearImage(image, shearFactor);
    cv::imwrite("sheared_image.jpg", shearedImage);
    return 0;
}

この例では、水平方向のシアー係数をランダムに決めて画像に適用しています。

バウンディングボックス補正ロジック

物体検出タスクでアフィン変換を行う場合、画像の変形に合わせてバウンディングボックスの座標も正しく補正する必要があります。

アフィン変換は線形変換なので、バウンディングボックスの4頂点を変換行列で変換し、新しいバウンディングボックスを計算します。

補正手順

  1. バウンディングボックスの4頂点を取得

左上、右上、右下、左下の4点をcv::Point2fで表現します。

  1. アフィン変換行列を3×3に拡張

cv::getAffineTransformは2×3行列ですが、座標変換には3×3の同次変換行列が必要です。

2×3行列を以下のように拡張します。

[a11a12txa21a22ty001]

  1. 頂点を同次座標に変換し行列を掛ける

各頂点 (x,y)(x,y,1)T に変換し、行列を掛けて新座標を得ます。

  1. 新しいバウンディングボックスを計算

変換後の4点のx座標の最小値・最大値、y座標の最小値・最大値を求め、新しい矩形を作成します。

#include <opencv2/opencv.hpp>
#include <vector>
std::vector<cv::Point2f> transformBoundingBox(const cv::Rect& bbox, const cv::Mat& affineMat) {
    std::vector<cv::Point2f> points(4);
    points[0] = cv::Point2f(bbox.x, bbox.y);
    points[1] = cv::Point2f(bbox.x + bbox.width, bbox.y);
    points[2] = cv::Point2f(bbox.x + bbox.width, bbox.y + bbox.height);
    points[3] = cv::Point2f(bbox.x, bbox.y + bbox.height);
    cv::Mat affineMat33 = cv::Mat::eye(3, 3, affineMat.type());
    affineMat.copyTo(affineMat33(cv::Rect(0, 0, 3, 2)));
    std::vector<cv::Point2f> transformedPoints(4);
    for (int i = 0; i < 4; ++i) {
        cv::Mat pt = (cv::Mat_<double>(3,1) << points[i].x, points[i].y, 1);
        cv::Mat rpt = affineMat33 * pt;
        transformedPoints[i] = cv::Point2f(static_cast<float>(rpt.at<double>(0,0)), static_cast<float>(rpt.at<double>(1,0)));
    }
    return transformedPoints;
}
cv::Rect getBoundingBoxFromPoints(const std::vector<cv::Point2f>& points) {
    float minX = points[0].x;
    float minY = points[0].y;
    float maxX = points[0].x;
    float maxY = points[0].y;
    for (const auto& pt : points) {
        if (pt.x < minX) minX = pt.x;
        if (pt.y < minY) minY = pt.y;
        if (pt.x > maxX) maxX = pt.x;
        if (pt.y > maxY) maxY = pt.y;
    }
    return cv::Rect(cv::Point2f(minX, minY), cv::Point2f(maxX, maxY));
}

このコードを使い、アフィン変換後のバウンディングボックスを正確に補正できます。

物体検出の学習データ拡張で必須の処理です。

パースペクティブ変換

4点からのwarpPerspective実装

パースペクティブ変換は、画像の遠近感を変化させる変換で、カメラの視点変更や斜めからの撮影効果を模擬できます。

OpenCVでは4点の対応関係から変換行列を計算し、cv::warpPerspectiveで画像に適用します。

4点指定の意味

パースペクティブ変換は3×3のホモグラフィ行列Hで表され、2D座標の変換は以下の式で行います。

[xyw]=H[xy1](x,y)=(xw,yw)

この行列は、元画像の4点と変換後の4点の対応から一意に決定されます。

実装手順

  1. 元画像の4点を指定

変換前の画像上の4点srcPointscv::Point2fで指定します。

通常は画像の4隅を指定します。

  1. 変換後の4点を指定

変換後の画像上の対応する4点dstPointsを指定します。

これにより遠近感や歪みが決まります。

  1. ホモグラフィ行列の計算

cv::getPerspectiveTransform(srcPoints, dstPoints)で3×3の変換行列を取得します。

  1. 画像変換の実行

cv::warpPerspectiveに変換行列を渡して画像を変換します。

#include <opencv2/opencv.hpp>
int main() {
    cv::Mat src = cv::imread("image.jpg");
    if (src.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    // 元画像の4点(左上、右上、右下、左下)
    std::vector<cv::Point2f> srcPoints = {
        cv::Point2f(0, 0),
        cv::Point2f(src.cols - 1, 0),
        cv::Point2f(src.cols - 1, src.rows - 1),
        cv::Point2f(0, src.rows - 1)
    };
    // 変換後の4点(例:遠近感を付与)
    std::vector<cv::Point2f> dstPoints = {
        cv::Point2f(src.cols * 0.0f, src.rows * 0.33f),
        cv::Point2f(src.cols * 0.85f, src.rows * 0.25f),
        cv::Point2f(src.cols * 0.85f, src.rows * 0.85f),
        cv::Point2f(src.cols * 0.15f, src.rows * 0.7f)
    };
    // ホモグラフィ行列の計算
    cv::Mat perspectiveMat = cv::getPerspectiveTransform(srcPoints, dstPoints);
    // 変換後画像のサイズは元画像と同じ
    cv::Mat dst;
    cv::warpPerspective(src, dst, perspectiveMat, src.size(), cv::INTER_LINEAR, cv::BORDER_REFLECT);
    cv::imwrite("perspective_transformed.jpg", dst);
    return 0;
}

このコードでは、4点の対応からパースペクティブ変換行列を計算して適用しています。

仮想カメラ行列を意識した歪み付与

パースペクティブ変換は、実際のカメラの視点変化を模擬するため、仮想カメラの内部パラメータ(カメラ行列)や外部パラメータを意識して設計するとより自然な歪みを作れます。

カメラ行列の概要

カメラ行列は以下のように表されます。

K=[fx0cx0fycy001]

  • fx,fy: 焦点距離(ピクセル単位)
  • cx,cy: 画像中心座標

仮想カメラ行列を使った変換設計

  1. 元画像のカメラ行列を設定

画像サイズから焦点距離や中心座標を決めます。

例えば、焦点距離は画像幅の約1.2倍程度が一般的です。

  1. 外部パラメータ(回転・平行移動)を設定

仮想カメラの位置や向きを変えるための回転行列や平行移動ベクトルを用意します。

  1. 射影変換行列を計算

内部パラメータと外部パラメータを組み合わせて射影行列を作成し、元画像の座標を変換します。

  1. 変換行列をwarpPerspectiveに渡す

射影行列を使って画像に自然な遠近歪みを付与します。

メリット

  • 実際のカメラ撮影に近い歪みを再現できるため、モデルの実環境適応性が向上します
  • 物体の形状変化や視点変化をリアルに模擬可能です

注意点

  • カメラ行列や外部パラメータの設定は専門的で複雑なため、用途に応じて簡略化することもあります
  • 変換後の画像サイズや切り出し範囲に注意し、必要に応じてトリミングやパディングを行います

補正処理との組み合わせ例

パースペクティブ変換は画像の歪みを加えるため、場合によっては補正処理と組み合わせて使うことがあります。

例えば、以下のようなシナリオがあります。

歪み付与+逆変換によるデータ拡張

  • 元画像にパースペクティブ変換で歪みを加えます
  • モデルの学習後、推論時に逆変換で元の視点に戻す処理を行います

これにより、学習時に多様な視点を学習しつつ、推論時には標準的な視点で処理できます。

歪み付与+幾何学的補正

  • パースペクティブ変換で歪みを加えた画像に対し、幾何学的補正(例えばカメラキャリブレーションによる補正)を適用
  • 補正後の画像でモデルを学習し、実際の歪みを考慮したロバストなモデルを作成

マスクやアノテーションの同時変換

  • セグメンテーションマスクやバウンディングボックスの座標もパースペクティブ変換行列で同時に変換
  • 変換後のアノテーションを補正し、正確なラベル付けを維持

実装例(マスクの同時変換)

cv::Mat transformedMask;
cv::warpPerspective(mask, transformedMask, perspectiveMat, mask.size(), cv::INTER_NEAREST);

INTER_NEAREST補間を使うことで、マスクのラベル値を保持しつつ変換できます。

これらの組み合わせにより、パースペクティブ変換を効果的に活用し、より多様で実用的なデータ拡張が可能になります。

擬似的照明変化

シャドウ領域の合成アルゴリズム

擬似的な照明変化の一つとして、画像にシャドウ(影)を合成する手法があります。

これにより、異なる照明条件下での画像を模擬し、モデルの照明変化に対するロバスト性を高められます。

シャドウ合成の基本アイデア

シャドウ領域は、画像の一部を暗くすることで表現します。

具体的には、影を落とす形状(マスク)を作成し、その領域のピクセル値を減衰させます。

アルゴリズムの流れ

  1. シャドウマスクの生成
  • 多角形や楕円形、ランダムな不規則形状を画像上に生成します
  • 形状の位置や大きさ、角度はランダムに設定し、多様な影を作ります
  1. マスクのぼかし
  • シャドウの境界を自然にするため、マスクにガウシアンぼかしをかけます
  • これにより、影のエッジが滑らかになります
  1. 画像への適用
  • マスクの値(0〜1)を使って、元画像のピクセル値を減衰させます
  • 例えば、マスク値が0.5の部分はピクセル値を50%に減らします

実装例(擬似コード)

// 1. シャドウマスク作成(例:楕円形)
cv::Mat shadowMask = cv::Mat::ones(src.size(), CV_32FC1);
cv::ellipse(shadowMask, cv::Point(cx, cy), cv::Size(ax, ay), angle, 0, 360, cv::Scalar(0.0), -1);
// 2. マスクにぼかしをかける
cv::GaussianBlur(shadowMask, shadowMask, cv::Size(21, 21), 0);
// 3. 画像をfloat型に変換
cv::Mat srcFloat;
src.convertTo(srcFloat, CV_32FC3, 1.0 / 255.0);
// 4. シャドウを適用
cv::Mat dstFloat = srcFloat.mul(shadowMask);
// 5. 元の形式に戻す
cv::Mat dst;
dstFloat.convertTo(dst, src.type(), 255.0);

この方法で、自然な影を画像に合成できます。

ハイライト追加によるコントラスト強調

ハイライトは、画像の明るい部分を強調することで、コントラストを高める効果があります。

擬似的にハイライトを追加することで、照明の強い部分や反射を模擬できます。

ハイライト追加の手順

  1. ハイライト領域の生成
  • 円形や楕円形の明るい領域をランダムに画像上に配置します
  • 位置や大きさ、強度はランダムに設定し、多様なハイライトを作成します
  1. ハイライトマスクの作成
  • 領域の中心が最も明るく、周辺に向かって徐々に減衰するようにガウス関数でマスクを作成します
  1. 画像への加算
  • マスクの値を元画像のピクセル値に加算し、明るさを増加させます
  • ピクセル値が255を超えないようにクリッピングします

実装例(擬似コード)

// 1. ハイライトマスク作成(例:ガウス分布の円形)
cv::Mat highlightMask = cv::Mat::zeros(src.size(), CV_32FC1);
cv::circle(highlightMask, cv::Point(cx, cy), radius, cv::Scalar(1.0), -1);
cv::GaussianBlur(highlightMask, highlightMask, cv::Size(31, 31), 0);
// 2. 画像をfloat型に変換
cv::Mat srcFloat;
src.convertTo(srcFloat, CV_32FC3, 1.0 / 255.0);
// 3. ハイライト強度を調整
float intensity = 0.3f; // 明るさの増加量
std::vector<cv::Mat> channels(3);
cv::split(srcFloat, channels);
for (int i = 0; i < 3; ++i) {
    channels[i] += highlightMask * intensity;
    cv::threshold(channels[i], channels[i], 1.0, 1.0, cv::THRESH_TRUNC);
}
cv::merge(channels, srcFloat);
// 4. 元の形式に戻す
cv::Mat dst;
srcFloat.convertTo(dst, src.type(), 255.0);

この方法で、画像に自然なハイライトを追加できます。

局所コントラスト変調の手順

局所コントラスト変調は、画像の特定領域のコントラストを強調または弱める処理で、照明のムラや陰影を模擬するのに有効です。

全体のコントラストを変えるのではなく、局所的に変化を加えることでリアルな照明変化を表現します。

手順概要

  1. 局所領域の選択
  • 画像内のランダムな位置に複数の小さな領域(円形や矩形)を設定します
  1. 局所コントラストマスクの作成
  • 各領域に対して、コントラストを強調または減衰させるためのマスクを作成します
  • マスクはガウシアンぼかしなどで滑らかに境界を作ります
  1. 局所コントラスト変調の適用
  • 画像をグレースケールまたは輝度成分に変換し、局所領域のピクセル値を平均値からの差分にマスクを乗じて増減させます
  • これにより、局所的にコントラストが変化します
  1. カラー画像への反映
  • 変調後の輝度成分を元にカラー画像を調整するか、HSV空間のVチャンネルに適用します

実装例(擬似コード)

// 1. 輝度成分の抽出(HSVのVチャンネル)
cv::Mat hsv;
cv::cvtColor(src, hsv, cv::COLOR_BGR2HSV);
std::vector<cv::Mat> hsvChannels;
cv::split(hsv, hsvChannels);
cv::Mat& v = hsvChannels[2];
// 2. 局所コントラストマスク作成(例:円形マスク)
cv::Mat contrastMask = cv::Mat::zeros(src.size(), CV_32FC1);
cv::circle(contrastMask, cv::Point(cx, cy), radius, cv::Scalar(1.0), -1);
cv::GaussianBlur(contrastMask, contrastMask, cv::Size(31, 31), 0);
// 3. 輝度の平均値を計算
double meanVal = cv::mean(v)[0];
// 4. 局所コントラスト変調
cv::Mat vFloat;
v.convertTo(vFloat, CV_32FC1);
vFloat = meanVal + (vFloat - meanVal) * (1.0 + contrastMask * alpha); // alphaは強度
// 5. クリッピングと型変換
cv::threshold(vFloat, vFloat, 255, 255, cv::THRESH_TRUNC);
vFloat.convertTo(v, v.type());
// 6. HSVマージとBGR変換
cv::merge(hsvChannels, hsv);
cv::Mat dst;
cv::cvtColor(hsv, dst, cv::COLOR_HSV2BGR);

この方法で、画像の特定領域のコントラストを局所的に変化させ、リアルな照明ムラを表現できます。

これらの擬似的照明変化手法を組み合わせることで、多様な照明条件を模擬し、モデルの照明変化に対する耐性を強化できます。

複合パイプライン構築

変換シーケンスの最適配置

データ拡張では複数の変換を組み合わせて適用することが多く、変換の順序(シーケンス)が結果に大きく影響します。

最適な変換シーケンスを設計することで、より効果的なデータ拡張が可能になります。

基本的な考え方

  • 幾何学変換は先に行う

回転、平行移動、スケーリング、アフィン変換、パースペクティブ変換などの幾何学的変換は、画像の形状や位置を変えるため、先に適用するのが一般的です。

これにより、後続の色調変換やノイズ付加が正しく適用されます。

  • 色調変換やノイズ付加は後に行う

明度・彩度・コントラスト調整、ノイズ付加、ぼかし、シャープ化などは画像のピクセル値を変える処理なので、幾何学変換の後に適用します。

  • 順序の例
  1. ランダム回転・平行移動・スケーリング
  2. アフィン・パースペクティブ変換
  3. 反転
  4. 彩度・明度・コントラスト調整
  5. ノイズ付加・ぼかし・シャープ化

注意点

  • 変換の順序を変えると画像の見た目や特徴が大きく変わるため、タスクに応じて最適な順序を検証してください
  • 物体検出などアノテーションが必要な場合は、幾何学変換の後にアノテーションの更新を行うことが重要です

乱数シード固定での再現性確保

データ拡張で乱数を使う場合、同じシード値を使うことで処理の再現性を確保できます。

再現性はデバッグや実験の比較に不可欠です。

シード固定の方法

C++標準ライブラリの乱数生成器std::mt19937などは、初期化時にシードを指定できます。

unsigned int seed = 12345; // 任意の固定シード
std::mt19937 gen(seed);

このgenを使って乱数を生成すれば、毎回同じ乱数列が得られます。

複数変換でのシード管理

  • 各変換で同じgenを使い回すか、変換ごとに異なるシードを生成しても良いです
  • 複数スレッドで乱数を使う場合は、スレッドごとに異なるシードを割り当てるか、スレッドセーフな乱数生成器を使います

実例

std::mt19937 gen(seed);
std::uniform_real_distribution<> disAngle(-30.0, 30.0);
double angle = disAngle(gen);
std::uniform_int_distribution<> disShift(-10, 10);
int dx = disShift(gen);
int dy = disShift(gen);

このようにすれば、同じシードで同じ角度や移動量が得られ、処理が再現されます。

OpenMPによる並列バッチ処理

大量の画像に対してデータ拡張を行う場合、処理時間が膨大になるため並列化が効果的です。

OpenMPを使うと、簡単にマルチスレッドでバッチ処理を高速化できます。

OpenMPの基本構文

#pragma omp parallel for
for (int i = 0; i < numImages; ++i) {
    // 画像iに対するデータ拡張処理
}

この#pragma omp parallel forを付けるだけで、ループの各イテレーションが複数スレッドで並列実行されます。

乱数生成器のスレッド安全対策

  • 乱数生成器はスレッドごとに独立して用意する必要があります。共有すると競合や再現性の問題が発生します
#pragma omp parallel for
for (int i = 0; i < numImages; ++i) {
    unsigned int thread_id = omp_get_thread_num();
    std::mt19937 gen(seed + thread_id); // スレッドごとに異なるシード
    // 乱数を使ったデータ拡張処理
}

メモリ管理

  • 並列処理時は、各スレッドが独立した出力バッファを使うか、排他制御を行う必要があります
  • 大きな画像データを扱う場合は、メモリ使用量に注意してください

効果

OpenMPによる並列化で、CPUコア数に応じて処理時間を大幅に短縮できます。

特に大規模データセットの前処理やオンラインデータ拡張で有効です。

これらのポイントを踏まえ、複数のデータ拡張手法を組み合わせた効率的で再現性のあるパイプラインを構築しましょう。

セグメンテーションマスク対応

マスク画像の同時変換方法

セグメンテーションタスクでは、元画像と対応するマスク画像(ラベル画像)を同時に変換する必要があります。

変換の不整合があると、学習データのラベルがずれてしまい、モデルの性能低下を招きます。

同時変換の基本原則

  • 幾何学変換は同じパラメータで適用

回転、平行移動、スケーリング、アフィン変換、パースペクティブ変換などの幾何学的変換は、元画像とマスクに対して同じ変換行列やパラメータを使って適用します。

  • 補間方法の違いに注意

マスク画像はクラスラベルの整数値を持つため、補間方法にcv::INTER_NEARESTを使い、ラベルの値が変わらないようにします。

元画像はcv::INTER_LINEARcv::INTER_CUBICなど滑らかな補間を使います。

実装例(回転の場合)

cv::Mat rotateImage(const cv::Mat& src, double angle) {
    cv::Point2f center(src.cols / 2.0f, src.rows / 2.0f);
    cv::Mat rotMat = cv::getRotationMatrix2D(center, angle, 1.0);
    cv::Mat dst;
    cv::warpAffine(src, dst, rotMat, src.size(), cv::INTER_LINEAR, cv::BORDER_REFLECT);
    return dst;
}
cv::Mat rotateMask(const cv::Mat& mask, double angle) {
    cv::Point2f center(mask.cols / 2.0f, mask.rows / 2.0f);
    cv::Mat rotMat = cv::getRotationMatrix2D(center, angle, 1.0);
    cv::Mat dst;
    cv::warpAffine(mask, dst, rotMat, mask.size(), cv::INTER_NEAREST, cv::BORDER_CONSTANT, cv::Scalar(0));
    return dst;
}

このように、同じ回転行列を使い、補間方法だけ変えて画像とマスクを変換します。

クラス面積比の保全テクニック

データ拡張でマスクを変換すると、特にクロップや回転などでクラスの面積比が大きく変わることがあります。

極端な変化は学習の偏りや不安定化を招くため、面積比の保全が望まれます。

面積比の計算

クラスごとのピクセル数をカウントし、全体に対する割合を計算します。

std::map<int, int> classPixelCount;
int totalPixels = mask.rows * mask.cols;
for (int y = 0; y < mask.rows; ++y) {
    for (int x = 0; x < mask.cols; ++x) {
        int label = mask.at<uchar>(y, x);
        classPixelCount[label]++;
    }
}
for (auto& kv : classPixelCount) {
    double ratio = static_cast<double>(kv.second) / totalPixels;
    std::cout << "Class " << kv.first << ": " << ratio * 100 << "%\n";
}

面積比保全の方法

  • 変換パラメータの制約

ランダムクロップや回転角度の範囲を制限し、極端にクラスが消失しないようにします。

  • 面積比チェックとリトライ

変換後にクラス面積比を計算し、一定の閾値以下になった場合は変換をやり直すロジックを入れます。

  • 重み付けサンプリング

小さいクラスの面積が減らないよう、拡張時にそのクラスを含む画像を優先的に選択します。

  • マスクの補正

変換後に小さな穴や欠損ができた場合、モルフォロジー処理で補正することもあります。

透過PNGの読み書きポイント

セグメンテーションマスクを透過PNG形式で保存・読み込みする場合、アルファチャンネルの扱いに注意が必要です。

透過PNGの特徴

  • 透過PNGはRGBA(4チャンネル)形式で、アルファチャンネルが透明度を表します
  • マスク画像で透過を使う場合、背景を透明にし、対象領域を不透明にすることが多いです

読み込み時の注意

OpenCVのcv::imreadで透過PNGを読み込む場合、cv::IMREAD_UNCHANGEDフラグを指定しないとアルファチャンネルが読み込まれません。

cv::Mat mask = cv::imread("mask.png", cv::IMREAD_UNCHANGED);
if (mask.channels() == 4) {
    // アルファチャンネルを分離
    std::vector<cv::Mat> channels;
    cv::split(mask, channels);
    cv::Mat alpha = channels[3];
    // alphaをマスクとして利用可能
}

書き込み時の注意

  • マスクを透過PNGとして保存する場合、アルファチャンネルを含む4チャンネル画像として保存します
  • アルファチャンネルは0(完全透明)〜255(不透明)で設定します
std::vector<cv::Mat> channels = {mask, mask, mask, alphaChannel};
cv::Mat rgba;
cv::merge(channels, rgba);
cv::imwrite("mask_transparent.png", rgba);
  • 透過PNGはマスクの背景透過表現に便利だが、読み書き時にアルファチャンネルの扱いを明示的に行う必要があります
  • マスクのラベル情報はアルファチャンネルとは別に管理することが多いので、用途に応じて使い分けます

これらのポイントを押さえ、セグメンテーションマスクのデータ拡張を正確かつ効率的に行いましょう。

キーポイント・ランドマーク対応

vector<Point2f>一括変換方法

キーポイントやランドマークは、画像上の特徴的な点の座標をstd::vector<cv::Point2f>で管理することが多いです。

画像に対して幾何学変換(回転、平行移動、スケーリング、アフィン変換など)を行う際は、これらの点群も同じ変換を適用して座標を更新する必要があります。

一括変換の基本

OpenCVにはcv::transform関数があり、2×3または3×3の変換行列を使って複数の2D点を一括で変換できます。

これにより、ループで1点ずつ変換するより効率的かつ簡潔に処理できます。

2×3行列(アフィン変換)を使った例

#include <opencv2/opencv.hpp>
#include <vector>
int main() {
    // 変換前のキーポイント
    std::vector<cv::Point2f> keypoints = {
        cv::Point2f(100.0f, 50.0f),
        cv::Point2f(150.0f, 80.0f),
        cv::Point2f(120.0f, 100.0f)
    };
    // アフィン変換行列(例:回転30度、スケール1.0、中心は(128,128))
    cv::Point2f center(128.0f, 128.0f);
    double angle = 30.0;
    double scale = 1.0;
    cv::Mat affineMat = cv::getRotationMatrix2D(center, angle, scale);
    // 変換後のキーポイント格納用
    std::vector<cv::Point2f> transformedKeypoints;
    // 一括変換
    cv::transform(keypoints, transformedKeypoints, affineMat);
    // 結果表示
    for (size_t i = 0; i < transformedKeypoints.size(); ++i) {
        std::cout << "Original: " << keypoints[i] << " -> Transformed: " << transformedKeypoints[i] << std::endl;
    }
    return 0;
}

このコードでは、cv::transformにより複数の点を一括でアフィン変換しています。

3×3のホモグラフィ行列を使う場合も同様にcv::perspectiveTransform関数を使います。

3×3行列(パースペクティブ変換)を使った例

cv::Mat perspectiveMat = cv::getPerspectiveTransform(srcPoints, dstPoints);
cv::perspectiveTransform(keypoints, transformedKeypoints, perspectiveMat);

可視フラグの保持と更新

キーポイントやランドマークには、検出の有無や視認性を示す「可視フラグ(visibility flag)」が付与されることがあります。

データ拡張で変換を行う際は、このフラグの保持と適切な更新が重要です。

可視フラグの役割

  • 1(またはtrue): キーポイントが画像内に存在し、視認可能であることを示します
  • 0(またはfalse): キーポイントが視認できない、または画像外に出ていることを示します

変換後の可視フラグ更新

変換によりキーポイントが画像の範囲外に移動した場合、可視フラグをfalseに更新します。

これにより、学習時に無効な点を除外できます。

実装例

#include <opencv2/opencv.hpp>
#include <vector>
void updateVisibilityFlags(const std::vector<cv::Point2f>& points, std::vector<bool>& visibility, int imgWidth, int imgHeight) {
    for (size_t i = 0; i < points.size(); ++i) {
        const cv::Point2f& pt = points[i];
        // 画像範囲内かどうか判定
        bool visible = (pt.x >= 0 && pt.x < imgWidth && pt.y >= 0 && pt.y < imgHeight);
        visibility[i] = visible;
    }
}
int main() {
    std::vector<cv::Point2f> keypoints = { {100, 50}, {150, 80}, {300, 400} };
    std::vector<bool> visibility = { true, true, true };
    // 例として画像サイズを設定
    int imgWidth = 256;
    int imgHeight = 256;
    // 変換後のキーポイント(例としてそのまま)
    std::vector<cv::Point2f> transformedKeypoints = keypoints;
    // 可視フラグを更新
    updateVisibilityFlags(transformedKeypoints, visibility, imgWidth, imgHeight);
    for (size_t i = 0; i < visibility.size(); ++i) {
        std::cout << "Keypoint " << i << " visibility: " << (visibility[i] ? "visible" : "not visible") << std::endl;
    }
    return 0;
}

注意点

  • 変換前に可視フラグがfalseの点は、変換後もfalseのまま保持することが多いです
  • 画像の境界条件は厳密に設定してください。例えば、x == imgWidthは範囲外とみなすのが一般的です
  • 物体の一部が画像外に出る場合、可視フラグの扱いはタスクやデータセットの仕様に合わせて調整してください

これらの方法で、キーポイントやランドマークの座標と可視フラグを正確に管理し、データ拡張時の整合性を保つことができます。

大規模データへの適用

メモリ効率を高めるストリーム処理

大規模な画像データセットに対してデータ拡張を行う場合、一度にすべての画像をメモリに読み込むのは非現実的です。

メモリ効率を高めるために、ストリーム処理(逐次処理)を活用します。

ストリーム処理のポイント

  • バッチ単位で読み込み・処理

画像を小さなバッチに分割し、必要な分だけメモリに読み込みます。

処理が終わったらメモリから解放し、次のバッチを読み込みます。

  • 遅延読み込み(Lazy Loading)

画像ファイルを必要になるまで読み込まないことで、メモリ使用量を抑えます。

  • パイプライン化

読み込み、前処理、拡張、保存をパイプラインとして連結し、各ステップを非同期に処理することで効率化します。

実装例(擬似コード)

const int batchSize = 32;
std::vector<std::string> imagePaths = loadImagePaths();
for (size_t i = 0; i < imagePaths.size(); i += batchSize) {
    size_t end = std::min(i + batchSize, imagePaths.size());
    std::vector<cv::Mat> batchImages;
    // バッチ単位で画像を読み込み
    for (size_t j = i; j < end; ++j) {
        cv::Mat img = cv::imread(imagePaths[j]);
        if (!img.empty()) {
            batchImages.push_back(img);
        }
    }
    // バッチに対してデータ拡張を適用
    for (auto& img : batchImages) {
        cv::Mat augmented = applyDataAugmentation(img);
        // 保存や次処理へ
    }
    // バッチ画像はスコープ外で解放されるためメモリ解放される
}

この方法で、メモリ使用量を一定に保ちながら大規模データを処理できます。

UMatとGPUアクセラレーション活用

OpenCVのUMatは、CPUとGPU間のデータ転送を自動管理し、GPUアクセラレーションを活用できるデータ構造です。

大規模データの高速処理に有効です。

UMatの特徴

  • 自動メモリ管理

CPUとGPUのメモリ間で必要に応じてデータを転送し、プログラマが明示的に管理する必要がありません。

  • GPU対応関数の利用

OpenCVの多くの関数はUMatを引数に取るとGPUで処理されます。

  • 互換性

cv::Matとほぼ同様のAPIで扱え、既存コードの改修が容易です。

利用例

cv::UMat src, dst;
cv::Mat img = cv::imread("image.jpg");
img.copyTo(src); // MatからUMatへコピー
// GPU対応のぼかし処理
cv::GaussianBlur(src, dst, cv::Size(5, 5), 1.5);
// UMatからMatへ戻す
cv::Mat result = dst.getMat(cv::ACCESS_READ);
cv::imwrite("blurred.jpg", result);

注意点

  • GPU対応関数はOpenCVのビルド時にCUDAやOpenCLが有効になっている必要があります
  • 全ての関数がGPU対応しているわけではないため、処理の一部はCPUで行われることがあります

スレッドプールでの高速化

大量の画像を並列処理する際、スレッドプールを使うことで効率的にCPUリソースを活用できます。

スレッドプールはスレッドの生成・破棄のオーバーヘッドを減らし、タスクのスケジューリングを最適化します。

スレッドプールの基本構成

  • 固定数のワーカースレッド

CPUコア数に合わせてスレッド数を決定し、常に待機しています。

  • タスクキュー

処理すべきタスクをキューに入れ、空いているスレッドに割り当てます。

  • 同期制御

タスクの完了待ちや排他制御を適切に行います。

C++11以降の簡易実装例(擬似コード)

#include <thread>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
class ThreadPool {
public:
    ThreadPool(size_t numThreads);
    ~ThreadPool();
    void enqueue(std::function<void()> task);
private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queueMutex;
    std::condition_variable condition;
    bool stop;
    void workerThread();
};
// 実装は省略
int main() {
    ThreadPool pool(std::thread::hardware_concurrency());
    for (const auto& path : imagePaths) {
        pool.enqueue([path]() {
            cv::Mat img = cv::imread(path);
            if (!img.empty()) {
                cv::Mat augmented = applyDataAugmentation(img);
                // 保存など
            }
        });
    }
    // デストラクタで全タスク完了まで待機
}

効果

  • スレッドの生成コストを削減し、タスクのスループットが向上します
  • CPUコアを最大限活用し、処理時間を大幅に短縮可能です

注意点

  • スレッド間の競合やデータ競合を避けるため、共有リソースのアクセスは排他制御が必要です
  • I/O待ちが多い場合は、スレッド数をCPUコア数より多めに設定することも検討します

これらの技術を組み合わせることで、大規模データセットに対して効率的かつ高速にデータ拡張を適用できます。

データ不均衡対策としての拡張比率

クラス別重み付けサンプリング

データセットにおけるクラス不均衡は、モデルの学習に悪影響を及ぼしやすいため、データ拡張時にクラスごとに重み付けを行い、少数クラスのサンプルを多めに利用する手法が有効です。

重み付けサンプリングの基本

  • 各クラスのサンプル数に応じて重みを計算し、少数クラスほど高い重みを割り当てます
  • サンプル選択時に重みを考慮して確率的にサンプルを抽出します
  • これにより、少数クラスのデータが拡張される頻度が増え、学習時のバランスが改善されます

重みの計算例

クラス c のサンプル数を Nc、全サンプル数を N とすると、重み wc は以下のように計算できます。

wc=NK×Nc

ここで、K はクラス数です。

これにより、サンプル数が少ないクラスほど大きな重みが割り当てられます。

実装例(擬似コード)

std::map<int, int> classCounts = countSamplesPerClass(dataset);
int totalSamples = dataset.size();
int numClasses = classCounts.size();
std::map<int, double> classWeights;
for (auto& kv : classCounts) {
    classWeights[kv.first] = static_cast<double>(totalSamples) / (numClasses * kv.second);
}
// サンプル選択時に重みを用いた確率分布を作成し、重み付きサンプリングを実施

この方法で、少数クラスのサンプルがより多く選ばれ、拡張比率を調整できます。

オンザフライ拡張とキャッシュ戦略

データ拡張は「オンザフライ(リアルタイム)」で行う方法と、事前に拡張データを生成して保存する方法があります。

大規模データや不均衡対策では、オンザフライ拡張とキャッシュ戦略の組み合わせが効果的です。

オンザフライ拡張の特徴

  • 学習時にバッチ単位でランダムに拡張を適用
  • ストレージ容量を節約できます
  • 多様な拡張パターンを生成可能です

キャッシュ戦略

  • オンザフライで生成した拡張画像を一時的にメモリやディスクにキャッシュ
  • 同じ拡張パターンを複数回利用する場合に高速化
  • キャッシュサイズはメモリ容量やストレージに応じて調整

実装例

std::unordered_map<std::string, cv::Mat> cache;
cv::Mat getAugmentedImage(const std::string& imagePath, const std::string& augKey) {
    std::string cacheKey = imagePath + "_" + augKey;
    if (cache.find(cacheKey) != cache.end()) {
        return cache[cacheKey];
    } else {
        cv::Mat img = cv::imread(imagePath);
        cv::Mat augmented = applyAugmentation(img, augKey);
        cache[cacheKey] = augmented;
        return augmented;
    }
}

このように、オンザフライ拡張の柔軟性とキャッシュの高速性を両立できます。

画像品質劣化モニタリング指標

データ拡張を繰り返すと、画像の品質が劣化し、学習に悪影響を与えることがあります。

品質劣化をモニタリングする指標を用いて、拡張比率やパラメータを適切に調整することが重要です。

主な品質指標

指標名説明備考
PSNR(ピーク信号対雑音比)元画像と拡張画像の画質差を数値化高いほど画質良好
SSIM(構造類似度指数)画像の構造的類似度を評価1に近いほど類似度高い
MSE(平均二乗誤差)元画像と拡張画像のピクセル差の二乗平均小さいほど画質良好

モニタリングの活用例

  • 拡張後の画像と元画像のPSNRやSSIMを計算し、閾値以下の場合は拡張パラメータを調整
  • 拡張比率を増やす際に品質指標を監視し、劣化が激しい場合は拡張強度を抑制
  • 品質指標をログに記録し、学習の安定性と関連付けて分析

実装例(PSNR計算)

double computePSNR(const cv::Mat& I1, const cv::Mat& I2) {
    cv::Mat s1;
    cv::absdiff(I1, I2, s1);
    s1.convertTo(s1, CV_32F);
    s1 = s1.mul(s1);
    cv::Scalar s = cv::sum(s1);
    double sse = s.val[0] + s.val[1] + s.val[2];
    if (sse <= 1e-10) {
        return 0;
    } else {
        double mse = sse / (double)(I1.channels() * I1.total());
        double psnr = 10.0 * log10((255 * 255) / mse);
        return psnr;
    }
}

この関数で元画像と拡張画像のPSNRを計算し、品質を評価できます。

これらの手法を組み合わせて、データ不均衡に強い効果的な拡張比率の設計と品質管理を行い、モデルの性能向上を目指しましょう。

オンライン学習との連携

バッチ単位リアルタイム拡張の流れ

オンライン学習(インクリメンタルラーニング)では、モデルの学習中にリアルタイムでデータ拡張を行い、常に新鮮で多様なデータを供給することが重要です。

バッチ単位でリアルタイムに拡張を適用する流れは以下のようになります。

  1. バッチデータの読み込み

学習用のバッチサイズ分の画像やラベルをストレージやメモリから読み込みます。

  1. ランダムパラメータの生成

各画像に対して回転角度、スケール、平行移動量、色調変換パラメータなどの乱数を生成します。

  1. リアルタイム拡張の適用

生成したパラメータを用いて、画像と対応するアノテーション(バウンディングボックス、マスク、キーポイントなど)に対して拡張処理を行います。

  1. バッチデータの整形

拡張後の画像をモデルの入力サイズにリサイズし、テンソル形式に変換します。

  1. モデルへの入力

拡張済みバッチをモデルに渡して学習を行います。

  1. 次バッチの準備

学習中に次のバッチの読み込みと拡張を並列で準備し、処理のボトルネックを減らします。

この流れにより、ストレージ容量を節約しつつ、多様なデータを効率的に学習に供給できます。

DataLoaderとのインターフェース設計

オンライン学習でリアルタイム拡張を行う際、DataLoader(データ読み込み・前処理モジュール)とのインターフェース設計が重要です。

拡張処理を柔軟かつ効率的に組み込むためのポイントを解説します。

拡張処理のモジュール化

  • 拡張処理はDataLoader内の独立したモジュールや関数として実装し、必要に応じて呼び出せるようにします
  • 拡張パラメータの生成と適用を分離し、パラメータの再利用やデバッグを容易にします

インターフェース例

class DataLoader {
public:
    DataLoader(const std::vector<std::string>& imagePaths, int batchSize);
    std::vector<BatchData> getNextBatch();
private:
    std::vector<std::string> imagePaths;
    int batchSize;
    int currentIndex;
    BatchData applyAugmentation(const cv::Mat& image, const Annotation& anno);
};
  • getNextBatch()でバッチ単位のデータを返し、内部でリアルタイム拡張を適用
  • applyAugmentation()で画像とアノテーションに対して拡張を行います

拡張パラメータの管理

  • 乱数シードやパラメータをDataLoader内で管理し、再現性を確保
  • 拡張のオンオフ切り替えやパラメータ範囲の調整を容易にする設定インターフェースを用意

並列処理対応

  • バッチの読み込みと拡張を別スレッドや非同期処理で行い、学習の待ち時間を削減
  • スレッドセーフなデータ構造やキューを利用してデータの受け渡しを管理

マルチGPU環境での同期処理

マルチGPU環境でオンライン学習を行う場合、複数GPU間でのデータ拡張や学習処理の同期が必要です。

同期処理を適切に設計しないと、学習の不整合や性能低下を招きます。

データ拡張の同期ポイント

  • 乱数シードの共有

各GPUで同じ拡張パラメータを使うか、異なるパラメータを使うかを明確に決めます。

一般的には多様性を保つためにGPUごとに異なる乱数シードを使います。

  • バッチ分割と割り当て

大きなバッチをGPU数で分割し、それぞれのGPUに割り当てます。

各GPUで独立に拡張を適用します。

通信と同期

  • 勾配同期

学習時は各GPUで計算した勾配を通信ライブラリ(NCCLなど)で同期し、パラメータ更新を一貫させます。

  • データ同期

拡張済みデータの同期は不要ですが、学習状態や乱数シードの管理は必要です。

実装例(PyTorch風擬似コード)

# 各GPUで独立に拡張

def data_augmentation_on_gpu(batch):
    augmented_batch = []
    for img in batch:
        params = generate_random_params()
        augmented_img = apply_augmentation(img, params)
        augmented_batch.append(augmented_img)
    return augmented_batch

# マルチGPUでの学習ループ

for batch in dataloader:

    # バッチをGPUごとに分割

    batches_per_gpu = split_batch(batch, num_gpus)
    outputs = []
    for gpu_id in range(num_gpus):
        with torch.cuda.device(gpu_id):
            augmented = data_augmentation_on_gpu(batches_per_gpu[gpu_id])
            output = model(augmented)
            outputs.append(output)

    # 勾配同期などの処理

    synchronize_gradients(outputs)

注意点

  • 拡張処理の乱数シード管理をGPUごとに分け、再現性と多様性を両立
  • 通信コストを考慮し、拡張は各GPUで独立に行うのが一般的
  • 学習の安定性を保つため、同期ポイントを適切に設計

これらの設計と実装を踏まえ、オンライン学習環境で効率的かつ安定したリアルタイムデータ拡張を実現しましょう。

まとめ

本記事では、C++とOpenCVを用いた多彩な画像データ拡張手法を詳しく解説しました。

ランダム回転や平行移動、スケーリング、反転などの基本変換から、色調調整やノイズ付加、ぼかし・シャープ化、部分切り抜き、アフィン・パースペクティブ変換まで幅広く扱っています。

さらに、セグメンテーションマスクやキーポイント対応、大規模データ処理、データ不均衡対策、オンライン学習との連携方法も紹介。

これらを組み合わせることで、モデルの汎化性能を効果的に向上させるデータ拡張パイプラインを構築できます。

関連記事

Back to top button
目次へ