開眼シェルスクリプト2013年4月号

Pocket
LINEで送る

出典: 技術評論社SoftwareDesign

16. 開眼シェルスクリプト 第16回画像処理で遊ぶ(2)

 今回は前回に引き続き、シェルスクリプトで画像処理をして遊んでみましょう。
前回はコマンドで扱いやすくするために、
カラー画像を1ピクセル1レコードにしてから処理しました。
ただこの方法だけだとできることが限られるので、
今回は、awkをフルに使って画像処理をやってみます。
配列を操作するので、本連載史上、最も「普通の」プログラミングをやります。
そうは言っても、普通ではありませんが・・・。
しかし、この人もこんなことを言っているのでよいということにしましょう。
(注:完全に言い訳に使っています。)

『人生を楽しむ秘訣は普通にこだわらないこと。
普通と言われる人生を送る人間なんて、
一人としていやしない。いたらお目にかかりたいものだ』
— アルバート・アインシュタイン

16.1. 環境

 今回は、12年間親しんだThinkPadからMacBookAirに乗り換えたことを記念して、
Mac上のbashでコーディングします。なぜ乗り換えたかというと、
2月号の特集で「Macにはbashが入っているからターミナル使って欲しい」と書いた時に、
自分が率先しないといかん、と使命感に駆られたからです。
シャレ乙野郎になろうという気は毛頭ありません。
が、もともとカフェ中毒者なので「ドヤ顔mac」とか言われても仕方ありません。
言う側(言ってたのか)から言われる側になって辛いですが、
今月号からしばらくはMacでいきます。

  • リスト1: 環境等
1
2
3
4
5
6
7
8
$ uname -a
Darwin uedamac.local 12.2.1 Darwin Kernel Version 12.2.1:
Thu Oct 18 16:32:48 PDT 2012; root:xnu-2050.20.9~2/RELEASE_X86_64 x86_64
$ bash --version
GNU bash, version 3.2.48(1)-release (x86_64-apple-darwin12)
Copyright (C) 2007 Free Software Foundation, Inc.
$ awk --version
awk version 20070501

 リスト1に、今回の環境を示します。
多くのLinuxディストリビューションと違って、
awkgawk ではないので注意が必要ですが、
今回の内容では出力の違いはありません。

16.2. AWKのおさらい

16.2.1. パターン

 「パターン」は、これまで何度も使っていたとおり、
入力されたファイルから条件に合う行を抽出するためのものです。
パターンは grep の機能を担っていると考えてよいでしょう。
grep は抽出だけですが、AWKは抽出した行に対して
「アクション」で演算ができます。

 リスト2の例は、パターンで偶数を抽出して、
アクションで10で割るというものです。
jot 10 の出力は、 seq 1 10 のものと同じです。

  • リスト2: パターンとアクションの例
1
2
3
4
5
6
$ jot 10 | awk '$1%2==0{print $1/10}'
0.2
0.4
0.6
0.8
1

 パターンとアクションの組みは、いくつも書くことができます。
リスト3のコードはAWKのプログラムで、偶数と奇数を数えるものです。
パターンは、「START」、「END」も含めて4個ですね。
紙面の関係と一行野郎中毒が祟って1行1パターンにしましたが、
Cのように改行・インデントをする方がAWKのスクリプトとしてはまともでしょう。

  • リスト3: パターンを並べたAWKのコードの例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ cat oddeven.awk
#!/usr/bin/awk -f

START{even=0;odd=0}
$1%2==0{even++}
$1%2==1{odd++}
END{print "奇数:",odd;print "偶数:",even}
$ jot 9 | ./oddeven.awk
奇数: 5
偶数: 4

 一つの行が複数のパターンにマッチする時は、リスト4のように、
パターンに書いた順に何回も出力されます。
この辺の挙動は、単なるif文とは違うので注意が必要です。

  • リスト4: 複数のパターンにマッチする場合
1
2
3
4
$ echo 1 | awk '{print $1,"a"}NR==1{print $1,"b"}NR!=2{print $1,"c"}'
1 a
1 b
1 c

16.2.2. 関数

 関数の書き方はjavascriptに似ています。
function 名前(変数,...){文;文;...}
というように表記します。
リスト5は、関数の名前の書き方と使い方の例です。

  • リスト5: 関数の書き方
1
2
3
4
5
6
7
8
$ cat func.sh
#!/bin/bash

echo $1 |
awk '{print scream($1,10)}
     function scream(a,n){return n==1?a:(scream(a,n-1) a)}'
$ ./func.sh あ
ああああああああああ

わざと再帰を使ってややこしくしており、
例としてはちょっと不適切かもしれませんが、
function の行が関数になっています。
この例のように、関数は使う場所より後ろに書いても大丈夫です。

16.2.3. 配列

 AWKは言語なのでもちろん配列があります。
AWKの配列は、連想配列として実装されています。
ですので、リスト6のような使い方ができます。

  • リスト6: 配列の使い方
1
2
$ awk 'BEGIN{a["猫"]="まっしぐら";print a["猫"]}'
まっしぐら

 もちろん、普通の配列としても使えます。
配列として使うときは、リスト6のように、
インデックスを0からではなく1から始めます。
自身で使うときは0からでも動きますが、
関数が配列を返すときは1に最初の要素が入っているので、
他に理由がなければ合わせましょう。

  • リスト6: 配列の使い方その2
1
2
3
4
5
6
7
$ echo 南海 ホークス | awk '{\
        a[1]=$1;a[2]=$2;for(i=1;i<=2;i++){print a[i]}}'
南海
ホークス
//split関数で文字列を切って配列aに代入
$ echo 'OH!MY!GOD!' | awk '{split($1,a,"!");print a[2]}'
MY

 表記に区別がないので、リスト7のようなこともできます。
Cでやったら間違いなく怒られますが大丈夫です。

  • リスト7: インデックスが大きくても大丈夫
1
2
$ awk 'BEGIN{a[123456789]=10;print a[123456789]}'
10

こういうことができるので、
例えば、$1はいらないけど$2や$3を配列に入れたいという場合、
それぞれ f[2], f[3] に入れてやればよいということになります。

 他の言語では配列と連想配列は区別されることが多いのですが、
AWKでは実装上も表記上も区別がありません。気軽に使える一方、
連想配列なので、あまり速度は期待できません。

 二次元配列は、次のようにインデックスをカンマで区切って表記します。
もちろん数字も使うことができます。リスト8に使用例を示します。

  • リスト8: 二次元配列の使用例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ cat hoge.sh
#!/bin/bash

echo $1 $2      |
awk 'BEGIN{
        a["グー","チョキ"] = "グー";
        a["パー","チョキ"] = "チョキ";
        (略)
        }
      END{print a[$1,$2] "の勝ち"}'
uedamac:201304 ueda$ ./hoge.sh パー チョキ
チョキの勝ち

 察しのよい人にはお分かりかもしれませんが、
この配列は実際にはC言語の二次元配列とは全く異なるものです。
AWKではインデックスを全部連結した文字列をキーにして、
一つの連想配列に記録しているようです。
もちろん、文字列の連結は、 12,31,23
が区別できるように行われます。
ここらへんの仕様は、
いかにもLL (lightweight language) の元祖らしい潔さです。

16.3. AWK 多めのシェルスクリプトで画像処理

 では、ここから本題です。
今回もjpeg等の画像をアスキー形式のppm画像に変換し、処理します。
ImageMagickのインストールをお願いします。

前号でも説明しましたが、アスキー形式のppm画像は、
スペースか改行区切りで数字の並んだテキストファイルです。
リスト9に例を示します。
最初のP3が画像の形式、次の二つが画像のサイズ、
次いで画素値の刻み幅(深さ)です。
その後、左から右、上から下の画素に向けて
r(赤)、g(緑)、b(青)の値が並びます。

  • リスト9: ppm画像をheadした例
1
2
3
4
5
6
$ head 1.ppm
P3               <- 画像のタイプ
#*               <- コメント
960 640          <- 画像の幅、高さ
255              <- 深さ
125 94 50 126 95 51 127 96 52 128 97 53 128 97 53...

16.3.1. パターンを使って画素を配列に記録

 まず、画像をAWKの配列に記録するまでのコードをリスト10に示します。
6行目で、画像( $1 に指定する)をppm画像に直しています。
12〜15行目でppm画像を読み込み、データを縦一列に並べ、
中間ファイルに落としています。
18〜20行目でヘッダ部分(幅、高さ、深さ)を変数に落とした後、
23行目以降で画像の本体部分の数字をAWKに入力しています。

  • リスト10: AWKの配列にRGBの値を入れるまで
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ cat donothing.sh
#!/bin/bash -xv

tmp=/tmp/$$

### 画像の変換
convert -compress none "$1" $tmp-i.ppm

### データを縦一列に並べる

#コメント除去
sed 's/#.*$//' $tmp-i.ppm       |
tr ' ' '\n'                     |
#空行を除去
awk 'NF==1'     > $tmp-ppm

### ヘッダ情報取り出し
W=$(head -n 2 $tmp-ppm | tail -n 1)
H=$(head -n 3 $tmp-ppm | tail -n 1)
D=$(head -n 4 $tmp-ppm | tail -n 1)

### 画素の値を配列に
tail -n +5 $tmp-ppm     |
awk -v w=$W -v h=$H -v d=$D \
        'NR%3==1{n=(NR-1)/3;r[n%w,int(n/w)] = $1}
        NR%3==2{n=(NR-2)/3;g[n%w,int(n/w)] = $1}
        NR%3==0{n=(NR-3)/3;b[n%w,int(n/w)] = $1}'

rm -f $tmp-*
exit 0

 AWKに書いてあるパターンは三つで、
上から順にそれぞれr, g, bの値を二次元配列に代入しています。
パイプから流れてくる数字は、1行目にr、2行目にg、3行目にb、
というように3個毎に値が並んでいるので、
rgbそれぞれをフィルタしたければリスト10のように、
NR (行番号)を3で割った余りで判定すればよいことになります。

 各フィルタに対応するアクションでは、
行番号から画像での横位置、縦位置を求めて配列に値を代入しています。
横位置は左側から 0,1,2,...
縦位置は上側から 0,1,2,... と数えることとしました。
AWKの掟に反してゼロから数えていますが、
n%wint(n/w)
に1を足すのは面倒なのでこのようにしています。

16.3.2. 光を発射

 後は、これに自分のやりたい処理を実装するだけです。
・・・と言ってもこれは画像処理の本を買ってくるか
ウェブで調べるかしないとチンプンカンプンな人もいるかと思います。
ここでは二つほど例を見せます。

 まず、画像の位置を使った処理の例です。
図1のサンプル画像はUSP友の会の勇壮なLL写真です。
見えないかと思いますが、後ろの男(注:私です。)
は手にビール瓶を持っています。
ビール瓶からフラッシュを出してみましょう。

  • 図1: 加工する画像(1.jpg)
_images/1.jpg

 図2に仕上がり、リスト11に、
この処理を行うAWKの部分を示します。
配列に値を読み込む部分まではリスト10と一緒で、
新たにENDパターンに対する処理と、
関数を一つ追加しています。
このシェルスクリプトの名前は flash.sh
で、リスト12のように使ってjpg画像を得ました。

  • 図2: ビール瓶の先から光線を出す
http://blog.ueda.asia/wp-content/uploads/2014/12/flash.jpg
  • リスト11: ビール瓶の先から光を出すためのAWK
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
### ビール瓶の先から国民に光を与える
tail -n +5 $tmp-ppm     |
awk -v w=$W -v h=$H -v d=$D \
    'NR%3==1{n=(NR-1)/3;r[n%w,int(n/w)] = $1}
    NR%3==2{n=(NR-2)/3;g[n%w,int(n/w)] = $1}
    NR%3==0{n=(NR-3)/3;b[n%w,int(n/w)] = $1}
    END{
        print "P3",w,h,d;
        for(y=0;y<h;y++){
            for(x=0;x<w;x++){
                ex = x - w*0.87;
                ey = y - h*0.32;
                deg = atan2(ey,ex)*360/3.141592 + 360;
                weight = (int(deg/15)%2) ? 1 : 4;

                p(r[x,y]*weight);
                p(g[x,y]*weight);
                p(b[x,y]);
            }
        }
    }
    function p(n){ print (n>d)?d:n }'
  • リスト12: 画像を加工するシェル操作
1
2
$ ./flash.sh 1.jpg > flash.ppm
$ convert flash.ppm flash.jpg

 リスト11のENDパターンでは、
まず8行目でppm画像のヘッダ部分を出力しています。
その後の二重の for 文で、
1画素ずつ、r, g, bの順番に値を加工して出力しています。

  for のループ内では、まず11, 12行目で、
その画素が光を出す中心の画素に対してどの位置にあるかを求めています。
中心の画素は、私が手で調べてハードコーディングしました。
変数にしてもよいですね。

 その後、13行目で、「その画素が光を出す中心に対してどの方角にあるか」
を求めています。 atan2 はC言語にもある関数ですが、
見たことが無い人もいるかもしれません。
図3のように角度を返す関数です。
atan2 の返した値を π で割って360をかけると、
いわゆる普通の角度(degree)になります。

  • 図3: atan2(y,x)の返す角度
_images/atan2.png

 ところで、 (x,y) = (0,0) だと atan2
が何を返すか不安ですが、AWKですので、

1
2
$ awk 'BEGIN{print atan2(0,0)}'
0

のように実用的な値を返してくれます。
(注:全部のバージョンのAWKに当てはまるかは未調査です。)

 14行目では、角度15度刻みで weight という変数の値を
1にしたり4にしたりしています。
完成した画像をよく見ると15度刻みで光っていますが、
この準備です。
細かい話ですが、 atan2 が返す値がプラスの場合と
マイナスの場合がある影響で360度きれいに15度刻みにならないので、
13行目で360を足して、 deg の値がプラスになるようにしています。

 これでいよいよ標準出力に値を出していきます(16〜18行目)。
白黒で分かりにくいですが、金色(黄色)に光らせたいので、
rとgの値に weight をかけて強調します。
p という関数は22行目で実装しており、
値が最大値 d を超えると d で打ち切って出力するというものです。
ところで、AWKの変数は基本的にすべてグローバル変数なので、
オプションで定義された d は、関数の中でも使えます。
長いプログラミングをするとちょっと辛いかなと、個人的には思います。

16.3.3. エンボス加工する

 もう一つ例をリスト13に示します。
これは、エンボス加工風に画像を変換する処理です。
図4に、処理前後の画像を示します。
このようなアイコンの処理だけでなく、
写真を処理すると絵画のような風合いになります。
http://www.usptomo.com/PAGE=20130113IMAGE
で公開していますので、遊んでみてください。

  • リスト13: エンボス加工処理
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
tail -n +5 $tmp-ppm     |
awk -v w=$W -v h=$H -v d=$D \
        'NR%3==1{n=(NR-1)/3;r[n%w,int(n/w)] = $1}
        NR%3==2{n=(NR-2)/3;g[n%w,int(n/w)] = $1}
        NR%3==0{n=(NR-3)/3;b[n%w,int(n/w)] = $1}
        END{print "P3",w-2,h-2,d;
            for(y=1;y<h-1;y++){
                for(x=1;x<w-1;x++){
                        a = 2*g[x-1,y-1] + g[x-1,y] + g[x,y-1] - g[x,y+1] - g[x+1,y] - 2*g[x+1,y+1];
                        p(r[x,y] - a); p(g[x,y] - a); p(b[x,y] - a);
                }
        }}
        function p(v){print (v < 0) ? 0 : (v > d ? d : v)}'
  • 図4: エンボス加工前後の画像
_images/CHINJYU.JPG
_images/enbos.chinjyu.jpg

 リスト13の処理では、まず変数 a に、
ある画素とその周囲の画素のg値を比較した値を代入しています。
この処理は「sobelフィルタ」と言われるもので、
この演算だと、画像の斜め方向で緑色が急激に変わっている画素の
a の値が正、あるいは負の方向に大きくなります。
図5に、 a の値でグレースケール画像を作ったものを示します。
本当はgだけでなく、r,g,bの値で平均値をとって a
の値を求めるべきですが、コードがややこしくなるので緑だけにしています。

  • 図5: a の値で画像を作ったもの
_images/chinjyu.edge.jpg

 この a の値を、10行目のように各rgb値から引くと、
色の変化の急激なところが強調されて、
人間の目には画像に凹凸があるように見えます。

16.4. おわりに

 今回は、シェルスクリプト(ただしAWK多め)で画像処理をしてみました。
筆者は遊びのつもりで始めましたが、
テキストにすると処理の流れが分かりやすいので、
これは画像処理の教育用によいかもしれません。

 今回はAWKの説明を充実させました。
パターンや配列、関数の書き方などを説明しました。
特徴的なのはパターンの存在そのものと、あとは配列の実装でしょう。
パターンをたくさん並べてプログラミングをすると、
「一行ずつ読み込み、パターンで振り分けて何かする」
という、他の言語との違いが際立ちます。
この動作はシェルスクリプトで使う他のコマンドと似ており、
やはり相性という点でAWKとシェルスクリプトは切っても切れない縁があります。
逆に言えば、AWKが使いこなせることが、
シェルスクリプトでなんでもやろうという発想にもつながります。

 次回は作り物を一旦お休みして、
「コマンドでどうしてもできないややこしい処理」
を1行AWKで処理する方法を扱いたいと思います。

Pocket
LINEで送る