C言語で画像を回転

このページでは、画像の回転についての説明と、そのプログラムの例の紹介を行います。

画像の回転

まず画像の回転について説明します。画像とは画素の集まりであることを下の記事で紹介しました。

画像データの構造・画素・RAWデータについて解説画像データの構造・画素・RAWデータについて解説

画像の回転では、この画素全てを回転後の座標に移動させる処理になります。下の図で言えば(x, y)座標の画素をθ分回転させて(x’, y’)座標に移動させる処理になります。

なので、(x’, y’)座標に来る画素は回転前画像のどの座標(x, y)の画素かが分かれば回転のプログラムは簡単に実装することができます。

言い換えれば、回転後の座標であるx’ をxとθを用いて表し、y’ をyとθを用いて表す計算式さえ分かれば良いです。

回転後座標を求める

ここから結構数学的要素が出てきますので注意です。画像処理やる上では数学的要素は必ず出てきますので興味のある方はプログラミングしながら少しずつ考え方を理解していくと良いと思います。

上の図にもう少しパラメータを記載してみましょう。rは原点Oからの(x, y)座標および(x’, y’)までの距離とし、(x, y)とx軸との成す角をφとしています。

この時、x, yはそれぞれ下の式で表すことができます。三角式とか懐かしい!

x = r * cosφ

y = r * sinφ

さらに、x’ と y’ は下記の式で表すことができます。

x’ = r * cos(θ + φ)

y’ = r * sin(θ + φ)

ここでさらに下記の加法定理を思い出しましょう。サイタコスモスコスモスサイタ・・・

sin(θ + φ) = sinθ * cosφ + cosθ * sinφ

cos(θ + φ) = cosθ * cosφ – sinθ * sinφ

なのでx’ とy’ は下記のように展開することができます。

x’ = r * cosθ * cosφ – r * sinθ * sinφ

y’ = r * sinθ * cosφ + r * cosθ * sinφ

x = r * cosφ、y = r * sinφなので、

x’ = x * cosθ – y * sinθ

y’ = x * sinθ + y * cosθ

として求まります。つまり(x, y)座標の画素をθ分回転させれば上式で求まる(x, y’)座標に移動します。

プログラムとしては移動後の座標である(x’, y’)に対してループを行った方が実装しやすいので、下のプログラムでは、下記の式に基づいて移動後の座標(x’, y’)から移動前の座標(x, y)を計算しています。この移動処理を全(x’, y’)座標の画素に対して行えば画像の回転を行うことができます。

x = x’ * cosθ + y’ * sinθ

y = – x’ * sinθ + y’ * cosθ

行列を用いた表現

ちなみに上の式は下記の行列の演算で表すことが可能です。ここで出てくる行列は回転行列と呼ばれます。つまり、画像の回転後座標(x’, y’)は回転行列と元の画像の座標(x, y)との積で取得することが可能です。

$$ \left ( \begin{array}{c} x’ \\ y’ \end{array} \right ) = \left ( \begin{array}{cc} \cos \theta & – \sin \theta \\ \sin \theta & \cos \theta \end{array} \right ) \left ( \begin{array}{c} x \\ y \end {array} \right ) $$

回転後の画像の座標(x’, y’)から元の画像の座標(x, y)を求める際は下記の行列計算で求めることが可能です。

$$ \left ( \begin{array}{c} x \\ y \end{array} \right ) = \left ( \begin{array}{cc} \cos \theta & \sin \theta \\ – \sin \theta & \cos \theta \end{array} \right ) \left ( \begin{array}{c} x’ \\ y’ \end {array} \right ) $$

スポンサーリンク

プログラム例

main.c
#include "myJpeg.h"
#include <math.h>
#include <string.h>

#define PI 3.14

int main(int argc, char *argv[]){

  RAWDATA_t raw, rotatedRaw;
  int m, n, c;
  int angle;
  int m0, m1, n0, n1;
  double originalm, originaln;
  double dm, dn;
  double rad;
  unsigned int a;

  char outname[256];

  FILE *fo;

  if(argc != 3){
    printf("ファイル名と回転角度(0 - 359)を引数に指定してください\n");
    return -1;
  }

  angle = atoi(argv[2]);
  if(angle > 359 || angle < 0){
    printf("ファイル名と回転角度(0 - 359)を引数に指定してください\n");
    return -1;
  }

  rad = (double)angle * PI / (double)180;

  if(jpegFileReadDecode(&raw, argv[1]) == -1){
    printf("jpegFileReadDecode error\n");
    return -1;
  }

  /* ここから画像処理 */
  /* 最近傍補間法で画像を角度で回転 */
  if(raw.width > raw.height){
    rotatedRaw.width = raw.width;
    rotatedRaw.height = raw.width;
  } else {
    rotatedRaw.width = raw.height;
    rotatedRaw.height = raw.height;
  }
  rotatedRaw.ch = raw.ch;

  if(rotatedRaw.width == 0 || rotatedRaw.height == 0){
    printf("回転後の幅もしくは高さが0です\n");
    freeRawData(&raw);
    return -1;
  }

  rotatedRaw.data = (unsigned char*)malloc(sizeof(unsigned char) * rotatedRaw.width * rotatedRaw.height * rotatedRaw.ch);
  if(rotatedRaw.data == NULL){
    printf("malloc rotatedRaw error\n");
    freeRawData(&raw);
    return -1;
  }

  memset(rotatedRaw.data, 0xFF, rotatedRaw.width * rotatedRaw.height * rotatedRaw.ch);

  for(n = 0; n < rotatedRaw.height; n++){
    for(m = 0; m < rotatedRaw.width; m++){
      originalm =
        (m - (int)rotatedRaw.width / 2) * cos(rad) +
        (n - (int)rotatedRaw.height / 2) * sin(rad)  + raw.width/ 2;
      m0 = originalm + 0.5;
      if(m0 >= raw.width || m0 < 0) continue;

      originaln =
        - (m - (int)rotatedRaw.width / 2) * sin(rad) +
        (n - (int)rotatedRaw.height / 2) * cos(rad)  + raw.height / 2;
      n0 = originaln + 0.5;
      if(n0 >= raw.height || n0 < 0) continue;

      for(c = 0; c < rotatedRaw.ch; c++){
        rotatedRaw.data[rotatedRaw.ch * (m + n * rotatedRaw.width) + c]
          = raw.data[raw.ch * (m0 + n0 * raw.width) + c];
      }
    }
  }
  /* ここまで画像処理 */

  sprintf(outname, "%s", "rotated.jpeg");

  if(jpegFileEncodeWrite(&rotatedRaw, outname) == -1){
    printf("jpegFileEncodeWrite error\n");
    freeRawData(&raw);
    return -1;
  }

  freeRawData(&raw);

  return 0;
}

プログラムの説明

JPEGファイルの読み込み・書き込み

下記記事と全く同じですのでこちらをご参照ください

C言語で画像の拡大縮小(最近傍補間編)

sin関数・cos関数

sin関数とcos関数はmath.hに定義されていますので使用する場合はこのファイルのインクルードが必要です。

#include <math.h>

またsin関数とcos関数の引数はラジアン[rad]ですので、下記で入力された角度をラジアンへ変換しています。

rad = (double)angle * PI / (double)180;

PIは3.14で自分で定義しています。

回転後の座標計算

もう一度回転後をイメージした図を載せておきます。このプログラムでは、m が x’、n が y’、x が originalm、y が originalnと対応づいています。

・横方向座標

回転後の横方向座標を下記で計算しています。

      originalm =
        (m - (int)rotatedRaw.width / 2) * cos(rad) +
        (n - (int)rotatedRaw.height / 2) * sin(rad)  + raw.width/ 2;
      m0 = originalm + 0.5;
      if(m0 >= raw.width || m0 < 0) continue;

mをwidth / 2で割った値で引いたり、nをheight / 2で割った値で引いたりしているところは1つのポイントです。上の方で説明した回転の計算式は、原点Oを中心とした座標の場合のものです。一方でプログラム内のループ文のようにmやnは0から始まる座標として考えており、原点O中心ではありません。そこでこれらの引き算を行うことで一旦中心を原点Oにしてから回転の計算式を実行しています。

m0 = originalm + 0.5は最近傍補間を行うための計算です。回転でも回転前座標が画像上に存在しないケースがあるため、補間処理によりその座標を推測する計算を行っています。最近傍補間処理については画像の拡大縮小記事で紹介しています。

C言語で画像の拡大縮小(最近傍補間編)

m0が元画像をはみ出しているような場合はcontinueでその座標の計算は飛ばしています。

・縦方向座標

      originaln =
        - (m - (int)rotatedRaw.width / 2) * sin(rad) +
        (n - (int)rotatedRaw.height / 2) * cos(rad)  + raw.height / 2;
      n0 = originaln + 0.5;
      if(n0 >= raw.height || n0 < 0) continue;

横方向座標の計算と同じ考えですので説明は割愛します。

回転後画像の画素への輝度値の代入

      for(c = 0; c < rotatedRaw.ch; c++){
        rotatedRaw.data[rotatedRaw.ch * (m + n * rotatedRaw.width) + c]
          = raw.data[raw.ch * (m0 + n0 * raw.width) + c];
      }

このループ内で回転後の画素に回転前座標の各RGBの輝度値を代入しています。

回転前後の画像

・画像処理前

・画像処理後(200度回転後の画像)

画像の回転方向

画像が時計回りになっていることが気になる方は、

rad = (double)angle * PI / (double)180;

を下記のように変更してみてください。

rad = - (double)angle * PI / (double)180;

これにより画像の回転方向が時計回りではなく反時計回りになります。

わざわざ角度をマイナスにする理由ですが、数学の授業等で習った座標系だと上方向がy軸の正方向になるのに対し、画像データにおける座標系は下方向がy軸の正方向になるという違いがあるためです。

数学で習った座標

画像データの座標

スポンサーリンク

まとめ

今回はC言語で画像を回転する方法とそのプログラムについて解説しました。sin や cos などでてきて戸惑った方もいるかもしれませんが、三角関数や行列は画像の変形を行う上では合った方が良い知識です。この分野に興味のある方は今からでも遅くないので復習しておくと良いと思います!

2 COMMENTS

ccdcmos

丁寧な説明で初心者にも分かりやく、参考にさせてもらっています。
ところで、画像Rawデーターの型宣言でしょうか、
RAWDATA_t raw, rotatedRaw;
のところでエラーが出ます。
RAWDATA_tという変数が見つからないエラーです。
私の環境はMacOsX xcodeです。

返信する
daeu

ccdcmosさん

ご質問ありがとうございます。
また説明が不明瞭なところがあり申し訳ございません。

おそらくRAWDATA_t構造体の型宣言がうまく認識されていないのだと思います。
こちらのソースコードはRAWDATA_tの型宣言は下記ページのmyJpeg.hで行うことを前提としています。

https://daeudaeu.com/programming/c-language/libjpeg/#LibJPEG-5

以下を行えばおそらくエラーが消えるはずです。
・↑からmyJpeg.hのソースコードのコピペしてmyJpeg.hのファイルを作る
・さらに↑からmyJpeg.cのソースコードをコピペしてmyJpeg.cのファイルを作る
・main.cとmyJpeg.cをコンパイルし、コンパイル結果とlibjpegをリンクして実行ファイル生成

ターミナルでなら下記のコマンドで実行ファイルが生成されます。
gcc main.c -c
gcc myJpeg.c -c
gcc main.o myJpeg.o -ljpeg -o rotation.exe

ただし、myJpeg.hとmyJpeg.cはlibjpegを使用することを前提としています。libjpegを使用してJPEGファイルの読み込み等を行うのであれば、こちらも下記ページでインストール手順を紹介していますので参考にしてください。MacOSであれば同様の手順でインストールすることができると思います。

https://daeudaeu.com/programming/c-language/libjpeg/

正直XCodeに関してはあまり使用したことがないため勉強不足なのですが、libjpegをXCodeで使用する方法も上記ページの最後に追記してみましたので、こちらも参考にしてください。

もし質問内容が解決出来ないようであればまたコメントしていただければと思います。
お手数おかけしますがよろしくお願いいたします。

返信する

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です