画像処理:反転・回転(MATLAB・C言語)

ここでは画像処理に慣れ親しんでもらうことを目的に、基本的な画像処理を紹介します。前半ではMATLABという商用ソフトウェアを使って、後半ではC言語でプログラミングして処理する内容を紹介します。

画像におけるデータ構成

そもそも、画像ファイルにはどのような仕組みでデータが記録されているのでしょうか?大別して、画像の大きさ等の情報が記録されているヘッダ部分と、画像データ本体に分かれます。画像は1つ1つの単位である画素(ピクセル)の集合体で、下記に示しますように、画像そのものは行と列から構成されているように見えます。リモートセンシングの分野では、画像上の行は ライン(line)(または行: row)、列はピクセル(pixel)(または列: column)と呼ばれることもあります。「ピクセル」という表現は、画素と列のどちらを意味するか、文脈から判断するようにしましょう。

画像のライン、ピクセル

しかし実際のファイルには、ライン数、ピクセル数に関係なく、単に一列のデータが並んでいるだけです。こちらがわが画像の幅であるピクセル数を指定することで、画像処理ソフトはピクセル数ごとに折り返してくれるので、行と列から構成される画像として表示されるというわけです。

大事な点としては、

画素単位での処理の場合には、「ファイルの終わりまで処理する」という命令を指示すればいいですから、特にライン数、ピクセル数は要りません。ライン数、ピクセル数を明示し、処理しても構いませんが、必ずしも要求されるわけではない、ということです。

一方、「近傍での処理」と書きましたが、ある画素を中心とした3×3や5×5の領域において、平均や差分などを計算することもよくあります。その場合には、画像における縦と横がわかっていないといけませんので、ライン数、ピクセル数は必要不可欠になります。


MATLABを用いた演習

演習用画像の保存

それでは実際の処理に移ります。まずは下記の画像を保存して下さい。 リンク先で右クリックして、「保存」を選択して画像を保存して下さい。

MATLABの起動

演習室の端末上で、下記のいずれかの方法でMATLABを起動しましょう。

Windows → すべてのプログラム → 専門ソフト → MATLAB → R2011b → MATLAB R2011b

または

C:\Program Files\MATLAB\R2011b\bin\matlab.exe

画像の読み込み

MATLABが起動すると、下記のように「>>」が表示されて、入力できる状態になります。ここで、適当な変数を使って(ここでは「A」)画像データを読み込んでいきます。下記のように入力して下さい。以下、入力文字を赤字・下線付きで示します。

>> A = imread('sampleimg1.jpg')

この時点で変数Aは、3次元の配列

A (行:列:バンド)

の構造を持つことになります。「バンド」と書きましたが、RGB画像の場合、3バンドが与えられます。例えば、Rバンドの10行目、3列目の輝度値を表示させたい場合には、

>> A (10,3,1)

ans =

  116

と入力します。「116」という値が記録されていると返ってきました。この構造の概念は本ページの後の演習で早速使いますので、理解して置いて下さい。

画像の表示

単に表示させるだけなら「image()」という関数を使います。

>> image(A)

または

>> imshow(A)

新しい表示画面の生成

複数の画面に異なる画像を表示させたい時が生じます。そのためのコマンドです。

>> figure(1)

と入力すると1番目の画面が現れます(上記のimage等を用いて既に画面が現れている場合には、何の変化もありません)。続いて2番目の画面を表示させたい時には、次のように入力します。

>> figure(2)

演習1:輝度値を2倍へ変換

わざわざimreadを用いた読み込みも書いていますが、既に実行している場合には省略して下さい。

>> A = imread('sampleimg1.jpg')
>> B = A * 2
>> image(B)

演習2:画像の左右を反転

わざわざimreadを用いた読み込みも書いていますが、既に実行している場合には省略して下さい。

>> A = imread('sampleimg1.jpg')
>> B = A(:,end:-1:1,:)
>> image(B)

ここで補足をします。RGBのJPEG画像のデータが配列Aに記録されています上述したように、配列Aは、

A (行:列:バンド)

という構造を持っています。

A (:,:,:)

と書くと、

A (1:1:end,1:1:end,1:1:end)

の意味になります。つまり、最初の要素(1)から最後の要素(end)まで、1ずつ加算して処理することを意味しています。

>> B = A(:,end:-1:1,:)

は、

>> B = A(1:1:end,end:-1:1,1:1:end)

と同値で、列を逆順に読み込んでBに代入することを意味します。ですので、左右を反転させた画像が表示される訳です。

演習3:画像の上下を反転

上記の演習2を参考に自分で考えましょう。

演習4:画像を1/nの大きさに縮小

演習2・3の応用でできます。例えば、1/3に縮小することを考えると3つ置きにデータを読み取ればいい訳です。

>> A = imread('sampleimg1.jpg')
>> B = A(1:3:end,1:3:end,:)
>> image(B)

表示される画像の座標値が1/3になり、また画像が粗くなっていることを確認しましょう。この応用で、ラインだけあるいはピクセルだけを縮小することも可能です。

演習5:画像を回転

imrotateという関数を使います。例えば、反時計回りに30度回転する場合には、次のように実行します。

>> A = imread('sampleimg1.jpg')
>> B = imrotate(A, 30)
>> image(B)

エディタでの処理

これまでは、逐一コマンドを入力して実行させる処理を説明してきました。ただ処理が多数に亘る複雑な一連の処理や、また後日再度利用する可能性のある処理はMATLABファイル(またはM-ファイル)と呼ばれるファイルに保存するのが適切です。以下、その方法を説明します。

エディタの起動

下記のように入力すると編集画面(エディタ)が起動します。

>> edit

事前に新規のM-ファイルの名前を決めていたり(例えば、image1.m)、あるいは既存のM-ファイルを開きたい時にはそのファイル名を続けて入力します。

>> edit image1.m

M-ファイルの実行

M-ファイルに記述した内容を実行するには、下記の方法かあるいはF5キーを押します。

デバッグ → ファイルを保存して実行

演習1:輝度値を2倍へ変換

コマンドラインを用いた演習1で同じ内容を演習しましたが、全く同じ内容を記述します。このM-ファイルを「image1.m」というファイル名で保存し、実行してみて下さい。

A = imread('sampleimg1.jpg')
B = A * 2
image(B)

C言語での処理

これまではMATLABでの処理を説明してきました。自分が処理したい内容に相当する関数がMATLABで用意されていない場合には、自分自身で処理内容を実装する必要があります。また画像処理ではfor文を使った処理が多数出てきますが、MATLABでのfor文等の繰り返し処理は極めて遅いです。そのため、MATLABを使わなくても画像処理ができるようになるために、ここではC言語を使った画像処理を演習します。

演習用画像の保存

それでは実際の処理に移ります。まずは下記の画像を保存して下さい。 リンク先で右クリックして、「保存」を選択して画像を保存して下さい。

Visual C++の起動

演習室の端末上で、Visual C++を起動しましょう。

Windows → すべてのプログラム → プログラミング → Microsoft Visual C++ 2010 Express

演習1:輝度値を2倍へ変換

C言語を用いて処理を記述すると下記のようになります。 C言語をコンパイル、実行できる環境を備えたソフトウェアを起動し、下記の内容を単純にコピーして貼り付け、 image1.cという名前で保存します。

/* 
 image1.c

 PBMフォーマット画像(RGB)を読み込んで、
 輝度値に一定値を掛けた上で出力するプログラム

*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

// 輝度値の倍数
#define SCALE    2

// 入力ファイル名
char INPUTFILE[100]="sampleimg1.pbm";

// 出力ファイル名
char OUTPUTFILE[100]="sampleimg1_rst.pbm";


int main(int argc, char *argv[])
{
    int i, j, k;
    int line, pixel;
    FILE *fpi, *fpo;
    unsigned char data[3];
    char str[256];
    

    ///// 入出力ファイルを開く処理:ファイルポインタを設定 /////

    // 入力ファイル(PBMフォーマット)オープン
    if((fpi = fopen(INPUTFILE, "rb")) == NULL){
        fprintf(stderr, "Can not open %s \n", INPUTFILE);
        exit(1);
    }

    // 出力ファイル(PBMフォーマット)オープン
    if((fpo = fopen(OUTPUTFILE, "wb")) == NULL){
        fprintf(stderr, "Can not open %s \n", OUTPUTFILE);
        exit(1);
    }
    
    
    ///// ヘッダファイルの処理 /////
    
    // 入力ファイル:1行目読み込み(PBMタイプ:P6=RGB rawフォーマット)
    fgets(str, 256, fpi);
    fputs(str, fpo);    // 出力ファイルへ書き込み
    fputs(str, stdout);    // 標準出力(画面)へ書き込み
    
    // 入力ファイル:2行目読み込み(ピクセル、ライン数)
    fgets(str, 256, fpi);
    fputs(str, fpo);
    fputs(str, stdout);
    sscanf(str, "%d %d", &pixel, &line); // 整数値を変数へ代入
    
    // 入力ファイル:3行目読み込み(階調数:255=8ビット=1バイト)
    fgets(str, 256, fpi);
    fputs(str, fpo);
    fputs(str, stdout);
    
    // 確認用:ライン、ピクセル数を標準出力へ表示
    fprintf(stdout, "line = %d, pixel = %d\n", line, pixel);
    


    ///// データ本体の処理 /////

    for (i=0; i<line; i++) {
        for (j=0; j<pixel; j++) {

            // 1画素ずつ入力ファイルからデータを読み込む
            // RGBの計3つ分をまとめて読み込む
            fread(data, sizeof(unsigned char), 3, fpi);
            
            // 画像輝度値をSCALE倍
            for (k=0; k<3; k++) {
                if (data[k] * SCALE > 255) {
                    data[k] = 255;
                }
                else {
                    data[k] *= SCALE;
                }
            }

            // RGBの計3つ分を出力ファイルへ書き込む
            fwrite(data, sizeof(unsigned char), 3, fpo);
        }
    }

    ///// 入出力ファイルを閉じる処理 /////

    fclose(fpi);
    fclose(fpo);
}

上記の「image1.c」を実行すると、ファイル「sampleimg1.pbm」を読み込んで、「sampleimg1_rst.pbm」というファイルが出力されます。正しく実行されたか画像を確認してみましょう。画像を開くエディタは何でもいいのですが、上記でMATLABの使い方を説明しましたので、MATLABで開いてみましょう。(参考:画像閲覧・処理用のフリーソフトウェアとしてはgimpが有名です)

>> A = imread('sampleimg1_rst.pbm')
>> image(A)

演習2:画像の左右を反転

演習1と同じで、下記の内容を単純にコピーして貼り付け、 image2.cという名前で保存し、その後実行してみて下さい。

/* 
 image2.c

 PBMフォーマット画像(RGB)を読み込んで、
 画像の左右を反転した結果を出力するプログラム

*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

// 入力ファイル名
char INPUTFILE[100]="sampleimg1.pbm";

// 出力ファイル名
char OUTPUTFILE[100]="sampleimg2_rst.pbm";


int main(int argc, char *argv[])
{
    int i, j, k;
    int line, pixel;
    FILE *fpi, *fpo;
    unsigned char tmp[3];
    char str[256];
    
    // RGB輝度値を格納する構造体を自分で定義
    struct rgb_data {
        unsigned char    r, g, b;
    };
    struct rgb_data    *data;    // この時点では必要な大きさが未定
    

    ///// 入出力ファイルを開く処理:ファイルポインタを設定 /////

    // 入力ファイル(PBMフォーマット)オープン
    if((fpi = fopen(INPUTFILE, "rb")) == NULL){
        fprintf(stderr, "Can not open %s \n", INPUTFILE);
        exit(1);
    }

    // 出力ファイル(PBMフォーマット)オープン
    if((fpo = fopen(OUTPUTFILE, "wb")) == NULL){
        fprintf(stderr, "Can not open %s \n", OUTPUTFILE);
        exit(1);
    }
    
    
    ///// ヘッダファイルの処理 /////
    
    // 入力ファイル:1行目読み込み(PBMタイプ:P6=RGB rawフォーマット)
    fgets(str, 256, fpi);
    fputs(str, fpo);    // 出力ファイルへ書き込み
    fputs(str, stdout);    // 標準出力(画面)へ書き込み
    
    // 入力ファイル:2行目読み込み(ピクセル、ライン数)
    fgets(str, 256, fpi);
    fputs(str, fpo);
    fputs(str, stdout);
    sscanf(str, "%d %d", &pixel, &line); // 整数値を変数へ代入
    
    // 入力ファイル:3行目読み込み(階調数:255=8ビット=1バイト)
    fgets(str, 256, fpi);
    fputs(str, fpo);
    fputs(str, stdout);
    
    // 確認用:ライン、ピクセル数を標準出力へ表示
    fprintf(stdout, "line = %d, pixel = %d\n", line, pixel);
    

    ///// データ本体の処理 /////

    // 確定したライン、ピクセル数を用いて、構造体用のメモリを確保
    data = (struct rgb_data *)malloc(sizeof(struct rgb_data)*line*pixel);


    // データ全体を一度読み込んで構造体に保存
    for (i=0; i<line; i++) {
        for (j=0; j<pixel; j++) {

            // 1画素ずつ入力ファイルからデータを読み込む
            // RGBの計3つ分をまとめて読み込む
            fread(tmp, sizeof(unsigned char), 3, fpi);
            
            // 画像輝度値を構造体に代入
            data[i * pixel + j].r = tmp[0];
            data[i * pixel + j].g = tmp[1];
            data[i * pixel + j].b = tmp[2];
        }
    }

    // 画像の左右を反転しながら出力
    for (i=0; i<line; i++) {
        for (j=0; j<pixel; j++) {
            
            // 左右の反転:j → pixel - 1 - jに変換される
            tmp[0] = data[i * pixel + (pixel - 1 - j)].r;
            tmp[1] = data[i * pixel + (pixel - 1 - j)].g;
            tmp[2] = data[i * pixel + (pixel - 1 - j)].b;

            // RGBの計3つ分を出力ファイルへ書き込む
            fwrite(tmp, sizeof(unsigned char), 3, fpo);
        }
    }
    
    
    // メモリの開放
    free (data);

    ///// 入出力ファイルを閉じる処理 /////

    fclose(fpi);
    fclose(fpo);
}

演習3:画像の上下を反転

上記の演習2を参考に自分で考えましょう。

演習4:画像を1/3の大きさに縮小

上記の演習2を参考に自分で考えましょう。

まず、下記のように「SKIP」という変数を定義しましょう。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#define SKIP	3

ヒント1:全ての画素の値を読み込んだ後で、出力する際にはライン、ピクセル方向共にSKIPだけ飛ばして処理します。

    // ライン、ピクセル方向共にSKIPだけ飛ばして処理
    for (i=0; i<line; i += SKIP) {
        for (j=0; j<pixel; j += SKIP) {

             // 処理内容を自分で考えてここに記述する

        }
    }

ヒント2:「ヘッダファイルの処理」の「2行目の処理」において、出力ファイルのライン数、ピクセル数共に1/SKIPの値を出力する必要があります。

    // 入力ファイル:2行目読み込み(ピクセル、ライン数)
    fgets(str, 256, fpi);
    fputs(str, stdout);
    sscanf(str, "%d %d", &pixel, &line); // 整数値を変数へ代入
    // 縮小後のピクセル、ライン数書き込み
    fprintf(fpo, "%d %d\n", pixel/SKIP, line/SKIP); 

演習5:画像中央を回転中心として、画像を60度反時計回りに回転

下記に示すソースを利用して、プログラムを完成して下さい。但し、一部***で隠した処理は自分自身で考えて下さい。

/* 
 image5.c

 PBMフォーマット画像(RGB)を読み込んで、
 画像中央を回転中心として、画像をthetaだけ反時計回りに
 回転して出力するプログラム

*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#define THETA    60    // 度
#define PI    3.1415926535
#define COS(a) cos((a)*PI/180) // 度を入力して計算
#define SIN(a) sin((a)*PI/180) // 度を入力して計算
#define TAN(a) tan((a)*PI/180) // 度を入力して計算

// 入力ファイル名
char INPUTFILE[100]="sampleimg1.pbm";

// 出力ファイル名
char OUTPUTFILE[100]="sampleimg5_rst.pbm";


int main(int argc, char *argv[])
{
    int i, j, k, ii, jj;
    int line, pixel;
    int line2, pixel2;
    FILE *fpi, *fpo;
    unsigned char tmp[3];
    char str[256];
    
    // RGB輝度値を格納する構造体を自分で定義
    struct rgb_data {
        unsigned char    r, g, b;
    };
    struct rgb_data    *data;    // この時点では必要な大きさが未定
    
    double d, alpha;
    int x, y;
    int h, w;
    double h2, w2;
    unsigned char nodata[3] = {0, 0, 0};
    
    

    ///// 入出力ファイルを開く処理:ファイルポインタを設定 /////

    // 入力ファイル(PBMフォーマット)オープン
    if((fpi = fopen(INPUTFILE, "rb")) == NULL){
        fprintf(stderr, "Can not open %s \n", INPUTFILE);
        exit(1);
    }

    // 出力ファイル(PBMフォーマット)オープン
    if((fpo = fopen(OUTPUTFILE, "wb")) == NULL){
        fprintf(stderr, "Can not open %s \n", OUTPUTFILE);
        exit(1);
    }
    
    
    ///// ヘッダファイルの処理 /////
    
    // 入力ファイル:1行目読み込み(PBMタイプ:P6=RGB rawフォーマット)
    fgets(str, 256, fpi);
    fputs(str, fpo);    // 出力ファイルへ書き込み
    fputs(str, stdout);    // 標準出力(画面)へ書き込み
    
    // 入力ファイル:2行目読み込み(ピクセル、ライン数)
    fgets(str, 256, fpi);
    fputs(str, stdout);
    sscanf(str, "%d %d", &pixel, &line); // 整数値を変数へ代入
    
    // 回転後の画像サイズを確定

    // 中心から画像端までの距離
    d = pow((double)line*line + pixel*pixel, 0.5) / 2.0; 

    // 対角線とx軸の間の角度
    alpha = atan ((double)line/pixel) * 180 / PI; 

    
    // 回転後のピクセル数
    pixel2 = ****************************;

    // 回転後のライン数
    line2 = ****************************;
    
    fprintf(fpo, "%d %d\n", pixel2, line2); // 回転後のピクセル、ライン数

    // 確認用:ライン、ピクセル数を標準出力へ表示
    fprintf(stdout, "d = %6.2f, alpha = %6.2f\n", d, alpha);
    fprintf(stdout, "line = %d, pixel = %d\n", line, pixel);
    fprintf(stdout, "line2 = %d, pixel2 = %d\n", line2, pixel2);

    
    // 入力ファイル:3行目読み込み(階調数:255=8ビット=1バイト)
    fgets(str, 256, fpi);
    fputs(str, fpo);
    fputs(str, stdout);
    

    ///// データ本体の処理 /////

    // 確定したライン、ピクセル数を用いて、構造体用のメモリを確保
    data = (struct rgb_data *)malloc(sizeof(struct rgb_data)*line*pixel);


    // データ全体を一度読み込んで構造体に保存
    for (i=0; i<line; i++) {
        for (j=0; j<pixel; j++) {

            // 1画素ずつ入力ファイルからデータを読み込む
            // RGBの計3つ分をまとめて読み込む
            fread(tmp, sizeof(unsigned char), 3, fpi);
            
            // 画像輝度値を構造体に代入
            data[i * pixel + j].r = tmp[0];
            data[i * pixel + j].g = tmp[1];
            data[i * pixel + j].b = tmp[2];
        }
    }

    // 回転前の座標を計算しながら出力
    
    w = pixel / 2;
    h = line / 2;
    
    w2 = pixel2 / 2.0;
    h2 = line2 / 2.0;
    
    for (ii=0; ii<line2; ii++) {
        for (jj=0; jj<pixel2; jj++) {
            
            // 回転前のxy座標の値
            x = **************************************;
            y = **************************************;
            
            // 回転前のij座標の値
            i = **************************************;
            j = **************************************;
            
            if (i < 0 || i > line - 1 || j < 0 || j > pixel - 1) {

                // 該当するデータが存在しない
                // →黒の輝度値{0,0,0}を書き込む
                fwrite(nodata, sizeof(unsigned char), 3, fpo);
            }
            else {
                // 該当するデータが存在する
                tmp[0] = data[i * pixel + j].r;
                tmp[1] = data[i * pixel + j].g;
                tmp[2] = data[i * pixel + j].b;

                // RGBの計3つ分を出力ファイルへ書き込む
                fwrite(tmp, sizeof(unsigned char), 3, fpo);
            }
        }
    }
    
    
    // メモリの開放
    free (data);


    ///// 入出力ファイルを閉じる処理 /////

    fclose(fpi);
    fclose(fpo);
}

須崎純一 京都大学大学院 工学研究科社会基盤工学専攻 空間情報学講座
[須ア純一のホームページ] [社会基盤工学専攻 空間情報学講座]