画像処理プログラミングにおける逆行列の重要性

特にアフィン変換・回転などの座標変換系の画像処理を行うためには逆行列の存在が非常に重要です。なぜ画像処理プログラミングに逆行列が重要であるかを解説します。

例えば回転を行うプログラムを下記で紹介しています。

C言語で画像を回転

このプログラムで注目していただきたいのはループを回転後の座標に対して実施している点です。

回転後座標でループを行うということは、回転後座標が与えられており、そこから回転前座標を計算する必要があります。これを行うためには、よく数学で使う回転行列をそのまま使用するのではなく、その行列の逆行列を用いて計算を行う必要があります

またスキューにおいても同様にスキュー後の座標でのループを行っています。特にこの場合は逆行列が複雑なものになっていますがそれでも逆行列を用いてスキュー処理を行っています。

C言語で画像のスキュー

逆に変換前座標のループであれば、もっとシンプルな行列の演算で済むのに、なぜわざわざ逆行列を求めているのでしょうか。これには明確な理由があります。それは画質向上のためです。

プログラム

回転を例に、回転前座標でのループの中で回転行列を用いて回転後座標を計算しているプログラムと回転後座標ループの中で回転行列の逆行列を用いて開店前座標を計算しているプログラムとで、プログラムと実行結果がどのように変わるか確認してみましょう。

プログラムの大まかな説明は画像の回転のページをご参照ください。下記2つのプログラムの違いは回転処理を行っているループのところのみです。回転前座標のループの方は回転行列をそのまま使用してoriginalm, originalnを計算し、回転後座標のループの方は回転行列の逆列を使用してoriginalm, originalnを計算しています。

C言語で画像を回転

回転行列を使用したソースコード

回転後座標計算時に用いる行列は下記のものになります。

$$ \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 < raw.height; n++){
    for(m = 0; m < raw.width; m++){
      /* 回転行列から回転後座標を計算 */
      originalm =
        (m - (int)raw.width / 2) * cos(rad) -
        (n - (int)raw.height / 2) * sin(rad)  + rotatedRaw.width/ 2;
      m0 = originalm + 0.5;
      if(m0 >= rotatedRaw.width || m0 < 0) continue;

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

      for(c = 0; c < rotatedRaw.ch; c++){
        rotatedRaw.data[rotatedRaw.ch * (m0 + n0 * rotatedRaw.width) + c]
          = raw.data[raw.ch * (m + n * 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;
}

回転行列の逆行列を用いたソースコード

回転前座標計算時に用いる行列は下記のものになります。

$$ \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;
}

回転後画像の違い

回転前座標のループ

回転後座標のループ

画像の違い

回転前座標でのループの画像は穴があって汚くなっているのが確認できます。

これが起こっている理由は回転後座標計算時の量子化誤差が発生しているためです。この量子化誤差は小数点付きの実数を整数に変換する時に発生します。例えば5.3を5に変換する時は0.3の誤差が発生しますよね?この誤差です。デジタルな値を扱うコンピュータではこの誤差はいろんな場面で発生します。

回転前座標から回転後座標を求める際にこの誤差が発生するとどうなるかというと、異なる回転前座標から回転後座標を求めたとしても、結果が同じになる場合があります。つまり、同じ座標に重複して何回も回転前座標の画素値をコピーすることになります。重複した分、回転後画像に画素値がコピーされないので穴が空いているように見えるのです。

試しに回転前座標でのループのプログラムを少し改造して、回転後座標のどの部分がどれくらい重複して画素値コピーされているかを可視化してみました。白いところほど何回も重複して画素値コピーを行われているところで、黒い部分はコピーが行われなかった部分になります。

回転後座標でループしてやれば、回転後座標全てに対して画素値がコピーされることになるので穴ができるようなことはありません。このため、同じループ回数だとしても画質に大きな差が出ることになります。

スポンサーリンク

まとめ

  • 画像処理(特に座標変換を行う画像処理)では画像処理後の座標に対するループを用いて処理を行った方が良い
  • 画像処理前の座標に対するループだと量子化誤差により画像処理後画像に穴ができてしまう

コメントを残す

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