Pythonで画像処理 (その2)


指定するフォルダにあるTIFFファイル全てを2値化するコードを書いてみる。

# 再帰的にフォルダを検索する

実に簡単にフォルダを掘ることができる。os.walk() というメソッドがあった。finddir()みたいな関数を作って、ソレを再帰呼び出しをする必要がない。

```python
import os
for root, dirs,files in os.walk("."):
    for f in files:
        print(os.path.join(root, f))
```

というワケで、カレントフォルダの全ファイルを表示できた。

os.walk() で、root, dir, files のリストとして指定したディレクトリ以下の全ファイルと全サブディレクトリを取得できる。
この場合filesがファイルの一覧になっているので、そこから一個ずつfという変数に格納しつつ取り出す。それをos.path.join()することで、rootからのファイルパスとして結合できる。

# 画像の2値化を実行する

指定したフォルダの全画像ファイルに対して2値化処理を実行する。

```python
import cv2
import os
import numpy as np
from matplotlib import pyplot as plt

for root,dirs,files in os.walk("img"):
    for f in files:
        if os.path.splitext(f)[1] == ".tiff":
            img = cv2.imread(os.path.join(root, f), 0)
            blur = cv2.GaussianBlur(img,(5,5),0)
            ret,imgb = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
```

とりあえず、これでimgフォルダ以下の拡張子が".tiff"な全ファイルを2値化できる。
まず、cv2.imread()で、第二引数として0を指定することで、グレー画像として画像ファイルを読み込んだ後で、cv2.GaussianBlur()で、ガウスぼかしをかける。その後、cv2.THRESH_BINARY+cv2.THRESH_OTSUで2値化する。

## シグナルの取得

cv2.threshold() の戻り値は、今回は2値化画像なので、2次元配列になっていて、各画素ごとに0か255かいずれかの値が格納されている。例えば、3×3の白黒市松模様ならば、こんな感じ。

	([255,0,255],
	 [0,255,0]
	 [255,0,255])

2値化した画像は、白地に黒の画像になっている。黒の画素というのは、要素が0になっている画素のことだ。なので、この全画素を走査して、シグナルがあった画素の数をカウントするとしたら、画像の縦横のサイズを取得して、(x,y)=(0,0)から、(x,y)=(max(x), max(y))まで処理を繰り返すことになる。

画像ファイルの座標は、なんとなく慣例として横軸X縦軸Yで(x,y)って書くけれど、numpyの2次元配列として取得されている画像の画素の情報は「行」「列」の順に記載されているので、ImageJ等で確認した(x,y) = (300, 200)の位置の画素のシグナルをnumpyの配列から取り出すときには、arrary[200, 300]のように指定する必要がある。考えれば分かるんだけれど、よく間違える。

以下は、白地に黒の2値化画像から黒の画素の数を数えて表示するもの。

```python
cnt=0
for x in range(0, imgb.shape[0]):
    for y in range(0, imgb.shape[1]):
        if imgb[x,y] == 0:
            cnt += 1
print(cnt)
```

実際には、これはnumpyのarrayなので、全要素を走査するだけなのであれば、もう少しだけ簡単にできる。

```python
cnt =0
for val in imgb.flat:
    if val == 0:
        cnt += 1
print(cnt)
```

cntのインクリメントを他の言語同様にcnt++って書いたらエラーになった。

## あるフォルダの下の画像を2値化してシグナルのあった画素数をカウントする

ここまでのコードをまとめると、次のようになる。imgフォルダの下の”.tiff”ファイルのみを対象に、画像を2値化して、シグナルが0の画素数をカウントする。

```python
import numpy as np
import cv2
from matplotlib import pyplot as plt
import os

for root,dirs,files in os.walk("img"):
    for f in files:
        if os.path.splitext(f)[1] == ".tiff":
            img = cv2.imread(os.path.join(root, f), 0)
            blur = cv2.GaussianBlur(img,(5,5),0)
            ret,imgb = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
            cnt = 0
            for val in imgb.flat:
                if val == 0:
                    cnt += 1
            print( f + ":\t" + str(cnt) )
```

## 関数を作る

上記の処理は、imgフォルダ決め打ちだったり、.tiffという拡張子決め打ちだったりするので、これを自由に指定できるように、一連の処理を関数としてまとめる。第一引数は対象とするフォルダで、第二引数は対象とするファイルの拡張子である。

```python
import numpy as np
import cv2
from matplotlib import pyplot as plt
import os

def convert_and_count( dir, ext ):
    for root,dirs,files in os.walk(dir):
        for f in files:
            if os.path.splitext(f)[1] == ext:
                img = cv2.imread(os.path.join(root, f), 0)
                blur = cv2.GaussianBlur(img,(5,5),0)
                ret,imgb = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
                cnt = 0
                for val in imgb.flat:
                    if val == 0:
                        cnt+=1
                msg = f + ":\t" + str(cnt)
                print( msg )

if __name__ == "__main__":
    convert_and_count( "img", ".tiff" )
```

ここまで、やると、だいぶnestが深くなってきていて、ちょっと見通しが悪くなってくる。pythonはindentの数でブロックを表現するので、ちょっと何か複雑なことをやろうとするとどんどんnestが深くなる。あまり階層が深くならないように適切なブロックでコードを書くようにしなければいけないのかもしれない。
たまに、いくつかのエディタをまたがって編集していると、タブとスペースが混在して、見た目のnestのレベルとpythonインタープリタが理解するnestが食い違って、エラーになることがある。

あまり意味は無いんだけれど、2値化した画像で「黒い領域」が全体の何％をしめるのか？ということを求めたい場合は、定義したconvert_and_count() 変数内にソレを計算する行を追加する。

```python
def convert_and_count( dir, ext ):
    for root,dirs,files in os.walk(dir):
        for f in files:
            if os.path.splitext(f)[1] == ext:
                img = cv2.imread(os.path.join(root, f), 0)
                blur = cv2.GaussianBlur(img,(5,5),0)
                ret,imgb = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
                cnt = 0
                for range in imgb.flat:
                    if val == 0:
                        cnt += 1
                ratio = cnt / imgb.size
                msg = f  + ":\t" + str(ratio)
                print( msg )
```

計算したratioを、小数点3桁まで表示したいという場合は、どうするのだろう。C言語のprintf(“%.3f”, ratio); みたいなイメージ。と思ったら、以下のような感じだった。

```python
msg = "%s:\t%.3f" % (f, ratio)
```

以上のように、Pythonを使うことで、画像の2値化を簡単に実行することができた。