syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

OpenCVの輝度スケーリング

OpenCVで画像ロードしてRGB→YCbCr変換して、特殊なカーネルコードにより輝度YをスケーリングしたのちYCbCr→RGBに戻す、というコードを書いてみました。つまりOpenCVのフィルター系関数や変換マトリックスは使ってません。単純に線形スケーリングするだけならばcvConvertScale()使えばいいと思うんですが。ちなみにHSB(HSV)の明度B(V)とYCbCrの輝度Yは似てますが意味が異なるので注意です*1
なお、OpenCVのIPL画像は24bppの場合BGR,BGR,...の順で各8bitのカラーチャンネルが並んでます。Windowsビットマップと同じです。
IplImage::widthStepは4バイトアライメントとパディングを考慮したスキャンラインのバイト長、いわゆるストライド情報になります。

RGB⇔HSV変換処理に関しては、機会があればまた説明しましょう。ちなみにカスタムフィルター適用結果を単純にリアルタイムプレビューするのが主目的であれば、C++で書いてCPUで処理させるよりも、GLSL/HLSLなどを使ってGPUで処理させたほうがよいです。GPUアクセラレーションを活用する手段としてはCUDAやOpenCLもありますが、画面表示が前提であればOpenGL/Direct3Dのほうが効率的です。

#define _CRT_SECURE_NO_WARNINGS
// OpenCV 2.4.7 の "opencv2/flann/logger.h" 内で fopen() が使われているため、
// デフォルトで「SDL チェック」が「はい (/sdl)」になっている VC++ 2012 以降では、
// _CRT_SECURE_NO_WARNINGS 定義がないとコンパイル エラーになる。
// また、"opencv2/legacy/compat.hpp" に C3H+96H の非 ASCII バイト列が含まれるため、警告 C4819 が出る。
#include <cstdio>
#pragma warning(push)
#pragma warning(disable:4819)
#include <opencv/cv.h>
#include <opencv/highgui.h>
#pragma warning(pop)
#undef _CRT_SECURE_NO_WARNINGS
#include <cassert>
#include <algorithm>
#include <conio.h>

#pragma comment(lib, "vc11\\lib\\opencv_core247.lib")
#pragma comment(lib, "vc11\\lib\\opencv_highgui247.lib")

// VC++ 2010 以降は <cstdint> を使えばよい。
typedef unsigned char uint8_t;

namespace
{
  template<typename T> const T& Clamp(const T& x, const T& minVal, const T& maxVal)
  {
    if (x < minVal) return minVal;
    if (x > maxVal) return maxVal;
    return x;
  }

  template<typename T> T* SafePointerCast(void* p)
  { return static_cast<T*>(p); }

  template<typename T> const T* SafePointerCast(const void* p)
  { return static_cast<const T*>(p); }

  template<typename T> inline double MyContrastFunc(T src, double brightScale)
  {
    return src + (sqrt(src) - src) * brightScale;
  }

  inline uint8_t ClampMyContrastFunc(uint8_t src, double brightScale)
  {
    return uint8_t(Clamp<double>(MyContrastFunc(src, brightScale), 0, 255));
  }

  const double NtscGrayCoeffR = 0.298912;
  const double NtscGrayCoeffG = 0.586611;
  const double NtscGrayCoeffB = 0.114478;

  inline uint8_t ToNtscGrayscale(uint8_t red, uint8_t green, uint8_t blue)
  {
    return uint8_t(NtscGrayCoeffR * red + NtscGrayCoeffG * green + NtscGrayCoeffB * blue);
  }

  // http://ja.wikipedia.org/wiki/YUV

  void RgbToYCbCr(uint8_t red, uint8_t green, uint8_t blue, double& y, double& cb, double& cr)
  {
    y  = +NtscGrayCoeffR * red + NtscGrayCoeffG * green + NtscGrayCoeffB * blue;
    cb = -0.168736 * red - 0.331264 * green + 0.5 * blue;
    cr = 0.5 * red - 0.418688 * green - 0.081312 * blue;
  }

  void YCbCrToRgb(double y, double cb, double cr, uint8_t& red, uint8_t& green, uint8_t& blue)
  {
    red   = uint8_t(Clamp<double>((y + 1.402 * cr), 0, 255));
    green = uint8_t(Clamp<double>((y - 0.344136 * cb - 0.714136 * cr), 0, 255));
    blue  = uint8_t(Clamp<double>((y + 1.772 * cb), 0, 255));
  }

  inline uint8_t AppendColorVal(uint8_t color, double diff)
  {
    //return uint8_t(Clamp<double>(color * (1 + diff / 255), 0, 255));
    return uint8_t(Clamp<double>(color + diff, 0, 255));
  }

  void DoMyContrastProc(IplImage* dstImg, const IplImage* srcImg, double brightScale)
  {
    assert(
      (dstImg->width == srcImg->width) &&
      (dstImg->height == srcImg->height) &&
      (dstImg->widthStep == srcImg->widthStep)
      );
    const uint8_t* srcDib = SafePointerCast<const uint8_t>(srcImg->imageData);
    uint8_t* dstDib = SafePointerCast<uint8_t>(dstImg->imageData);
    if (srcImg->depth == 8)
    {
      if (srcImg->nChannels == 1)
      {
        printf("Grayscale format. BrightScale = %+5.1f\n", brightScale);
        for (int y = 0; y < srcImg->height; ++y)
        {
          for (int x = 0; x < srcImg->width; ++x)
          {
            const int index = srcImg->widthStep * y + x;
            dstDib[index] = ClampMyContrastFunc(srcDib[index], brightScale);
          }
        }
        return;
      }
      else if (srcImg->nChannels == 3)
      {
        printf("Color BGR format. BrightScale = %+5.1f\n", brightScale);
        for (int y = 0; y < srcImg->height; ++y)
        {
          for (int x = 0; x < srcImg->width; ++x)
          {
            // HLSL, GLSL, OpenCL, CUDA などを使って GPU アクセラレートする場合、
            // この処理をピクセル シェーダーやカーネル関数で実行する。
            // OpenCV にも OpenCL アクセラレートできる ocl モジュールや、
            // CUDA でアクセラレートできる gpu モジュールが用意されているが、
            // 基本的に定義済みフィルターを組み合わせて実行するタイプなので、
            // カーネル関数を直接制御することはできない?
            const int index = srcImg->widthStep * y + x * 3;
            const uint8_t b1 = srcDib[index + 0];
            const uint8_t g1 = srcDib[index + 1];
            const uint8_t r1 = srcDib[index + 2];
#if 1
            double srcGray = 0, srcCb = 0, srcCr = 0;
            uint8_t r2 = 0, g2 = 0, b2 = 0;
            RgbToYCbCr(r1, g1, b1, srcGray, srcCb, srcCr);
            const double newGray = MyContrastFunc(srcGray, brightScale);
            YCbCrToRgb(newGray, srcCb, srcCr, r2, g2, b2);
#else
            const uint8_t srcGray = ToNtscGrayscale(r1, g1, b1);
            const double newGray = MyContrastFunc(srcGray, brightScale);
            const double diff = (newGray - srcGray);
            const uint8_t b2 = AppendColorVal(b1, diff);
            const uint8_t g2 = AppendColorVal(g1, diff);
            const uint8_t r2 = AppendColorVal(r1, diff);
#endif

#if 1
            dstDib[index + 0] = b2;
            dstDib[index + 1] = g2;
            dstDib[index + 2] = r2;
#else
            // 原画像がグレースケールの場合との比較確認用。
            const uint8_t gray2 = ToNtscGrayscale(r2, g2, b2);
            dstDib[index + 0] = gray2;
            dstDib[index + 1] = gray2;
            dstDib[index + 2] = gray2;
#endif
          }
        }
        return;
      }
    }
    puts("Not supported format.");
  }
}

int main(int argc, char* argv[])
{
  // テスト画像入手元:
  // http://www.ess.ic.kanagawa-it.ac.jp/app_images_j.html
#if 1
  const char* imgfilePath = "Lenna.bmp";
#else
  const char* imgfilePath = "Lenna_gray.bmp";
#endif

  // 画像の読み込み。
  IplImage* srcImg = cvLoadImage(imgfilePath, CV_LOAD_IMAGE_ANYCOLOR | CV_LOAD_IMAGE_ANYDEPTH);
  if (!srcImg)
  {
    printf("Failed to load the image file: <%s>\n", imgfilePath);
    _getch();
    return -1;
  }
  IplImage* dstImg = cvCloneImage(srcImg);
  assert(dstImg != NULL);

  const char* windowName = "OpenCV Test";

  cvNamedWindow(windowName, CV_WINDOW_AUTOSIZE);

  cvShowImage(windowName, dstImg);

  const int EscKey = 27;
  int key = 0;
  double brightScale = 0;
  do
  {
    // キー入力待ち
    key = cvWaitKey(0);
    switch (key)
    {
    case 'b':
      brightScale -= 0.5;
      DoMyContrastProc(dstImg, srcImg, brightScale);
      cvShowImage(windowName, dstImg);
      break;
    case 'd':
      brightScale += 0.5;
      DoMyContrastProc(dstImg, srcImg, brightScale);
      cvShowImage(windowName, dstImg);
      break;
    default:
      break;
    }
  } while (key != EscKey);

  cvDestroyWindow(windowName);

  // 生成した画像データを解放する。
  cvReleaseImage(&srcImg);
  cvReleaseImage(&dstImg);

  return 0;
}

今回は簡単のため、fopen()に絡むコンパイルエラーを防止するのに_CRT_SECURE_NO_WARNINGSシンボルを定義しましたが、Visual C++プロジェクトプロパティで、「C/C++」→「全般」→「SDL チェック」を「いいえ (/sdl-)」にすることでエラーを警告にとどめることもできます。

ちなみにOpenCVはファイルパス文字列を受け取るAPIがconst char*にしか対応しておらず、ワイド文字列const wchar_t*版が存在しないので、Windows環境ではファイルパスにUnicode文字列を使えないことに注意が必要です。
FBX SDKなどは内部でUTF-8を使っているので、Windows版で非ASCII文字を扱う場合、WideCharToMultiByte()関数を使ってUTF-16ワイド文字列からUTF-8形式のマルチバイト文字列に変換して渡せばよいのですが、WindowsOpenCVは内部で単純にANSIマルチバイト文字列(扱える文字はシステムロケール設定に依存)を使っているだけのようです。国際化対応の乏しい古いライブラリは、ANSIマルチバイト文字列にしか対応していないものが多いです。

*1:HSB(HSV)の彩度Sがゼロのときは明度B(V)と輝度Yは等しくなりますが、だからといってカラー画像をグレースケール化(モノクロ化)する際に彩度Sを無理やりゼロにする方法でグレースケール化したりしないようにしましょう。RGBから輝度Yを計算してグレースケール化するのが正しい方法です。