cv::calcHist の使い方【OpenCV/C++】〜ヒストグラムを計算する〜
cv::calcHist は画像のヒストグラムを計算する関数です。グレースケール・カラーを問わず、指定チャンネルの画素値分布を cv::Mat として取得できます。得られたヒストグラムは輝度分析、画像比較、露出補正の前処理など幅広い用途に使えます。
cv::calcHist(&src, 1, channels, mask, hist, dims, histSize, ranges);
動作環境
- OpenCV 4.x
- ビルド・実行:
g++ -std=c++17 main.cpp $(pkg-config --cflags --libs opencv4) -o main && ./main
環境構築がまだの方は環境構築ガイドを先にどうぞ。
基本の使い方
グレースケール画像の輝度ヒストグラムを計算し、棒グラフ状に描画して保存するサンプルです。
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
// グレースケールで読み込む
cv::Mat src = cv::imread("input.jpg", cv::IMREAD_GRAYSCALE);
if (src.empty()) {
std::cerr << "画像を読み込めませんでした" << std::endl;
return -1;
}
// ヒストグラムの設定
int channels[] = {0}; // チャンネル番号(グレースケールは 0 のみ)
int histSize[] = {256}; // ビン数
float range[] = {0.0f, 256.0f}; // 画素値の範囲 [0, 256)
const float* ranges[] = {range};
cv::Mat hist;
cv::calcHist(
&src, // 入力画像の配列
1, // 画像枚数
channels, // 使用するチャンネル
cv::Mat(), // マスク(なし)
hist, // 出力ヒストグラム
1, // 次元数(1次元ヒストグラム)
histSize, // 各次元のビン数
ranges // 各次元の値域
);
// 0〜1 に正規化して描画用に変換
cv::normalize(hist, hist, 0, 400, cv::NORM_MINMAX);
// 描画キャンバス(高さ400, 幅256, 白背景)
cv::Mat canvas(400, 256, CV_8UC1, cv::Scalar(255));
for (int i = 0; i < 256; ++i) {
int binVal = static_cast<int>(hist.at<float>(i));
// 下端から上に向かって線を引く
cv::line(canvas,
cv::Point(i, 400),
cv::Point(i, 400 - binVal),
cv::Scalar(0), 1);
}
cv::imwrite("hist_gray.png", canvas);
std::cout << "hist_gray.png に保存しました" << std::endl;
return 0;
}
実行結果
hist_gray.png に保存しました
hist_gray.png には、横軸が画素値 0〜255、縦軸が頻度を表す棒グラフが保存されます。暗い画像であれば左側(低輝度側)に、明るい画像であれば右側に山が寄ります。
行ごとの解説
| ポイント | 説明 |
|---|---|
channels[] |
どのチャンネルを対象にするかを配列で指定。グレースケールは {0} のみ |
histSize[] |
ビン数。256 で画素値 1 段階ごとにカウント |
ranges[] |
[0, 256) の半開区間で指定する。上端は含まれない |
cv::Mat() |
マスクなしの場合は空の cv::Mat を渡す |
hist.at<float>(i) |
出力は CV_32F の 1 チャンネル cv::Mat。アクセス型は float |
引数と戻り値
void cv::calcHist(
const Mat* images, // 入力画像の配列(先頭アドレス)
int nimages, // 配列の要素数(画像枚数)
const int* channels, // ヒストグラムを計算するチャンネル番号の配列
InputArray mask, // マスク画像(CV_8U、同サイズ)。不要なら Mat()
OutputArray hist, // 出力ヒストグラム(CV_32F の ndims 次元 Mat)
int dims, // ヒストグラムの次元数
const int* histSize, // 各次元のビン数
const float**ranges, // 各次元の値域(ranges[i] = {min, max})
bool uniform = true, // ビン幅が均一かどうか
bool accumulate= false // 既存ヒストグラムに加算するか
);
| 引数 | 型 | 説明 |
|---|---|---|
images |
const Mat* |
入力画像の配列。&src のように先頭アドレスを渡す |
nimages |
int |
入力画像の枚数 |
channels |
const int* |
使用するチャンネル番号の配列 |
mask |
InputArray |
有効画素を指定するマスク(不要なら cv::Mat() または cv::noArray()) |
hist |
OutputArray |
計算結果。CV_32F の Mat で返る |
dims |
int |
ヒストグラムの次元数(チャンネル数と一致させる) |
histSize |
const int* |
各次元のビン数 |
ranges |
const float** |
各次元の [min, max) 値域 |
uniform |
bool |
true(デフォルト)でビン幅均一 |
accumulate |
bool |
true にすると hist をクリアせず加算する |
戻り値はなし(void)。結果は hist に書き込まれます。
実践例
カラー画像の BGR 各チャンネルヒストグラムを重ね描き
輝度分布だけでなく色成分ごとの偏りを可視化したい場合に使います。
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
cv::Mat src = cv::imread("input.jpg");
if (src.empty()) {
std::cerr << "画像を読み込めませんでした" << std::endl;
return -1;
}
// BGR の各チャンネルに分割
std::vector<cv::Mat> planes;
cv::split(src, planes);
const int histSize = 256;
float range[] = {0.0f, 256.0f};
const float* ranges[] = {range};
int ch[] = {0};
// 描画キャンバス(黒背景)
cv::Mat canvas(400, 256, CV_8UC3, cv::Scalar::all(0));
// B/G/R の色と対応チャンネル
const cv::Scalar colors[3] = {
cv::Scalar(255, 0, 0), // Blue
cv::Scalar(0, 255, 0), // Green
cv::Scalar(0, 0, 255) // Red
};
for (int c = 0; c < 3; ++c) {
cv::Mat hist;
cv::calcHist(&planes[c], 1, ch, cv::Mat(), hist, 1, &histSize, ranges);
cv::normalize(hist, hist, 0, 400, cv::NORM_MINMAX);
for (int i = 1; i < 256; ++i) {
cv::line(canvas,
cv::Point(i - 1, 400 - static_cast<int>(hist.at<float>(i - 1))),
cv::Point(i, 400 - static_cast<int>(hist.at<float>(i))),
colors[c], 1);
}
}
cv::imwrite("hist_color.png", canvas);
std::cout << "hist_color.png に保存しました" << std::endl;
return 0;
}
hist_color.png に、青・緑・赤の折れ線グラフが重なった BGR ヒストグラム画像が保存されます。
マスクを使って ROI のヒストグラムだけを計算する
検査対象領域だけの分布を取りたい場面でよく使います。
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
cv::Mat src = cv::imread("input.jpg", cv::IMREAD_GRAYSCALE);
if (src.empty()) {
std::cerr << "画像を読み込めませんでした" << std::endl;
return -1;
}
// 関心領域(ROI)のマスクを作成
cv::Mat mask = cv::Mat::zeros(src.size(), CV_8U);
cv::Rect roi(src.cols / 4, src.rows / 4, src.cols / 2, src.rows / 2);
mask(roi) = 255; // ROI 内だけ白
int channels[] = {0};
int histSize[] = {256};
float range[] = {0.0f, 256.0f};
const float* ranges[] = {range};
cv::Mat hist;
cv::calcHist(&src, 1, channels, mask, hist, 1, histSize, ranges);
// ビンの最大値を出力して確認
double maxVal = 0;
cv::minMaxLoc(hist, nullptr, &maxVal);
std::cout << "ROI ヒストグラムの最大頻度: " << maxVal << std::endl;
return 0;
}
ROI ヒストグラムの最大頻度: 12345 (画像内容によって変わります)
つまずきポイント
⚠️ hist.at<float>() で取得しないとアクセス違反になる
cv::calcHist の出力 hist は型が CV_32F(32ビット浮動小数点)です。int や uchar でアクセスしようとすると、コンパイルは通るものの実行時に不正な値やクラッシュが起きます。
// NG: 型が違う
int val = hist.at<int>(i);
// OK
float val = hist.at<float>(i);
⚠️ ranges の上端は 含まれない(半開区間)
float range[] = {0.0f, 256.0f} と書くと範囲は [0, 256) です。画素値 255 は含まれますが、255 を上端に指定すると 255 がカウントされなくなります。CV_8U 画像では 上端は必ず 256 にしてください。
// NG: 255 が最終ビンに入らない
float range[] = {0.0f, 255.0f};
// OK
float range[] = {0.0f, 256.0f};
⚠️ カラー画像を分割せずにチャンネル番号だけ変えると期待通りにならない場合がある
cv::split で分割した 1ch 画像を渡す方法と、元の 3ch 画像を渡して channels[] でチャンネルを指定する方法のどちらでも動作しますが、後者の場合は channels[] に渡す番号が画像配列全体を通じたグローバルなチャンネル番号になります。複数画像をまとめて渡すときに番号がずれやすいので、単一画像の場合は cv::split で分割してから渡す方が混乱しません。
関連する関数
cv::normalize— ヒストグラムを描画前にスケーリングするために必ず使う関数。cv::NORM_MINMAXとの組み合わせが定番cv::equalizeHist— ヒストグラム平坦化による自動コントラスト調整。cv::calcHistで分布を確認してから適用判断する流れが実務的cv::compareHist— 2つのヒストグラムを比較して類似度を計算。画像検索や異常検出に使うcv::split— カラー画像を BGR チャンネルに分割するときにcv::calcHistと併用する
内部リンク候補の中では、輪郭解析と組み合わせた前処理として cv::findContours の使い方【OpenCV/C++】〜輪郭を検出する〜 も参考になります。ROI を輪郭から生成し、マスクとして cv::calcHist に渡すパターンは実務でよく登場します。
まとめ
cv::calcHist は画素値の分布を CV_32F の cv::Mat として取得する関数です。ranges の上端は半開区間(CV_8U なら 256.0f)、アクセス型は必ず float の2点を守るだけで、グレースケール・カラー・マスク付きいずれのケースも同じシグネチャで扱えます。正規化・比較・平坦化など後段の処理と組み合わせることで、画像品質の定量評価や前処理の自動化に応用できます。
🛠 画像処理のプロが開発するSDK/API
本ブログを運営するスワローインキュベートは、OpenCV ベースのなりすまし判定SDK/API(C++製・OpenCV 4.10)を開発しています。顔認証システムへの組み込み実績多数。
