[BMP]ビットマップの回転 | Assertion Failed!

[BMP]ビットマップの回転

<※ 1/17追記>


下記ソースではちょっとしたバグがあります。

例えば180度回転したとき、元画像の最上端と最右ラインの

ピクセルが表示されません。かつ左と下のラインに空白ができます。


下記だと、例えば100*200の画像で180度回転させたとき、回転後画像の

(0, 0)に対応する座標が(100, 200)となるためです。

座標として、そして画素を持つデータとして有効な範囲は(99, 199)までです。


直した記事はこちら

成長の過程ということで、下記は残します。正月だったし。



1週間悩みに悩んでやっと動くところまで。

あけましておめでとうございます。


Assertion Failed!-ビットマップ回転90度 90°回転
Assertion Failed!-ビットマップ回転135度 135度回転

[作成環境]

・Visual Studi 2008 Standart Edition

・MFC-ダイアログベース


[基本的な流れ]

①元画像情報取得(幅、高さ、画像データ)

②回転角から新しい幅と高さを計算

③回転後の座標に対応する座標を元の画像からもってくる

④回転後のデータを元に、ビットマップ作成

⑤描画


[画像回転について]

座標(cx, cy)を中心に(x1, y1)をR(rad)回転させたときの回転後の座標(x2, y2)


x2 = (x1 - cx) * cos(-R) - (y1 - cy) * sin(-R) + cx

y2 = (x1 - cx) * sin(-R) + (y1 - cy) * cos(-R) + cy


となる。


ただし、元の画像から回転後の座標を求めると、計算過程により

一部回転後画像に穴があいてしまう。


これを解消するために、回転後の座標から、対応する元の画像の座標を

算出し、あてはめていくという手法を取る。


x1 = (x2 - cx) * cos(R) - (y2 - cy) * sin(R) + cx

y1 = (x2 - cx) * sin(R) + (y2 - cy) * cos(R) + cy


回転角からの座標計算は数学的な分野になるため、求め方の詳細については割愛。

というかわからないし、計算してみるのも面倒くさい。



画像中心を原点に回転させる場合、角度によっては元の画像よりも高さ、幅ともに

大きくなる。

画像が収まりきるように、回転後の幅と高さも計算する必要がある。


元の画像の幅(sw)と高さ(sh)をR(rad)回転した後の幅(dw)と高さ(dh)は


dw = | sw * cos(R) | + | sh * sin(R) | ( |~~| は絶対値)

dh = | sw * sin(R) | + | sh * cos(R) |



上記を元にしたコード。


private:
  CSpinButtonCtrl m_spinctrl; // スピンコントロール
  CBitmap m_srcBmp;      // 元画像
  int m_srcWidth;         // 元画像幅
  int m_srcHeight;        // 元画像高さ
  RGBQUAD *m_psrcData;  // 元画像データポインタ
  int m_dstWidth;         // 回転後幅
  int m_dstHeight;        // 回転後高さ
  RGBQUAD *m_pdstData;   // 回転後データ
  int m_spinDeg;         // 回転任意角


BOOL CRotateBMPDlg::OnInitDialog()
{

  ・・・中略


  // TODO: 初期化をここに追加します。
  // スピンコントロール初期化
  m_spinctrl.SetRange(0,360);

  // 元画像の取得
  m_srcBmp.Attach((HBITMAP)LoadImage(0,_T("C:\\sample.bmp"),IMAGE_BITMAP,0,0,LR_LOADFROMFILE));


  // BITMAP構造体の取得
  BITMAP srcBmpInfo;
  m_srcBmp.GetBitmap(&srcBmpInfo);


  // 元画像幅、高さの取得
  m_srcWidth = srcBmpInfo.bmWidth;
  m_srcHeight = srcBmpInfo.bmHeight;


  // 元画像データの取得
  m_psrcData = new RGBQUAD [ m_srcWidth * m_srcHeight];
  m_srcBmp.GetBitmapBits(m_srcWidth * m_srcHeight * sizeof(RGBQUAD), m_psrcData);


  // 初期画像は回転なし
  RotateBMP(0);


  return TRUE; // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}


void CRotateBMPDlg::OnPaint()
{
  if (IsIconic())
  {
    ・・・中略
  }
  else
  {
    CDialog::OnPaint();

    // 回転後画像がなかったら即リターン
    if(m_pdstData == NULL) return;


    CDC *pDC = GetDC(); // デバイスコンテキスト
    CDC dcMem;       // メモリデバイスコンテキスト
    dcMem.CreateCompatibleDC(pDC);


    CBitmap rot; // 回転後ビットマップ
    CBitmap *old; // 古いビットマップオブジェクト


    // 回転後ビットマップ作成

    rot.CreateCompatibleBitmap(pDC, m_dstWidth, m_dstHeight);


    // 作成したビットマップに回転データを登録する
    rot.SetBitmapBits(m_dstWidth * m_dstHeight * sizeof(RGBQUAD), m_pdstData);


    old = dcMem.SelectObject(&rot);


    // 転送処理

    pDC->BitBlt(0,0,m_dstWidth,m_dstHeight,&dcMem,0,0,SRCCOPY);


    dcMem.SelectObject(old);


    // 終了処理

    rot.DeleteObject();
    dcMem.DeleteDC();
    ReleaseDC(pDC);
  }
}


// 回転処理
void CRotateBMPDlg::RotateBMP(int theta)
{
  // deg → rad変換
  double rad = (theta * M_PI / 180);


  // 回転後の高さと幅取得
  m_dstWidth = (int)((fabs(m_srcWidth * cos(rad)) + fabs(m_srcHeight * sin(rad))) + 0.5);
  m_dstHeight = (int)((fabs(m_srcWidth * sin(rad)) + fabs(m_srcHeight * cos(rad))) + 0.5);


  // 元画像、回転後画像の中心座標計算
  int srcCX = m_srcWidth/2;
  int srcCY = m_srcHeight/2;
  int dstCX = m_dstWidth/2;
  int dstCY = m_dstHeight/2;


  // 回転後イメージ格納バッファ
  if(m_pdstData != NULL) delete [] m_pdstData;

  m_pdstData = new RGBQUAD [ m_dstWidth * m_dstHeight ];
  memset(m_pdstData, 0x00, m_dstWidth * m_dstHeight * sizeof(RGBQUAD));


  // sin、cos値を事前に計算(注1)
  int int_sin = (int)(sin(rad) * 1024);
  int int_cos = (int)(cos(rad) * 1024);


  // 元画像、回転後座標
  int srcX, srcY, dstX, dstY;


  // 回転後イメージから元イメージの位置算出
  for(dstY = 0; dstY < m_dstHeight; dstY++)
  {
    for(dstX = 0; dstX < m_dstWidth; dstX++)
    {

      // 回転後の座標から対応する元画像の位置を計算(注1)
      srcX = (((dstX - dstCX)*int_cos - (dstY - dstCY)*int_sin) >> 10) + srcCX;
      srcY = (((dstX - dstCX)*int_sin + (dstY - dstCY)*int_cos) >> 10) + srcCY;


      if(srcX>=0 && srcX<m_srcWidth && srcY>=0 && srcY<m_srcHeight)
      {
        m_pdstData[dstX + dstY*m_dstWidth] = m_psrcData[srcX + srcY*m_srcWidth];
      }
    }
  }
}


※RGBQUADについて

メモリ確保の際にRGBQUAD型で(幅×高さ)分取ることによって、

1ピクセル単位の色データを丸ごと回転後のピクセルへコピーしている。


※注1

処理を高速にするための処理。

→sin、cosの計算は処理時間が結構かかるのでループの外で事前に計算。

→浮動小数点よりも整数のほうが処理が早い。


詳しくは、下記の参考サイト参照。


また、この段階では画像の穴抜けはないが、エッジが汚い。

これを解消するために、線形補間というものがあるようだが、

上記コードでは未反映。これも下記サイトで説明されています。


<参考URL>TSUGU software atelier


ひと目でわかるMicrosoft Visual C++ 2008 アプリケーション開発入門 (マイクロソフト公式解説書)/増田 智明
¥2,919
Amazon.co.jp